Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Sign Container Images with Cosign

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.

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.

Cosign Image Signing Supply ChainSource CodeGit commit SHACI Builddocker buildCosign SignAttach signatureOCI RegistryImage + sigDeploy PipelinePull by digestCosign VerifyCheck signatureProductionK8s / ECS / VPSUnsigned or tampered digest = deploy blockedVerification runs before kubectl apply, helm upgrade, or docker pull on prod
End-to-end flow when you sign container images with Cosign: build, sign, store, verify, then deploy

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.

CI/CD Pipeline with Cosign SigningLint + TestBuild ImageTrivy ScanPush DigestCosignSignCapture digest after pushIMAGE_DIGEST=$(crane digest registry.example.com/app:${CI_COMMIT_SHA})Sign the digest, not the tagcosign sign --key cosign.key registry.example.com/app@${IMAGE_DIGEST}
Correct CI order: scan vulnerabilities, push by digest, then sign container images with Cosign

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.

Cosign Verify Decision at Deploy TimePull image digestSignaturevalid?NoBlock deployYesIdentitymatches?NoBlock deployDeploy to production
Deploy-time verification: Cosign checks both signature validity and signer identity before the image runs

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.

CriteriaKeyless (OIDC + Fulcio)Static key pair
Private key storageNone in CI; short-lived cert per jobcosign.key in secret manager
Trust anchorOIDC issuer + certificate identity regexpYour distributed cosign.pub file
Transparency logEntry written to Rekor automaticallyOptional with --record-creation
Best fitGitHub Actions, GitLab OIDC, cloud CI with identity tokensSelf-hosted runners, air-gapped registries, simple VPS deploys
RotationNo long-lived key to rotate; update identity regexp if workflow path changesGenerate new pair, update secret + all verifiers, re-sign if needed
Offline verificationNeeds Rekor lookup or cached cert chainWorks fully offline with public key on disk
Keyless vs Static Key Signing ModelsKeyless OIDCStatic Key PairCI job requests OIDC tokenLoad cosign.key from secretsFulcio issues short-lived certSign with local private keyRekor logs signature entryVerify with cosign.pub fileTrust: OIDC issuer +workflow identity regexpTrust: distributed publickey on servers and in Kyverno
Keyless and static-key Cosign signing paths compared — same registry artifact, different trust models

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.

Cosign Production Gotchas to AvoidSign tag, not digestTag moves; signature becomes meaninglessVerify never wired inSign-only pipelines add no securityLoose identity regexpAny workflow can sign accepted imagesPrivate key in repoUse CI secrets or OIDC insteadSign before scan gateVulnerable image gets trusted signatureCorrect orderBuild → scan → push → sign → verify → deploy
Production mistakes when teams sign container images with Cosign — and the correct pipeline order
  1. Signing mutable tags: Always resolve and sign @sha256:…. Store the digest in your deploy artefact or environment file.
  2. Skipping verification on staging: If staging accepts unsigned images, attackers test there first. Use the same policy in staging and production.
  3. Committing private keys: Run dependency and secret scanning to catch accidental key commits before they reach main.
  4. Signing before vulnerability gates: A signature on a critical-CVE image tells your cluster to trust bad code. Sign only after scan thresholds pass.
  5. 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.
  6. 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.

Frequently Asked Questions

Cosign is a Sigstore tool for signing and verifying OCI container images with cryptographic signatures stored alongside the image in your registry. It proves an image was built by your CI pipeline and was not tampered with after push. On production deployments I manage, unsigned images are a common blind spot: anyone with registry write access can replace a tag. Cosign closes that gap without forcing you onto Docker Notary or a proprietary signing service. It works with Docker Hub, GHCR, GitLab Container Registry, Amazon ECR, and most OCI-compliant registries.

Yes. Cosign and Sigstore keyless signing are free; you pay only registry storage and CI minutes.

Download the latest release binary from the sigstore/cosign GitHub releases page, move it to /usr/local/bin, and chmod +x. On Ubuntu 22.04 or 24.04 I prefer the official binary over distro packages because Cosign releases move faster than apt repositories. Verify the checksum against the release checksums.txt before installing. Confirm with cosign version. For CI, pin a specific version in your GitLab CI or GitHub Actions image rather than curling latest on every run, so builds stay reproducible six months later.

Keyless signing uses Sigstore's Fulcio certificate authority and Rekor transparency log with a short-lived OIDC identity from GitHub Actions, GitLab CI, or Google. No private key file sits on disk or in a secret manager long-term. Key-based signing uses a cosign generate-key-pair output: a password-protected private key you store in CI secrets and a public key you distribute to verifiers. Keyless suits automated pipelines tied to a specific repository and branch. Key-based signing suits air-gapped environments, on-prem registries, or teams that want full control without depending on Sigstore's public infrastructure.

