Signing & attesting the artifact
cosign signatures, SBOMs, and SLSA provenance in the pipeline.
When your Jenkins pipeline finishes building a container image and pushes it to a registry, that image is anonymous. Anyone looking at it later has no way to tell whether your pipeline produced it or whether someone swapped it for a look-alike an hour after you pushed. The tag reads checkout:1.4.2, but a tag is a sticky label, and sticky labels peel off and move. This lesson adds three things that turn an anonymous image into one with a history you can check: a signature that ties the image to your pipeline, a list of everything inside it, and a signed record of how it was built. Then you gate the deploy stage so an image that fails those checks never reaches production.
An unsigned image is a package with no return address
A signature is a wax seal on a letter. Break the seal and the reader knows the contents were altered; see an unbroken seal from a sender they trust and they read on. For a container image, cosign (the tool that signs and verifies images, part of the Sigstore project) does the same job. It ties the image's exact contents to the identity that signed them. Those contents are named by a SHA-256 digest (SHA-256 is a hashing algorithm, and the digest it produces is a fingerprint that changes if a single byte changes). If an attacker rebuilds the image with a backdoor and pushes it under the same tag, the digest changes, the old signature no longer matches, verification fails, and your deploy stage refuses to ship it. With no signature there is nothing to check, and the swapped image sails straight through.
This is why you sign the digest and not the tag. A tag like 1.4.2 can be re-pointed at a different image at any time; the digest sha256:9f2a... names one specific set of bytes and nothing else. Sign that, and you have pinned down exactly what you are vouching for. Every step in this lesson works against the same digest, which is what makes the chain hold together.
Keyless signing: an ID badge, not a house key
Keyless signing skips the part everyone dreads, which is minding a private key. Instead of keeping a house key that you have to hide, rotate, and pray never leaks, keyless signing works like showing your ID badge to a guard who writes you a one-hour visitor pass. Your pipeline proves who it is with an OIDC (OpenID Connect) token, a short-lived signed statement of identity that Jenkins can issue for a build. cosign hands that token to Fulcio, the Sigstore certificate authority (CA), which returns a certificate valid for only a few minutes, long enough to sign one image. The signature and that certificate are recorded in Rekor, a public transparency log, so anyone can later confirm that this identity signed this digest at this time. Nothing long-lived is ever stored on disk.
# $OIDC_TOKEN is a short-lived identity token minted by Jenkins for this build.# In cosign 2.x keyless is the default: no key flag means "go keyless".cosign sign --yes \--identity-token "$OIDC_TOKEN" \registry.example.com/checkout@sha256:9f2a4c8e...b1
Generating ephemeral keys...Retrieving signed certificate...Note that there may be personally identifiable information associated with thissigned artifact. This information is stored in a public transparency log andcannot be removed later.Successfully verified SCT...tlog entry created with index: 74650912Pushing signature to: registry.example.com/checkout
The key-based fallback: a key in the Jenkins safe
Keyless needs your pipeline to mint an OIDC token and to reach the public Sigstore services. On a locked-down or air-gapped network where neither is possible, you fall back to a traditional key pair. You generate one with cosign, keep the public half anywhere you like, and treat the private half like the only key to a safe: it goes into the Jenkins credential store and is decrypted only inside the step that needs it. The withCredentials block loads the key file path and its password into environment variables that exist for that one sh step and are masked in the log, so the secret is present where cosign reads it and nowhere else.
stage('Sign (key-based)') {steps {withCredentials([file(credentialsId: 'cosign-key', variable: 'COSIGN_KEY'), // cosign.key filestring(credentialsId: 'cosign-pass', variable: 'COSIGN_PASSWORD') // decrypts the key]) {// cosign reads COSIGN_PASSWORD from the environment to unlock the key.// Neither the key path nor the password is ever echoed.sh 'cosign sign --yes --key "$COSIGN_KEY" registry.example.com/checkout@sha256:9f2a4c8e...b1'}}}
A bill of materials with syft
An SBOM (Software Bill of Materials) is the ingredients list on the side of a cereal box. It names every package, library, and version baked into your image, so that when a nasty bug lands in some library next month you can answer "is that in anything we shipped?" in seconds instead of guessing. syft is the tool that reads an image and writes that list. Here it emits the list in SPDX (Software Package Data Exchange) format, a widely used standard for SBOMs, into a file you will attach to the image in the next step.
syft registry.example.com/checkout@sha256:9f2a4c8e...b1 \-o spdx-json=sbom.spdx.json
✔ Pulled image✔ Loaded image registry.example.com/checkout@sha256:9f2a4c8e...✔ Parsed image sha256:9f2a4c8e...✔ Cataloged contents c0e6a2f4b8d1e3f5a7c9...├── ✔ Packages [153 packages]├── ✔ Executables [412 executables]├── ✔ File metadata [2,341 locations]└── ✔ File digests [2,341 files]
Attaching the SBOM as a signed attestation
A plain sbom.spdx.json file sitting next to the image proves nothing; anyone could hand you a fake one. An attestation staples the list to the image and seals the staple. cosign attest wraps your SBOM in a signed statement that says "this identity vouches that this SBOM describes this exact digest," stores it alongside the image in the registry, and logs it in Rekor. Swap the image and the attestation no longer matches; edit the SBOM and the signature breaks. The signing works the same as before, keyless with your OIDC token or key-based with your key.
cosign attest --yes \--identity-token "$OIDC_TOKEN" \--predicate sbom.spdx.json \--type spdxjson \registry.example.com/checkout@sha256:9f2a4c8e...b1
Using payload from: sbom.spdx.jsonGenerating ephemeral keys...Retrieving signed certificate...Successfully verified SCT...tlog entry created with index: 74651003
SLSA provenance: the chain-of-custody form
SLSA (Supply-chain Levels for Software Artifacts, said out loud as "salsa") provenance is the chain-of-custody form that ships with a package: which factory, which recipe, which raw materials, which batch. For a build, that means which pipeline built the image, from which git commit, at which build number. You write the facts into a small predicate file (in a real pipeline your Jenkinsfile fills these in from build variables), then sign it onto the image the same way you signed the SBOM. Now a verifier can confirm two things: that the image is yours, and that it came out of your build system rather than off someone's laptop.
{"buildDefinition": {"buildType": "https://jenkins.example.com/pipeline/v1","externalParameters": {"repository": "https://git.example.com/checkout","ref": "refs/heads/main"},"resolvedDependencies": [{ "uri": "git+https://git.example.com/checkout@4b8c9d1","digest": { "gitCommit": "4b8c9d1e2f..." } }]},"runDetails": {"builder": { "id": "https://jenkins.example.com/job/checkout-build" },"metadata": { "invocationId": "build-482" }}}
cosign attest --yes \--identity-token "$OIDC_TOKEN" \--predicate provenance.json \--type slsaprovenance1 \registry.example.com/checkout@sha256:9f2a4c8e...b1
Using payload from: provenance.jsonGenerating ephemeral keys...Retrieving signed certificate...Successfully verified SCT...tlog entry created with index: 74651078
Verify before you deploy, and gate on it
Signing is worthless if nobody checks the signature, so the deploy stage becomes the bouncer at the door. cosign verify pulls the signature and its certificate and confirms two things you spell out: that the signer's identity matches the pipeline you expect (--certificate-identity) and that the certificate came from the issuer you expect (--certificate-oidc-issuer). Those two flags are required for keyless verification in cosign 2.x precisely so you cannot accidentally accept a signature from any random signer; you are naming who is allowed to sign. If you signed with a key instead, you pass --key cosign.pub here. A mismatch, a missing signature, or a tampered image all make cosign exit with a non-zero code.
cosign verify \--certificate-identity "https://jenkins.example.com/job/checkout-build/" \--certificate-oidc-issuer "https://jenkins.example.com/oidc" \registry.example.com/checkout@sha256:9f2a4c8e...b1
Verification for registry.example.com/checkout@sha256:9f2a4c8e...b1 --The following checks were performed on each of these signatures:- The cosign claims were validated- Existence of the claims in the transparency log was verified offline- The code-signing certificate was verified using trusted certificate authority certificates[{"critical":{"identity":{"docker-reference":"registry.example.com/checkout"},"image":{"docker-manifest-digest":"sha256:9f2a4c8e...b1"},"type":"cosign container image signature"},"optional":{"Issuer":"https://jenkins.example.com/oidc","Subject":"https://jenkins.example.com/job/checkout-build/"}}]
stage('Verify & deploy') {steps {// cosign exits non-zero on any failure; a failed sh step aborts the stage,// so deploy.sh below is reached only when verification passed.sh '''cosign verify \--certificate-identity "https://jenkins.example.com/job/checkout-build/" \--certificate-oidc-issuer "https://jenkins.example.com/oidc" \registry.example.com/checkout@sha256:9f2a4c8e...b1'''sh './deploy.sh registry.example.com/checkout@sha256:9f2a4c8e...b1'}}
That ordering is the whole point of the gate. Because a failing sh step stops the stage, deploy.sh never runs unless cosign printed its green checks first. An image with no signature, a signature from the wrong identity, or a digest that does not match what was signed cannot slip past into production; the pipeline goes red and stops. Verify by digest here too, the same digest you signed, so the thing you check is the exact thing you ship.