
September 10, 2026
13 min read
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.
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.
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.
| Criteria | GPG signing | SSH signing (Git 2.34+) |
|---|---|---|
| Key management | Separate GPG keyring and gpg-agent | Reuses familiar ssh-keygen and ssh-agent |
| Host support | Universal on GitHub, GitLab, Bitbucket | GitHub and GitLab; verify your host version |
| Local verification | Built-in with gpg installed | Requires allowed_signers file |
| CI verification | Import GPG pubkey into CI secret | Import SSH pubkey; simpler for many teams |
| Expiry and rotation | Key expiry built into GPG model | Manual rotation; no built-in expiry |
| Learning curve | Steeper; pinentry and trust db issues | Lower 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.
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:
- Enable Require signed commits.
- Enable Require pull request reviews before merge.
- 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:
- Generate a new signing key and upload the public half to your host.
- Update local Git config to point at the new key ID or file path.
- Keep the old public key published until all open branches are rebased or merged.
- 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.
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.emaildiffers 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 trueglobally 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
mainand all release branches. - Add a
git verify-commitloop 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
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.

