Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
version: 2
updates:
# Security updates only: open-pull-requests-limit 0 disables scheduled version updates.
# gomod only — vm/go.mod is the sole manifest Trivy sees, the UI ships as a bundle with no node_modules.
- package-ecosystem: gomod
directory: /vm
schedule:
interval: weekly
open-pull-requests-limit: 0

- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
groups:
actions:
patterns:
- "*"
1 change: 1 addition & 0 deletions .github/workflows/build-push-docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,5 +53,6 @@ jobs:
platforms: linux/amd64,linux/arm64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
pull: true
cache-from: type=gha
cache-to: type=gha,mode=max
23 changes: 23 additions & 0 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: PR

on:
pull_request:
branches: [ main ]

concurrency:
group: pr-${{ github.ref }}
cancel-in-progress: true

jobs:
smoke-test:
name: Build and smoke-test
runs-on: ubuntu-latest
permissions:
contents: read

steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: why the specific hash?


- name: Build and smoke-test
run: make smoke-test
178 changes: 178 additions & 0 deletions .github/workflows/security-rebuild.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
name: Weekly Security Rebuild

# Rebuild weekly to pick up Go toolchain and OS patches, republishing only when it clears a CVE (COSY-926).

on:
schedule:
# ~22h ahead of the secops Wednesday 06:00 UTC scan, so anything cleared here is never filed.
- cron: "0 8 * * TUE"
workflow_dispatch:
inputs:
dry_run:
description: "Scan and rebuild, but never publish"
type: boolean
default: false
image:
description: "Image to scan and republish. Override to test in a fork."
type: string
default: localstack/localstack-docker-desktop

concurrency:
group: security-rebuild
cancel-in-progress: false

jobs:
security-rebuild:
name: Rebuild and republish if CVEs clear
runs-on: ubuntu-latest
permissions:
contents: write
env:
PUBLISHED_IMAGE: ${{ inputs.image || 'localstack/localstack-docker-desktop' }}
PLATFORMS: linux/amd64,linux/arm64
GIT_AUTHOR_NAME: localstack[bot]
GIT_AUTHOR_EMAIL: 88328844+localstack-bot@users.noreply.github.com
GIT_COMMITTER_NAME: localstack[bot]
GIT_COMMITTER_EMAIL: 88328844+localstack-bot@users.noreply.github.com
# Mirror the Trivy DBs to dodge GHCR rate limits (trivy-action#389), as aws_flink.yml does.
TRIVY_DB_REPOSITORY: "ghcr.io/aquasecurity/trivy-db,public.ecr.aws/aquasecurity/trivy-db"
TRIVY_JAVA_DB_REPOSITORY: "ghcr.io/aquasecurity/trivy-java-db,public.ecr.aws/aquasecurity/trivy-java-db"

steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Read published version
id: version
run: |
TAG=$(sed -n 's/^TAG?=\(.*\)$/\1/p' Makefile)
echo "Currently published: ${PUBLISHED_IMAGE}:${TAG}"
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"

- name: Scan published image
# --ignore-unfixed matches what the secops pipeline files, so an unfixable advisory can't loop here forever.
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: why the hash and not a stable version (live v1 or something)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using a hash is recommended by GH for security reasons.
Secure actions matter even if they're not in the final artifact because they get access to the job's credentials, they can potentially push any image to our docker repo or any code to our github repo etc.

Pinning an action to a full-length commit SHA is currently the only way to use an action as an immutable release. Pinning to a particular SHA helps mitigate the risk of a bad actor adding a backdoor to the action's repository
https://docs.github.com/en/actions/reference/security/secure-use#using-third-party-actions

Added the github actions to dependabot though, otherwise it's counterproductive and they end up never being bumped 6eb1c2c

with:
image-ref: ${{ env.PUBLISHED_IMAGE }}:${{ steps.version.outputs.tag }}
scanners: vuln
severity: HIGH,CRITICAL
ignore-unfixed: true
format: json
output: published.json

- name: Determine fixable CVEs on the published image
id: before
run: |
jq -r '[.Results[]?.Vulnerabilities[]?.VulnerabilityID] | unique | .[]' published.json | sort -u > published_cves.txt
echo "Fixable HIGH/CRITICAL CVEs on the published image:"; cat published_cves.txt || true
if [ -s published_cves.txt ]; then
echo "proceed=true" >> "$GITHUB_OUTPUT"
else
echo "Published image is clean — nothing to rebuild."
echo "proceed=false" >> "$GITHUB_OUTPUT"
fi

- name: Set up QEMU
if: steps.before.outputs.proceed == 'true'
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0

- name: Set up Buildx
if: steps.before.outputs.proceed == 'true'
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
with:
platforms: linux/amd64,linux/arm64

