Skip to content

STAC-25595 GH action to scan, sign and push docker images - #24

Merged
LukaszMarchewka merged 6 commits into
mainfrom
STAC-25595-scan-sign-push-action
Aug 20, 2026
Merged

STAC-25595 GH action to scan, sign and push docker images#24
LukaszMarchewka merged 6 commits into
mainfrom
STAC-25595-scan-sign-push-action

Conversation

@LukaszMarchewka

@LukaszMarchewka LukaszMarchewka commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

scan-sign-push — gate, publish, and cosign-sign multi-arch images

Jira: STAC-25595

What

Adds the scan-sign-push composite action. From per-arch inputs (tarballs or already-loaded local images) it scans each arch (secrets fail-closed, CVEs inform-only), pushes the per-arch manifests, assembles a multi-arch manifest list under every tag, then keyless-signs and verifies the assembled digest against the caller's GitHub Actions OIDC identity. The registry never sees bytes that have not passed the secret gate first.

This closes the signing gap for the core product images that ship in the suse-observability chart unsigned because they are published outside image-pipeline.

Key behaviour

  • Two source modes: tarball (load image-<arch>.tar) and local (images already in the daemon, <prefix><arch>).
  • Secret scan blocks; CVE scan informs (SARIF to Code Scanning).
  • Signs the manifest-list digest, so all tags on it — and any later retag — share the signature.
  • fail-on-existing-tags defaults to true (immutable-release safe); branch builds opt out.

Review fixes included

  • Fail fast on unsupported arches — only amd64/arm64 have a scan step; anything else is rejected before load/push/sign, so nothing is published unscanned.
  • Manifest integrity before signing — the list is assembled under a run-unique handle tag; its digest is read from that immutable-per-run ref (not a mutable release tag) and verified to reference exactly the per-arch children we pushed, before tags are attached and the digest is signed.- Per-arch architecture check — config .Architecture must match the declared arch (guards QEMU cross-build mislabelling).
  • fail-on-existing-tags defaults safe (true).
  • sig-handle-* / -list tags cleaned up after signing (best-effort, quay.io).

