Setting up GPG commit signing with subkeys Jump to heading

The default gpg --full-generate-key produces a single key that certifies, signs and often encrypts, and it lives on your laptop forever. That is the setup most people have, and it is the one that hurts most when the laptop is lost: the identity, every signature it made, and every trust relationship built on it go together. GPG’s subkey model fixes this. The primary key does nothing but certify subkeys and is kept somewhere safe; a signing subkey does the daily work and can be revoked and replaced without changing your identity. This page sets that up for Git. Whether you should use GPG at all is the subject of the parent topic, GPG vs SSH commit signing.

When to use this approach Jump to heading

  • Your organisation or project already uses GPG β€” for release signing, package signing, or a published keyring.
  • You need signatures that other tools verify through OpenPGP, such as distribution packaging.
  • You want a long-lived identity whose signing capability can be rotated without changing the fingerprint people trust.
  • If none of those apply, SSH commit signing is simpler and gives the same verification on forges.

Step 1 β€” Create a certify-only primary key Jump to heading

Generate the primary key with only the certify capability. It will never sign a commit; its job is to vouch for subkeys.

gpg --quick-generate-key "Alice Example <[email protected]>" ed25519 cert 5y
gpg --list-keys --keyid-format long [email protected]
# pub   ed25519/0xA1B2C3D4E5F60718 2026-10-02 [C] [expires: 2031-10-01]

The [C] flag confirms the key can only certify. Five years is a reasonable expiry for a primary key; you can extend it later as long as you still hold it, so expiry is a safety net, not a deadline.

Step 2 β€” Add a signing subkey with a shorter life Jump to heading

Add a subkey with only the sign capability and a shorter expiry. This is the key Git will actually use.

FPR=$(gpg --list-keys --with-colons [email protected] | awk -F: '/^fpr/{print $10; exit}')
gpg --quick-add-key "$FPR" ed25519 sign 1y
gpg --list-keys --keyid-format long [email protected]
# sub   ed25519/0x1122334455667788 2026-10-02 [S] [expires: 2027-10-02]
The roles in a subkey setupThe primary key certifies and stays offline. The signing subkey lives on the laptop and signs commits. Collaborators and forges trust the primary fingerprint, so a replaced subkey is trusted automatically once it is certified.what each key does, and where it livesPrimary key [C]certifies subkeys β€” kept offlineSigning subkey [S]signs commits β€” on the laptop, 1-year expiryCommits and tagsverified through the primary's fingerprintlose the laptop and you lose a subkey, not your identity
# Verification: there is exactly one usable signing subkey
gpg --list-keys --with-colons [email protected] | awk -F: '$1=="sub" && $12 ~ /s/' | wc -l

Step 3 β€” Back up the primary key and remove it from the laptop Jump to heading

Export the full secret key, store it offline, and then delete the primary’s secret part from the everyday keyring so only the subkey remains. The process is covered in depth in keeping a GPG primary key offline; the short version is below.

umask 077
gpg --export-secret-keys --armor "$FPR" > primary-and-subkeys.asc     # to offline storage
gpg --gen-revoke "$FPR" > revoke-$FPR.asc                              # to offline storage
gpg --export-secret-subkeys --armor "$FPR" > subkeys-only.asc
gpg --delete-secret-keys "$FPR"         # removes primary AND subkeys from this machine
gpg --import subkeys-only.asc           # bring back only the subkeys
shred -u subkeys-only.asc

⚠️ SAFETY WARNING: gpg --delete-secret-keys is irreversible on this machine. Confirm the offline export imports correctly on another machine or in a temporary GNUPGHOME before deleting: GNUPGHOME=$(mktemp -d) gpg --import primary-and-subkeys.asc. If you delete first and the backup is bad, the identity is gone and every collaborator must re-establish trust in a new key.

# Verification: sec# means the primary secret is absent; ssb means the subkey is present
gpg --list-secret-keys --keyid-format long [email protected]
# sec#  ed25519/0xA1B2C3D4E5F60718 ...
# ssb   ed25519/0x1122334455667788 ... [S]

Step 4 β€” Point Git at the subkey explicitly Jump to heading

Give Git the subkey’s ID with a trailing !. Without the exclamation mark GPG picks whichever signing-capable key it prefers, which after a rotation may be the expired one.

git config --global gpg.format openpgp
git config --global user.signingKey 0x1122334455667788!
git config --global commit.gpgSign true
git config --global tag.gpgSign true

Configure the agent so you are not asked for the passphrase on every commit, and so the prompt works in a terminal.

# ~/.gnupg/gpg-agent.conf
default-cache-ttl 3600
max-cache-ttl 28800
echo 'export GPG_TTY=$(tty)' >> ~/.profile
gpgconf --kill gpg-agent
Signing a commit with a subkeyGit asks gpg to sign the commit with the named subkey. gpg asks the agent, which prompts for the passphrase once and caches it, then returns the signature that Git embeds in the commit.gitgpggpg-agentsign with 0x1122…7788!need subkey secretpinentry, then cachesignaturearmoured signaturethe trailing ! pins the exact subkey so a rotation cannot pick the wrong one
# Verification
git commit --allow-empty -m "gpg subkey test"
git log -1 --format='%G? %GK'      # G 1122334455667788

Step 5 β€” Publish the public key to your forge Jump to heading

Export the public key β€” which includes the primary and all subkeys β€” and add it to your forge account. Forges match the subkey that made the signature to the primary you uploaded.

gpg --export --armor "$FPR" > alice.pub.asc
gh gpg-key add alice.pub.asc
gh api /user/gpg_keys --jq '.[] | {key_id, subkeys: [.subkeys[].key_id]}'

When you later rotate the subkey, re-upload the public key so the forge learns about the new subkey. Forges do not fetch keys from keyservers on their own.

Rotating the signing subkey without changing identityThe primary key is created once and kept offline. Each year a new signing subkey is added and the public key re-uploaded, while old subkeys expire but remain certified, so every past commit keeps verifying against the same fingerprint.Primary createdcertify-only, 5y2026-10Subkey 1sign, 1y2026-10Subkey 2 addedprimary brought out2027-09Subkey 1 expiresold commits still verify2027-10Re-uploadforge learns new subkeyeach yearcollaborators trust one fingerprint for five years while the working key changes yearly

Validation checklist Jump to heading

Frequently Asked Questions Jump to heading

How do I rotate the signing subkey when it expires? Jump to heading

Bring the primary key back temporarily β€” import it on an offline machine or into a temporary GNUPGHOME β€” add a new signing subkey, export the subkeys, import them on the laptop, update user.signingKey, and re-upload the public key. Old commits remain valid because the old subkey is still certified by the same primary.

Should I also create an encryption subkey? Jump to heading

Only if you need encrypted mail or files. Git signing never uses it. Keeping capabilities on separate subkeys is good hygiene, but there is no reason to create one you will not use.

Why does GPG still ask for my passphrase on every commit? Jump to heading

Usually because GPG_TTY is not set in the shell that runs Git, so the agent cannot prompt or cache properly, or because default-cache-ttl is zero. The fuller list of causes is in troubleshooting β€œgpg failed to sign the data”.