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.

GPG versus SSH commit signing and verification pathsTwo parallel pipelines. The GPG path: commit object signed by a GPG private key, verified against a GnuPG keyring and web of trust, then confirmed by the platform. The SSH path: commit object signed by an SSH private key, verified against an allowed_signers file, then confirmed by the platform. Both converge on a verified badge.Commit object (author + tree + parents)GPG pathSSH pathsign with GPG private keysign with SSH private keyGnuPG keyring + web of trustallowed_signers fileverify-commit / platformverify-commit / platformVerified

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 with gpg --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.signingkey at a .pub path that does not exist or is unreadable, Git aborts every commit with error: Load key ... invalid format and you cannot commit at all until it is fixed. Verify the path resolves before enabling commit.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_signers file 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

DimensionGPG (OpenPGP)SSH
Minimum Git version1.7.9 (long-standing)2.34
gpg.format valueopenpgp (default)ssh
user.signingkey refers toKey ID / fingerprintPath to .pub file (or key:: literal)
Tooling requiredGnuPG (gpg), gpg-agent, pinentryOpenSSH 8.2+ (ssh-keygen)
Local trust store~/.gnupg keyring + web of trustFlat allowed_signers file
Key expiry / revocationNative (expiry dates, revocation certs)No native expiry; rotate by editing allowed_signers
Web of trust / cross-signingYes β€” federated trust across partiesNo β€” trust is per-registry
Hardware token supportMature (OpenPGP smartcard, YubiKey)Yes (FIDO2 sk-ssh-ed25519, PIV via agent)
Setup complexityHigher β€” agent, TTY, keyring quirksLower β€” reuses existing SSH tooling
CI verification inputPublic keyring importallowed_signers file
Best fitOpen-source projects needing federated trust, existing PGP usersTeams 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

SymptomLikely causeFix
error: gpg failed to sign the datagpg-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 signaturePublic key not uploaded, or committer email does not match a verified account emailUpload the key (Step 3), and ensure user.email matches a verified platform email
SSH-signed commit reports No principal matchedNo allowed_signers file, or the signer email is absent from itCreate/point to gpg.ssh.allowedSignersFile and add the signer line (Step 6)
error: Load key ... invalid formatuser.signingkey points at a private key or missing file when gpg.format ssh is setPoint user.signingkey at the .pub file path; confirm it exists and is readable
Signatures stop verifying after two yearsGPG key hit its expiry dateExtend expiry with gpg --quick-set-expire <fpr> 2y, or rotate the key and re-upload
Verified locally but fails in CICI checkout lacks the keyring or allowed_signers fileCommit 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.