Cloud7 Min Read

Code Signing — Why and How

Gorav Singal

April 04, 2026

TL;DR

Code signing proves authenticity and integrity. Sign Git commits with GPG/SSH, Docker images with cosign, and verify signatures in CI. It's your last line of defense against supply chain attacks.

Code Signing — Why and How

This is Part 6 of the Cloud Security Engineering crash course. In previous parts we covered IAM hardening, secrets management, and supply chain security. Now we tackle the mechanism that ties everything together: code signing.

If supply chain security is about knowing what goes into your software, code signing is about proving who put it there and that nothing changed along the way. It is the cryptographic guarantee that the code running in production is exactly what a trusted developer wrote and a trusted pipeline built.

Why Code Signing Matters

Every deployment is a trust decision. When your Kubernetes cluster pulls an image, it is trusting that the image is what it claims to be. When a developer installs an npm package, they trust the publisher. When a reviewer merges a PR, they trust the commit author.

Without code signing, all of that trust is implicit. An attacker who compromises a registry, a CI runner, or a developer laptop can inject malicious code and nobody would know until it is too late.

Code signing solves three problems:

  1. Authenticity — it proves the identity of the signer.
  2. Integrity — it proves the artifact has not been modified since signing.
  3. Non-repudiation — the signer cannot deny they signed it.

Real-world incidents make the case clearly. The SolarWinds attack succeeded because a tampered build artifact passed through the pipeline without any signature verification. The Codecov bash uploader was modified in transit. The ua-parser-js npm hijack pushed malicious code under a trusted package name. In every case, signature verification at the point of consumption would have caught the tampering.

Signing Git Commits (GPG + SSH)

The first link in the chain of trust is the commit itself. Git supports two signing methods: GPG and SSH.

GPG Signing

Generate a GPG key and configure Git to use it:

# Generate a new GPG key (use RSA 4096 or ed25519)
gpg --full-generate-key

# List your keys to find the key ID
gpg --list-secret-keys --keyid-format=long

# Configure Git to use this key
git config --global user.signingkey YOUR_KEY_ID
git config --global commit.gpgsign true

# Export your public key for GitHub/GitLab
gpg --armor --export YOUR_KEY_ID

Now every commit you make will be signed automatically. Upload the public key to GitHub under Settings > SSH and GPG keys to get the green “Verified” badge.

SSH Signing (Git 2.34+)

If you already have SSH keys, you can skip GPG entirely. SSH signing is simpler to set up:

# Tell Git to use SSH for signing
git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519.pub

# Create an allowed-signers file for verification
echo "[email protected] $(cat ~/.ssh/id_ed25519.pub)" >> ~/.config/git/allowed_signers
git config --global gpg.ssh.allowedSignersFile ~/.config/git/allowed_signers

I prefer SSH signing for teams because everyone already has SSH keys and key distribution is already handled by GitHub/GitLab. One less tool in the stack.

Verifying Commits

# Verify a single commit
git verify-commit HEAD

# Show signature info in log
git log --show-signature -1

# Enforce signed commits in CI
git verify-commit HEAD || exit 1

Signing Docker Images with Cosign

Git commit signing covers source code. But what about the built artifact? A signed commit means nothing if the Docker image was tampered with after the build. This is where cosign from the Sigstore project comes in.

Install and Sign

# Install cosign
brew install cosign  # or go install github.com/sigstore/cosign/v2/cmd/cosign@latest

# Generate a key pair (for key-based signing)
cosign generate-key-pair

# Sign an image after pushing to a registry
cosign sign --key cosign.key ghcr.io/myorg/myapp:v1.2.3

# Verify the signature
cosign verify --key cosign.pub ghcr.io/myorg/myapp:v1.2.3

Cosign stores signatures as OCI artifacts alongside the image in the registry. No separate signature store needed.

Attaching Metadata

You can attach additional claims to a signature, which is powerful for policy enforcement:

# Sign with custom annotations
cosign sign --key cosign.key \
  -a "repo=github.com/myorg/myapp" \
  -a "workflow=release" \
  -a "commit=$(git rev-parse HEAD)" \
  ghcr.io/myorg/myapp:v1.2.3

Sigstore and Keyless Signing

Managing long-lived signing keys is a security problem in itself. If the private key is compromised, every past signature becomes suspect. Sigstore’s keyless signing eliminates this risk entirely.

Sigstore keyless signing flow

