Skip to content

Signed Commits in Git: GPG, SSH and Verification

Lesson 11 of 11Intermediate → Advanced11 min readModern Git Workflows · Modern Git ProductivityVerified: Git 2.43.0 on Ubuntu 24.04; SSH signing verified end to end. Platform behaviour checked against GitHub's signature-verification documentation

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.

Git records author and committer identities as free text. Nothing verifies them:

Terminal window
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.

SSH signingGPG / OpenPGP
SetupSimple — reuse an existing SSH keyMore involved
Key managementFiles, plus an allowed-signers listKeyring, web of trust
ExpiryNot built inBuilt in
RevocationManual list managementBuilt in
Toolingssh-keygengpg
Best forMost teamsOrganisations 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.

  1. Use an existing SSH key, or create one:

    Terminal window
    ssh-keygen -t ed25519 -C "you@example.com"
  2. Tell Git to sign with SSH, and which key:

    Terminal window
    git config --global gpg.format ssh
    git config --global user.signingkey ~/.ssh/id_ed25519.pub

    Note this is the public key. Git uses it to identify which private key to sign with.

  3. Sign automatically:

    Terminal window
    git config --global commit.gpgsign true
    git config --global tag.gpgsign true

    Without these, sign individual commits with git commit -S.

  4. 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_signers
    git config --global gpg.ssh.allowedSignersFile ~/.config/git/allowed_signers

    Each 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:

Terminal window
git commit -S -m "Signed commit"
git log --show-signature -1
commit 13aac4f6f0655e4f3eabcd2a6ae531d259c1d611
Good "git" signature for dev@example.com with ED25519 key SHA256:5Rme+nfCaCP9Md+7DkJbvUeBEBdcWI0wQeXCDCJsCwU
Author: Dev <dev@example.com>
Date: Sat Aug 22 18:58:09 2026 +0000
Signed commit
Terminal window
gpg --full-generate-key

Choose 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:

Terminal window
gpg --list-secret-keys --keyid-format=long
sec ed25519/ABCD1234EXAMPLE 2026-08-22 [SC] [expires: 2028-08-22]
1234567890ABCDEF1234567890ABCDEF12345678
uid [ultimate] Dev <dev@example.com>

The identifier after the algorithm — ABCD1234EXAMPLE here — is your key ID.

A signature is a header in the commit object, alongside tree, parent and author:

Terminal window
git cat-file -p HEAD | head -6
tree 4997ca7a42e3ad9b729fbad3acd44fbabd07b6bd
author Dev <dev@example.com> 1787425089 +0000
committer Dev <dev@example.com> 1787425089 +0000
gpgsig -----BEGIN SSH SIGNATURE-----
U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgzP4J+Mt7Gna5M2CxilW9hUVvHW
i2vj7/ZvE3KYSZ7tYAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5

Two 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.

Terminal window
git log --show-signature -1

For a compact view across many commits, use the format placeholders:

Terminal window
git log --format='%h %G? %GS %s' -3
13aac4f G dev@example.com Signed commit
61500c8 N Unsigned
%G?Meaning
GGood signature
BBad signature — the object was altered
UGood signature, unknown validity
XGood signature that has expired
YGood signature made by an expired key
RGood signature made by a revoked key
ESignature could not be checked (missing key)
NNo 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:

Terminal window
git tag -s v1.0.0 -m "Signed release"
git tag -v v1.0.0

A signed tag is often more valuable than signed commits for a release process: one signature attests to the exact tree being released.

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.

Because a rewrite creates new commits, signatures do not survive it. To re-sign during a rebase:

Terminal window
git rebase --exec 'git commit --amend --no-edit -S' main

Or more directly:

Terminal window
git rebase -S main

The same applies to cherry-picking:

Terminal window
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.

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.

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.

error: gpg failed to sign the data. Usually the agent cannot prompt:

Terminal window
export GPG_TTY=$(tty)

Test signing directly:

Terminal window
echo test | gpg --clearsign

No 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.

Requiring signatures is straightforward technically and mostly an adoption problem.

  1. Decide what you are actually protecting. “Every commit on main is 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.

  2. Pick one format. Mixing GPG and SSH across a team doubles the support burden. SSH is easier unless you specifically need expiry and revocation.

  3. Document the setup as copy-pasteable commands. Four lines of git config plus registering a public key. Anything longer and adoption stalls.

  4. Turn it on in warn mode first. Let people see unverified badges on their own commits before anything is blocked.

  5. Then enforce, via the branch protection rule.

  6. 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.

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:

Terminal window
gpg --output revoke.asc --gen-revoke ABCD1234EXAMPLE

Store 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.

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.

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.

  • 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.signingkey pointing 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.

SSH signing end to end, using a throwaway key. Nothing here touches your real keys.

  1. Create a scratch key and repository:

    Terminal window
    mkdir ~/signing-lab && cd ~/signing-lab
    ssh-keygen -q -t ed25519 -f ./signkey -N "" -C "dev@example.com"
    git init repo && cd repo
  2. Configure signing with the scratch key:

    Terminal window
    git config gpg.format ssh
    git config user.signingkey ~/signing-lab/signkey.pub
  3. Set up verification:

    Terminal window
    printf "dev@example.com $(cat ~/signing-lab/signkey.pub)\n" > ~/signing-lab/allowed_signers
    git config gpg.ssh.allowedSignersFile ~/signing-lab/allowed_signers
  4. Make a signed commit:

    Terminal window
    echo x > f.txt && git add . && git commit -S -m "Signed commit"
    git log --show-signature -1
  5. Look inside the object: git cat-file -p HEAD | head -6. Find the gpgsig header.

  6. Make an unsigned commit and compare:

    Terminal window
    echo y >> f.txt && git commit -am "Unsigned"
    git log --format='%h %G? %s' -2

    Predict the two status codes.

  7. Sign a tag and verify it:

    Terminal window
    git tag -s v1.0.0 -m "Signed release"
    git tag -v v1.0.0
  8. Watch a signature disappear:

    Terminal window
    git commit --amend --no-edit --no-gpg-sign
    git log --format='%h %G? %s' -1
  9. 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.

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.