kodebeat / papers / security

Enforcing image signatures without owning the cluster

An init container that blocks unsigned images at pod start.

Verifies image signatures with cosign before the main workload starts, so a signature that does not verify means the pod never runs. Key-based or keyless, configured entirely through environment variables, and adoptable one workload at a time with no webhook and no platform-team ticket. The repository is candid about what this buys: an init container is a speed bump, not a boundary.

The problem

An organisation signs its images. The release pipeline runs cosign sign, the signatures are in the registry, and the supply-chain slide in the security review has a green tick.

Nothing verifies them when the pod starts. The signature proves the image came from the pipeline to anyone who checks, and by default nobody does. The kubelet pulls whatever tag or digest the manifest names, and starts it. An image pushed to the registry by any path other than the pipeline runs identically to one that came from it.

The gap between signing and enforcing is wide and specific:

  • CI-time verification is not runtime verification. A pipeline step that verifies before deploying proves something about the pipeline's copy. The cluster pulls from the registry independently, later, and possibly a different artifact if a mutable tag moved in between.
  • Tags are mutable. app:1.4.2 verified on Tuesday and app:1.4.2 pulled on Thursday need not be the same image.
  • Not every deployment path goes through CI. The emergency fix applied by hand, the manifest edited in a hurry, the operator that reconciles from a different source.

The method

Use the pod's own lifecycle as the enforcement point. Kubernetes already guarantees that init containers complete successfully before app containers start. That guarantee is free, needs no installation, and is available to anyone who can write a pod spec:

initContainers:
  - name: verify
    image: fabiocicerchia/cosign-verify-init
    env:
      - { name: VERIFY_IMAGES, value: "registry.example.com/my-app:1.4.2" }
    volumeMounts:
      - { name: cosign-pub, mountPath: /keys, readOnly: true }

Support keyless verification properly. Key-based verification requires distributing and rotating a public key. Keyless binds the signature to an OIDC identity (a specific workflow, in a specific repository, at the ref that ran), which is a stronger and more operable statement:

env:
  - name: VERIFY_IMAGES
    value: "ghcr.io/org/app:1.2.3"
  - name: KEYLESS
    value: "true"
  - name: CERT_IDENTITY
    value: "https://github.com/org/app/.github/workflows/release.yml@.*"
  - name: CERT_OIDC_ISSUER
    value: "https://token.actions.githubusercontent.com"

Pinning CERT_IDENTITY is the substance. A signature that is merely valid only says somebody signed it; a signature bound to this workflow in this repository says who.

Configure through environment variables only. There is no config file or CRD. The verification policy for a workload lives in that workload's own manifest, next to the image it protects, reviewed in the same pull request.

State the limitation where it will be read. The README's caveat, which the rest of this paper refers back to:

An init container is a speed bump, not a boundary: it can't stop kubelet from pulling the unverified image, and a compromised namespace admin can remove it. For hard enforcement use a policy controller; use this where installing one isn't an option or as defense-in-depth.

The claim is that this stops an unverified image from running, and that anyone who can edit the pod spec can delete the init container. One nuance the README's wording misses: the kubelet pulls an app container's image only after the init containers have succeeded, so the residual gap is not the pull but the reference. A tag can move between verification and start.

The headline results

  • Verification is cosign's
  • The enforcement mechanism is Kubernetes' own guarantee
  • The test suite is one smoke test, and it covers the negative path.
  • Keyless identity pinning is the strong configuration.

The full results, with the tables and the reasoning behind each figure, are in the PDF.

What is in the PDF

  • The problem — what goes unanswered without it, and who notices first.
  • Why the obvious alternative falls short — stated plainly, including where it is the better choice.
  • How it works — the method, not a feature list.
  • Concrete use cases — with console output quoted from the repository, never reconstructed.
  • The methodology behind any number it emits — every term shown, so the figure survives a question.
  • What it deliberately does not do — the section most papers leave out.

Part of the Security & supply chain theme.

Get the PDF

This paper already selected; the download starts when you send the form. No follow-up sequence.

Get the PDF