Setting up SSH commit signing from scratch Jump to heading

SSH signing arrived in Git 2.34 and removed most of the friction that kept teams away from signed commits. There is no keyring to manage, no web of trust to understand, and most engineers already have ssh-keygen and an agent running. What remains are a handful of settings that are easy to get subtly wrong: signing with the authentication key you use for everything else, forgetting the allowed signers file so local verification always reports an error, or signing commits but not tags. This page walks through a clean setup in the order that avoids those traps. The choice between this and GPG is discussed in the parent topic, GPG vs SSH commit signing.

When to use this approach Jump to heading

  • You are setting up signing for the first time, on a machine with Git 2.34 or newer.
  • Your forge accepts SSH signing keys β€” all major hosted forges do.
  • You want something the whole team can configure in five minutes without learning GPG.
  • You may later want hardware-backed keys; the same steps work with a FIDO2 sk- key, covered in signing Git commits with a YubiKey.
  • If your organisation already runs GPG with subkeys and a published keyring, follow setting up GPG commit signing with subkeys instead.

Step 1 β€” Check the Git and OpenSSH versions Jump to heading

SSH signing needs Git 2.34 or newer and an OpenSSH client recent enough to support ssh-keygen -Y sign, which means 8.2 or newer. Older versions fail with confusing messages about unknown options rather than a clear version error.

git --version        # 2.34.0 or newer
ssh -V               # OpenSSH_8.2 or newer
ssh-keygen -Y sign 2>&1 | head -1   # should print usage, not "unknown option"

If either is too old, upgrade before going further; the rest of the configuration silently does nothing on an old Git.

Step 2 β€” Generate a dedicated signing key Jump to heading

Use a separate key for signing rather than reusing the key you log in to servers with. The two jobs have different risk profiles: an authentication key is loaded into agents, forwarded to jump hosts, and copied between machines; a signing key should do one thing and be revoked independently.

# Ed25519, with a comment that says what the key is for and when it was made
ssh-keygen -t ed25519 -C "git-signing $(whoami)@$(hostname) $(date +%Y-%m)" \
  -f ~/.ssh/id_ed25519_signing

Give it a passphrase. The agent will cache it, so you type it once per session rather than once per commit.

Authentication key versus signing keyAn authentication key is often forwarded, shared across hosts and loaded everywhere, which suits logging in but not proving authorship. A dedicated signing key stays on one machine, is used for one purpose, and can be rotated without breaking server access.Reused auth keyDedicated signing keyagent forwardingoften enablednever neededcopied to serverscommonnorotationbreaks loginssigning onlyregistered asauth + signingsigning onlyregister the signing key as a signing key β€” forges keep the two lists separate
# Verification: the key exists and has the expected type
ssh-keygen -l -f ~/.ssh/id_ed25519_signing.pub

Step 3 β€” Configure Git to sign with it Jump to heading

Four settings do the work: the format, the key, and automatic signing for commits and tags.

git config --global gpg.format ssh
git config --global user.signingKey ~/.ssh/id_ed25519_signing.pub
git config --global commit.gpgSign true
git config --global tag.gpgSign true

user.signingKey points at the public key file; Git hands it to ssh-keygen, which finds the matching private key through the agent or next to the public file. Pointing at the private key also works, but then every signature prompts for the passphrase unless the agent already holds it.

# Load the key into the agent so you are not prompted on every commit
ssh-add ~/.ssh/id_ed25519_signing
ssh-add -l | grep signing

Step 4 β€” Create an allowed signers file for local verification Jump to heading

Git can sign without any further setup, but it cannot verify SSH signatures without a list of trusted keys. Without it, git log --show-signature reports an error on every commit, which teaches people to ignore signature output. Create a personal file containing at least your own key.

mkdir -p ~/.config/git
printf '%s namespaces="git" %s\n' "$(git config user.email)" \
  "$(cat ~/.ssh/id_ed25519_signing.pub)" >> ~/.config/git/allowed_signers
git config --global gpg.ssh.allowedSignersFile ~/.config/git/allowed_signers

On a team, replace this personal file with a shared one generated from the roster, as described in building an allowed signers file from team membership, so everyone sees each other’s commits as verified.

How a signed commit is made and checked locallyGit writes the commit object, passes it to ssh-keygen with the signing key, and embeds the signature. When verifying, Git extracts the signature and asks ssh-keygen to check it against the allowed signers file, matching the committer email to a principal.git commitcommit.gpgSignssh-keygen -Y signnamespace gitSigned objectgpgsig headerssh-keygen -Y verifyallowed_signers%G? = Gprincipal matchedsigning needs only the key; verification also needs the list of who to trust
# Verification: a fresh commit reports a good signature
git commit --allow-empty -m "signing test"
git log -1 --show-signature
git log -1 --format='%G? %GS'      # G your@email

Step 5 β€” Register the key with your forge as a signing key Jump to heading

The forge needs the public key to show commits as verified. On every major forge there is a separate list for signing keys; adding the key only as an authentication key does not work.

# GitHub CLI: add as a signing key (requires the admin:ssh_signing_key scope)
gh auth refresh -s admin:ssh_signing_key
gh ssh-key add ~/.ssh/id_ed25519_signing.pub --type signing --title "laptop $(date +%Y-%m)"
gh api /user/ssh_signing_keys --jq '.[].title'

The email on your commits must also be a verified email on the forge account, or the forge shows the signature as unverified even though it is cryptographically valid.

# Verification: the committer email matches a verified address on the account
git config user.email
gh api /user/emails --jq '.[] | select(.verified) | .email'

Step 6 β€” Sign tags and verify them Jump to heading

With tag.gpgSign set, every annotated tag is signed automatically. Lightweight tags cannot be signed because they have no object of their own; the difference is covered in signed tags vs signed commits.

git tag -a v1.4.0 -m "Release 1.4.0"     # signed because tag.gpgSign is true
git verify-tag v1.4.0
git tag -v v1.4.0 | tail -2
The six settings, grouped by jobTwo settings choose how to sign, two decide when to sign, and one tells Git whom to trust when verifying. A sixth step outside Git β€” registering the key with the forge β€” makes the web interface agree with your terminal.Howgpg.format sshuser.signingKeyWhencommit.gpgSigntag.gpgSignTrustallowedSignersFileForgesigning keyverified emailmissing the trust setting is why so many people see errors on their own commits

Validation checklist Jump to heading

Frequently Asked Questions Jump to heading

Can I use the same key on several machines? Jump to heading

You can, but it is better not to. A separate key per device means losing one laptop revokes one key, not your ability to sign everywhere. The trade-offs are covered in using a separate signing key per device.

Why does the forge show my commit as unverified? Jump to heading

Almost always one of two things: the key was added as an authentication key rather than a signing key, or the commit’s email is not a verified address on your account. Check both before suspecting the signature itself.

Do I need to restart anything after changing these settings? Jump to heading

No. Git reads configuration on every command. The only state that persists is the agent; if you regenerate the key, remove the old one with ssh-add -d and add the new one.