Here is how keyless signing works:

  1. The developer (or CI system) authenticates via OIDC — GitHub Actions, Google, Microsoft, etc.
  2. Fulcio, the Sigstore certificate authority, issues a short-lived signing certificate bound to the OIDC identity. The certificate is valid for about 10 minutes.
  3. The artifact is signed with the ephemeral private key.
  4. The signature and certificate are recorded in Rekor, a tamper-evident transparency log.
  5. The private key is discarded. It never needed to be stored anywhere.

Verification works by checking the Rekor log entry and the Fulcio certificate chain. No keys to manage, no keys to rotate, no keys to leak.

# Keyless signing in CI (uses ambient OIDC credentials)
cosign sign ghcr.io/myorg/myapp:v1.2.3

# Keyless verification — checks Fulcio + Rekor automatically
cosign verify \
  --certificate-identity "https://github.com/myorg/myapp/.github/workflows/release.yml@refs/tags/v1.2.3" \
  --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
  ghcr.io/myorg/myapp:v1.2.3

In GitHub Actions, keyless signing happens automatically when you run cosign sign without specifying a key. The workflow’s OIDC token is used as the identity. This is the approach I recommend for most teams — zero key management overhead.

Verifying Signatures in CI

Signing is only half the equation. You must verify at every trust boundary. Here is a GitHub Actions workflow that builds, signs, and then verifies before deployment:

name: Build, Sign, and Deploy

on:
  push:
    tags: ["v*"]

permissions:
  contents: read
  packages: write
  id-token: write  # Required for keyless signing

jobs:
  build-sign:
    runs-on: ubuntu-latest
    outputs:
      digest: ${{ steps.build.outputs.digest }}
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      # Verify the Git commit is signed
      - name: Verify commit signature
        run: git verify-commit HEAD

      - name: Install cosign
        uses: sigstore/cosign-installer@v3

      - name: Log in to GHCR
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push
        id: build
        uses: docker/build-push-action@v5
        with:
          push: true
          tags: ghcr.io/myorg/myapp:${{ github.ref_name }}

      # Keyless sign — uses GitHub OIDC automatically
      - name: Sign image
        run: cosign sign ghcr.io/myorg/myapp@${{ steps.build.outputs.digest }}

  deploy:
    needs: build-sign
    runs-on: ubuntu-latest
    steps:
      - name: Install cosign
        uses: sigstore/cosign-installer@v3

      # Verify BEFORE deploying
      - name: Verify image signature
        run: |
          cosign verify \
            --certificate-identity "https://github.com/myorg/myapp/.github/workflows/release.yml@refs/tags/${{ github.ref_name }}" \
            --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
            ghcr.io/myorg/myapp@${{ needs.build-sign.outputs.digest }}

      - name: Deploy to production
        run: echo "Deploying verified image..."

The key detail: the deploy job verifies the signature before deploying. If someone tampered with the image in the registry between the build and deploy stages, the verification fails and the deployment is blocked.

npm Package Signing

The JavaScript ecosystem has its own signing story. npm introduced provenance in 2023, built on Sigstore. When you publish from a supported CI environment, npm automatically generates a provenance attestation linking the published package to its source repo and build.

# Publish with provenance from GitHub Actions
npm publish --provenance

# Check provenance on the npm website or CLI
npm audit signatures

In your package.json, you can add a publishConfig block:

{
  "publishConfig": {
    "provenance": true
  }
}

When a consumer runs npm audit signatures, npm verifies that every installed package with provenance was built from the claimed source repository. This catches scenarios where a maintainer’s token is stolen and used to publish from a different machine.

For private registries that do not support Sigstore, you can sign packages with GPG manually:

# Sign the tarball
npm pack
gpg --detach-sign --armor mypackage-1.0.0.tgz

# Consumers verify before installing
gpg --verify mypackage-1.0.0.tgz.asc mypackage-1.0.0.tgz

Building a Chain of Trust

Individual signing steps are useful, but the real power comes from connecting them into an unbroken chain from developer to production.

Chain of trust from developer to production

Each link in the chain verifies the previous one:

  1. Developer signs commits with GPG or SSH keys. The identity is tied to a verified email on GitHub/GitLab.
  2. CI verifies commit signatures before running the build. Unsigned or unverified commits are rejected.
  3. CI signs the build artifact using keyless signing (cosign + Sigstore). The signature is bound to the CI workflow identity, not a human.
  4. The registry stores the signature alongside the artifact as an OCI artifact or attestation.
  5. The deployment system verifies the signature before pulling the image. Kubernetes admission controllers like Kyverno or Connaisseur can enforce this automatically.
  6. Production runs only verified images. Any break in the chain halts deployment.

