fix(docker): stop the distro-upgrade layer being cached forever (12 HIGH live in github-actions:latest) - #388
Open
Cre-eD wants to merge 1 commit into
Open
fix(docker): stop the distro-upgrade layer being cached forever (12 HIGH live in github-actions:latest)#388Cre-eD wants to merge 1 commit into
Cre-eD wants to merge 1 commit into
Conversation
`simplecontainer/github-actions:latest` ships python3 3.14.5-r0 -- 12 HIGH, 32 MEDIUM, 4 LOW across python3/pyc/python3-pyc/python3-pycache-pyc0 -- while Alpine v3.24/main has served 3.14.7-r1 for a while. Our own binary in that image scans 0; every finding is the base OS layer. The cause is the build cache, not a dependency version. Release, staging and preview builds all pass `cache-from: type=gha`, and the final stage's base is digest-pinned with an `apk update && apk upgrade && apk add ...` RUN string that never changes. So that layer's cache key is permanently stable and the upgrade has not actually executed since the first ever build. From the release log for 076224a: --cache-from type=gha,scope=prod-github-actions #13 [stage-1 2/8] RUN apk update && apk upgrade --no-cache && apk add ... #13 CACHED `--no-cache` on the apk line is not related -- it governs apk's own index cache, not Docker layers. Fix: name the final stage `runtime` in all five Dockerfiles and pass `no-cache-filters: runtime` to docker/build-push-action in push.yaml, build-staging.yml and branch-preview.yaml. Scoped to the final stage deliberately. The expensive work -- the SHA-256 verified Pulumi and gcloud downloads, `gcloud components install` and the slimming pass -- lives in `builder` and keeps its cache. Measured on github-actions.Dockerfile with a warm builder: fully cached (today) 7.3 s python3 3.14.5-r0 48 os findings --no-cache-filter runtime 41.1 s python3 3.14.7-r1 0 os findings 34 seconds per image. Verified across all five: caddy, cloud-helpers, kubectl, github-actions and github-actions-staging each build clean with the filter and report 0 OS-package findings. Remaining findings are upstream binaries we do not compile -- gcloud-crc32c and gke-gcloud-auth-plugin (17), kubectl (17), cel-go in the Caddy binary (1) -- all already documented in #387. Why this matters beyond python3: git, openssh-client, curl, libssl3, libcrypto3 and aws-cli sit in the same frozen layer. The next advisory in any of them would have been just as invisible, in the image that runs every consumer's deploy holding their cloud credentials. caddy and cloud-helpers only looked healthy because #387 happened to change instructions upstream of their upgrade layers, which busted the chain by accident. Not switching these to distroless or scratch: `aws` is a `#!/usr/bin/python3` script, `gcloud` is a shell script that needs a Python interpreter, and SC shells out to bash, git, ssh, pulumi and gcloud via Pulumi local.Command. A shell-less base cannot run this image. Scratch is worth considering for cloud-helpers (a single static Go binary, already at 0 findings) and possibly caddy -- tracked separately, not mixed into a cache fix. Signed-off-by: Dmitrii Creed <creeed22@gmail.com>
Cre-eD
requested review from
Laboratory,
smecsia and
universe-ops
as code owners
August 21, 2026 18:21
Semgrep Scan ResultsRepository:
Scanned at 2026-08-21 18:21 UTC |
Security Scan ResultsRepository:
Scanned at 2026-08-21 18:22 UTC |
📊 Statement coverageMeasured on the documented included set (see
Baseline: |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
simplecontainer/github-actions:latestships python3 3.14.5-r0 — 12 HIGH, 32 MEDIUM, 4 LOW acrosspython3/pyc/python3-pyc/python3-pycache-pyc0— while Alpinev3.24/mainhas served3.14.7-r1for a while. Our own binary in that image scans 0; every finding is the base OS layer.The cause is the build cache, not a dependency version, so #387 could not have caught it.
Root cause
Release, staging and preview builds all pass
cache-from: type=gha. The final stage's base is digest-pinned, and itsapk update && apk upgrade --no-cache && apk add …RUN string never changes. So that layer's cache key is permanently stable — the upgrade has not actually executed since the first ever build of that scope. Straight from the release log for076224a:Confirmed from inside the published image — the package is available, we just never take it:
--no-cacheon the apk line is unrelated and easy to misread as protection: it governs apk's own index cache, not Docker layers.caddyandcloud-helperscurrently look healthy only by accident — #387 changed instructions upstream of their upgrade layers (a new base digest, a newRUN), which busted the chain.kubectlwasCACHEDtoo and is clean only because its package set happens to contain nothing vulnerable. Nothing about that is a property we should rely on.Fix
Name the final stage
runtimein all five Dockerfiles, and passno-cache-filters: runtimetodocker/build-push-actioninpush.yaml,build-staging.yml(both steps) andbranch-preview.yaml.Scoped to the final stage deliberately. The expensive work — the SHA-256-verified Pulumi and gcloud downloads,
gcloud components install, and the slimming pass — lives inbuilderand keeps its cache. This was the open question ("isn't the cache higher priority?"), so it was measured rather than assumed, ongithub-actions.Dockerfilewith a warm builder:--no-cache-filter runtime+34 s per image. The trust-tier cache scoping added in the earlier hardening work is untouched —
cache-from/cache-toand theprod-*/staging-*/preview-*separation are unchanged, so a lower-trust build still cannot write a scope a release build reads.Verification
All five images built locally with
--no-cache-filter runtime, then scanned:github-actionsgcloud-crc32c+gke-gcloud-auth-plugin— Google's binariesgithub-actions-stagingcloud-helperscaddycel-go— needs an upstream Caddy changekubectlkubectlbinaryEvery remaining finding is an upstream binary we do not compile, each already documented in #387. In-Dockerfile smoke tests (
caddy version, plugin greps,pulumi version,gcloud version, gke-auth-plugin presence,aws --version,scsymlink,test -x /cloud-helpers) all pass under the filter. YAML parse-checked on all three workflows.Why this matters beyond python3
git,openssh-client,curl,libssl3,libcrypto3andaws-clisit in the same frozen layer. The next advisory in any of them would have been just as invisible — in the image that runs every consumer's deploy while holding their cloud credentials. The python3 CVEs themselves are mostly DoS plus twotarfileextraction-filter bypasses (CVE-2026-11940,CVE-2026-4360) that need an attacker-controlled archive, and Python's inputs here are gcloud's authenticated Google API traffic — so live exploitability is low. The reason to fix is that the patching mechanism was dead, not the severity of what it happened to be hiding this month.Not switching to distroless / scratch
Considered and rejected for these images, on evidence rather than preference:
awsis a Python script,gcloudis a shell script that needs a Python interpreter, and SC shells out tobash/git/ssh/pulumi/gcloudthrough Pulumilocal.Command(aws s3 syncin the static-website template,gcloud auth configure-dockerin the GKE stack). A shell-less, Python-less base cannot run this image — its entire purpose is having those tools onPATH.distroless/python3would only trade an Alpine base we can patch for a Debian base we can't, minus gcloud, git and ssh.Scratch is worth considering for
cloud-helpers(a single static Go binary, already at 0 findings — the gain would be structural immunity to base drift rather than a fix) and possiblycaddy. Both are separate changes and do not belong in a cache fix.Test plan
docker build --no-cache-filter runtimefor all five Dockerfiles: build OK, in-Dockerfile smoke tests pass, 0 OS-package findings each.yaml.safe_loadonpush.yaml,build-staging.yml,branch-preview.yaml.go.modor test changes — this is Dockerfile stage naming plus one workflow input.simplecontainer/github-actions:latestshould come out withpython3 3.14.7-r1and 0 alpine findings.