- name: Build and smoke-test candidate (amd64, for scanning)
if: steps.before.outputs.proceed == 'true'
run: make smoke-test IMAGE=localstack-docker-desktop TAG=candidate

- name: Scan candidate
if: steps.before.outputs.proceed == 'true'
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
with:
image-ref: localstack-docker-desktop:candidate
scanners: vuln
severity: HIGH,CRITICAL
ignore-unfixed: true
format: json
output: candidate.json

- name: Compute cleared CVEs (published - candidate)
id: delta
if: steps.before.outputs.proceed == 'true'
run: |
jq -r '[.Results[]?.Vulnerabilities[]?.VulnerabilityID] | unique | .[]' candidate.json | sort -u > candidate_cves.txt
comm -23 published_cves.txt candidate_cves.txt > cleared.txt
comm -13 published_cves.txt candidate_cves.txt > introduced.txt

echo "Cleared by the rebuild:"; cat cleared.txt || true
# Reported, not gated: we'd still rather ship a net improvement.
if [ -s introduced.txt ]; then
echo "::warning::Rebuild introduced new CVEs: $(paste -sd', ' introduced.txt)"
fi

if [ -s cleared.txt ]; then
echo "cleared=true" >> "$GITHUB_OUTPUT"
else
echo "cleared=false" >> "$GITHUB_OUTPUT"
fi

- name: Report CVEs a rebuild cannot fix
# Surface these instead of no-op'ing weekly until the SLA on the secops ticket burns.
if: steps.before.outputs.proceed == 'true' && steps.delta.outputs.cleared == 'false'
run: |
echo "::warning::Rebuild cleared nothing; these need a dependency bump: $(paste -sd', ' published_cves.txt)"
{
echo "### Rebuild cleared no CVEs"
echo
echo "Still present after a clean rebuild — these need a \`vm/go.mod\` bump, not a rebuild:"
echo
sed 's/^/- /' published_cves.txt
} >> "$GITHUB_STEP_SUMMARY"

- name: Bump version
id: bump
if: steps.delta.outputs.cleared == 'true' && !inputs.dry_run
run: |
NEW=$(./scripts/bump-version.sh cleared.txt)
echo "Releasing ${NEW}"
echo "version=${NEW}" >> "$GITHUB_OUTPUT"

- name: Login to Docker Hub
if: steps.delta.outputs.cleared == 'true' && !inputs.dry_run
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: Publish (multi-arch)
# Published here rather than via build-push-docker.yml: GITHUB_TOKEN pushes don't trigger workflows.
if: steps.delta.outputs.cleared == 'true' && !inputs.dry_run
env:
NEW: ${{ steps.bump.outputs.version }}
run: |
echo "Publishing ${PUBLISHED_IMAGE}:${NEW}, clearing:"; cat cleared.txt
docker buildx build --push --pull --no-cache \
--platform "${PLATFORMS}" \
--tag "${PUBLISHED_IMAGE}:${NEW}" .

# After the publish on purpose: a failed publish must not leave TAG pointing at an unpushed image.
- name: Commit and tag
if: steps.delta.outputs.cleared == 'true' && !inputs.dry_run
run: |
git add Makefile Dockerfile CHANGELOG.md
git commit -m "Security rebuild ${{ steps.bump.outputs.version }}"
git tag "v${{ steps.bump.outputs.version }}"
git push origin HEAD:main "v${{ steps.bump.outputs.version }}"

