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 Commits with GPG and SSH for Supply-Chain Trust

By Kokil Thapa | Last reviewed: September 2026

Unsigned Git commits are easy to forge. Anyone who knows your name and email can push history that looks like yours. That gap is a real supply-chain risk when a compromised laptop, stolen token, or malicious fork can slip code into production. To Sign Commits with GPG and SSH for Supply-Chain Trust, you bind each commit to a private key only you hold. Platforms like GitHub and GitLab show a verified badge, and CI can reject unsigned work before deploy. This guide walks through both methods on Linux and macOS, with enforcement patterns I use on SLSA-aligned pipelines and shared Deployer releases.

Why should you sign Git commits for supply-chain trust?

Git stores author name and email as plain text. Those fields are not proof of identity. An attacker with write access can amend history or push from a fork with your metadata attached.

Signed commits add a cryptographic signature over the commit object. The signature includes the tree hash, parent commits, author, committer, and message. Change one byte and verification fails.

That matters for three practical reasons on teams I work with:

  • Impersonation resistance: Only someone with your private key can produce a valid signature tied to your published public key.
  • Audit trail integrity: Reviewers and compliance tools can confirm who actually authored each change, not just who the metadata claims.
  • Pipeline gatekeeping: CI can require verified signatures before merge, pairing well with SBOM generation and image signing via Cosign.

Commit signing is one layer. It does not replace code review, dependency scanning, or branch protection. It closes a gap that password-only Git auth leaves wide open.

Supply-Chain Trust FlowDeveloperPrivate signing keySigned CommitGPG or SSH sigGit HostVerified badgeCI PipelineSignature gateProductionTrusted deployAttack blocked: forged author metadata fails signature checkNo private key = no valid verified commit
Sign Commits with GPG and SSH for Supply-Chain Trust — end-to-end flow from local signing key to verified production deploy

On sister sites I maintain with Deployer 7 and GitLab CI, verified commits sit alongside signed container images. The goal is the same: prove origin before anything reaches users. If you already hardened server access with SSH key-only authentication, commit signing extends that identity model into version control.

How do you generate and configure a GPG key for Git commit signing?

GPG (GNU Privacy Guard) has been the default signing method for years. Git calls gpg to sign commit and tag objects. Most hosting platforms support GPG public keys out of the box.

Generate a dedicated signing key

Use a key separate from encryption or email. A 4096-bit RSA key or Ed25519 cert key both work. Ed25519 is faster and shorter.

gpg --full-generate-key
# Choose: (9) ECC, Curve 25519, Sign only
# Real name and email must match your Git host account
# Set a strong passphrase — use a /tools/password-generator if needed

List keys and copy the key ID:

gpg --list-secret-keys --keyid-format=long

# sec   ed25519/3F2A1B4C5D6E7F80 2026-09-01 [SC]
#                ^^^^^^^^^^^^^^^^
# This is your key ID

Configure Git to sign with GPG

Set global or per-repository config. I prefer global signing on with a per-repo override for legacy repos that are not ready yet.

git config --global user.signingkey 3F2A1B4C5D6E7F80
git config --global commit.gpgsign true
git config --global tag.gpgSign true
git config --global gpg.program gpg

On macOS, point Git at pinentry so passphrase prompts work in Terminal:

echo "pinentry-program $(brew --prefix)/bin/pinentry-mac" >> ~/.gnupg/gpg-agent.conf
gpgconf --kill gpg-agent

Publish the public key to GitHub or GitLab

Export the armored public key and paste it into your account settings:

gpg --armor --export 3F2A1B4C5D6E7F80

GitHub matches the signing key email to your verified account emails. GitLab follows the same rule. A mismatch produces an unverified commit even when the signature itself is valid.

Verify locally before you push

git commit -S -m "feat: add payment webhook retry logic"
git log --show-signature -1

# Good signature from Kokil Thapa <you@example.com>

If you see gpg: signing failed: No secret key, your user.signingkey does not match a secret key in your keyring. Run gpg --list-secret-keys and fix the ID.

GPG Commit Signing Steps1. Generate Keygpg --full-generate-key2. Configure Gitcommit.gpgsign true3. Export Publicgpg --armor --export4. Upload KeyGit host settingsgit commit -S creates signed objectSignature embedded in commit; push shows Verified on hostCommon failureEmail mismatch = UnverifiedFixMatch git config email to host
GPG commit signing workflow — generate key, configure Git, publish public key, and verify before push

Official reference: the Git documentation on signing your work covers tag signing and signature verification flags in detail.

How do you set up SSH commit signing in Git 2.34 and later?

Since Git 2.34, you can sign commits with an SSH key instead of GPG. If you already manage deploy keys or host authentication with Ed25519, this path reduces keyring friction. GitHub added SSH signing support in 2022; GitLab followed shortly after.

