Sign and Verify Django Docker Images With Cosign

Sign Django container images with Sigstore Cosign in CI using keyless signing through GitHub Actions OIDC, attach SBOM and provenance attestations, and verify signatures before deploy or with a Kubernetes admission policy.

Problem statement

Your deployment pulls ghcr.io/acme/shop:3f2a1c9 and trusts that it is what CI built. If a registry token leaks, or someone pushes an image by hand, production could run code that never went through review or tests. Image signing proves who built an image and from which workflow. Sigstore's Cosign makes it practical: CI signs images without managing private keys, and the deploy step or the cluster verifies the signature before running anything.

Cosign for Django imagesCI buildspush by digestcosign signkeyless via OIDCRegistryimage + signatureVerifydeploy step or policySignatures tie an image digest to the CI workflow that built it.
  1. In GitHub Actions, grant id-token: write and install Cosign with sigstore/cosign-installer.
  2. Push the image, capture its digest, and sign by digest: cosign sign --yes ghcr.io/acme/shop@sha256:....
  3. Optionally attach an SBOM and SLSA provenance as attestations.
  4. Verify before deploying: cosign verify with --certificate-identity and --certificate-oidc-issuer matching your workflow.
  5. Enforce in Kubernetes with an admission controller such as Sigstore policy-controller or Kyverno.

Step-by-step solution

1. Sign in CI with keyless signing

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
      id-token: write
    steps:
      - uses: actions/checkout@v4
      - uses: docker/login-action@v3
        with: { registry: ghcr.io, username: "${{ github.actor }}", password: "${{ secrets.GITHUB_TOKEN }}" }
      - uses: docker/build-push-action@v6
        id: build
        with: { push: true, tags: "ghcr.io/acme/shop:${{ github.sha }}" }
      - uses: sigstore/cosign-installer@v3
      - run: cosign sign --yes ghcr.io/acme/shop@${{ steps.build.outputs.digest }}

Keyless signing uses the workflow's OIDC token to get a short-lived certificate from Sigstore's Fulcio CA and records the signature in the Rekor transparency log. No private key is stored anywhere. Always sign the digest, not a tag, because tags can be moved.

2. Attach attestations

syft ghcr.io/acme/shop@$DIGEST -o spdx-json > sbom.spdx.json
cosign attest --yes --predicate sbom.spdx.json --type spdxjson ghcr.io/acme/shop@$DIGEST

docker/build-push-action can also generate provenance and SBOM attestations with provenance: true and sbom: true. Attestations let you later answer "which images contain this vulnerable package?" and "which commit built this?".

from build to admission1Build and push by digestGitHub Actions2Get OIDC certificateFulcio, short-lived3Sign, log to Rekorsignature in registry4Verify identity at deployworkflow + issuer5Admission policy enforcesunsigned rejectedVerification checks who signed, not just that a signature existsdjango-deployment.com
A signature from an unexpected identity should fail just like no signature.

3. Verify before deploying

cosign verify ghcr.io/acme/shop@sha256:abc123... \
  --certificate-identity "https://github.com/acme/shop/.github/workflows/build.yml@refs/heads/main" \
  --certificate-oidc-issuer "https://token.actions.githubusercontent.com"

The identity check is what makes signing meaningful: it confirms the image was signed by the build workflow on main, not by any GitHub Actions run anywhere. Add this step to your deploy job, and deploy by digest.

4. Enforce in Kubernetes

With Sigstore's policy-controller, a ClusterImagePolicy requires valid signatures for images matching ghcr.io/acme/**:

apiVersion: policy.sigstore.dev/v1beta1
kind: ClusterImagePolicy
metadata: { name: acme-signed }
spec:
  images: [{ glob: "ghcr.io/acme/**" }]
  authorities:
    - keyless:
        identities:
          - issuer: https://token.actions.githubusercontent.com
            subjectRegExp: "^https://github.com/acme/.+/.github/workflows/build.yml@refs/heads/main$"

Label namespaces to opt in. Kyverno's verifyImages rules achieve the same with its policy engine.

5. Handle base images and dependencies

Signing proves your build's integrity, not the safety of what went into it. Combine with dependency pinning (hashes or lock files), base image digests, and vulnerability scanning so the signed image is also one you want to run.

build to deployLocked dependencieshashes or lock fileBase image pinned by digestupdated by botImage scanned in CITrivy or GrypeSigned by digest, keylessCosign + OIDCVerified at admissionpolicy-controllerSigning completes a chain that starts with dependenciesdjango-deployment.com
Each link covers a different way bad code can reach production.

Explanation

Cosign stores signatures as OCI artifacts next to the image in the registry. With keyless signing, the signer's identity comes from an OIDC token (here, GitHub's workflow identity), embedded in a short-lived certificate, and the signing event is recorded in a public transparency log so it can be audited. Verification checks the signature, the certificate chain, the log entry, and that the identity matches the expected workflow. Deploying by digest ensures the verified image is exactly the one that runs.

Edge cases / notes

  • Private infrastructure. If public Sigstore services are not acceptable, run your own Fulcio and Rekor or sign with a KMS-backed key (cosign sign --key awskms://...).
  • Mutable tags. Verifying :latest is meaningless if the tag moves; resolve and deploy digests.
  • Registry support. GHCR, ECR, Artifact Registry, ACR, and Docker Hub all store Cosign signatures.

Next steps

FAQ

What is keyless signing?

Signing with a short-lived certificate issued from an OIDC identity, such as a GitHub Actions workflow, instead of a long-lived private key.

Why sign by digest instead of tag?

Tags can be moved to other images. A digest identifies exact image content.

Does signing replace vulnerability scanning?

No. Signing proves where an image came from; scanning checks what is inside it.

How do I block unsigned images in Kubernetes?

Use an admission controller such as Sigstore policy-controller or Kyverno with image verification policies.

Keep going