- name: Summary
if: always()
run: |
{
echo "### Weekly security rebuild"
echo
echo "- Published image: \`${PUBLISHED_IMAGE}:${{ steps.version.outputs.tag }}\`"
echo "- Fixable HIGH/CRITICAL on the published image: $(wc -l < published_cves.txt 2>/dev/null || echo 0)"
echo "- Cleared by rebuild: $(wc -l < cleared.txt 2>/dev/null || echo 0)"
echo "- Released: ${{ steps.bump.outputs.version || 'no (nothing cleared)' }}"
} >> "$GITHUB_STEP_SUMMARY"
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Change Log

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/).
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>`).

## [2026.8.0] — 2026-08-19

Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ RUN --mount=type=cache,target=/usr/src/app/.npm \
COPY ui /ui
RUN npm run build

FROM alpine
FROM alpine:3.24
RUN apk upgrade --no-cache
LABEL org.opencontainers.image.title="LocalStack" \
org.opencontainers.image.description="Extension of Localstack for Docker desktop" \
Expand Down
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,17 @@ NO_COLOR = \033[m

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

@carole-lavillonniere carole-lavillonniere Aug 25, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added --pull to force pull images if there were rebuilt with same tag and --no-cache to force apk upgrade to re-run even when the Alpine digest hasn't moved.


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

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

smoke-test: build-extension ## Verify the built image starts and ships everything it declares
./scripts/smoke-test.sh $(IMAGE):$(TAG)

debug: ## Start the extension in debug mode
docker extension dev debug $(IMAGE)

Expand Down
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,44 @@ To contribute, check out our [issue tracker](https://github.com/localstack/local
```bash
$ make stop-hot-reloading
```
## Security maintenance

Most CVEs reported against this image come from the Go toolchain compiled into
the `service` binary rather than from any dependency manifest, so they are fixed
by rebuilding on a newer `golang:1.25-alpine` rather than by bumping anything.

The [weekly security rebuild](.github/workflows/security-rebuild.yml) does this
automatically: it scans the published image, rebuilds from scratch, and
republishes a new patch version **only if the rebuild actually clears a CVE**.
Rebuilds that change nothing are not released, so no update badge appears in
Docker Desktop for a no-op.

Anything a rebuild cannot fix needs a dependency bump. Dependabot raises those
against `vm/go.mod` as security updates; they are reviewed and released with the
same workflow via `workflow_dispatch`.

Every PR builds the image and runs the smoke test via
[PR CI](.github/workflows/pr.yml), so a bump is verified on its own branch
before it lands. Testing before the merge matters here: if you test afterwards,
the next weekly rebuild sees the CVE count drop and releases the bump without
the test ever having run.

### Validating a CVE fix locally

```bash
# What is currently published?
trivy image --scanners vuln --severity HIGH,CRITICAL --ignore-unfixed \
localstack/localstack-docker-desktop:$(sed -n 's/^TAG?=//p' Makefile)

# Rebuild a candidate from scratch and check it still works. Builds are always
# --pull --no-cache, so a cached base layer cannot reproduce the old image and
# clear nothing. This is the same command CI runs.
make smoke-test IMAGE=dde-candidate TAG=scan
trivy image --scanners vuln --severity HIGH,CRITICAL --ignore-unfixed dde-candidate:scan
```

Test a change end to end in Docker Desktop with `make install-extension`.

## Releases

Please refer to [`CHANGELOG`](CHANGELOG.md) to see the complete list of changes for each release.
Expand Down
46 changes: 46 additions & 0 deletions scripts/bump-version.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#!/bin/bash
# Bump the version across the Makefile, the Dockerfile label and the CHANGELOG.
# Versions are calendar-based: <year>.<month>.<patch>, e.g. 2026.8.1. A release in
# the same month as the last one takes the next patch; the first release of a new
# month moves the date forward and restarts the patch at 0.
# Usage: ./scripts/bump-version.sh [cleared-cves-file] Prints the new version.

set -euo pipefail

CLEARED_FILE="${1:-}"

CURRENT=$(sed -n 's/^TAG?=\(.*\)$/\1/p' Makefile)
[ -n "$CURRENT" ] || { echo "could not read TAG from Makefile" >&2; exit 1; }

# Split 2026.8.1 into the date part (2026.8) and the patch (1).
CURRENT_DATE="${CURRENT%.*}"
CURRENT_PATCH="${CURRENT##*.}"
[[ "$CURRENT_PATCH" =~ ^[0-9]+$ ]] || { echo "unexpected TAG format: $CURRENT" >&2; exit 1; }

# 10# forces base 10, so a zero-padded month such as 08 is not read as octal.
TODAY_DATE="$(date -u +%Y).$((10#$(date -u +%m)))"

if [ "$CURRENT_DATE" = "$TODAY_DATE" ]; then
NEW="${TODAY_DATE}.$((CURRENT_PATCH + 1))"
else
NEW="${TODAY_DATE}.0"
fi

sed -i "s|^TAG?=${CURRENT}$|TAG?=${NEW}|" Makefile

# Rewrite whatever is there — this label was stale for several releases.
sed -i "s|org.opencontainers.image.version=[^ ]*|org.opencontainers.image.version=${NEW}|" Dockerfile

NOTE="Security update"
if [ -n "$CLEARED_FILE" ] && [ -s "$CLEARED_FILE" ]; then
NOTE="Security update — clears $(paste -sd, "$CLEARED_FILE" | sed 's/,/, /g')"
fi

ENTRY="## [${NEW}] — $(date -u +%Y-%m-%d)\n\n### Changed\n\n- ${NOTE}\n"

awk -v entry="$ENTRY" '
!done && /^## \[/ { printf "%s\n", entry; done = 1 }
{ print }
' CHANGELOG.md > CHANGELOG.md.tmp && mv CHANGELOG.md.tmp CHANGELOG.md

echo "$NEW"
Loading