Create or reuse an SSH signing key

Keep signing keys separate from authentication keys. Compromise of one should not automatically compromise the other.

ssh-keygen -t ed25519 -C "signing@example.com" -f ~/.ssh/id_ed25519_signing

# No passphrase skip on shared machines — always passphrase-protect

Configure Git for SSH signing format

git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519_signing.pub
git config --global commit.gpgsign true
git config --global tag.gpgSign true

Git reads the public key file path from user.signingkey. The private key must be available to the SSH agent or referenced directly.

Register the public key as a signing key on your host

On GitHub, go to Settings → SSH and GPG keys → New SSH key → Key type: Signing Key. Paste the .pub contents. Do not upload it as an authentication key unless you intend to use it for both purposes.

On GitLab, add the key under User Settings → SSH Keys and select Signing Key as usage type.

Allow Git to locate allowed signers for local verification

For local git log --show-signature with SSH format, maintain an allowed signers file:

# ~/.config/git/allowed_signers
you@example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5... signing@example.com
git config --global gpg.ssh.allowedSignersFile ~/.config/git/allowed_signers

Commit and push. Your host displays a verified badge when the signing key is registered correctly.

git commit -m "fix: validate webhook signature before processing"
git log --show-signature -1
# Good "git" signature with ED25519 key

Pair this setup with Git hooks that automate pre-commit checks so unsigned commits never reach your working branch locally.

Should you choose GPG or SSH keys for Git commit signatures?

Both satisfy the same trust goal. The right choice depends on what your team already operates and which host features you need.

CriteriaGPG signingSSH signing (Git 2.34+)
Key managementSeparate GPG keyring and gpg-agentReuses familiar ssh-keygen and ssh-agent
Host supportUniversal on GitHub, GitLab, BitbucketGitHub and GitLab; verify your host version
Local verificationBuilt-in with gpg installedRequires allowed_signers file
CI verificationImport GPG pubkey into CI secretImport SSH pubkey; simpler for many teams
Expiry and rotationKey expiry built into GPG modelManual rotation; no built-in expiry
Learning curveSteeper; pinentry and trust db issuesLower if team already uses SSH everywhere

My practical recommendation: new teams in 2026 should default to SSH signing if everyone already uses Ed25519 keys for server access. Choose GPG when you need key expiry, web-of-trust models, or cross-tool signature compatibility beyond Git.

On production Laravel apps I deploy through GitLab CI, SSH signing reduced onboarding time. Developers already had ssh-agent running for Git over SSH. Adding a signing key was one extra file, not a new agent stack.

GPG vs SSH Signing DecisionNeed commit signatures?Team uses SSH daily?Yes → SSH signingNeed key expiry?Yes → GPG signingSSH pathgpg.format ssh + ssh-agentGPG pathgpg-agent + key uploadEither method: enforce on protected branches + verify in CI
Decision chart for choosing GPG or SSH commit signing based on team tooling and key-management needs

How do you enforce signed commits in CI/CD and on Git hosting platforms?

Signing locally helps nothing if unsigned commits can merge to main. Enforcement belongs in branch protection and CI gates.

Enable branch protection rules

On GitHub, open Settings → Branches → Branch protection rules for main:

  1. Enable Require signed commits.
  2. Enable Require pull request reviews before merge.
  3. Restrict who can push directly to the protected branch.

GitLab offers similar settings under Settings → Repository → Push Rules. Enable Reject unsigned commits for protected branches.

Verify signatures in CI before deploy

On sister sites sharing Deployer 7 and GitLab CI — including Translation Nepal and related legal-tech portals — I add a verify stage before build:

verify-commits:
  stage: test
  image: alpine/git:latest
  script:
    - apk add --no-cache gnupg
    - gpg --import "$CI_GPG_PUBLIC_KEY"
    - |
      for commit in $(git rev-list origin/main..HEAD); do
        git verify-commit $commit || exit 1
      done
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"

For SSH-signed repos, import the team allowed signers file and use git verify-commit with gpg.format ssh configured in the job environment.

Sign release tags and pair with semantic versioning

Tags mark release boundaries. Unsigned tags are as forgeable as unsigned commits. Enable tag signing globally and treat annotated signed tags as release artifacts.

This pairs naturally with Conventional Commits and semantic versioning. Your changelog tooling reads commit messages; signature verification proves those commits are authentic.

Rotate keys without breaking history

When rotating a compromised or expired key:

  1. Generate a new signing key and upload the public half to your host.
  2. Update local Git config to point at the new key ID or file path.
  3. Keep the old public key published until all open branches are rebased or merged.
  4. Document the rotation date in your team runbook or internal wiki.

If you lose a private key entirely, past signatures remain valid. Future commits need a new key. Recovery of lost work is a separate problem — see recover lost commits with Git reflog for history rescue, not identity repair.