After docker push completes, run cosign sign with the full image reference including digest, not a floating tag. Example pattern: cosign sign --yes registry.example.com/myapp/myapi@sha256:abc123.... Using the digest prevents signing a tag that later points to a different image. In GitLab CI I run signing in the deploy stage immediately after push while the digest is known from the build output. For keyless mode add --identity-token or use the built-in GitLab and GitHub OIDC integrations. The signature artifact lands in the registry as additional OCI objects linked to your image.

Run cosign verify against the exact digest your orchestrator will pull. For keyless signatures, pass expected issuer and subject constraints: cosign verify --certificate-identity-regexp and --certificate-oidc-issuer. For key-based setups, pass --key cosign.pub. A successful verify confirms the signature matches and, for keyless, that the signer identity matches your CI pipeline. I wire this into Deployer or GitLab CI deploy jobs so production never pulls an image that fails verification. In Kubernetes, admission controllers like Kyverno or Ratify can enforce verification automatically on every pod create.

Yes. GitLab 15 and later exposes OIDC ID tokens to CI jobs, which Cosign uses for keyless signing without storing a long-lived private key. Configure your .gitlab-ci.yml sign stage with id_tokens and cosign sign against registry.gitlab.com/your-group/your-project:tag@sha256:digest. Set CI variables for COSIGN_EXPERIMENTAL if your Cosign version still requires it for certain registry backends. Verification runs the same way locally or in a deploy job. This fits naturally into the GitLab CI pipelines I already use for Laravel deployments that also build sidecar or PHP-FPM container images.

Choose Cosign. Docker Content Trust (Notary v1) is legacy, tag-centric, awkward in CI, and poorly supported outside Docker Hub.

Cosign works with any OCI-compliant registry that supports storing signatures as separate artifacts or cosign-compatible attachments. Amazon ECR, Google Artifact Registry, Azure Container Registry, Harbor, and GitLab Container Registry all work in production setups I've seen documented and tested. Authenticate with the registry first using aws ecr get-login-password, docker login, or your CI registry credentials. Private on-prem Harbor instances commonly use key-based signing because keyless OIDC may not reach an internal Fulcio endpoint. Always confirm your registry version supports OCI referrers or the cosign signature storage format your Cosign release expects.

The failures I see most often: signing a tag instead of a digest after someone repushed the tag; expired or missing registry login in CI; OIDC token audience mismatch in keyless mode; and verifying with the wrong public key or identity regexp. Fix by pinning sha256 digests in sign and verify commands, refreshing registry credentials before sign, setting correct id_tokens aud in GitLab CI, and matching --certificate-identity to the exact GitLab project path or GitHub repository. Rekor upload timeouts occasionally happen on slow networks; retry or use --tlog-upload=false only if your security policy allows skipping transparency log submission.

Use an admission policy engine. Kyverno has verifyImages rules that call Cosign under the hood and reject pods pulling unsigned or untrusted images. OPA Gatekeeper with ratify or standalone Ratify as an admission webhook are common alternatives. Configure allowed issuers, subject patterns, and optional public keys matching your CI identity. Apply the policy in audit mode first to see which workloads would fail, then switch to enforce. On clusters running application containers alongside PHP apps deployed traditionally, this matters when you move background workers or queue consumers into Kubernetes.

Sign every image digest your production and staging environments can pull, not every transient CI build. A practical rule: sign main-branch builds destined for staging, all semver release tags, and any hotfix digest deployed to production. Skip signing throwaway feature-branch images unless you deploy them to shared environments. Tag signing alone is weak because tags move; always record and verify the digest. This keeps Rekor log volume and CI time reasonable while covering the images that actually matter if an attacker tries registry substitution.

For key-based signing, generate keys with cosign generate-key-pair and store the encrypted private key in your CI secret store: GitLab CI masked variables, GitHub Actions secrets, or HashiCorp Vault. Never commit cosign.key to git. Distribute cosign.pub to verification points: deploy scripts, Kubernetes ConfigMaps, or policy repos. Rotate by generating a new key pair, updating CI secrets, publishing the new public key alongside the old during overlap, re-signing active release images if policy requires, then retiring the old key. Keyless signing avoids long-lived key rotation entirely because identity is tied to OIDC and certificates are short-lived.

No. Signing proves integrity and trusted origin; scanning finds CVEs inside layers. Use both.

Start when you deploy containers to production from a CI pipeline, share a registry across people or environments, or compliance asks for supply-chain controls. A two-person team running one Laravel app in Docker on a single VPS can defer signing until they use a shared registry or Kubernetes. Once GitLab CI builds and pushes images that production auto-deploys, add Cosign in one afternoon: install Cosign in CI, keyless-sign after push, verify before deploy. Cost is zero beyond roughly 30 to 60 seconds per pipeline run. For Nepal-based teams billing in NPR, that is still far cheaper than a registry compromise or unplanned downtime from a swapped image.

Share this article

Quick Contact Options
Choose how you want to connect me: