Tools · Cosign & Sigstore

Cosign & Sigstore

Every image reference in every manifest you have ever written is a promise nobody is enforcing: ghcr.io/mission-control/flight-planner:1.4.0 is a mutable pointer, and by default your cluster will run whatever bytes currently sit behind it without asking who put them there. Sigstore closes that gap without asking you to own a signing key. cosign is the CLI you actually run; Fulcio hands out a certificate that lives for about ten minutes, tied to a real identity from an OIDC login rather than a password only you know; Rekor writes every signing event into a public log nobody can quietly edit afterward. This page is the depth behind the Kyverno page's verifyImages section: the architecture, the resources you write in CI and at admission, the commands, and the gotchas that turn a "Verified OK" into false confidence.

☺ Explain it like I'm 10

Before any cargo leaves Mission Control's supply depot for a launch, it gets sealed in a crate stamped with a badge. Nobody keeps a permanent stamp lying around — a technician checks in at the depot gate, borrows a stamp for ten minutes, seals the crate with their name on it, and hands the stamp straight back before it can be stolen or copied. Every single stamping gets written into a logbook chained to the last page, so nobody can tear a page out later without it being obvious. When the crate reaches the launch pad, the loading crew doesn't just check that a seal exists — anyone can press a seal-shaped stamp. They check the logbook entry and confirm the name on it is someone actually cleared to pack that crate. cosign does the sealing, Fulcio lends the ten-minute stamp, Rekor is the logbook — and Rocky is the inspector at the loading dock who always checks the name, because Rocky's the one who'd know exactly how to fake just the seal.

🦝Your host for this topic: Rocky the Raccoon — the red-teamer. Rocky's whole instinct is to ask how an attacker slips past a check that merely "passes" — which is exactly the gap between a signature that verifies and one that's actually pinned to somebody you trust.

What it is and the problem it solves

☺ Like you're 10: A borrowed stamp instead of a stolen key, and a logbook nobody can quietly edit.

Sigstore is a Linux Foundation project developed under the OpenSSF umbrella — it is not a CNCF project and carries no exam tile of its own on this course's nine-certification shelf, but it sits directly underneath one: the verifyImages rule type that makes up a real slice of KCA's heaviest domain. It is three cooperating pieces. cosign is the client you run to sign and verify. Fulcio is a certificate authority that issues X.509 certificates valid for roughly ten minutes, each one bound to an OIDC identity rather than to a key you generated yourself. Rekor is an append-only, cryptographically verifiable transparency log recording every signing event, so a signature cannot be minted quietly — it leaves a permanent, publicly auditable trace.

The problem underneath all of it: a tag is a mutable pointer, not a fact. A compromised CI token, a typosquatted base image, or a dependency that goes bad mid-build can all put different bytes behind flight-planner:1.4.0, and nothing in Kubernetes will notice. Pinning to a digest (@sha256:...) fixes mutability — the bytes cannot change out from under a fixed reference — but says nothing about provenance: an unchanged image is not the same claim as a legitimate one. Supply-chain attacks live in exactly that gap.

◆ Key idea

The classic fix — a long-lived signing key — trades one problem for a worse one: the key ends up in a CI secret, never rotates, and becomes the single object an attacker most wants, because whoever holds it can sign anything, forever, and every verifier will believe them. Sigstore's answer is to make the private key too short-lived to be worth stealing. cosign generates a keypair in memory, trades an OIDC token for a Fulcio certificate, signs, publishes to Rekor and the registry, and throws the private key away before the job even finishes. There is nothing left at rest to leak.

What you get at the end is three separable claims, and it's worth being able to name them apart. Integrity — these exact bytes, addressed by digest. Identity — signed by this workload's OIDC identity, vouched for by this issuer. Transparency — the event sits in a log anyone can audit. Attach an attestation on top and you get a fourth: provenance — a machine-readable statement of how the artifact was built and what's inside it.

Where it fits in the Golden Astronaut roadmap

☺ Like you're 10: Sealing happens back at the depot; checking the seal happens at the launch pad door — and they're different jobs, done by different people, at different times.

Signing belongs to the delivery plane — a step in CI, right after an image is built and pushed, alongside Argo Workflows or a GitHub Actions pipeline. Verification belongs to the governance plane — it happens at admission, inside the cluster, on every pod, and it is Kyverno's job to do it. Do only the first half and you have signatures nobody checks — a compliance decoration. Do only the second and nothing ever deploys, because nothing was ever signed to begin with.

