Skip to content

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

  1. 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.
  2. HEALTHCHECK: NONE. Probes live in the orchestrator (Helm readiness/liveness on /healthz / /readyz). Pulling curl back into the image to satisfy a HEALTHCHECK was rejected — it would undo every other property we just won.
  3. Build provenance: SLSA v1.0 attestation via actions/attest-build-provenance@v2 on every binary, every container digest, the SHA256SUMS file, and the SBOM. Signed by the workflow's OIDC identity through Sigstore; the signature is recorded in the public Rekor transparency log.
  4. Image signing: cosign sign keyless. 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.
  5. SBOM: CycloneDX 1.5 generated by cargo cyclonedx, attached as a release asset and as a cosign verify-attestation --type cyclonedx attestation bound to the image digest.
  6. Multi-arch: linux/amd64 + linux/arm64 via Docker Buildx; the release pipeline cross-compiles 7 binary targets via cargo-zigbuild.
  7. Reproducible-ish: SOURCE_DATE_EPOCH flowed through the builder; cargo build --locked everywhere; 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 verify for binaries, cosign verify + cosign verify-attestation for the image. Recipes in docs/security/supply-chain.md.
  • Positive: Kubernetes admission can enforce signature presence via policy-controller using the certificate-identity-regexp we publish.
  • Negative: no in-image debug shell. Operators dropping into a pod for live forensics need an ephemeral-container with 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/static variant 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 up tzdata. Considered for a future "ultra-lean" image; the SLSA / cosign properties remain the same.

References

Released under the MIT License.