Most engineers treat container registries as glorified file servers, assuming that if a tag exists, it is the one they built. This is a dangerous oversight in any environment handling sensitive data. Relying on image tags alone provides no guarantee that the image running in your orchestration layer is the same bits-and-bytes package that passed your CI suite.
If you aren't cryptographically signing your container images, your data pipelines are vulnerable to man-in-the-middle attacks, registry poisoning, and unauthorized image swapping. Signing images with Sigstore Cosign allows you to attach a verifiable digital signature to your image in the container registry, ensuring that only trusted artifacts reach your production environment.
Prerequisites
To follow this tutorial, you need the following:
- Docker: Version 20.10 or later.
-
Cosign: Installed via
brew install cosignor the official binary from the Sigstore documentation. - A Container Registry: A registry that supports OCI artifacts (e.g., Docker Hub, GitHub Container Registry, or AWS ECR).
-
Environment: A local machine with Docker daemon access and an authenticated session to your registry (
docker login).
We assume you are using standard Linux-based images. This guide focuses on signing with local key pairs, though Cosign supports Keyless signing using OIDC identities, which is recommended for mature CI/CD pipelines.
Photo by catcher xuefeng on Unsplash
Step 1: Generate a signing key pair
Before you can sign an image, you need a private key to perform the signing and a public key to verify it. Cosign generates these as a pair.
Run the following command to generate a key pair:
cosign generate-key-pair
This will create two files: cosign.key and cosign.pub.
-
cosign.key: The private key used for signing. Keep this secret. In a real CI environment, you would store this in a secret manager like AWS Secrets Manager, GitHub Secrets, or HashiCorp Vault. -
cosign.pub: The public key used for verification. This is what your deployment infrastructure (like a Kubernetes admission controller or a CI gate) will use to check if an image is legitimate.
Step 2: Build and push a hardened image
For this example, imagine a standard Python-based data processing image. Use a multi-stage Dockerfile to keep the footprint minimal and the security posture strong.
Create a Dockerfile:
# Use a pinned version for reproducibility
FROM python:3.11-slim-bookworm AS builder
# Prevent python from writing pyc files and enable unbuffered logging
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --user -r requirements.txt
# Final stage
FROM python:3.11-slim-bookworm
RUN groupadd -r appuser && useradd -r -g appuser appuser
WORKDIR /app
COPY --from=builder /root/.local /home/appuser/.local
COPY --chown=appuser:appuser . .
USER appuser
HEALTHCHECK --interval=30s --timeout=3s \
CMD curl -f http://localhost:8080/health || exit 1
ENTRYPOINT ["python", "main.py"]
Build and push the image:
docker build -t <your-registry>/<your-repo>:<tag> .
docker push <your-registry>/<your-repo>:<tag>
Step 3: Sign the image
Now that the image is in the registry, you can sign it. Cosign stores the signature as a separate OCI artifact in the same registry, linked to the image digest.
Execute the following command to sign the image:
cosign sign --key cosign.key <your-registry>/<your-repo>@<digest>
Note that we are using the digest (@sha256:...) rather than the tag. Tags are mutable and can be pointed to different images; digests are immutable content hashes. Always sign the digest to ensure you are signing the exact bytes you tested.
You can verify that the signature was pushed by checking your registry's UI or by running:
cosign triangulate <your-registry>/<your-repo>:<tag>
Step 4: Verify the signature before deployment
This is the most critical step for your CI/CD pipeline. Whether you are running a standalone script or a Kubernetes cluster, you must block any execution if the image signature doesn't match your cosign.pub file.
To verify manually:
cosign verify --key cosign.pub <your-registry>/<your-repo>:<tag>
If the signature is valid, Cosign returns the signature metadata and exits with code 0. If someone has replaced the image, or if the image was not signed with the corresponding private key, the command will fail and exit with a non-zero code.
In your CI pipeline (e.g., GitHub Actions), you should include this as a mandatory step:
- name: Verify Image Signature
run: cosign verify --key cosign.pub <your-registry>/<your-repo>:<tag>
If this step fails, the pipeline stops, and no vulnerable or unauthorized code is deployed.
Photo by Ankit Manoharan on Unsplash
Common pitfalls
Signing tags instead of digests
As mentioned, tags are mutable. If you sign my-image:latest, an attacker can push a new image to the latest tag, and the registry will point to it. While the signature on the previous image remains, the latest tag becomes untrusted. Always use the @sha256: digest in your signatures and your deployment manifests.
Key management
The cosign.key file is your root of trust. If this is leaked, an attacker can sign malicious images that your pipeline will blindly accept. Never commit cosign.key to source control. Use your CI provider’s secret storage. For more advanced setups, look into Cosign’s support for KMS to keep your keys in hardware security modules.
Ignoring verification failures
A signature check is only effective if your deployment process strictly enforces it. If your deployment scripts continue execution even after a verification failure, you have provided a false sense of security. Ensure your automation scripts use set -e or equivalent error handling to halt immediately upon a non-zero exit code from cosign verify.
Conclusion
Cryptographic signing is not an optional feature for regulated environments; it is the baseline for supply chain integrity. By moving from "trusting the registry" to "verifying the signature," you eliminate the risk of image tampering and ensure that your data pipelines run exactly what you intended.
Try it: Generate a local key pair, sign a "Hello World" image, and intentionally change the Dockerfile to break the hash. Run the verification command and observe how the tool correctly identifies the mismatch between the signed digest and the modified image.
Cover photo by Daniel Miksha on Unsplash.
This article was drafted with AI assistance and is intended for technical review and fact-checking before publication.
Top comments (0)