CI Enforcement PipelinePush / MRSigned commitsVerify Siggit verify-commitLint + TestPHPUnit, ESLintDeployDeployer 7Unsigned commit → pipeline fails at verify stageBranch protection blocks merge before CI even runsHost-level gateRequire signed commits ruleon main and release branchesPipeline-level gateverify-commit loop on MRrange before build stage
CI/CD pipeline enforcing signed commits before tests and Deployer release on production Laravel applications

GitHub documents SSH commit signing at their commit signature verification guide. GitLab push rules are described in the GitLab push rules documentation.

For broader context, read zero-trust security for multi-cloud and AI code review in CI. Signing proves authorship; review still catches bad logic.

What production mistakes break commit signing workflows?

These failures show up repeatedly on client projects and shared infrastructure I maintain through Linux system administration engagements.

  • Email mismatch: Git user.email differs from the verified email on your signing key. Fix config, not the host.
  • Agent not running in CI: Headless runners cannot prompt for a passphrase. Use a dedicated CI signing key with passphrase stored in a protected secret, or sign only on developer machines and verify in CI.
  • Signing only commits, not tags: Release tags float outside branch protection. Sign tags with tag.gpgSign true.
  • Mixing auth and signing keys: One compromised key should not unlock both SSH login and commit identity. Split them.
  • Forgetting allowed_signers with SSH: Local verification fails silently or with cryptic errors until the file exists.

On booking platforms like Adventure Third Pole Trek, a bad deploy from unverified history is a business outage, not just a git hygiene issue. Treat signature enforcement as part of your release checklist alongside database backups and ongoing maintenance.

Harden the full chain: SSH hardening with fail2ban, signed commits, signed images, and dependency lockfiles audited in CI. Each layer catches a different attacker path.

Key Takeaways

  • Enable commit.gpgsign true globally so unsigned commits cannot slip through by habit.
  • Choose SSH signing for teams already fluent with ssh-agent; use GPG when you need built-in key expiry.
  • Upload public keys to your Git host and confirm email addresses match exactly.
  • Turn on require-signed-commits branch protection on main and all release branches.
  • Add a git verify-commit loop in CI before your build or Deployer stage runs.
  • Sign annotated release tags and document key rotation in your team runbook.

People Also Ask

Does commit signing slow down my Git workflow?

Signing adds roughly 100–300 milliseconds per commit on modern hardware. Ed25519 SSH signing is typically faster than 4096-bit RSA GPG. The passphrase prompt is the main friction point, not the crypto itself. Running gpg-agent or ssh-agent with a reasonable cache timeout removes most daily prompts.

Can I sign commits in GitHub Actions or GitLab CI automatically?

Yes, but store the private key in a protected CI variable and limit which branches may use it. Many teams prefer developers sign locally and CI only verifies. That keeps private keys off shared runners. If CI must sign release tags, use a dedicated machine identity key with narrow scope.

Are signed commits required for SLSA compliance?

SLSA Level 3 and above expects provenance that ties artifacts to source revision and builder identity. Signed commits are one input to that chain. They pair with provenance attestations and signed build outputs. Read the full picture in supply-chain security with SLSA.

What happens to old commits when I rotate my signing key?

Existing signatures remain valid as long as the old public key stays published on your Git host. New commits use the new key. There is no need to rewrite history unless you are removing a compromised key from active trust and your policy requires it.

Build verifiable supply chains starting at the commit

Sign Commits with GPG and SSH for Supply-Chain Trust is a low-cost control with high payoff. Setup takes under an hour per developer. Enforcement in branch rules and CI takes another hour on the repository side. The result is a verifiable authorship chain from laptop to production.

If you want help wiring signed commits into GitLab CI, Deployer releases, or a full hardening review for a Laravel or legal-tech platform, see custom software development or reach out through contact us. Strong identity at the commit layer is where a trustworthy deploy pipeline starts.

Frequently Asked Questions

Git author name and email are plain text anyone can forge. Signed commits add a cryptographic signature over the commit object—tree hash, parents, author, committer, and message. Change one byte and verification fails. Platforms like GitHub and GitLab show a verified badge, and CI can reject unsigned work before deploy. It closes an impersonation gap that password-only Git auth leaves open, though it does not replace code review or dependency scanning.

Generate a dedicated signing-only key separate from encryption keys—Ed25519 or 4096-bit RSA both work, with Ed25519 faster. Run gpg --full-generate-key, choose ECC Curve 25519 Sign only, and match your real name and email to your Git host account. Set git config --global user.signingkey to your key ID, enable commit.gpgsign and tag.gpgSign, then export the armored public key with gpg --armor --export and paste it into GitHub or GitLab account settings.

