Skip to content

Commit b2fc334

Browse files
Weekly rebuild + Dependabot (#99)
* [COSY-926] Automate CVE remediation Add a gated weekly security rebuild, scope Dependabot to security updates for vm/go.mod, and add a smoke test the rebuild must pass before it ships. Most CVEs on this image come from the Go toolchain baked into the service binary rather than a dependency manifest, so they are fixed by rebuilding rather than by bumping anything. The rebuild only republishes when it actually clears a CVE, so a no-op never surfaces as an update in Docker Desktop. * Trim comments to one-liners * Add PR CI that builds the image and runs the smoke test Dependency bumps are the only way to clear CVEs a rebuild cannot, but nothing verified them before a merge. Run make smoke-test on every PR so a bump is tested on its branch rather than after it lands. * Never cache builds, and smoke-test through one parameterised target Builds are now always --pull --no-cache, in the Makefile rather than at each call site: a cached base layer reproduces the old image and clears no CVE, and that property matters for every build, not just the weekly rebuild's. IMAGE and TAG were already overridable, so the rebuild's separate buildx call and script invocation collapse into make smoke-test IMAGE=dde-candidate TAG=scan, leaving scripts/smoke-test.sh with a single caller. build-extension already guards the binary download, so that step goes too. * Keep the release build cached, refreshed by --pull no-cache on build-push-docker.yml put every push to main and every tag through an uncached QEMU arm64 build, on the path you wait on when shipping a security fix. pull: true invalidates the downstream layers whenever a base digest moves, which is what the freshness actually depends on, and the weekly rebuild covers the apk upgrade layer that only no-cache can re-run. Restores the gha cache: without it the runner has nothing to import, so dropping no-cache alone would have left the build cold anyway. * Publish before committing the version bump Pushing the bump and the tag first meant a failed publish left main claiming TAG?=x.y.z for an image that was never pushed. The next run reads TAG from the Makefile and scans that tag, so the scan failed and the job died every week after, with nothing watching for CVEs in the meantime. Publishing first fails safe: the working-tree edits go with the runner and main is untouched. The reverse failure -- published, then the push to main rejected because main moved -- leaves an unreferenced image and republishes the same version next week. * Make the target image a dispatch input IMAGE was hardcoded in the job env, so testing the publish path meant editing the workflow to point somewhere safe -- easy to get wrong on a file whose whole job is pushing to production. Now a fork can override it at dispatch. The || fallback keeps the scheduled run working: inputs is null on a schedule trigger, so the default only applies to workflow_dispatch. * Derive year.month from the date, not from the previous tag Bumping only the patch meant the date part froze at whatever the last hand-made feature release set, so a security release in December 2026 would still have shipped as 2026.8.x. Take year.month from the current date and bump the patch only within the same month; the first release of a new month restarts at 0. Also drops the CHANGELOG's claim that the project follows semver, which it has not for as long as versions have looked like 2026.8.0. * Name the two images apart IMAGE meant the published Docker Hub image in the job env, and then line 83 reused the same name for the throwaway local build, which is what made dde-candidate:scan hard to place. Now PUBLISHED_IMAGE is the thing on Docker Hub and localstack-docker-desktop:candidate is the local build being evaluated, with the scan artefacts named after whichever image they describe. * Commit as localstack[bot] rather than github-actions[bot] Matches the identity the rest of the org releases under (openapi, localstack-cli, localstack-sdk-python, localstack-pro all set these four vars). git honours the GIT_AUTHOR_*/GIT_COMMITTER_* environment directly, so the git config calls go. Attribution only: the push still uses GITHUB_TOKEN, which the publish step relies on, since pushes made with it do not trigger workflows. Pushing the tag as the bot with a PAT would fire build-push-docker.yml and publish the tag twice. * Let Dependabot bump the action SHA pins A commit SHA is immutable, which is the point, but it also means nothing tells you a newer release exists. Dependabot updates the pin and the trailing version comment together, so the pins stay current without being unpinned. Unlike the gomod entry this one wants version updates, so no limit of 0; grouped into a single PR so the pins arrive as one review rather than one per action. * Use the ID-prefixed noreply address for localstack[bot] The plain localstack-bot@users.noreply.github.com form copied from openapi's release.yml can render as an unlinked author: the account (id 88328844) was created in 2021, and GitHub links post-2017 accounts by the ID-prefixed address. localstack-cli's homebrew.yml already commits with that form. Cosmetic — attribution only, no change to how the push authenticates.
1 parent 3c53c08 commit b2fc334

10 files changed

Lines changed: 364 additions & 3 deletions

File tree

‎.github/dependabot.yml‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
version: 2
2+
updates:
3+
# Security updates only: open-pull-requests-limit 0 disables scheduled version updates.
4+
# gomod only — vm/go.mod is the sole manifest Trivy sees, the UI ships as a bundle with no node_modules.
5+
- package-ecosystem: gomod
6+
directory: /vm
7+
schedule:
8+
interval: weekly
9+
open-pull-requests-limit: 0
10+
11+
- package-ecosystem: github-actions
12+
directory: /
13+
schedule:
14+
interval: weekly
15+
groups:
16+
actions:
17+
patterns:
18+
- "*"

‎.github/workflows/build-push-docker.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,5 +53,6 @@ jobs:
5353
platforms: linux/amd64,linux/arm64
5454
push: ${{ github.event_name != 'pull_request' }}
5555
tags: ${{ steps.meta.outputs.tags }}
56+
pull: true
5657
cache-from: type=gha
5758
cache-to: type=gha,mode=max

‎.github/workflows/pr.yml‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
name: PR
2+
3+
on:
4+
pull_request:
5+
branches: [ main ]
6+
7+
concurrency:
8+
group: pr-${{ github.ref }}
9+
cancel-in-progress: true
10+
11+
jobs:
12+
smoke-test:
13+
name: Build and smoke-test
14+
runs-on: ubuntu-latest
15+
permissions:
16+
contents: read
17+
18+
steps:
19+
- name: Checkout
20+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
21+
22+
- name: Build and smoke-test
23+
run: make smoke-test
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
name: Weekly Security Rebuild
2+
3+
# Rebuild weekly to pick up Go toolchain and OS patches, republishing only when it clears a CVE (COSY-926).
4+
5+
on:
6+
schedule:
7+
# ~22h ahead of the secops Wednesday 06:00 UTC scan, so anything cleared here is never filed.
8+
- cron: "0 8 * * TUE"
9+
workflow_dispatch:
10+
inputs:
11+
dry_run:
12+
description: "Scan and rebuild, but never publish"
13+
type: boolean
14+
default: false
15+
image:
16+
description: "Image to scan and republish. Override to test in a fork."
17+
type: string
18+
default: localstack/localstack-docker-desktop
19+
20+
concurrency:
21+
group: security-rebuild
22+
cancel-in-progress: false
23+
24+
jobs:
25+
security-rebuild:
26+
name: Rebuild and republish if CVEs clear
27+
runs-on: ubuntu-latest
28+
permissions:
29+
contents: write
30+
env:
31+
PUBLISHED_IMAGE: ${{ inputs.image || 'localstack/localstack-docker-desktop' }}
32+
PLATFORMS: linux/amd64,linux/arm64
33+
GIT_AUTHOR_NAME: localstack[bot]
34+
GIT_AUTHOR_EMAIL: 88328844+localstack-bot@users.noreply.github.com
35+
GIT_COMMITTER_NAME: localstack[bot]
36+
GIT_COMMITTER_EMAIL: 88328844+localstack-bot@users.noreply.github.com
37+
# Mirror the Trivy DBs to dodge GHCR rate limits (trivy-action#389), as aws_flink.yml does.
38+
TRIVY_DB_REPOSITORY: "ghcr.io/aquasecurity/trivy-db,public.ecr.aws/aquasecurity/trivy-db"
39+
TRIVY_JAVA_DB_REPOSITORY: "ghcr.io/aquasecurity/trivy-java-db,public.ecr.aws/aquasecurity/trivy-java-db"
40+
41+
steps:
42+
- name: Checkout
43+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
44+
45+
- name: Read published version
46+
id: version
47+
run: |
48+
TAG=$(sed -n 's/^TAG?=\(.*\)$/\1/p' Makefile)
49+
echo "Currently published: ${PUBLISHED_IMAGE}:${TAG}"
50+
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
51+
52+
- name: Scan published image
53+
# --ignore-unfixed matches what the secops pipeline files, so an unfixable advisory can't loop here forever.
54+
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
55+
with:
56+
image-ref: ${{ env.PUBLISHED_IMAGE }}:${{ steps.version.outputs.tag }}
57+
scanners: vuln
58+
severity: HIGH,CRITICAL
59+
ignore-unfixed: true
60+
format: json
61+
output: published.json
62+
63+
- name: Determine fixable CVEs on the published image
64+
id: before
65+
run: |
66+
jq -r '[.Results[]?.Vulnerabilities[]?.VulnerabilityID] | unique | .[]' published.json | sort -u > published_cves.txt
67+
echo "Fixable HIGH/CRITICAL CVEs on the published image:"; cat published_cves.txt || true
68+
if [ -s published_cves.txt ]; then
69+
echo "proceed=true" >> "$GITHUB_OUTPUT"
70+
else
71+
echo "Published image is clean — nothing to rebuild."
72+
echo "proceed=false" >> "$GITHUB_OUTPUT"
73+
fi
74+
75+
- name: Set up QEMU
76+
if: steps.before.outputs.proceed == 'true'
77+
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
78+
79+
- name: Set up Buildx
80+
if: steps.before.outputs.proceed == 'true'
81+
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
82+
with:
83+
platforms: linux/amd64,linux/arm64
84+
85+
- name: Build and smoke-test candidate (amd64, for scanning)
86+
if: steps.before.outputs.proceed == 'true'
87+
run: make smoke-test IMAGE=localstack-docker-desktop TAG=candidate
88+
89+
- name: Scan candidate
90+
if: steps.before.outputs.proceed == 'true'
91+
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
92+
with:
93+
image-ref: localstack-docker-desktop:candidate
94+
scanners: vuln
95+
severity: HIGH,CRITICAL
96+
ignore-unfixed: true
97+
format: json
98+
output: candidate.json
99+
100+
- name: Compute cleared CVEs (published - candidate)
101+
id: delta
102+
if: steps.before.outputs.proceed == 'true'
103+
run: |
104+
jq -r '[.Results[]?.Vulnerabilities[]?.VulnerabilityID] | unique | .[]' candidate.json | sort -u > candidate_cves.txt
105+
comm -23 published_cves.txt candidate_cves.txt > cleared.txt
106+
comm -13 published_cves.txt candidate_cves.txt > introduced.txt
107+
108+
echo "Cleared by the rebuild:"; cat cleared.txt || true
109+
# Reported, not gated: we'd still rather ship a net improvement.
110+
if [ -s introduced.txt ]; then
111+
echo "::warning::Rebuild introduced new CVEs: $(paste -sd', ' introduced.txt)"
112+
fi
113+
114+
if [ -s cleared.txt ]; then
115+
echo "cleared=true" >> "$GITHUB_OUTPUT"
116+
else
117+
echo "cleared=false" >> "$GITHUB_OUTPUT"
118+
fi
119+
120+
- name: Report CVEs a rebuild cannot fix
121+
# Surface these instead of no-op'ing weekly until the SLA on the secops ticket burns.
122+
if: steps.before.outputs.proceed == 'true' && steps.delta.outputs.cleared == 'false'
123+
run: |
124+
echo "::warning::Rebuild cleared nothing; these need a dependency bump: $(paste -sd', ' published_cves.txt)"
125+
{
126+
echo "### Rebuild cleared no CVEs"
127+
echo
128+
echo "Still present after a clean rebuild — these need a \`vm/go.mod\` bump, not a rebuild:"
129+
echo
130+
sed 's/^/- /' published_cves.txt
131+
} >> "$GITHUB_STEP_SUMMARY"
132+
133+
- name: Bump version
134+
id: bump
135+
if: steps.delta.outputs.cleared == 'true' && !inputs.dry_run
136+
run: |
137+
NEW=$(./scripts/bump-version.sh cleared.txt)
138+
echo "Releasing ${NEW}"
139+
echo "version=${NEW}" >> "$GITHUB_OUTPUT"
140+
141+
- name: Login to Docker Hub
142+
if: steps.delta.outputs.cleared == 'true' && !inputs.dry_run
143+
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
144+
with:
145+
username: ${{ secrets.DOCKERHUB_USERNAME }}
146+
password: ${{ secrets.DOCKERHUB_TOKEN }}
147+
148+
- name: Publish (multi-arch)
149+
# Published here rather than via build-push-docker.yml: GITHUB_TOKEN pushes don't trigger workflows.
150+
if: steps.delta.outputs.cleared == 'true' && !inputs.dry_run
151+
env:
152+
NEW: ${{ steps.bump.outputs.version }}
153+
run: |
154+
echo "Publishing ${PUBLISHED_IMAGE}:${NEW}, clearing:"; cat cleared.txt
155+
docker buildx build --push --pull --no-cache \
156+
--platform "${PLATFORMS}" \
157+
--tag "${PUBLISHED_IMAGE}:${NEW}" .
158+
159+
# After the publish on purpose: a failed publish must not leave TAG pointing at an unpushed image.
160+
- name: Commit and tag
161+
if: steps.delta.outputs.cleared == 'true' && !inputs.dry_run
162+
run: |
163+
git add Makefile Dockerfile CHANGELOG.md
164+
git commit -m "Security rebuild ${{ steps.bump.outputs.version }}"
165+
git tag "v${{ steps.bump.outputs.version }}"
166+
git push origin HEAD:main "v${{ steps.bump.outputs.version }}"
167+
168+
- name: Summary
169+
if: always()
170+
run: |
171+
{
172+
echo "### Weekly security rebuild"
173+
echo
174+
echo "- Published image: \`${PUBLISHED_IMAGE}:${{ steps.version.outputs.tag }}\`"
175+
echo "- Fixable HIGH/CRITICAL on the published image: $(wc -l < published_cves.txt 2>/dev/null || echo 0)"
176+
echo "- Cleared by rebuild: $(wc -l < cleared.txt 2>/dev/null || echo 0)"
177+
echo "- Released: ${{ steps.bump.outputs.version || 'no (nothing cleared)' }}"
178+
} >> "$GITHUB_STEP_SUMMARY"

‎CHANGELOG.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Change Log
22

3-
All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/).
3+
All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project uses calendar versioning (`<year>.<month>.<patch>`).
44

