
August 28, 2026
12 min read
Table of Contents
By Kokil Thapa | Last reviewed: August 2026
You deploy a Laravel app inside a container every week, but how do you know the image running in production is the one your pipeline built? A compromised registry credential, a poisoned base image, or a manual pull of an untagged digest can slip a tampered image into your cluster without anyone noticing. Learning to sign container images with Cosign closes that gap by attaching a cryptographic signature to every image digest, so your deploy step can reject anything that was not signed by your CI system. If you already run GitLab CI pipelines for Laravel deployments, adding Cosign takes one extra job and a verification gate at deploy time.
cosign sign against the image digest after your build, and store the signature alongside the image in the registry. At deploy time, run cosign verify against the same digest before pulling it into Kubernetes, ECS, or a VPS.Why should you sign container images with Cosign?
Container images are immutable bundles of code, libraries, and OS packages. Immutability helps reproducibility, but it does not prove provenance. Anyone with registry write access can push a malicious tag over an existing one, or you might accidentally deploy a digest that never passed your Trivy vulnerability scan. Signing binds a cryptographic identity to a specific digest, not just a mutable tag like latest.
Cosign is the signing tool maintained under the Sigstore project. It stores signatures as OCI artifacts attached to the image in the same registry, so you do not need a separate signature server. That design fits small teams running a single GitLab runner on a VPS in Kathmandu as well as teams on managed Kubernetes.
On production Laravel deployments I maintain, image signing sits alongside scanning and secret detection. Scanning tells you about known CVEs inside the image layers. Cosign tells you the image digest you are about to run was produced by your pipeline and has not been replaced since signing. Both checks belong in a mature build pipeline automation strategy.
- Provenance: Signatures tie an image digest to an identity (a CI OIDC subject or a known public key).
- Integrity: Any change to image layers invalidates the signature because Cosign signs the digest.
- Policy enforcement: Kubernetes admission controllers like Kyverno or Gatekeeper can require valid Cosign signatures before a pod starts.
- Audit trail: Keyless signatures are recorded in Rekor, a public transparency log maintained by Sigstore.
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 GitHub releases page or use your package manager if a recent version is available. As of 2026, Cosign 2.x is the stable line; verify 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 Generate a static key pair (optional path)
If you are not using keyless signing yet, generate an encrypted key pair locally and 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 other production secret and rotate it on the same schedule you use for deploy keys.
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. 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}" GitHub Actions keyless signing
Keyless signing uses OpenID Connect so your CI job gets a short-lived certificate from Fulcio without storing a long-lived private key. 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 }}" Keyless signing is convenient for open-source repos and teams that do not want to manage cosign.key rotation. Your verification step must trust the correct OIDC issuer and subject pattern, which we cover in the next section.
How do you verify Cosign signatures before deployment?
Verification is where signing delivers value. If you sign in CI but deploy without checking, you have 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 because any workflow in the org could sign an image your cluster accepts.
Enforce signatures in Kubernetes with Kyverno
For clusters, move verification out of shell scripts and into admission policy so unsigned images never schedule:
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.
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 Cosign 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 because "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 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, or 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 is increasingly relevant for teams pursuing SOC 2 or responding to customer security questionnaires — 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.
Ready to add Cosign signing to your deployment pipeline?
When you sign container images with Cosign and verify every digest at deploy time, you turn your registry from a passive file store into an auditable supply chain checkpoint. Start with one production image, static keys if that is fastest for your setup, and a hard cosign verify gate in your deploy script. Expand to keyless signing and Kyverno policy once the basics hold under real releases. If you want help wiring signing, scanning, and zero-downtime deploys into an existing Laravel or eCommerce stack, get in touch through the contact page — I integrate these controls on production systems regularly.

