Verifying Slsa Provenance For Oci Artifacts With Cosign And Rekor In Github Actions
Written by
Vera Crypt
The problem I kept hitting in software supply chain security
I got tired of “trust me” release pipelines. Even when I used code signing and pinned dependencies, I still had an uncomfortable question:
What exactly did the binary in my container registry come from, and can I prove it cryptographically?
So I went digging into SLSA provenance (SLSA = Supply-chain Levels for Software Artifacts, a framework for describing how an artifact was built). In particular, I focused on a very specific failure mode:
The build system signs the container image, but the provenance isn’t actually verified (or it’s verified against the wrong policy), so an attacker can swap artifacts.
To make this tangible, I built a small pipeline that:
- Builds an app into an OCI image (Open Container Initiative format—what tools like Docker use).
- Generates SLSA provenance during the build (using
slsa-github-generator). - Signs the image with Sigstore Cosign.
- Verifies both the signature and the provenance inside a GitHub Actions deployment workflow using Rekor (Rekor is Sigstore’s transparency log, letting verification check that the signed artifacts were logged).
Below is the working setup I ended up with.
What I built: a tiny OCI image with verifiable SLSA provenance
Repository structure
. ├─ app/ │ ├─ main.py ├─ Dockerfile ├─ .github/ │ ├─ workflows/ │ ├─ build.yml │ └─ deploy.yml ├─ policy/ │ └─ provenance-policy.json └─ README.md
A super small app
app/main.py:
print("hello from supply-chain")
Dockerfile
FROM python:3.12-slim WORKDIR /app COPY app/main.py /app/main.py CMD ["python", "/app/main.py"]
When this image runs, it prints a fixed string—simple enough that any mismatch stands out immediately.
Build workflow: generate SLSA provenance + sign the image + upload to a registry
I used two Sigstore components:
- Cosign to sign and verify signatures.
- slsa-github-generator to emit SLSA provenance for GitHub Actions builds.
Build workflow: .github/workflows/build.yml
name: build on: push: branches: [ "main" ] permissions: contents: read id-token: write # required for OIDC signing/provenance generation attestations: write env: REGISTRY: ghcr.io IMAGE_NAME: supply-chain-provenance-demo/app jobs: build_and_sign: runs-on: ubuntu-latest 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: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push image (OCI) id: build uses: docker/build-push-action@v6 with: context: . push: true tags: | ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest # --- SLSA provenance generation --- - name: Generate SLSA provenance (GitHub Actions) id: attest uses: slsa-framework/slsa-github-generator/actions/attest@v1 with: # The action produces an attestation for the pushed image digest. # It uses OIDC to create a signed provenance statement. subject-path: | ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} # If your registry supports digest lookup, the action will resolve digests. # For simplicity, we use the tag for subject-path and the verification policy # matches via the image digest or ref depending on config. format: 'slsa-provenance/v1' # --- Sign the image digest with Cosign --- - name: Install Cosign uses: sigstore/cosign-installer@v3 with: cosign-release: 'v2.2.4' - name: Sign image with Cosign env: COSIGN_EXPERIMENTAL: "1" run: | set -euo pipefail IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}" # Cosign will sign using GitHub OIDC (keyless signing) because id-token permissions exist. cosign sign --yes "$IMAGE"
What happens when I ran it
- Docker pushes the image to
ghcr.io/...:SHA. slsa-github-generatorcreates a provenance attestation (a signed statement saying what workflow built the artifact, from what source, with what parameters).- Cosign signs the image using keyless signing (OIDC identity from GitHub).
At this point, I had:
- A signed image artifact.
- A signed provenance attestation tied to the build.
But the critical part is verifying that in deployment—where the “wrong policy” failure mode lives.
Provenance verification policy: constrain what “good” means
I wanted strict checks, not “anything signed is fine.”
I created a policy file that enforces:
- The workflow identity that built it (issuer + subject pattern).
- The builder environment (GitHub Actions workflow).
- That the SLSA subject matches the image reference/digest.
policy/provenance-policy.json:
{ "version": "slsa-provenance/v1", "predicateType": "https://slsa.dev/provenance/v1", "builderIdentity": { "type": "https://github.com/github-actions", "issuer": "https://token.actions.githubusercontent.com" }, "materials": { "type": "https://slsa.dev/materials/v1" }, "require": [ "github-actions-workflow", "workflow-run-identity", "source-repository" ] }
This is intentionally opinionated. In real setups, you’d tune this to your org/repo/workflow and pin expected values.
Deploy workflow: verify Cosign signature + verify SLSA provenance via Rekor log
Deploy workflow: .github/workflows/deploy.yml
name: deploy on: workflow_dispatch: permissions: contents: read id-token: write attestations: read env: REGISTRY: ghcr.io IMAGE_NAME: supply-chain-provenance-demo/app REKOR_URL: https://rekor.sigstore.dev # public rekor instance jobs: verify: runs-on: ubuntu-latest steps: - name: Install Cosign uses: sigstore/cosign-installer@v3 with: cosign-release: 'v2.2.4' - name: Verify image signature with Rekor transparency log env: COSIGN_EXPERIMENTAL: "1" run: | set -euo pipefail IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}" # Keyless signature verification checks: # 1) The signature exists, # 2) The signature certificate/OIDC identity matches expectations, # 3) The signature is present in transparency log (Rekor). # # For simplicity, this example checks issuer by using the OIDC-based identity # baked into the cert claims. In production, pin to your repo/workflow. cosign verify --yes \ --rekor-url "$REKOR_URL" \ "$IMAGE" - name: Install SLSA verifier (provenance verification) run: | set -euo pipefail # Install slsa-verifier for provenance checks. # The exact binary name depends on release; using gh release installer pattern. curl -sSfL https://github.com/slsa-framework/slsa-verifier/releases/latest/download/slsa-verifier-linux-amd64 \ -o slsa-verifier chmod +x slsa-verifier - name: Verify SLSA provenance attestation run: | set -euo pipefail IMAGE_REF="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}" # slsa-verifier verifies the attestation associated with the artifact reference. # It uses the local policy to determine whether the provenance is acceptable. ./slsa-verifier verify-attestation \ --policy ./policy/provenance-policy.json \ --artifact "$IMAGE_REF" - name: Deployment step (only after verification) run: | echo "Provenance and signature verified. Proceeding with deployment."
The step-by-step logic I cared about
1) cosign verify --rekor-url ...
This is where transparency log verification helps. Without Rekor checks, you can sometimes verify a signature but still be vulnerable to ecosystem issues like stale keys or mismatch between what you think is signed and what is actually logged.
2) slsa-verifier verify-attestation --policy ...
This ensures the attestation’s content matches what I configured in provenance-policy.json.
In practice, the most dangerous “but it was signed!” scenario is:
- the signature exists,
- the attestation exists,
- but verification doesn’t enforce the right constraints (like the workflow identity or repository source).
This step blocks that.
What I learned (and what I’d do differently next)
When I first built this, I made a classic mistake: I verified signatures but treated provenance like “extra metadata.” The pipeline still worked, but it was possible to accidentally accept artifacts with the wrong build identity because the verification step didn’t enforce policy.
After adding:
- Cosign verification with Rekor, and
- SLSA provenance verification using an explicit policy,
I got a workflow where “trusted artifact” means “trusted artifact with cryptographic proof of how it was built,” not just “it came from a pipeline that claims it built something.”
Conclusion
I implemented a concrete software supply chain hardening flow for OCI artifacts: build a container, generate SLSA provenance during the GitHub Actions run, sign with Cosign, and then verify both the signature (with Rekor transparency log) and the provenance (with a policy) before deployment. The key takeaway from my tinkering: in supply chain security, signing alone isn’t enough—policy-backed provenance verification is what turns “security theater” into enforceable trust.