The policy that enforces this belongs in Git like everything else this course treats as a guardrail: a ClusterPolicy reconciled by Argo CD or Flux, the same discipline the CGOA track builds around, so nobody can quietly weaken the signature requirement by hand against a live cluster. And because Sigstore sits underneath a real KCA competency without being a certification in its own right, it is exactly the kind of tool this course covers in depth even though it never appears on an exam blueprint by name — see Policy-as-Code Philosophy for the wider argument about what belongs at the cluster door at all. If you're also working the CNPA/CNPE track, Platform Engineering's cosign page covers the same tool with that exam's Security & Policy domain in mind — the mechanics below are identical either way.

How it works — the keyless flow

☺ Like you're 10: Borrow the stamp, seal the crate, write it in the logbook, hand the stamp back — then, on launch day, check the name in the logbook, not just that a seal exists.

Six steps, worth narrating from memory, because every gotcha later in this page is one of these steps failing quietly.

CI workload cosign sign --yes Fulcio (CA) ~10-minute cert bound to OIDC identity Rekor (log) append-only entry inclusion proof Registry image + .sig + .att private key discarded OIDC token certificate record push sig Pod create API server 🐢 Kyverno verifyImages rule mutating phase full architecture: see Kyverno the check that matters signature valid AND identity == subject AND issuer == pinned OIDC issuer inclusion proof fetch signature Signing proves nothing on its own. Verification pinned to an identity is the control.

Keyed vs keyless

cosign supports both, and knowing when each is right matters more than memorizing flags.

DimensionKeyed (--key)Keyless (cosign v2 default)
Private keyLong-lived file, KMS, or a Kubernetes SecretGenerated in memory, discarded after signing
Signer identityWhoever holds the key — unknowable from the signature aloneAn OIDC subject: a workflow, service account, or human login
Verification needs--key cosign.pub--certificate-identity + --certificate-oidc-issuer, both mandatory
Blast radius if compromisedEverything, forever, until the key is rotatedTen minutes, one identity, and it is in a public log
Best fitAir-gapped estates, a hardware-rooted release key a regulator wants namedCI-built artifacts — nearly everything a platform team ships

A key can also live outside a flat file: cosign understands awskms://, gcpkms://, azurekms://, hashivault:// and k8s://namespace/secret references, strictly better than a key on disk when keyed signing is genuinely the right call.

Where signatures actually live, and attestations

A cosign signature is not embedded in the image manifest — it is a separate OCI artifact pushed to the same repository, under a tag derived from the digest. For sha256:abc123..., the signature lands as sha256-abc123....sig, attestations as ....att. Two consequences follow immediately: a registry garbage-collection policy that targets "untagged manifests" will happily delete signatures while leaving images behind, and a plain docker pull / docker push between registries leaves signatures behind entirely — cosign copy is what brings them along.

A signature says "I vouch for these bytes." An attestation says something richer: a DSSE-wrapped in-toto Statement carrying a subject (name plus digest), a predicateType, and a predicate payload. The two predicates that matter in practice are an SBOM — CycloneDX or SPDX, usually generated by Trivy or Syft — and SLSA provenance, which records the builder identity, source commit, and build parameters. SLSA grades that provenance in build levels: L1 means provenance merely exists; L2 means a hosted build service signed it; L3 means the build ran on hardened, isolated infrastructure resistant to forgery by the build itself — signing your own provenance from a shared CI runner is roughly L2, and getting to L3 is an infrastructure decision, not a cosign flag.

The resources you actually write

☺ Like you're 10: One pipeline step that seals the crate, and one policy that checks the name on the seal before the crate reaches the pad.

Signing in CI

Keyless means the pipeline needs no secret to sign with. In GitHub Actions the load-bearing line is permissions: id-token: write, which lets the job mint an OIDC token cosign finds automatically. Always sign the digest, never the tag — signing :1.4.0 signs whatever that tag resolves to at that exact instant, which is the same ambiguity the whole exercise exists to remove.

name: build-and-sign
on: { push: { branches: [main] } }

permissions:
  contents: read
  packages: write
  id-token: write            # REQUIRED — mints the OIDC token Fulcio trusts

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: sigstore/cosign-installer@v3
      - uses: docker/login-action@v3
        with: { registry: ghcr.io, username: ${{ github.actor }}, password: ${{ secrets.GITHUB_TOKEN }} }

      - name: Build, push, capture the digest
        id: push
        run: |
          IMAGE=ghcr.io/mission-control/flight-planner
          docker build -t "$IMAGE:${GITHUB_SHA}" .
          docker push "$IMAGE:${GITHUB_SHA}"
          DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' "$IMAGE:${GITHUB_SHA}")
          echo "digest=$DIGEST" >> "$GITHUB_OUTPUT"

      - name: Sign the image (keyless)
        run: cosign sign --yes "${{ steps.push.outputs.digest }}"

      - name: Generate and attest an SBOM
        run: |
          trivy image --format cyclonedx --output sbom.json "${{ steps.push.outputs.digest }}"
          cosign attest --yes --predicate sbom.json --type cyclonedx "${{ steps.push.outputs.digest }}"
--yes is not decoration

Without --yes, cosign prompts interactively to confirm you understand the signing event will be published to a public transparency log — in CI that prompt is an unexplained hang until the job times out. It is also a real decision, not a formality: for a private repository, the Rekor entry publicly records the digest, the repository path in the certificate identity, and the timestamp. Usually acceptable; decide it deliberately rather than by accident.

Enforcing it at admission — the Kyverno side

Signing without enforcement is a checkbox, not a control. This is the exact rule Kyverno's page introduces and cross-links back here — read this one for what each field actually buys you.

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-signed-images
spec:
  webhookTimeoutSeconds: 30              # registry + Rekor round-trips are not instant
  rules:
    - name: verify-keyless-signature
      match:
        any:
          - resources: { kinds: [Pod] }
      verifyImages:
        - imageReferences: ["ghcr.io/mission-control/*"]   # ours only — public bases can't be pinned this way
          failureAction: Enforce
          required: true
          mutateDigest: true              # rewrite tag -> digest once verified, closing the re-tag race
          attestors:
            - count: 1
              entries:
                - keyless:
                    # BOTH lines below are load-bearing — omit either and the rule is theatre
                    subject: "https://github.com/mission-control/*/.github/workflows/build-and-sign.yml@refs/heads/main"
                    issuer: "https://token.actions.githubusercontent.com"
                    rekor: { url: https://rekor.sigstore.dev }
          attestations:
            - predicateType: cyclonedx
              attestors:
                - count: 1
                  entries: [{ keyless: { subject: "https://github.com/mission-control/*/.github/workflows/build-and-sign.yml@refs/heads/main", issuer: "https://token.actions.githubusercontent.com" } }]

Two things worth carrying forward from the Kyverno page: mutateDigest: true is why verifyImages runs in the mutating phase rather than the validating one, and the default webhookTimeoutSeconds is tighter than a slow registry can comfortably meet — raise it deliberately on any policy carrying this rule, and read Kyverno's full breakdown of the four controllers before assuming a timeout is a broken signature.

Day-to-day commands

☺ Like you're 10: Four verbs — seal it, check the seal, write a note about what's inside, and check the note. Everything else is plumbing.

# --- signing ---
cosign sign --yes ghcr.io/mission-control/flight-planner@sha256:abc123...
cosign sign --key cosign.key ghcr.io/mission-control/flight-planner@sha256:abc...   # keyed, offline verification
cosign sign-blob --yes --bundle release.tgz.bundle release.tgz                      # sign a plain file, not an image

# --- verifying — BOTH pins are mandatory in cosign v2 ---
cosign verify \
  --certificate-identity "https://github.com/mission-control/flight-planner/.github/workflows/build-and-sign.yml@refs/heads/main" \
  --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
  ghcr.io/mission-control/flight-planner@sha256:abc123... | jq .

# regex variant — "any workflow in this org, on main"
cosign verify \
  --certificate-identity-regexp '^https://github\.com/mission-control/.+@refs/heads/main$' \
  --certificate-oidc-issuer-regexp '^https://token\.actions\.githubusercontent\.com$' \
  ghcr.io/mission-control/flight-planner@sha256:abc123...

# --- where does it actually live? ---
cosign triangulate ghcr.io/mission-control/flight-planner@sha256:abc123...   # prints the .sig tag
cosign tree ghcr.io/mission-control/flight-planner@sha256:abc123...          # sigs + attestations, in one view
cosign copy ghcr.io/mission-control/flight-planner:1.4.0 registry.internal/mc/flight-planner:1.4.0  # brings sigs along

# --- attestations ---
cosign attest --yes --predicate sbom.json --type cyclonedx IMAGE@sha256:...
cosign attest --yes --predicate prov.json --type slsaprovenance IMAGE@sha256:...
cosign verify-attestation --type cyclonedx \
  --certificate-identity-regexp '^https://github\.com/mission-control/' \
  --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
  IMAGE@sha256:... | jq -r '.payload' | base64 -d | jq '.predicate'
🦝 Rocky's workshop · 20 min

Push any small image to a registry you control. Sign it keyless with cosign sign --yes — you'll be sent to a browser to authenticate, which is the OIDC exchange. Run cosign triangulate, then look in your registry UI and physically find the sha256-....sig tag sitting beside your image. Verify correctly with your own email as --certificate-identity. Then do the dangerous version: verify with an identity regexp of .* and watch it still pass — anyone on earth could have signed that image. Finally, apply require-signed-images above against your own registry pattern and try to run an unsigned image through Kyverno. The rejection is the whole point of the exercise.

Gotchas and failure modes

☺ Like you're 10: Checking that a seal exists is not the same as checking whose name is on it.

An unpinned verify proves almost nothing

Fulcio issues a certificate to anyone who can authenticate with any supported OIDC provider — that is the design, a public good rather than a gate. A signature that merely "verifies" with no --certificate-identity only proves that somebody with an email address signed those bytes. An attacker who compromises the registry can push a malicious image, sign it with their own throwaway identity, and sail through an unpinned check. Be equally careful with regexes: an unanchored mission-control also matches mission-control-evil-corp — always anchor with ^ and $ and escape the dots.

⚠ The flags that quietly disable the control

--insecure-ignore-tlog skips the transparency-log check, --insecure-ignore-sct skips the certificate-transparency check, and --allow-insecure-registry drops TLS to the registry. Each is legitimate in a narrow, genuinely air-gapped scenario, and each shows up in forum answers as a fix for what was actually a network hiccup. If one of these is sitting in a production pipeline as a workaround, the signing story has a hole in it — treat them like --insecure anywhere else: never permanent, always commented.

Signatures are separate artifacts, and they get lost

Three practical bites follow from the tag scheme. Registry garbage collection or retention rules keyed on "untagged manifests" will delete signatures while leaving images behind, and admission starts rejecting things that worked yesterday. Promotion between registries with a plain pull-and-push silently drops signatures — use cosign copy. And rebuilding instead of promoting defeats the whole model: if staging and production build separately, the digest differs, the old signature doesn't apply, and nothing about what actually runs has been verified.

Reachability turns into a cluster-wide pod-creation failure

Keyless verification is a network operation — the verifier needs the registry, a cached Sigstore trust root, and Rekor for the inclusion proof. Put that inside an admission webhook and a slow Rekor becomes pods failing to schedule cluster-wide, with symptoms that look like a broken policy rather than a slow dependency. Mitigations: raise webhookTimeoutSeconds, scope imageReferences narrowly instead of trying to verify public base images you can't pin anyway, and for genuinely disconnected estates, sign with the transparency log off (--tlog-upload=false) or run a private Sigstore deployment via cosign initialize --mirror.

Identity churn breaks a policy that used to pass

The certificate subject bakes in the workflow file's path and the ref it ran on. Rename the workflow, move the repository, or switch a trigger from refs/heads/main to a tag push, and every pinned policy stops matching — a perfectly legitimate image fails verification, usually noticed first as pods refusing to schedule. Treat identity strings as an interface with a change process, not a fact you write once and forget. And remember: signed does not mean safe — a faithfully signed image full of critical CVEs verifies perfectly, which is why cosign and Trivy are complements, never substitutes for each other.

Alternatives and when to choose it

☺ Like you're 10: Other people also invented seals. Sigstore's is the one that doesn't need you to guard a stamp between uses.

DimensionSigstore / cosignNotation (CNCF Notary Project)GPG detached signatures
Key modelEphemeral, identity-based (or keyed)X.509 certs you provision and manageLong-lived personal keys
Transparency logYes — RekorNo (optional, external)No
Attestations / SBOMYes — in-toto DSSEYes, via OCI referrersNot natively
Kubernetes admissionKyverno verifyImages, or the Sigstore Policy ControllerRatify admission pluginNone built for this
Ecosystem momentumVery high — default in npm, PyPI, Kubernetes release signingModerate, enterprise/CNCF-alignedLegacy, still common for release tarballs

Choose keyless cosign for anything built by CI — nearly everything a platform team ships — because it removes the key-custody problem outright and its verifiers already exist everywhere. Choose keyed cosign with a KMS for disconnected environments or a regulator who wants a hardware-rooted release key with a documented custody chain. Look at Notation when the organisation already runs a PKI it must reuse. Don't let the comparison stall you — one correctly pinned verifyImages rule in production beats a perfect design of nothing.

🦆 Dot's-eye view

"Honestly, I didn't notice this ship. My pipeline grew two steps, I have no key to babysit, and my day didn't change. The one time it did matter, someone pushed a hand-built image straight from a laptop to the shared registry to 'just test something,' and the cluster refused it with a message naming the exact policy. Ten seconds of confusion instead of a week of forensics."

🎬 At Mission Control
🦊

Foxy: Good news — every image is signed now. cosign says Verified OK on all of them. Supply chain: solved.

🦝

Rocky: Show me the verify command first — that's the part I never trust until I've tried to break it myself. …There it is. No --certificate-identity. Give me ten minutes and a throwaway GitHub account and I'll sign a bad image that sails right through this exact check.

🦊

Foxy: But it printed Verified!

🦝

Rocky: It verified the maths. It never verified the signer — and that gap is always the first thing I go looking for. Right now, anyone on Earth with an OIDC login clears this check.

👺

Gizmo: While you're in there, just add --insecure-ignore-tlog too — Rekor was slow this morning and it cost my build nine whole seconds. 🤑

🐢

Timmy: That flag deletes the transparency log from the trust model, Gizmo — nine seconds isn't a price, it's the receipt. Rocky just showed you exactly why we need it: pin the identity and the issuer, wire it into the ClusterPolicy, failureAction: Enforce.

🤖

Recon the Robot: And that policy fix goes through the same pull request I reconcile everything else through. Nobody hand-patches a ClusterPolicy against a live cluster on my watch.

Exam relevance and going further

☺ Like you're 10: You can't open Sigstore's website during a closed-book exam — the shape of the flow has to already be in your head.

Sigstore isn't one of this course's nine project certifications, but it sits directly under the domain that carries the most weight on KCA: verifyImages is one of the four rule types inside Writing Policies, and the KCA is a closed-book, multiple-choice knowledge exam — there is no terminal, no docs lookup, and no partial credit for "I'd look that up." That makes three things worth having cold: the six-step keyless flow with Fulcio and Rekor named correctly, the fact that an unpinned cosign verify proves almost nothing, and the shape of a verifyImages block with attestors[].entries[].keyless{subject,issuer}. Drill the full domain table and logistics on the KCA blueprint — this page is the depth behind that one line item. If GitOps delivery of the enforcing policy itself is the gap, CGOA is the adjacent shelf.

🐢 Timmy's checkpoint

1. Narrate the keyless signing flow, naming Fulcio and Rekor and saying what happens to the private key. 2. A colleague runs cosign verify IMAGE, it prints Verified OK, and they call the image trusted. What have they actually proven, and what's missing? 3. Where does a cosign signature actually live, and name two operational consequences of that. 4. What's the difference between a signature and an attestation, and give two predicate types you'd attach in a real pipeline. 5. In the Kyverno rule above, which field rewrites the tag to a digest once verification succeeds, and which phase does that put verifyImages in? 6. Why sign the digest rather than the tag? 7. Roughly what KCA domain does verifyImages fall under, and why does that make Sigstore worth knowing even though it isn't its own exam?

Check your answers
  1. cosign generates an ephemeral keypair in memory, presents an OIDC identity token to Fulcio, which returns a certificate valid for roughly ten minutes binding that key to the identity; cosign signs the image digest, uploads the signature and certificate to Rekor's transparency log, pushes the signature to the registry, and discards the private key. Nothing signable is left at rest afterward.
  2. Only that someone holding a valid Fulcio certificate signed those exact bytes — Fulcio issues certificates to anyone who can authenticate with any supported OIDC provider. Missing: --certificate-identity (or its regexp variant) and --certificate-oidc-issuer, which pin who signed it. Without both, an attacker who can push to the registry can sign with their own identity and pass.
  3. As a separate OCI artifact in the same repository, under a derived tag sha256-<digest>.sig (attestations use .att). Consequences: registry garbage collection can delete signatures while keeping images, and a plain pull/push between registries drops the signature — cosign copy is what preserves it.
  4. A signature asserts only "I vouch for these bytes." An attestation is a signed, structured statement about the artifact — an in-toto Statement in a DSSE envelope with a subject, predicateType, and predicate. Common predicates: a CycloneDX or SPDX SBOM and SLSA provenance.
  5. mutateDigest: true. That's why verifyImages runs in the mutating phase rather than the validating one — only a mutating webhook is allowed to rewrite the object on its way through.
  6. Because a tag is a mutable pointer — signing :1.4.0 signs whatever it resolves to at that instant, and the tag can be re-pointed afterward to different bytes your signature appears to cover. A digest is the content itself, so the signature is unambiguous.
  7. Writing Policies, the domain that carries the single heaviest weight on the exam — verifyImages is one of its four rule types. Sigstore is worth knowing cold precisely because the exam tests the policy field, and the policy field only makes sense once you understand what it's actually checking.