Enforcing in Kubernetes with Kyverno

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: verify-image-signatures
spec:
  validationFailureAction: Enforce
  rules:
    - name: check-cosign-signature
      match:
        any:
          - resources:
              kinds:
                - Pod
      verifyImages:
        - imageReferences:
            - "ghcr.io/myorg/*"
          attestors:
            - entries:
                - keyless:
                    subject: "https://github.com/myorg/*"
                    issuer: "https://token.actions.githubusercontent.com"

This policy ensures that no image from your organization’s registry can run in the cluster unless it has a valid Sigstore signature from your GitHub Actions workflows. An attacker who pushes a malicious image directly to the registry is blocked.

Key Takeaways

  1. Sign at every stage — commits, build artifacts, and packages. Each signature closes a different attack surface.
  2. Prefer keyless signing with Sigstore/cosign for CI pipelines. No keys to manage means no keys to compromise.
  3. Verify at every trust boundary — before build, before deploy, before pod admission. Signing without verification is security theater.
  4. Use SSH signing for Git commits if your team already uses SSH keys. It is simpler than GPG and has first-class GitHub support.
  5. Enable npm provenance for public packages. It is a one-flag change that protects your downstream consumers.
  6. Enforce with admission controllers in Kubernetes. Kyverno and Connaisseur can reject unsigned images at the cluster level.
  7. Automate everything. If signing or verification requires a manual step, it will be skipped. Bake it into the pipeline.

Code signing is not glamorous work. It does not add features or improve performance. But it is the cryptographic backbone that makes every other security control trustworthy. Without it, your SBOM is unverified, your vulnerability scans are unattested, and your deployment pipeline is an honor system.

Start with Git commit signing this week. Add cosign to your container builds next week. Enforce verification in your cluster the week after. Three weeks, three layers of trust, and a supply chain that is dramatically harder to compromise.

Share

Related Posts

Container Security — Docker and Kubernetes Hardening

Container Security — Docker and Kubernetes Hardening

Containers make deployment easy and security hard. That Dockerfile you copied…

Supply Chain Security — Protecting Your Software Pipeline

Supply Chain Security — Protecting Your Software Pipeline

In 2024, a single malicious contributor nearly compromised every Linux system on…

Security Ticketing and Incident Response

Security Ticketing and Incident Response

The worst time to figure out your incident response process is during an…

Security Mindset for Engineers — Think Like an Attacker

Security Mindset for Engineers — Think Like an Attacker

Most engineers think about security the way they think about flossing — they…

Secrets Management — Vault, SSM, and Secrets Manager

Secrets Management — Vault, SSM, and Secrets Manager

I’ve watched a production database get wiped because someone committed a root…

OWASP Top 10 for Cloud Applications

OWASP Top 10 for Cloud Applications

The OWASP Top 10 was written for traditional web applications. But in the cloud…

Latest Posts

AI Video Generation in 2025 — Models, Costs, and How to Build a Cost-Effective Pipeline

AI Video Generation in 2025 — Models, Costs, and How to Build a Cost-Effective Pipeline

AI video generation went from “cool demo” to “usable in production” in 2024-202…

AI Models in 2025 — Cost, Capabilities, and Which One to Use

AI Models in 2025 — Cost, Capabilities, and Which One to Use

Choosing the right AI model is one of the most impactful decisions you’ll make…

AI Image Generation in 2025 — Models, Costs, and How to Optimize Spend

AI Image Generation in 2025 — Models, Costs, and How to Optimize Spend

Generating one image with AI costs between $0.002 and $0.12. That might sound…

AI Agents Demystified — It's Just Automation With a Better Brain

AI Agents Demystified — It's Just Automation With a Better Brain

Let’s cut through the noise. If you read Twitter or LinkedIn, you’d think “AI…

AI Coding Assistants in 2025 — Every Tool Compared, and Which One to Actually Use

AI Coding Assistants in 2025 — Every Tool Compared, and Which One to Actually Use

Two years ago, AI coding meant one thing: GitHub Copilot autocompleting your…

Supply Chain Security — Protecting Your Software Pipeline

Supply Chain Security — Protecting Your Software Pipeline

In 2024, a single malicious contributor nearly compromised every Linux system on…