55
## [2026.8.0] — 2026-08-19
66

‎Dockerfile‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ RUN --mount=type=cache,target=/usr/src/app/.npm \
2222
COPY ui /ui
2323
RUN npm run build
2424

25-
FROM alpine
25+
FROM alpine:3.24
2626
RUN apk upgrade --no-cache
2727
LABEL org.opencontainers.image.title="LocalStack" \
2828
org.opencontainers.image.description="Extension of Localstack for Docker desktop" \

‎Makefile‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,17 @@ NO_COLOR = \033[m
88

99
build-extension: ## Build service image to be deployed as a desktop extension
1010
ls binaries/linux/localstack-* > /dev/null 2>&1 || ./downloadBinaries.sh
11-
docker build --tag=$(IMAGE):$(TAG) .
11+
docker build --pull --no-cache --tag=$(IMAGE):$(TAG) .
1212

1313
install-extension: build-extension ## Install the extension
1414
docker extension install $(IMAGE):$(TAG)
1515

1616
update-extension: build-extension ## Update the extension
1717
docker extension update $(IMAGE):$(TAG)
1818

19+
smoke-test: build-extension ## Verify the built image starts and ships everything it declares
20+
./scripts/smoke-test.sh $(IMAGE):$(TAG)
21+
1922
debug: ## Start the extension in debug mode
2023
docker extension dev debug $(IMAGE)
2124

‎README.md‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,44 @@ To contribute, check out our [issue tracker](https://github.com/localstack/local
5656
```bash
5757
$ make stop-hot-reloading
5858
```
59+
## Security maintenance
60+
61+
Most CVEs reported against this image come from the Go toolchain compiled into
62+
the `service` binary rather than from any dependency manifest, so they are fixed
63+
by rebuilding on a newer `golang:1.25-alpine` rather than by bumping anything.
64+
65+
The [weekly security rebuild](.github/workflows/security-rebuild.yml) does this
66+
automatically: it scans the published image, rebuilds from scratch, and
67+
republishes a new patch version **only if the rebuild actually clears a CVE**.
68+
Rebuilds that change nothing are not released, so no update badge appears in
69+
Docker Desktop for a no-op.
70+
71+
Anything a rebuild cannot fix needs a dependency bump. Dependabot raises those
72+
against `vm/go.mod` as security updates; they are reviewed and released with the
73+
same workflow via `workflow_dispatch`.
74+
75+
Every PR builds the image and runs the smoke test via
76+
[PR CI](.github/workflows/pr.yml), so a bump is verified on its own branch
77+
before it lands. Testing before the merge matters here: if you test afterwards,
78+
the next weekly rebuild sees the CVE count drop and releases the bump without
79+
the test ever having run.
80+
81+
### Validating a CVE fix locally
82+
83+
```bash
84+
# What is currently published?
85+
trivy image --scanners vuln --severity HIGH,CRITICAL --ignore-unfixed \
86+
localstack/localstack-docker-desktop:$(sed -n 's/^TAG?=//p' Makefile)
87+
88+
# Rebuild a candidate from scratch and check it still works. Builds are always
89+
# --pull --no-cache, so a cached base layer cannot reproduce the old image and
90+
# clear nothing. This is the same command CI runs.
91+
make smoke-test IMAGE=dde-candidate TAG=scan
92+
trivy image --scanners vuln --severity HIGH,CRITICAL --ignore-unfixed dde-candidate:scan
93+
```
94+
95+
Test a change end to end in Docker Desktop with `make install-extension`.
96+
5997
## Releases
6098

6199
Please refer to [`CHANGELOG`](CHANGELOG.md) to see the complete list of changes for each release.

‎scripts/bump-version.sh‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#!/bin/bash
2+
# Bump the version across the Makefile, the Dockerfile label and the CHANGELOG.
3+
# Versions are calendar-based: <year>.<month>.<patch>, e.g. 2026.8.1. A release in
4+
# the same month as the last one takes the next patch; the first release of a new
5+
# month moves the date forward and restarts the patch at 0.
6+
# Usage: ./scripts/bump-version.sh [cleared-cves-file] Prints the new version.
7+
8+
set -euo pipefail
9+
10+
CLEARED_FILE="${1:-}"
11+
12+
CURRENT=$(sed -n 's/^TAG?=\(.*\)$/\1/p' Makefile)
13+
[ -n "$CURRENT" ] || { echo "could not read TAG from Makefile" >&2; exit 1; }
14+
15+
# Split 2026.8.1 into the date part (2026.8) and the patch (1).
16+
CURRENT_DATE="${CURRENT%.*}"
17+
CURRENT_PATCH="${CURRENT##*.}"
18+
[[ "$CURRENT_PATCH" =~ ^[0-9]+$ ]] || { echo "unexpected TAG format: $CURRENT" >&2; exit 1; }
19+
20+
# 10# forces base 10, so a zero-padded month such as 08 is not read as octal.
21+
TODAY_DATE="$(date -u +%Y).$((10#$(date -u +%m)))"
22+
23+
if [ "$CURRENT_DATE" = "$TODAY_DATE" ]; then
24+
NEW="${TODAY_DATE}.$((CURRENT_PATCH + 1))"
25+
else
26+
NEW="${TODAY_DATE}.0"
27+
fi
28+
29+
sed -i "s|^TAG?=${CURRENT}$|TAG?=${NEW}|" Makefile
30+
31+
# Rewrite whatever is there — this label was stale for several releases.
32+
sed -i "s|org.opencontainers.image.version=[^ ]*|org.opencontainers.image.version=${NEW}|" Dockerfile
33+
34+
NOTE="Security update"
35+
if [ -n "$CLEARED_FILE" ] && [ -s "$CLEARED_FILE" ]; then
36+
NOTE="Security update — clears $(paste -sd, "$CLEARED_FILE" | sed 's/,/, /g')"
37+
fi
38+
39+
ENTRY="## [${NEW}] — $(date -u +%Y-%m-%d)\n\n### Changed\n\n- ${NOTE}\n"
40+
41+
awk -v entry="$ENTRY" '
42+
!done && /^## \[/ { printf "%s\n", entry; done = 1 }
43+
{ print }
44+
' CHANGELOG.md > CHANGELOG.md.tmp && mv CHANGELOG.md.tmp CHANGELOG.md
45+
46+
echo "$NEW"

0 commit comments

Comments
 (0)