Validation

  • Smoke test (action-ci.yml, scan-sign-push-smoke): builds two per-arch tarballs, runs the action against a registry:2service, asserts both tags resolve to a single signed manifest list with two children. Triggers now includescan-sign-push/**`.
  • Real consumer (StackVista/stackstate, STAC-25595): backend images (server/receiver/correlate/kafka-to-es) via local mode and the frontend via tarball-loaded local mode produce signed multi-arch images on quay; cosign verify passes against the workflow OIDC identity, and OCI labels match a pre-change build byte-for-byte (only per-build values differ).

Notes for reviewers

  • The nested scan-image is pinned to @6284a6fc… (main); bump when scan-image changes.
  • The arch check is config-level (.Architecture), not entrypoint-binary file extraction — flag if the stricter STAC-25033 check is wanted here.

@LukaszMarchewka
LukaszMarchewka force-pushed the STAC-25595-scan-sign-push-action branch 3 times, most recently from bda5464 to b8894e1 Compare August 19, 2026 09:28

@viliakov viliakov left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reviewed alongside StackVista/stackstate#367.

Blocking

Arches other than amd64/arm64 are pushed unscanned. The gate is two hardcoded steps behind contains(inputs.arches, 'amd64') / 'arm64'. With any other value in arches, no scan step runs, but load → push → assemble → sign all still do — so the README's "the registry never sees bytes that have not passed the secret gate first" no longer holds. Fail fast on an unrecognised arch, or drive the scan from the arch loop.

Regressions vs. the actions this replaces

Manifest-integrity check dropped. merge-multiarch does imagetools create --dry-run, re-fetches the pushed manifest, and compares canonicalised sha256 before signing. Here the signed digest comes from imagetools inspect "$first_ref" — a mutable tag, read after the push. Two runs re-pointing the same moving tag can make you sign a manifest you did not assemble. Worth keeping the byte-for-byte comparison in the one action whose job is signing.

Binary-arch verification dropped. push-single-arch has Verify binary architecture matches linux/<arch> (STAC-25033), which extracts the entrypoint and files it. #367's backend path builds arm64 under QEMU on an amd64 runner — the exact mislabelling case that check was added for.

fail-on-existing-tags defaults to false. merge-multiarch refuses to overwrite unconditionally. Prefer defaulting safe and letting branch builds opt out — #367 does not set it, so immutability rests entirely on the caller's own pre-existing tag check.

Other

  • sig-handle-<run>-<attempt>-<arch> tags are never cleaned up — two permanent quay tags per image per build. Deleting them later is not obviously safe either (manifest-list child GC), so this wants a cleanup step or an explicit retention decision rather than a README note.
  • tarball mode has no caller. #367 loads the tarballs itself and calls source-mode: local, duplicating the docker load | awk logic. Either let the frontend pass source-mode: tarball and drop the loader there, or remove tarball-dir/tarball-prefix here.
  • No test. action-ci.yml only triggers on scan-image/**. The empty-target-registry-user escape hatch is documented for "an unauthenticated local registry in tests", but no such test exists — a registry:2 assemble+sign smoke test would be cheap. (push-single-arch/merge-multiarch are untested too, so this is a gap rather than an inconsistency.)
  • Please add a description with the STAC-25595 link and how this was validated.

@LukaszMarchewka

Copy link
Copy Markdown
Contributor Author

Thanks for the review — all points addressed. Summary:

# Comment Status How
1 Arches other than amd64/arm64 pushed unscanned (blocking) ✅ Fixed Added a Validate arches are scannable step that runs before load/push/sign and fails on any arch without a scan step, so nothing is published unscanned.
2 Manifest-integrity check dropped ✅ Fixed The list is now assembled under a run-unique sig-handle-…-list tag; the digest is read from that immutable-per-run ref (not a moving release tag) and its child digests are verified to equal exactly the per-arch images we pushed, before release tags are attached and the digest is signed.
3 Binary-arch verification dropped ✅ Fixed (config-level) Added a Verify per-arch image architecture step comparing docker image inspect --format '{{.Architecture}}' to the declared arch. See justification below.
4 fail-on-existing-tags defaults to false ✅ Fixed Default flipped to true; branch/PR callers opt out explicitly.
5 sig-handle tags never cleaned up ✅ Fixed Added an always() cleanup step deleting the -list and per-arch handle tags via the quay API. Safe because the release tags still reference the same child digests, so no referenced manifest is GC'd.
6 tarball mode has no caller ✅ Resolved tarball mode is kept — the StackGraph conversion uses source-mode: tarball. The stackstate frontend stays on local mode by design (shared prepare step), so no duplicated loader in the action.
7 No test ✅ Fixed Added a scan-sign-push-smoke job in action-ci.yml: builds two per-arch tarballs, runs the action against a registry:2 service, and asserts both tags resolve to one signed 2-child manifest list. Trigger paths now include scan-sign-push/**.
8 PR description ✅ Added Description updated with the STAC-25595 link and how it was validated.

On #3 — why config-level rather than the full entrypoint file check

I implemented the architecture check as a comparison of the image config.Architecture against the arch we publish it as, rather than extracting theentrypoint binary and running file on it (as push-single-arch does).

Rationale:

  • It catches the failure mode this PR actually introduces: a per-arch image whose manifest/config platform does not match the slot it is assembled into. That is the QEMU-on-amd64 mislabelling case, and it is caught before push/assemble/sign.
  • It has no extra dependencies and works uniformly across all our images, including distroless/scratch-based ones where there is no shell and the entrypoint is not always a single ELF that file can classify cleanly.

Trade-off I'm explicitly flagging: the config check does not catch the rarer case where the config is labelled correctly but an actual binary inside the layers was built for the wrong arch. If we want that stricter guarantee (STAC-25033), I'm happy to add the entrypoint-file extraction on top — it's additive. Let me know and I'll include it.

@viliakov viliakov left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Two blocking items; the rest of my notes are non-blocking and I've left them out.

Comment thread .github/actions/scan-sign-push/action.yml
Comment thread .github/actions/scan-sign-push/action.yml Outdated
@LukaszMarchewka
LukaszMarchewka force-pushed the STAC-25595-scan-sign-push-action branch from 91c66a1 to 451414a Compare August 20, 2026 12:11
@LukaszMarchewka
LukaszMarchewka merged commit 371d0e8 into main Aug 20, 2026
8 checks passed
@LukaszMarchewka
LukaszMarchewka deleted the STAC-25595-scan-sign-push-action branch August 20, 2026 12:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants