STAC-25033: verify pushed image binary arch matches requested platform#16
Conversation
The manifest of a pushed image always claims whatever --platform requested, regardless of what the Dockerfile actually compiled or copied in. A Dockerfile that hardcodes GOARCH (or copies a foreign-arch binary) therefore publishes an image that scans clean, gets signed, and only fails on the target nodes with 'exec format error'. Found while reviewing the kafkaup-operator migration, whose scaffold Dockerfile hardcodes GOARCH=amd64 while the release workflow pushes both amd64 and arm64. Add a verification step between push and cosign signing: pull the pushed digest for the requested platform, extract the entrypoint (or cmd) binary via docker create + docker cp -L (no container start, so no emulation needed), and compare the ELF e_machine field against the requested arch. Mismatch fails the action before signing. Indeterminate cases (no entrypoint, relative path, non-ELF entrypoint such as shell scripts) warn and pass so existing script-entrypoint consumers are unaffected. A verify-binary-arch input (default true) can disable the check and its image pull-back. Also add .github/actions/** to the zizmor lint workflow path filters so composite-action changes trigger the security audit; this change itself was the first to miss it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the hand-rolled ELF header parser (dd/od, endianness handling, e_machine table) with file(1), which ships on all GitHub-hosted runners and prints the architecture in one line. Also drop the verify-binary-arch opt-out input — the skip paths already cover every legitimate image shape (script entrypoints, no entrypoint, extraction failure), so there is no valid reason to publish an ELF entrypoint whose architecture differs from the platform — and the explicit docker pull and absolute-path pre-check, which docker create and the docker cp failure path already cover. Map only amd64/arm64 since those are the architectures we publish; anything else skips with a warning. Re-validated against the same local test images: arch-mismatch fails, match passes, symlinked entrypoint dereferences, script entrypoint and cmd-only images skip. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Due diligence on "isn't there a tool for this?" — researched before settling on the ~30-line step; short answer: no, and this step is the textbook encoding of what the ecosystem recommends.
The step reduces to three irreducible parts: extract without executing ( 🤖 Generated with Claude Code |
IMHO the reason why there is no a tool is that probably we should test the image is working before publishing it 😂 |
Andreagit97
left a comment
There was a problem hiding this comment.
As discussed i'm ok with that but i'm happy to remove this workaround in case of future issues
STAC-25033
Why
The manifest of a pushed image always claims whatever
--platformrequested — regardless of what the Dockerfile actually compiled or copied in. A Dockerfile that hardcodesGOARCH(or copies a foreign-arch binary) therefore publishes an image that scans clean, gets cosign-signed, and only fails at runtime on the target nodes withexec format error. We hit this class of bug reviewing the kafkaup-operator migration (kafkaup-operator#1): its scaffold Dockerfile hardcodesGOARCH=amd64while the release workflow pushes both amd64 and arm64.What
One new step in
push-single-arch, between push and signing (~30 lines):docker createthe pushed digest for the requested platform (never started — no QEMU needed) anddocker cp -Lout the entrypoint (fallback: cmd) binary.file(1)what it is, and fail if it's an ELF binary for the wrong architecture (x86-64vsaarch64).Only a definitive ELF mismatch fails. Everything else passes: no entrypoint, extraction failure, script entrypoints (common in docker-images family images), and arches other than amd64/arm64 (the two we publish).
Also adds
.github/actions/**to the zizmor lint workflow path filters — composite-action changes (like this one) previously didn't trigger the security audit.Validation
Tested end-to-end locally against real images: arch-mismatched image (amd64 platform, arm64 binary) → fails with
ELF 64-bit LSB executable, ARM aarch64...; matching image → passes; symlinked entrypoint (/app -> /real) → dereferenced, passes; shell-script entrypoint → skips; cmd-only image (no entrypoint) → falls back to cmd, passes.zizmorno findings;shellcheckclean.🤖 Generated with Claude Code