
August 28, 2026
14 min read
By Kokil Thapa | Last reviewed: September 2026
You push a new container every week, but can you prove the digest running in production came from your CI job? A leaked registry token, a swapped base layer, or a manual pull of the wrong tag can put untrusted code on your cluster without raising an alarm. Image signing closes that gap by binding a cryptographic identity to an exact digest, so deploy scripts reject anything your pipeline did not produce. Cosign, from the Sigstore project, stores signatures as OCI artifacts beside the image — no separate signature server required. If you already run GitLab CI pipelines for Laravel deployments, adding Cosign is one job and a verify gate before rollout.
cosign sign against an immutable digest after your build pushes to the registry, then running cosign verify on that same digest before Kubernetes, ECS, or a VPS pulls it. Signatures live in the registry as OCI attachments.Why is image signing necessary for container deployments?
Container images bundle code, libraries, and OS packages into one immutable unit. Immutability helps reproducibility, but it does not prove provenance. Anyone with registry write access can retag a digest, or you might deploy a build that never passed your Trivy vulnerability scan.
Signing ties a cryptographic identity to a specific digest — not a floating tag like latest. Cosign fits small teams on a single Ubuntu VPS in Kathmandu as well as teams on managed Kubernetes. The signatures sit in the same OCI registry as the image layers.
On production systems I maintain, image signing sits alongside scanning and secret detection. Scanning finds known CVEs in layers. Signing proves the digest you are about to run came from your pipeline and was not replaced afterward. Both belong in a mature build pipeline automation strategy.
- Provenance: Signatures tie a digest to an identity — a CI OIDC subject or a known public key.
- Integrity: Any layer change invalidates the signature because Cosign signs the digest hash.
- Policy enforcement: Kubernetes admission controllers like Kyverno can require valid signatures before a pod starts. See Kyverno vs OPA Gatekeeper for policy engine trade-offs.
- Audit trail: Keyless signatures are recorded in Rekor, Sigstore's public transparency log.
Container signing is not a replacement for server hardening or network controls. It is one layer in a defence-in-depth stack that also includes RBAC on the registry and locked-down deploy credentials.
How do you install Cosign and prepare signing credentials?
Cosign ships as a single static binary for Linux, macOS, and Windows. On an Ubuntu 24.04 CI runner or local dev machine, install the current release from the Sigstore Cosign releases page or use your package manager if a recent version is available. As of 2026, Cosign 2.x is the stable line. Check your installed version before scripting commands.
Install Cosign on Ubuntu
# Download latest Cosign release (check https://github.com/sigstore/cosign/releases)
COSIGN_VERSION=2.4.1
curl -LO "https://github.com/sigstore/cosign/releases/download/v${COSIGN_VERSION}/cosign-linux-amd64"
sudo install cosign-linux-amd64 /usr/local/bin/cosign
cosign version Authenticate to your container registry
Cosign uses the same registry credentials as Docker or crane. Log in before signing or verifying:
echo "$CI_REGISTRY_PASSWORD" | cosign login registry.example.com -u "$CI_REGISTRY_USER" --password-stdin Validate your pipeline YAML with a JSON or YAML formatter before committing CI config changes. A typo in a secret variable name fails silently until the sign job runs.
Generate a static key pair (optional path)
If you are not using keyless signing yet, generate an encrypted key pair locally. Store the private key in your CI secret store — never commit it to Git:
cosign generate-key-pair
# Creates cosign.key (private) and cosign.pub (public)
# Add cosign.key contents to GitLab CI/CD variables or GitHub Actions secrets Export the public key to a file your deploy servers or cluster policy engine can read. Treat cosign.key like any production secret. Rotate it on the same schedule you use for deploy keys. Teams running self-hosted CI runners should restrict filesystem access on the runner host so the key never lands on disk unencrypted.
How do you sign container images with Cosign in CI/CD?
The signing step must run after the image is pushed and against the immutable digest, not a floating tag. Tags are pointers. Digests identify exact content. A common mistake is signing myapp:latest while the registry later retags that name to a different digest.
GitLab CI example
This pattern fits the GitLab CI workflows I use on shared EC2 infrastructure. It aligns with the steps in GitLab CI/CD for PHP projects and deploying Laravel to a VPS. Adjust registry URLs and secret variable names to match your project:
stages:
- build
- sign
- deploy
variables:
IMAGE: registry.example.com/myapp
build-image:
stage: build
image: docker:27
services:
- docker:27-dind
script:
- docker build -t ${IMAGE}:${CI_COMMIT_SHA} .
- docker push ${IMAGE}:${CI_COMMIT_SHA}
- echo "IMAGE_DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' ${IMAGE}:${CI_COMMIT_SHA} | cut -d@ -f2)" >> build.env
artifacts:
reports:
dotenv: build.env
sign-image:
stage: sign
image: cgr.dev/chainguard/cosign:latest
needs: [build-image]
script:
- cosign login registry.example.com -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD"
- cosign sign --key env://COSIGN_PRIVATE_KEY --yes "${IMAGE}@${IMAGE_DIGEST}"
only:
- main
deploy-prod:
stage: deploy
script:
- cosign verify --key cosign.pub "${IMAGE}@${IMAGE_DIGEST}"
- ./deploy.sh "${IMAGE}@${IMAGE_DIGEST}" Pair this with zero-downtime Deployer releases so verification runs before the symlink swap. On booking platforms like Adventure Third Pole Trek, a bad deploy during peak season is worse than a delayed one.
GitHub Actions keyless signing
Keyless signing uses OpenID Connect so your CI job gets a short-lived certificate from Fulcio. No long-lived private key sits in secrets. GitHub Actions has native OIDC support. Enable id-token: write permission and sign with the identity tied to your repository:
permissions:
id-token: write
contents: read
jobs:
sign:
runs-on: ubuntu-latest
steps:
- uses: sigstore/cosign-installer@v3
- name: Sign image keylessly
env:
COSIGN_EXPERIMENTAL: "1"
run: |
cosign sign --yes "ghcr.io/myorg/myapp@${{ steps.meta.outputs.digest }}" Compare CI platforms in GitHub Actions vs GitLab CI before choosing keyless. GitHub OIDC is mature. GitLab OIDC works too but needs explicit configuration on self-managed instances.
How do you verify Cosign signatures before deployment?
Verification is where image signing delivers value. If you sign in CI but deploy without checking, you added ceremony without security. Every production deploy path — Helm, kubectl, Docker Compose on a VPS, Amazon ECS task updates — should call cosign verify against the digest you intend to run.
Verify with a static public key
cosign verify --key cosign.pub registry.example.com/myapp@sha256:abc123...
# Exit code 0 = signature valid; non-zero = reject deploy Verify keyless signatures from GitHub Actions
cosign verify \
--certificate-identity-regexp="https://github.com/myorg/myrepo/.github/workflows/release.yml@refs/heads/main" \
--certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
ghcr.io/myorg/myapp@sha256:abc123... Pin the identity regexp tightly. A loose pattern like .* defeats the purpose. Any workflow in the org could then sign an image your cluster accepts.
Enforce signatures in Kubernetes with Kyverno
For clusters, move verification out of shell scripts and into admission policy. Unsigned images never schedule. Read admission controllers and webhooks for the broader context on how policies intercept pod creation:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: verify-image-signatures
spec:
validationFailureAction: Enforce
rules:
- name: verify-cosign-signature
match:
any:
- resources:
kinds: [Pod]
verifyImages:
- imageReferences: ["registry.example.com/myapp*"]
attestors:
- entries:
- keys:
publicKeys: |-
-----BEGIN PUBLIC KEY-----
MFYwEAYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...
-----END PUBLIC KEY----- Kyverno evaluates signatures at admission time. Combined with network policies and secrets scanning in CI, you get defence in depth without rewriting application code. If you need help wiring this on production infrastructure, see Linux system administration services for VPS and cluster hardening support.
Cosign keyless signing vs static keys: which should you use?
Both approaches sign the same digest with the same underlying cryptography. The difference is key lifecycle and trust anchoring. Small VPS deployments often start with static keys because verification is a one-line shell command. Larger teams on GitHub or GitLab frequently adopt keyless signing to avoid storing private keys in CI variables.
| Criteria | Keyless (OIDC + Fulcio) | Static key pair |
|---|---|---|
| Private key storage | None in CI; short-lived cert per job | cosign.key in secret manager |
| Trust anchor | OIDC issuer + certificate identity regexp | Your distributed cosign.pub file |
| Transparency log | Entry written to Rekor automatically | Optional with --record-creation |
| Best fit | GitHub Actions, GitLab OIDC, cloud CI with identity tokens | Self-hosted runners, air-gapped registries, simple VPS deploys |
| Rotation | No long-lived key to rotate; update identity regexp if workflow path changes | Generate new pair, update secret + all verifiers, re-sign if needed |
| Offline verification | Needs Rekor lookup or cached cert chain | Works fully offline with public key on disk |
My practical recommendation: if your CI platform exposes OIDC tokens and your registry is on the public internet, start keyless. If you deploy from a self-hosted runner on a single Ubuntu VPS with no OIDC integration, static keys plus a verify step in your Deployer hook is simpler and still effective. Either way, pair signing with image scanning. Cosign does not replace CVE detection.
What are common container image signing mistakes in production?
Most failures I see are operational, not cryptographic. Teams add Cosign to a pipeline demo, then skip verification in the actual deploy path. The reasoning is usually "we only pull from our private registry." Private registries reduce casual tampering. They do not stop a leaked credential or an insider push.
- Signing mutable tags: Always resolve and sign
@sha256:…. Store the digest in your deploy artefact or environment file. - Skipping verification on staging: If staging accepts unsigned images, attackers test there first. Use the same policy in staging and production.
- Committing private keys: Run dependency and secret scanning to catch accidental key commits before they reach main.
- Signing before vulnerability gates: A signature on a critical-CVE image tells your cluster to trust bad code. Sign only after scan thresholds pass.
- Ignoring attestations: Cosign also supports SLSA-style attestations (SBOM, build metadata). Signatures prove who signed. Attestations prove how the image was built. Add them when compliance asks for a software bill of materials.
- Registry mirror drift: If you mirror images to a secondary registry, sign at the canonical source and verify at pull time on each mirror. Alternatively, re-sign after promotion with a controlled promotion key.
Attestations and SBOM (next step)
Once basic signing works, attach an SBOM generated by Syft or Trivy as a Cosign attestation:
syft packages registry.example.com/myapp@${IMAGE_DIGEST} -o cyclonedx-json > sbom.json
cosign attest --key env://COSIGN_PRIVATE_KEY --predicate sbom.json --type cyclonedx "${IMAGE}@${IMAGE_DIGEST}" Verification of attestations uses cosign verify-attestation with the same key or OIDC identity rules. This matters for teams pursuing SOC 2 or responding to customer security questionnaires. It is common for SaaS products built with production Docker builds for Laravel.
Local testing before CI integration
Test the full loop on a throwaway image before touching production pipelines:
docker build -t localhost:5000/demo:test .
docker push localhost:5000/demo:test
DIGEST=$(crane digest localhost:5000/demo:test)
cosign sign --key cosign.key "localhost:5000/demo@${DIGEST}"
cosign verify --key cosign.pub "localhost:5000/demo@${DIGEST}" If verification fails locally, fix the key paths, registry auth, or digest resolution before adding the step to GitLab CI. Debugging signing inside a long pipeline wastes runner minutes and obscures the actual error. You can also reach out through the contact page if you want a second pair of eyes on your pipeline YAML.
Key Takeaways
- Image signing binds a cryptographic identity to an immutable digest — never sign mutable tags like
latest. - Run
cosign signafter push and scan gates pass; runcosign verifyon every deploy path before the image starts. - Keyless OIDC signing avoids long-lived private keys; static keys work well on self-hosted VPS deploys with no OIDC.
- Enforce container signing in Kubernetes with Kyverno admission policies so unsigned pods never schedule.
- Pair Cosign with Trivy scanning, secret detection, and SBOM attestations for a complete supply chain checkpoint.
- Test sign-and-verify locally on a throwaway registry before wiring Cosign into production CI.
People Also Ask
What is the difference between image signing and image scanning?
Image scanning checks layer contents for known CVEs and misconfigurations. Image signing proves a specific digest was produced by a trusted identity and has not been altered since signing. You need both: scanning catches vulnerable code, signing catches swapped or tampered images.
Does Cosign work with private container registries?
Yes. Cosign stores signatures as OCI artifacts in the same registry as the image. Any registry that supports OCI distribution — GitLab Container Registry, Amazon ECR, Google Artifact Registry, or a self-hosted Harbor instance — works. You authenticate with the same credentials Docker uses.
Can you verify Cosign signatures without internet access?
Static key verification works fully offline if you have cosign.pub on disk. Keyless verification needs Rekor or a cached certificate chain unless you configure offline trust bundles. For air-gapped environments, static keys are the practical default.
Is container image signing required for SOC 2 or ISO 27001?
Neither standard mandates Cosign by name, but both expect controls over software supply chain integrity. Image signing with audit logs in Rekor gives auditors evidence that only approved CI pipelines promoted images to production. Pair it with SBOM attestations when customers ask how software was built.
Ready to add image signing to your deployment pipeline?
When you sign container images with Cosign and verify every digest at deploy time, your registry becomes an auditable supply chain checkpoint — not a passive file store. Start with one production image, static keys if that is fastest, and a hard cosign verify gate in your deploy script. Expand to keyless signing and Kyverno policy once the basics hold under real releases. For help wiring signing, scanning, and zero-downtime deploys into an existing Laravel or eCommerce stack, contact us about DevOps and deployment hardening.
Frequently Asked Questions
0 Comments
Leave a comment
Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

