Signed Commits in Git: GPG, SSH and Verification
Signing attaches a cryptographic signature to a commit or tag, proving it was created by someone holding a particular private key and has not been altered since.
Understanding the boundary of that claim matters as much as the mechanics. A signature says a key signed this object. It does not say the code is correct, the change was reviewed, or the person behind the key is who you think.
Why author fields are not enough
Section titled “Why author fields are not enough”Git records author and committer identities as free text. Nothing verifies them:
git commit --author="Linus Torvalds <torvalds@linux-foundation.org>" -m "Definitely legitimate"That commit is created without complaint. Git has no user model and no authentication — the fields are metadata you supply.
For most repositories this is fine. Where attribution carries weight — a security-sensitive project, a regulated environment, an open source project accepting external contributions — cryptographic signatures are what make it verifiable.
Two formats
Section titled “Two formats”| SSH signing | GPG / OpenPGP | |
|---|---|---|
| Setup | Simple — reuse an existing SSH key | More involved |
| Key management | Files, plus an allowed-signers list | Keyring, web of trust |
| Expiry | Not built in | Built in |
| Revocation | Manual list management | Built in |
| Tooling | ssh-keygen | gpg |
| Best for | Most teams | Organisations needing expiry and revocation |
There is also S/MIME, using X.509 certificates, which appears mainly in organisations that already run a certificate authority.
For most people SSH signing is the right starting point: you probably already have a key, and the setup is three configuration lines.
SSH signing
Section titled “SSH signing”-
Use an existing SSH key, or create one:
Terminal window ssh-keygen -t ed25519 -C "you@example.com" -
Tell Git to sign with SSH, and which key:
Terminal window git config --global gpg.format sshgit config --global user.signingkey ~/.ssh/id_ed25519.pubNote this is the public key. Git uses it to identify which private key to sign with.
-
Sign automatically:
Terminal window git config --global commit.gpgsign truegit config --global tag.gpgsign trueWithout these, sign individual commits with
git commit -S. -
Set up verification. To verify signatures locally, Git needs to know which keys belong to whom:
Terminal window echo "you@example.com $(cat ~/.ssh/id_ed25519.pub)" >> ~/.config/git/allowed_signersgit config --global gpg.ssh.allowedSignersFile ~/.config/git/allowed_signersEach line is an email address followed by a public key. Without this file Git can confirm a signature exists but cannot say whose it is.
Now commit:
git commit -S -m "Signed commit"git log --show-signature -1commit 13aac4f6f0655e4f3eabcd2a6ae531d259c1d611Good "git" signature for dev@example.com with ED25519 key SHA256:5Rme+nfCaCP9Md+7DkJbvUeBEBdcWI0wQeXCDCJsCwUAuthor: Dev <dev@example.com>Date: Sat Aug 22 18:58:09 2026 +0000
Signed commitGPG signing
Section titled “GPG signing”gpg --full-generate-keyChoose RSA 4096 or Ed25519, set an expiry — one to two years is a reasonable default — and use the same email address as your Git identity.
List the key:
gpg --list-secret-keys --keyid-format=longsec ed25519/ABCD1234EXAMPLE 2026-08-22 [SC] [expires: 2028-08-22] 1234567890ABCDEF1234567890ABCDEF12345678uid [ultimate] Dev <dev@example.com>The identifier after the algorithm — ABCD1234EXAMPLE here — is your key ID.
git config --global gpg.format openpgpgit config --global user.signingkey ABCD1234EXAMPLEgit config --global commit.gpgsign trueIf Git cannot find gpg, point at it explicitly:
git config --global gpg.program gpgTo upload to a hosting provider:
gpg --armor --export ABCD1234EXAMPLEPaste the block into the provider’s GPG key settings. The private key never leaves your machine.
What the signature looks like
Section titled “What the signature looks like”A signature is a header in the commit object, alongside tree, parent and author:
git cat-file -p HEAD | head -6tree 4997ca7a42e3ad9b729fbad3acd44fbabd07b6bdauthor Dev <dev@example.com> 1787425089 +0000committer Dev <dev@example.com> 1787425089 +0000gpgsig -----BEGIN SSH SIGNATURE----- U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgzP4J+Mt7Gna5M2CxilW9hUVvHW i2vj7/ZvE3KYSZ7tYAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5Two consequences follow from the signature being part of the commit object:
Signing changes the commit ID. The signature is included in the hash, so a signed commit and its unsigned equivalent are different objects.
Any rewrite drops the signature. Rebase, amend, cherry-pick and squash all create new commits, and the new ones are unsigned unless the operation signs them.
Verifying
Section titled “Verifying”git log --show-signature -1For a compact view across many commits, use the format placeholders:
git log --format='%h %G? %GS %s' -313aac4f G dev@example.com Signed commit61500c8 N Unsigned%G? | Meaning |
|---|---|
G | Good signature |
B | Bad signature — the object was altered |
U | Good signature, unknown validity |
X | Good signature that has expired |
Y | Good signature made by an expired key |
R | Good signature made by a revoked key |
E | Signature could not be checked (missing key) |
N | No signature |
git log --format='%h %G? %s' is the practical audit command: it shows at a glance which commits on a
branch are signed.
Tags verify separately:
git tag -s v1.0.0 -m "Signed release"git tag -v v1.0.0A signed tag is often more valuable than signed commits for a release process: one signature attests to the exact tree being released.
Platform verification
Section titled “Platform verification”Hosting platforms show a badge next to commits. The mechanics are the same, with the platform holding the public keys you have registered.
GitHub supports GPG, SSH and S/MIME signatures, and shows:
- Verified — signed, and the signature was successfully verified.
- Unverified — signed, but the signature could not be verified.
- Nothing at all — the commit is unsigned.
Vigilant mode changes what unsigned commits mean. With it enabled, unsigned commits attributed to you are marked Unverified rather than simply unbadged, and a Partially verified state appears when a signed commit’s author has vigilant mode on but did not consent to the change. It is off by default.
Repository administrators can require signed commits on a branch through protected-branch settings.
Signing and rebasing
Section titled “Signing and rebasing”Because a rewrite creates new commits, signatures do not survive it. To re-sign during a rebase:
git rebase --exec 'git commit --amend --no-edit -S' mainOr more directly:
git rebase -S mainThe same applies to cherry-picking:
git cherry-pick -S <commit>This is a genuine friction point for teams that both require signed commits and rebase routinely. The
practical resolutions are to sign automatically with commit.gpgsign true — which covers --amend — and
to remember -S on rebase and cherry-pick.
What a signature proves
Section titled “What a signature proves”Being precise here matters, because signing is frequently oversold.
A signature does prove:
- The commit object was signed by someone holding that private key.
- The object has not been modified since it was signed — the tree, parents, author, committer and message are all covered.
- If you have independently established which key belongs to whom, it links the commit to that key holder.
A signature does not prove:
- That the code is correct or safe. Signing attests to origin, not quality. A signed commit can introduce a vulnerability.
- That the change was reviewed. That is a process claim, recorded elsewhere.
- That the person is who they claim. It proves control of a key. Whether that key belongs to a particular human is established by whatever key-distribution process you trust.
- That the key was not stolen. A compromised key produces perfectly valid signatures.
- That the author wrote it. The committer signs. After a rebase, the signer is whoever rebased, while the author fields still name someone else.
- That the whole history is trustworthy. A signature covers one commit. Unsigned ancestors remain unsigned.
When it is worth it
Section titled “When it is worth it”Adopt signing when:
- Attribution has compliance or contractual weight.
- The project accepts contributions from people you do not know.
- You are publishing releases others depend on — sign the tags at minimum.
- Your organisation requires it.
- The repository is a high-value target.
Probably not worth it when:
- A small trusted team on a private repository, where everyone already has authenticated access.
- Nobody will ever check the signatures. Unverified signatures provide no security, only ceremony.
The middle path many projects take: sign release tags, do not require signed commits. It gives consumers something verifiable at the point it matters, without imposing setup on every contributor.
Troubleshooting
Section titled “Troubleshooting”error: gpg failed to sign the data. Usually the agent cannot prompt:
export GPG_TTY=$(tty)Test signing directly:
echo test | gpg --clearsignNo secret key. The configured user.signingkey does not match a key you hold. Check with
gpg --list-secret-keys --keyid-format=long.
Signatures show as Unverified on the platform. The public key is not registered, or the commit’s email does not match a verified address on your account.
gpg.ssh.allowedSignersFile needs to be configured. Local verification needs the allowed-signers file.
Signing works without it; verifying does not.
Signatures vanished after a rebase. Expected — rewriting creates new commits. Re-sign with
git rebase -S.
Different results locally and on the platform. They use different key stores: your keyring or allowed-signers file locally, your registered keys on the platform.
Rolling it out to a team
Section titled “Rolling it out to a team”Requiring signatures is straightforward technically and mostly an adoption problem.
-
Decide what you are actually protecting. “Every commit on
mainis attributable to a registered key” is a different goal from “releases are verifiably ours”. The second is achieved by signed tags alone and costs contributors nothing. -
Pick one format. Mixing GPG and SSH across a team doubles the support burden. SSH is easier unless you specifically need expiry and revocation.
-
Document the setup as copy-pasteable commands. Four lines of
git configplus registering a public key. Anything longer and adoption stalls. -
Turn it on in warn mode first. Let people see unverified badges on their own commits before anything is blocked.
-
Then enforce, via the branch protection rule.
-
Have a plan for automation. CI systems that create commits need their own signing identity, or an exemption. This is the step that gets forgotten and blocks the release pipeline on the day you enforce.
Key hygiene
Section titled “Key hygiene”Protect the private key with a passphrase. An unprotected key is a credential in plain text.
Set an expiry on GPG keys. One to two years. Expiry can be extended; it limits the damage from a key you forget about.
Have a revocation certificate ready before you need it:
gpg --output revoke.asc --gen-revoke ABCD1234EXAMPLEStore it somewhere separate from the key. Without it, a lost key cannot be revoked.
Never commit key material. Add *.gpg, *.key, id_* and similar to your ignore rules.
Use different keys for different purposes where practical — a signing key separate from an authentication key limits the blast radius of a compromise.
Revoke immediately on suspicion, and rotate.
Common mistakes
Section titled “Common mistakes”Believing a signature means the code is safe. It attests to origin only.
Signing commits but not release tags. Tags are what consumers verify.
Forgetting signatures do not survive rewriting.
Configuring user.signingkey with a private key path for SSH. Use the .pub file.
Requiring signed commits without checking the merge method. Platform-generated commits are signed by the platform, not by you.
No passphrase on the signing key.
No expiry and no revocation certificate.
Requiring signatures nobody verifies. If no automation checks them, the requirement is ceremony.
Mental Model
Section titled “Mental Model”A signature is a wax seal.
It shows the envelope was closed by whoever holds that seal, and that nobody has opened it since. It says nothing about whether the letter inside is true, sensible or safe.
And if someone steals the seal, their letters look exactly as authentic as yours — which is why key protection, expiry and revocation matter as much as the signing itself.
What You Learned
Section titled “What You Learned”- Git author and committer fields are unverified free text; signatures make attribution verifiable.
- SSH signing is the simplest option; GPG adds built-in expiry and revocation.
- SSH signing needs
gpg.format ssh,user.signingkeypointing at the public key, and an allowed-signers file for local verification. - The signature is a header inside the commit object, so it is covered by the commit’s hash.
- Any rewrite — rebase, amend, cherry-pick, squash — drops signatures unless the operation re-signs.
git log --format='%h %G? %s'audits which commits are signed.- Platform web-based merges create commits without your signature.
- A signature proves key control and integrity — not correctness, review, identity, or that the key is uncompromised.
Try It Yourself
Section titled “Try It Yourself”SSH signing end to end, using a throwaway key. Nothing here touches your real keys.
-
Create a scratch key and repository:
Terminal window mkdir ~/signing-lab && cd ~/signing-labssh-keygen -q -t ed25519 -f ./signkey -N "" -C "dev@example.com"git init repo && cd repo -
Configure signing with the scratch key:
Terminal window git config gpg.format sshgit config user.signingkey ~/signing-lab/signkey.pub -
Set up verification:
Terminal window printf "dev@example.com $(cat ~/signing-lab/signkey.pub)\n" > ~/signing-lab/allowed_signersgit config gpg.ssh.allowedSignersFile ~/signing-lab/allowed_signers -
Make a signed commit:
Terminal window echo x > f.txt && git add . && git commit -S -m "Signed commit"git log --show-signature -1 -
Look inside the object:
git cat-file -p HEAD | head -6. Find thegpgsigheader. -
Make an unsigned commit and compare:
Terminal window echo y >> f.txt && git commit -am "Unsigned"git log --format='%h %G? %s' -2Predict the two status codes.
-
Sign a tag and verify it:
Terminal window git tag -s v1.0.0 -m "Signed release"git tag -v v1.0.0 -
Watch a signature disappear:
Terminal window git commit --amend --no-edit --no-gpg-signgit log --format='%h %G? %s' -1 -
Clean up:
rm -rf ~/signing-lab.
Step 8 is the lesson people learn the hard way: rewriting a signed commit produces an unsigned one, and nothing warns you.
You have finished Pillar 2
Section titled “You have finished Pillar 2”Across four clusters you have covered how work diverges, how it comes back together, how history is reshaped, and the features that make all of it workable at scale.