Modern software is assembled from hundreds of dependencies and build steps you do not fully control, and a single tampered artifact can cascade into a critical production incident. In this lesson, you will learn to make tampering detectable: sign container images with cosign using keyless Sigstore infrastructure, attach tamper-evident in-toto provenance attestations, and wire it all into a GitHub Actions release pipeline engineered to meet SLSA Level 3 requirements.

1. Learning Objectives

By the end of this lesson, you will be able to:

  • Explain why software supply chain attacks are a top security threat
  • Describe the SLSA framework and what Level 3 requires
  • Sign container images and OCI artifacts with cosign using keyless Sigstore infrastructure
  • Verify signatures and audit them in the Rekor transparency log
  • Generate tamper-evident provenance with in-toto attestations
  • Wire signing and verification gates into a GitHub Actions release pipeline

2. Why This Matters

The software supply chain is everything between your source code and the artifact running in production: dependencies, build tools, CI runners, registries, and distribution channels. Every link is a potential attack surface. The SolarWinds compromise showed how tampering with a trusted build can distribute malware to thousands of organizations, and the xz-utils backdoor showed how a single compromised maintainer account can inject a malicious payload into a library used across the Linux ecosystem. Attackers increasingly do not break into your servers at all — they poison the artifacts you consume.

Traditional defenses scan dependencies for known vulnerabilities, but a CVE scan cannot tell you whether the artifact about to be deployed is exactly the one your developers built and tested. Signing, provenance, and verification answer that question: they make tampering detectable and give your pipeline an auditable chain of custody from commit to production.

3. Core Concepts

The software supply chain attack surface

Think of your pipeline as a chain: source code, build environment, dependencies, packaging, artifact registry, and finally deployment. An attacker can strike anywhere along it — by compromising a source repository, injecting a malicious dependency, hijacking a CI runner, tampering with an artifact in a registry, or intercepting a download. Supply chain security makes every link verifiable.

SLSA: Supply Chain Levels for Software Artifacts

SLSA (pronounced salsa) is a security framework from Google and the OpenSSF that defines incremental trust levels for how software is built and distributed. Higher levels add stronger, harder-to-fake guarantees about where an artifact came from and how it was produced.

SLSA Level 0: No guarantees
SLSA Level 1: Build process is documented and provenance exists
SLSA Level 2: Build runs on a hosted platform; provenance is signed
SLSA Level 3: Provenance is tamper-resistant; builds are hardened
SLSA Level 4: Hermetic, reproducible builds with two-person review
The five SLSA levels at a glance

Sigstore: signing infrastructure for everyone

Sigstore is a Linux Foundation project that makes software signing free, automated, and easy to adopt. Three components work together: cosign, the CLI used to sign and verify artifacts; Fulcio, a certificate authority that issues short-lived certificates bound to your OIDC identity; and Rekor, an append-only transparency log that records every signature so it can be audited publicly. With keyless signing, you never manage long-lived private keys — your CI provider's OIDC token is enough.

# Install cosign (Linux x86_64)
wget -O cosign https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64
chmod +x cosign
sudo mv cosign /usr/local/bin/
cosign version
Installing the cosign CLI

in-toto attestations and SLSA provenance

Signing proves who signed an artifact; attestations prove how it was built. in-toto is a framework for guaranteeing supply chain integrity, and its attestation format is a signed statement about how an artifact was produced. The SLSA provenance predicate records the builder, the build type, the materials (source commit, base images), and the digest of the final artifact. cosign attest wraps this predicate in an in-toto statement and signs it with the same keyless flow.

{
  "_type": "https://in-toto.io/Statement/v0.1",
  "predicateType": "https://slsa.dev/provenance/v0.2",
  "subject": [
    {
      "name": "ghcr.io/org/app",
      "digest": {"sha256": "<image-sha256>"}
    }
  ],
  "predicate": {
    "builder": {"id": "https://github.com/org/app/.github/workflows/release.yml"},
    "buildType": "https://github.com/actions/runner",
    "materials": [
      {"uri": "git+https://github.com/org/app", "digest": {"sha1": "<commit-sha>"}}
    ]
  }
}
A simplified in-toto statement with a SLSA provenance predicate

4. Hands-On Practice: Building a SLSA Level 3 Pipeline

We will build a GitHub Actions release pipeline that builds a container image, signs it keylessly with cosign, attaches a SLSA provenance attestation, and verifies the signature before anything is deployed. Every signature lands in Rekor, giving you a public, tamper-evident audit trail.

Step 1: Enable OIDC and job permissions

Keyless signing requires the id-token permission so the workflow can mint a short-lived GitHub OIDC token. Add it to the job along with packages: write so the workflow can push to GitHub Container Registry (GHCR).

permissions:
  id-token: write   # required for keyless Sigstore signing
  contents: read
  packages: write   # required to push to GHCR
OIDC permissions for keyless signing

Step 2: Build and push the image

Build the image with docker/build-push-action and push it to GHCR. Capture the image digest from the build step — you will sign the digest, not the tag, because tags can be moved but digests are immutable.

jobs:
  release:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
      packages: write

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Log in to GHCR
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push image
        id: build
        uses: docker/build-push-action@v6
        with:
          push: true
          tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
