ADR-0005: Distroless image, SLSA L3 provenance, cosign keyless signatures
- Status: accepted
- Date: 2026-05-04 (Track A)
- Tags: supply-chain, container, security, slsa
Context
The previous container image was debian:bookworm-slim plus curl for the HEALTHCHECK. That gave us:
- A shell, package manager, and ~50 SUID-capable binaries we never use.
- A larger image (~80 MB) with no signature, no provenance, no SBOM.
- A health probe inside the image that pulled in
curl(~7 MB) for one HTTP call.
Compliance review (SOC 2, FedRAMP-readiness) flagged: no provenance, no signature, no SBOM, mutable tags.
Decision
- Runtime image:
gcr.io/distroless/cc-debian12:nonroot. UID 65532, no shell, no apt, no SUID. The C runtime + ca-certificates + tzdata are the only userland. - HEALTHCHECK:
NONE. Probes live in the orchestrator (Helm readiness/liveness on/healthz//readyz). Pullingcurlback into the image to satisfy a HEALTHCHECK was rejected — it would undo every other property we just won. - Build provenance: SLSA v1.0 attestation via
actions/attest-build-provenance@v2on every binary, every container digest, theSHA256SUMSfile, and the SBOM. Signed by the workflow's OIDC identity through Sigstore; the signature is recorded in the public Rekor transparency log. - Image signing:
cosign signkeyless. The Fulcio cert binds the signature to the GitHub Actions workflow identity (https://github.com/fabriziosalmi/zion/.github/workflows/release.yml@refs/tags/v…). No long-lived signing keys exist on the maintainer's machine. - SBOM: CycloneDX 1.5 generated by
cargo cyclonedx, attached as a release asset and as acosign verify-attestation --type cyclonedxattestation bound to the image digest. - Multi-arch:
linux/amd64+linux/arm64via Docker Buildx; the release pipeline cross-compiles 7 binary targets viacargo-zigbuild. - Reproducible-ish:
SOURCE_DATE_EPOCHflowed through the builder;cargo build --lockedeverywhere;tar --sort=name --mtime=@$SOURCE_DATE_EPOCH.
Consequences
- Positive: image attack surface dropped from a Debian userland to the C runtime. No shell ⇒ no
sh -c "evil"injection target if a Rust panic ever decided to spawn one. - Positive: every consumer can verify the chain end-to-end:
gh attestation verifyfor binaries,cosign verify+cosign verify-attestationfor the image. Recipes in docs/security/supply-chain.md. - Positive: Kubernetes admission can enforce signature presence via policy-controller using the
certificate-identity-regexpwe publish. - Negative: no in-image debug shell. Operators dropping into a pod for live forensics need an
ephemeral-containerwith their own toolset. - Negative: no in-image HTTP probe. The Helm chart now owns probe semantics; deployments not using the chart must wire their own.
- Negative: distroless ships a glibc-based runtime. Truly static binaries (musl) ship as separate release artifacts; an even smaller
distroless/staticvariant of the image is a future enhancement.
Alternatives considered
debian:slim+ cosign — minimal supply-chain win, all the bloat. Rejected.alpine+ musl — smaller, but musl's allocator behaviour under high concurrency has historically diverged from glibc's; we ship a musl release artifact for static-binary use cases but not as the default container userland.scratch+ fully static binary — the cleanest, but requires baking ca-certificates by hand and giving uptzdata. Considered for a future "ultra-lean" image; the SLSA / cosign properties remain the same.