Since Git 2.34, you can sign commits with an SSH key instead of GPG. Create a separate Ed25519 signing key with ssh-keygen, keeping it apart from authentication keys. Set gpg.format to ssh, point user.signingkey at the .pub file path, and enable commit.gpgsign and tag.gpgSign globally. Register the public key on GitHub or GitLab as a Signing Key—not an authentication key. For local verification, maintain a gpg.ssh.allowedSignersFile listing trusted signers.

Both satisfy the same trust goal. SSH signing fits teams already fluent with ssh-agent and Ed25519 server keys—it reduced onboarding time on Laravel apps I deploy through GitLab CI because developers already had ssh-agent running. Choose GPG when you need built-in key expiry, web-of-trust models, or cross-tool signature compatibility beyond Git. GPG has universal host support; SSH works on GitHub and GitLab but requires an allowed_signers file for local verification.

Signing locally helps nothing if unsigned commits can merge to main. On GitHub, enable Require signed commits under branch protection for main, plus pull request reviews and push restrictions. GitLab offers Reject unsigned commits under Push Rules. In CI, add a verify stage before build—import the team GPG public key or allowed signers file, then loop git verify-commit over each commit in the merge request. On sister sites using Deployer 7 and GitLab CI, I run this verify stage before the Deployer release.

The most common cause is email mismatch. GitHub matches the signing key email to your verified account emails; GitLab follows the same rule. If git user.email differs from the email on your signing key, the host shows unverified even though gpg or SSH verification succeeds locally. Fix your Git config, not the host settings. Also confirm you uploaded the public key to the correct account and registered SSH keys as Signing Key type, not authentication keys only.

Signing adds roughly 100–300 milliseconds per commit on modern hardware. Ed25519 SSH signing is typically faster than 4096-bit RSA GPG. The passphrase prompt is the main friction, not the crypto itself.

Email mismatch between Git config and verified host emails is the top failure. In CI, headless runners cannot prompt for passphrases—use a dedicated CI signing key with passphrase in a protected secret, or have developers sign locally while CI only verifies. Teams often sign commits but forget tags; enable tag.gpgSign globally because release tags sit outside branch protection. Mixing authentication and signing keys means one compromise hits both. With SSH format, forgetting the allowed_signers file causes cryptic local verification errors.

Yes, but store the private key in a protected CI variable and limit which branches may use it. Many teams prefer developers sign locally and CI only verifies—that keeps private keys off shared runners. If CI must sign release tags, use a dedicated machine identity key with narrow scope rather than a developer personal key. On production pipelines I maintain, verification before build is the safer default than automated signing on shared infrastructure.

SLSA Level 3 and above expects provenance tying artifacts to source revision and builder identity. Signed commits are one input to that chain, not the whole picture.

Existing signatures remain valid as long as the old public key stays published on your Git host. Generate a new signing key, upload its public half, update local Git config to point at the new key ID or file path, and keep the old public key published until all open branches are rebased or merged. Document the rotation date in your team runbook. If you lose a private key entirely, past signatures stay valid but future commits need a new key—recovery of lost work is a separate problem from identity repair.

Yes. Release tags mark release boundaries and float outside branch protection rules, making unsigned tags as forgeable as unsigned commits. Enable tag.gpgSign true globally alongside commit.gpgsign. Treat annotated signed tags as release artifacts paired with semantic versioning—your changelog tooling reads commit messages while signature verification proves those commits are authentic. On Deployer 7 release pipelines I maintain, unsigned tags have slipped through branch protection before; tag signing closes that gap.

Setup takes under an hour per developer for key generation, Git config, and uploading the public key to GitHub or GitLab. Enforcement in branch protection rules and a CI verify-commit stage takes roughly another hour on the repository side. The article describes this as a low-cost control with high payoff—a verifiable authorship chain from laptop to production without heavy infrastructure investment.

When using gpg.format ssh, local git log --show-signature requires an allowed signers file Git can read. Create ~/.config/git/allowed_signers with lines mapping each trusted email to an ssh-ed25519 public key, then set gpg.ssh.allowedSignersFile in global Git config. Without this file, local SSH signature verification fails silently or with cryptic errors even when GitHub or GitLab shows a verified badge remotely. CI jobs verifying SSH-signed commits should import the same team allowed signers file into the job environment.

Commit signing is one layer, not a replacement for code review, dependency scanning, or branch protection. On sister sites I maintain with Deployer 7 and GitLab CI, verified commits sit alongside signed container images via Cosign—the goal is proving origin before anything reaches users. Pair signing with SBOM generation, dependency lockfile auditing in CI, SSH hardening with fail2ban, and SLSA-aligned provenance attestations. Each layer catches a different attacker path, from compromised laptops and stolen tokens to malicious forks pushing forged history into production.

Share this article

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.

Quick Contact Options
Choose how you want to connect me: