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.
- In GitHub Actions, grant
id-token: writeand install Cosign withsigstore/cosign-installer. - Push the image, capture its digest, and sign by digest:
cosign sign --yes ghcr.io/acme/shop@sha256:.... - Optionally attach an SBOM and SLSA provenance as attestations.
- Verify before deploying:
cosign verifywith--certificate-identityand--certificate-oidc-issuermatching your workflow. - 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?".
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.
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
:latestis 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
- Scan what you sign with Scan Django Docker Images With Trivy.
- Review dependencies with the Django Dependency Security Checklist.
- Build the pipeline with Deploy Django With GitHub Actions CI/CD.
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.