Build and push the image to GHCR, capturing the digest

Step 3: Sign the image keylessly with cosign

Now sign the image. cosign exchanges the GitHub OIDC token for a short-lived Fulcio certificate tied to your workflow identity, signs the digest, and records everything in Rekor. No long-lived secrets, no key management.

- name: Sign image with cosign
  run: |
    cosign sign \
      --yes \
      ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}
Keyless signing with cosign

Step 4: Attach SLSA provenance with in-toto

Generate a provenance predicate for the build and attach it as a signed in-toto attestation. The attestation binds the artifact digest to the builder, the source commit, and the build type — the core evidence SLSA Level 3 demands.

- name: Generate SLSA provenance
  run: |
    cosign attest \
      --predicate predicate.json \
      --type https://slsa.dev/provenance/v0.2 \
      ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}
Attach a signed SLSA provenance attestation

Step 5: Verify before you deploy

Trust is only useful if you enforce it. In the deploy job, verify the signature and its certificate identity before releasing. Pinning the expected OIDC issuer and a regexp for the workflow identity prevents an attacker from substituting their own signed image.

- name: Verify signature before deploy
  run: |
    cosign verify \
      --certificate-identity-regexp '^https://github.com/org/app/.*$' \
      --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
      ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}
Enforce signature verification as a deployment gate
kubectl get pods -w
$ cosign verify \
--certificate-identity-regexp '^https://github.com/org/app/.*$' \
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
ghcr.io/org/app@sha256:8b5c9f2e1a...

Verification for ghcr.io/org/app@sha256:8b5c9f2e1a...
The following checks were performed on each of these signatures:
- The cosign claims were validated
- The signatures were verified against the Fulcio CA
- The code-signing certificate was verified
- The certificate was validated against the OIDC issuer
Expected cosign verify output

Step 6: Map the pipeline to SLSA Level 3

SLSA Level 3 requires the build to run on a hosted platform (GitHub-hosted runners qualify), provenance to be non-falsifiable (the signed attestation in Rekor cannot be altered by the project), and the build to be isolated from the deploy environment. Our pipeline satisfies all three: provenance is generated in the same workflow that builds the artifact, signed keylessly, and recorded in an append-only log. The verification gate in Step 5 is what turns the evidence into an actual security control.

5. Common Errors and Solutions

Error: signing image failed ... keyless, expected a valid OIDC token — the job is missing the id-token: write permission. Add it to both the job and the workflow top-level permissions block.

Error: no matching signatures found / unexpected end of JSON input — you verified a tag instead of a digest, or the image was never signed. Always verify the immutable reference: image@sha256:....

Error: certificate identity does not match expected regexp — the OIDC subject for GitHub Actions includes the repository and workflow path. Match it precisely, e.g. ^https://github.com/org/app/.github/workflows/release.yml$, or use a deliberately scoped regexp like the one in Step 5.

Error: rekor: server returned error / tlog entry not found — transparency log lookups can be temporarily slow or the entry is still indexing. Retry after a few seconds. Only use --insecure-ignore-tlog for local debugging, never in production enforcement.

Error: attestation verification type mismatch — the --type passed to cosign attest must exactly match the --type passed to cosign verify-attestation. Use the full URL form https://slsa.dev/provenance/v0.2 in both.

6. Summary Checklist

  • I can explain the software supply chain attack surface
  • I can describe the SLSA levels and what Level 3 requires
  • I can install cosign and sign an OCI artifact keylessly
  • I can verify a signature against Fulcio and Rekor
  • I can attach an in-toto SLSA provenance attestation
  • I can enforce signature verification before deployment

7. Practice Exercise

Harden one of your existing GitHub Actions workflows with supply chain controls:

  1. Add id-token: write to the release job permissions
  2. Sign the produced artifact or image with cosign sign
  3. Attach provenance with cosign attest using the SLSA provenance predicate type
  4. Search Rekor for your artifact and confirm the entry exists
  5. Add a verification gate before the deploy step that fails the build when verification fails
# Search Rekor for an artifact by digest
rekor-cli search --sha <image-sha256>

# Or query the Rekor API directly
curl -s "https://rekor.sigstore.dev/api/v1/index/retrieve" \
  -d '{"hash": "sha256:<image-sha256>"}' \
  -H "Content-Type: application/json"
Auditing your signatures in the Rekor transparency log

8. Next Steps

Your pipeline now produces signed, attested artifacts with an auditable trail in Rekor — the foundation of a SLSA Level 3 supply chain. The next CI/CD lesson will extend that trust boundary into policy: enforcing with Kyverno and the Sigstore policy controller that only verified, attested images are admitted to your cluster. Meanwhile, the DevOps roadmap now turns to Terraform and IaC, where you will apply the same automation and verification discipline to provisioning infrastructure as code.

Gataya Med

DevOps Engineer & Backend Developer. Sharing insights on cloud, automation, and scalable systems.

Comments (0)

Sarah Chen August 10, 2026

This is exactly what I needed! The initContainer approach solved our migration issues completely. Thanks for the detailed guide!

Reply

Leave a Comment