GPG vs SSH Commit Signing Jump to heading
Commit signing is the mechanism that turns Gitβs authorship metadata from a self-asserted claim into a cryptographically verifiable fact. Anyone can set user.name and user.email to impersonate a colleague; only the holder of a private key can produce a valid signature over a commit object. This guide sits inside the broader Commit Signing & Git Supply-Chain Security discipline and answers the first architectural decision every team faces: sign with GPG (OpenPGP), the long-established standard, or with SSH, the newer format that Git has supported since v2.34. Both produce the same end result β a verified badge on your commits and a signature that verification gates can enforce β but they differ sharply in setup complexity, key distribution, and trust semantics. The sections below walk through configuring each end to end, then give you a comparison table and a decision framework so you can commit to one model and roll it out consistently.
Prerequisites Jump to heading
Before configuring either signing method, verify your environment meets these requirements:
GPG vs SSH Signing Paths Jump to heading
The diagram below contrasts the two signing pipelines. Both start from a commit object and end at a verified status, but the GPG path routes trust through a keyring and (optionally) a web of trust, while the SSH path routes trust through a flat allowed_signers file.
Step 1 β Generate a GPG Signing Key Jump to heading
Intent: Create an OpenPGP key pair whose private half signs commits and whose public half is distributed for verification.
Generate a modern key non-interactively. Ed25519 is compact and fast; use RSA 4096 only if a downstream tool cannot parse ECC keys:
# Generate an Ed25519 GPG key without the interactive prompt walkthrough
gpg --quick-generate-key "Ada Lovelace <[email protected]>" ed25519 sign 2y
# The email MUST match a verified email on your platform account.
# 2y sets a two-year expiry β a feature, not a limitation (forces rotation). Verify: List secret keys and capture the signing keyβs long ID (the 16-hex-digit fingerprint segment after the algorithm marker):
gpg --list-secret-keys --keyid-format=long
# Look for a line like: sec ed25519/ABCD1234EF567890 2026-07-05 [SC] [expires: 2028-07-05]
# ABCD1234EF567890 is the key ID you will hand to Git in Step 2. SAFETY WARNING: The generated private key lives in
~/.gnupg. Back up the entire directory (or export withgpg --export-secret-keys) to an encrypted, offline location before you rely on it. If you lose the private key with no backup, every signature it produced becomes unverifiable and you must generate a new key and re-establish trust. Never commit the exported secret key to a repository.
Step 2 β Configure Git to Sign with GPG Jump to heading
Intent: Tell Git to attach a GPG signature to every commit automatically, using the key from Step 1.
Set the format, the signing key, and enable automatic signing. Do this at --global scope for personal machines, or per-repository if you sign in only some repos:
# Select the OpenPGP signing backend (the default, but set it explicitly)
git config --global gpg.format openpgp
# Point Git at the signing key ID captured in Step 1
git config --global user.signingkey ABCD1234EF567890
# Sign every commit β not just when you pass -S
git config --global commit.gpgsign true
# Sign annotated tags too
git config --global tag.gpgsign true
# If gpg is not on PATH or you use gpg2, set the program explicitly:
git config --global gpg.program gpg Verify: Create a signed commit and inspect the signature. A valid signature prints Good signature:
git commit --allow-empty -m "chore: verify gpg signing"
git log --show-signature -1
# Expected excerpt:
# gpg: Good signature from "Ada Lovelace <[email protected]>" [ultimate] If signing fails with error: gpg failed to sign the data, your GPG agent likely cannot prompt for the passphrase. Export the TTY so the agent can reach your terminal:
# Add to your shell profile so pinentry can find the terminal
export GPG_TTY=$(tty) Step 3 β Upload the GPG Public Key to Your Platform Jump to heading
Intent: Give GitHub, GitLab, or Bitbucket the public half of your key so it can render the Verified badge on your commits.
Export the public key in ASCII-armored form and paste it into the platformβs GPG key settings:
# Print the ASCII-armored public key for the signing key ID
gpg --armor --export ABCD1234EF567890
# Copy the entire block, including the BEGIN/END PGP PUBLIC KEY BLOCK lines.
# GitHub: Settings β SSH and GPG keys β New GPG key
# GitLab: Preferences β GPG Keys On systems with a clipboard utility you can pipe it directly:
# macOS
gpg --armor --export ABCD1234EF567890 | pbcopy
# Linux (X11)
gpg --armor --export ABCD1234EF567890 | xclip -selection clipboard Verify: Push the signed commit from Step 2 and confirm the platform shows a green Verified label next to it. Alternatively, query the API β on GitHub, GET /repos/{owner}/{repo}/commits/{sha} returns "verification": { "verified": true, "reason": "valid" }.
SAFETY WARNING: Uploading the wrong armored block β for example exporting a subkey or an unrelated key β will leave commits showing Unverified with no obvious error. Always confirm the key ID in the export command matches
user.signingkey. Never paste the output of--export-secret-keys; that is your private key and would compromise your identity if leaked.
Step 4 β Generate or Reuse an SSH Signing Key Jump to heading
Intent: Provide an SSH key pair for the alternative signing backend. A dedicated signing key is preferable to reusing your authentication key.
Generate an Ed25519 SSH key reserved for signing:
# Dedicated signing key β kept separate from your push/authentication key
ssh-keygen -t ed25519 -C "[email protected] signing" -f ~/.ssh/id_ed25519_signing
# Choose a passphrase when prompted; store it in your password manager. If you already push over SSH and accept the tradeoff, you may reuse the existing key instead β but see the FAQ on why separation is the better default. Either way, the .pub file is what Git will reference.
Verify: Confirm the public key exists and is a valid single-line SSH key:
cat ~/.ssh/id_ed25519_signing.pub
# Expected: ssh-ed25519 AAAAC3Nza... [email protected] signing SAFETY WARNING: If you point
user.signingkeyat a.pubpath that does not exist or is unreadable, Git aborts every commit witherror: Load key ... invalid formatand you cannot commit at all until it is fixed. Verify the path resolves before enablingcommit.gpgsign, and keep the matching private key backed up β losing it invalidates all signatures it produced.
Step 5 β Configure Git to Sign with SSH Jump to heading
Intent: Switch Gitβs signing backend from OpenPGP to SSH and point it at the public key from Step 4.
The critical difference from GPG: with gpg.format ssh, user.signingkey holds a path to the public key file (prefixed with key:: for a literal key string), not a key ID:
# Switch the signing backend to SSH
git config --global gpg.format ssh
# Point at the PUBLIC key file β Git finds the matching private key via ssh-agent or the sibling file
git config --global user.signingkey ~/.ssh/id_ed25519_signing.pub
# Sign every commit and tag
git config --global commit.gpgsign true
git config --global tag.gpgsign true Verify: Create a signed commit. Note the signature will report an error at this stage β No principal matched β because Git does not yet know which SSH keys to trust. That is expected and fixed in Step 6:
git commit --allow-empty -m "chore: verify ssh signing"
git log --show-signature -1
# Expected at this stage:
# error: No principal matched.
# The commit IS signed; it is just not yet verifiable locally. The signature itself is valid β the platform will still verify it once you upload the public key as a signing key (Settings β SSH and GPG keys β New SSH key β Key type: Signing Key on GitHub).
Step 6 β Build an allowed_signers File for Local Verification Jump to heading
Intent: Give Git and CI a registry mapping signer identities to trusted SSH public keys, so git verify-commit and git log --show-signature succeed locally and inside pipelines.
Create the file and register Git to use it:
# Create an allowed_signers file β one signer per line: <identity> <key-type> <key>
mkdir -p ~/.config/git
printf '%s %s\n' "[email protected]" "$(cat ~/.ssh/id_ed25519_signing.pub)" \
> ~/.config/git/allowed_signers
# Tell Git where to find it
git config --global gpg.ssh.allowedSignersFile ~/.config/git/allowed_signers For a team, commit a shared allowed_signers into the repository (for example .github/allowed_signers) and point CI at it β this is exactly what the checks in signed commits in CI pipelines consume.
Verify: Re-inspect the SSH-signed commit from Step 5. It should now report a good signature:
git log --show-signature -1
# Expected excerpt:
# Good "git" signature for [email protected] with ED25519 key SHA256:...
git verify-commit HEAD && echo "verified" SAFETY WARNING: An
allowed_signersfile that is world-writable or checked in without review is a trust-injection risk β anyone who can add a line can make their own commits verify as a teammate. Treat it like an access-control list: require code-owner review on changes, and store the canonical copy where key protection practices from protecting and rotating signing keys apply.
GPG vs SSH: Dimension-by-Dimension Comparison Jump to heading
| Dimension | GPG (OpenPGP) | SSH |
|---|---|---|
| Minimum Git version | 1.7.9 (long-standing) | 2.34 |
gpg.format value | openpgp (default) | ssh |
user.signingkey refers to | Key ID / fingerprint | Path to .pub file (or key:: literal) |
| Tooling required | GnuPG (gpg), gpg-agent, pinentry | OpenSSH 8.2+ (ssh-keygen) |
| Local trust store | ~/.gnupg keyring + web of trust | Flat allowed_signers file |
| Key expiry / revocation | Native (expiry dates, revocation certs) | No native expiry; rotate by editing allowed_signers |
| Web of trust / cross-signing | Yes β federated trust across parties | No β trust is per-registry |
| Hardware token support | Mature (OpenPGP smartcard, YubiKey) | Yes (FIDO2 sk-ssh-ed25519, PIV via agent) |
| Setup complexity | Higher β agent, TTY, keyring quirks | Lower β reuses existing SSH tooling |
| CI verification input | Public keyring import | allowed_signers file |
| Best fit | Open-source projects needing federated trust, existing PGP users | Teams whose platform is the trust root, SSH-native shops |
Integration with Adjacent Workflows Jump to heading
Choosing and configuring a signing format is the foundation; the value only materialises when signatures are enforced and verified downstream.
Verification in CI is the first consumer. Once commits are signed, wire a pipeline job that rejects any unsigned or unverifiable commit β the patterns and reusable allowed_signers setup live in Verifying Signed Commits in CI Pipelines. This closes the gap where a local hook is bypassed with --no-verify; CI is the authoritative checkpoint, exactly as it is for pre-push validation rules.
Branch-level enforcement is the second. A signature that is never required is advisory. The Commit Verification Gates workflow shows how to turn on the platformβs βrequire signed commitsβ branch protection so unsigned pushes are rejected at the remote β the same protection surface used to gate trunk-based development merges.
Key lifecycle is the third. Whichever format you pick, the private key becomes a high-value secret. Handling β storage, rotation cadence, and incident response β is documented in Protecting & Rotating Signing Keys. The boundary is clean: this page decides which key format signs; that page decides how the key is guarded once it exists.
Two deeper recipes build directly on this decision. If you are already on GPG and want the simpler SSH model, follow Migrating a Team from GPG to SSH Commit Signing, which covers dual-format transition windows so no oneβs history shows as unverified mid-migration. If your security posture requires the private key never touch disk, Signing Git Commits with a YubiKey walks through hardware-backed signing for both formats.
Troubleshooting Jump to heading
| Symptom | Likely cause | Fix |
|---|---|---|
error: gpg failed to sign the data | gpg-agent cannot prompt for the passphrase (no TTY) | Add export GPG_TTY=$(tty) to your shell profile and restart the shell |
Commit shows Unverified on the platform despite a local Good signature | Public key not uploaded, or committer email does not match a verified account email | Upload the key (Step 3), and ensure user.email matches a verified platform email |
SSH-signed commit reports No principal matched | No allowed_signers file, or the signer email is absent from it | Create/point to gpg.ssh.allowedSignersFile and add the signer line (Step 6) |
error: Load key ... invalid format | user.signingkey points at a private key or missing file when gpg.format ssh is set | Point user.signingkey at the .pub file path; confirm it exists and is readable |
| Signatures stop verifying after two years | GPG key hit its expiry date | Extend expiry with gpg --quick-set-expire <fpr> 2y, or rotate the key and re-upload |
| Verified locally but fails in CI | CI checkout lacks the keyring or allowed_signers file | Commit a shared allowed_signers and configure the CI job to reference it |
Frequently Asked Questions Jump to heading
Is SSH commit signing as secure as GPG? Jump to heading
For the threat model that matters β proving a commit came from a holder of a specific private key β SSH signing is cryptographically equivalent to GPG. Both use asymmetric keys (Ed25519, RSA) and both sign the full commit object. SSH lacks GPGβs web of trust and its native expiry and revocation semantics, so for federated open-source trust across parties who have never met, GPG retains an edge. But for a team whose platform account is the source of truth for identity, SSH is equally sound and considerably simpler to operate.
Can I use the same SSH key for authentication and signing? Jump to heading
Technically yes β Git will sign with the same key you push with β but a dedicated signing key is better practice. Separating the two lets you rotate or revoke the signing key without losing push access, and keeps the audit trail clean. On GitHub the same public key can even be registered twice, once as an authentication key and once as a signing key, but generating a distinct key pair keeps the roles cleanly isolated.
Do I need to sign every commit or just tags? Jump to heading
Signing tags alone proves the release artifact is authentic but leaves individual commits unattributed, which defeats per-commit verification gates. If your branch protection or CI checks require signed commits, set commit.gpgsign true so every commit carries a signature. Tag signing via tag.gpgsign is complementary β it authenticates releases β but it is not a substitute for commit signing.
Why does my commit show as Unverified on GitHub even though it is signed? Jump to heading
The three usual causes are: the public key is not uploaded to the platform, the committer email on the commit does not match a verified email on your account, or the key has expired. Run git log --show-signature to confirm the local signature is valid, then reconcile the signing identity with the uploaded key and your verified emails. For SSH keys, confirm you uploaded it with key type Signing Key, not authentication.
What is the allowed_signers file for? Jump to heading
With gpg.format ssh, Git has no built-in registry of which SSH keys are trusted. The allowed_signers file maps signer identities β usually emails β to their public keys so that git log --show-signature and git verify-commit can validate signatures locally and in CI. Without it, SSH-signed commits verify to No principal matched even though the underlying signature is perfectly valid.
Related Jump to heading
- Migrating a Team from GPG to SSH Commit Signing β a staged, dual-format transition plan that keeps every commit verified throughout the switch
- Signing Git Commits with a YubiKey β hardware-backed signing for GPG and SSH so the private key never touches disk
- Verifying Signed Commits in CI Pipelines β pipeline jobs that reject unsigned or unverifiable commits using a shared allowed_signers file
- Commit Verification Gates β enforcing required-signature branch protection so unsigned pushes are rejected at the remote
- Commit Signing & Git Supply-Chain Security β the parent discipline covering signing, verification, and key protection end to end