
Sign and Verify Git Commits with Existing SSH Keys
Sign and Verify Git Commits with Existing SSH Keys
A commit hash proves that a commit’s contents have not changed. It does not, by itself, prove who created the commit. A cryptographic signature adds evidence that the person or system controlling a particular private key approved that exact commit object.
Git can sign commits with SSH keys, which is useful when your development environment already has OpenSSH and an SSH agent. You do not need a separate OpenPGP setup, but you still need to make careful decisions about key ownership, trusted signers, rotation, and automated verification.
This tutorial configures SSH signing, verifies signatures locally, handles multiple identities, and adds a simple CI policy. SSH commit signing requires Git 2.34 or newer. The current Git documentation is published for Git 2.55.0 as of June 29, 2026, but the core configuration below also works on earlier supported releases with SSH signing.
What a signed commit proves—and what it does not
A valid signature proves that the commit object was signed with the private key corresponding to a public key. Your trust policy decides whose key that is.
Signing does not prove that:
- the author reviewed every line of code;
- the author’s machine was uncompromised;
- dependencies or build artifacts are safe;
- the displayed name and email are truthful;
- the commit passed tests;
- a hosted “Verified” badge matches your organization’s local trust rules.
Treat signatures as one control in a supply-chain policy. Combine them with protected branches, code review, CI, least-privileged credentials, and key revocation.
Check the prerequisites
Confirm the installed versions:
git --version
ssh -V
You need Git 2.34 or later for SSH-format commit signatures. Most current Linux distributions, macOS package managers, and Git for Windows releases meet that requirement, but long-lived enterprise hosts may not.
Also verify your Git identity:
git config --global user.name
git config --global user.email
The email becomes the commit identity and is often used as the principal in your local allowed-signers file. It is not automatically proven merely because the signature is valid.
Choose or create a signing key
Git can use an existing SSH key, but a separate signing key often makes rotation and auditing easier. It lets you revoke commit-signing authority without changing access to every server.
Create an Ed25519 key with a meaningful filename:
ssh-keygen -t ed25519 \
-f ~/.ssh/id_ed25519_git_signing \
-C "git-signing@example.com"
Use a strong passphrase. Protect the private key and confirm the public key is readable:
chmod 0600 ~/.ssh/id_ed25519_git_signing
chmod 0644 ~/.ssh/id_ed25519_git_signing.pub
ssh-keygen -lf ~/.ssh/id_ed25519_git_signing.pub
The fingerprint is the durable identifier to compare through a trusted channel. A filename or comment can be edited and should not be treated as proof of identity.
Load the private key into your SSH agent:
ssh-add ~/.ssh/id_ed25519_git_signing
ssh-add -l
On macOS, Windows, or a desktop Linux session, an agent may already be managed by the operating system or credential manager. Avoid starting nested agents in every shell. Confirm which socket and keys your Git client actually sees.
Configure Git to sign with SSH
Tell Git to use the SSH signature format and the public-key path:
git config --global gpg.format ssh
git config --global user.signingKey ~/.ssh/id_ed25519_git_signing.pub
git config --global commit.gpgSign true
Despite the historical gpg.* names, gpg.format ssh selects SSH signatures. The default SSH signing program is ssh-keygen.
When user.signingKey points to a public key, the matching private key must be available to the SSH agent. Git also accepts a private-key path, but keeping the private key behind an agent is usually easier to operate and audit.
Inspect the effective configuration and where each value came from:
git config --show-origin --get gpg.format
git config --show-origin --get user.signingKey
git config --show-origin --get commit.gpgSign
Global configuration is convenient for a single identity. Use repository-local configuration when different projects require different signing keys:
git config --local user.email work@example.com
git config --local user.signingKey ~/.ssh/id_ed25519_work_signing.pub
Do not set commit.gpgSign=true globally until one manual signed commit succeeds. A broken global setting can block commits in every repository.
Create an allowed-signers trust file
Signing and verifying are separate operations. Git can create an SSH signature with your key, but local verification needs a list mapping trusted principals to public keys.
Create the directory:
install -d -m 0700 ~/.config/git
Add one line to ~/.config/git/allowed_signers. Replace the email and public-key data with your real values:
alice@example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIExampleOnlyReplaceMe
The first field is a principal. The remaining fields contain an SSH public key. To build a line from an existing .pub file without copying its optional comment:
awk '{print "alice@example.com " $1 " " $2}' \
~/.ssh/id_ed25519_git_signing.pub \
> ~/.config/git/allowed_signers
chmod 0600 ~/.config/git/allowed_signers
Now point Git to the file:
git config --global gpg.ssh.allowedSignersFile \
~/.config/git/allowed_signers
Git’s documentation explains that SSH does not have OpenPGP-style trust levels. A signature receives fully trusted status when its public key appears in the allowed-signers file; otherwise its trust is undefined and git verify-commit fails.
For a team, the allowed-signers file is security policy, not just developer preference. Generate it from an authoritative identity system or maintain it in a protected location. If the file is stored in the repository being verified, an untrusted pull request must not be able to replace it before verification.
Make and verify a test commit
Create an isolated test repository:
mkdir /tmp/git-signing-test
cd /tmp/git-signing-test
git init
git commit --allow-empty -S -m "Test SSH commit signing"
-S requests a signed commit explicitly. If commit.gpgSign=true is already active, normal git commit also signs. The explicit flag is useful while validating the setup.
Inspect the result:
git log --show-signature -1
git verify-commit HEAD
A successful verification prints a good SSH signature and exits with status zero. Scripts should trust the exit code, not grep a human-readable sentence that may vary by version or locale:
if git verify-commit HEAD; then
echo "Commit signature accepted"
else
echo "Commit signature rejected" >&2
exit 1
fi
You can confirm that the signature is embedded in the commit object:
git cat-file commit HEAD | sed -n '/^gpgsig /,/^$/p'
Do not edit the commit after signing. Amending, rebasing, or cherry-picking creates a new commit object with a new hash; the new object needs a new signature.
Sign tags deliberately
Release tags often deserve the same protection as commits. Create a signed annotated tag:
git tag -s v1.4.0 -m "Release v1.4.0"
git verify-tag v1.4.0
To request signed tags by default:
git config --global tag.gpgSign true
Avoid enabling this blindly in automation. A release job must have controlled access to the signing key, and the key should be scoped to the release identity rather than copied from a developer laptop.
Handle multiple Git identities
A common mistake is signing a work commit with a personal key or email. Git’s conditional includes keep identities separated by directory.
Add this to ~/.gitconfig:
[includeIf "gitdir:~/work/"]
path = ~/.gitconfig-work
[includeIf "gitdir:~/personal/"]
path = ~/.gitconfig-personal
Then create ~/.gitconfig-work:
[user]
name = Alice Example
email = alice@company.example
signingKey = ~/.ssh/id_ed25519_work_signing.pub
[gpg]
format = ssh
[commit]
gpgSign = true
Use a matching personal file and keep both public keys in your allowed-signers file under the correct principals. In a repository, verify the resolved values:
git config --show-origin --get-regexp \
'^(user\.(name|email|signingkey)|gpg\.format|commit\.gpgsign)$'
This catches path-pattern mistakes before they produce commits with the wrong identity.
Verify every commit in CI
A CI job can reject unsigned or untrusted commits before merge. First, provision an allowed-signers file from a protected secret, trusted base branch, or identity service. Do not accept the trust file from unreviewed pull-request changes.
Then verify the commits introduced by a branch:
#!/usr/bin/env bash
set -euo pipefail
git config gpg.format ssh
git config gpg.ssh.allowedSignersFile "$CI_ALLOWED_SIGNERS"
git fetch --no-tags origin main
base=$(git merge-base origin/main HEAD)
while read -r commit; do
printf 'Verifying %s\n' "$commit"
git verify-commit "$commit"
done < <(git rev-list "$base"..HEAD)
This example verifies each commit reachable from HEAD but not from the merge base. Adapt the base ref to your platform’s trusted pull-request metadata. Fetch enough history for merge-base to work; a shallow checkout can otherwise produce incomplete results.
Decide whether merge commits, bot commits, and release commits use separate identities. A policy is only useful when exceptions are explicit and auditable.
Troubleshooting SSH commit signing
“Couldn't load public key” or signing failed
Confirm the configured path, permissions, and agent state:
git config --get user.signingKey
ssh-add -l
ssh-keygen -lf ~/.ssh/id_ed25519_git_signing.pub
If the public key is configured but the agent lacks the matching private key, run ssh-add or fix the desktop credential manager integration.
allowedSignersFile needs to be configured
The commit may be signed, but Git cannot decide whether the key is trusted locally. Create the file, point gpg.ssh.allowedSignersFile to it, and ensure the current user can read it.
A signature is good but the principal is unexpected
The public key can be listed under more than one principal. Review the allowed-signers entry and your user.email. The principal displayed during verification comes from the trust mapping, not magically from ownership of an email account.
A hosting service does not show “Verified”
Local verification and hosting-provider verification use different trust stores. Register the public key as a signing key using the provider’s official workflow and ensure the commit email matches its account rules. Never upload the private key.
A GUI client creates unsigned commits
Some clients use bundled Git builds, isolated configuration, or their own signing integration. Check the Git executable and effective config inside the client. Verify the resulting commit with git verify-commit; do not infer success from a checkbox.
Rebasing removed a valid signature
Rebase recreates commits. The rewritten commits must be signed again by whoever performs the rebase. This is expected and should influence whether your team uses merge, squash, or rebase workflows.
Production checklist
- Require Git 2.34 or newer on signing clients.
- Prefer a dedicated Ed25519 signing key with a passphrase.
- Keep private keys in an agent or managed signer.
- Compare fingerprints through a trusted channel.
- Set
gpg.format=sshand the intendeduser.signingKey. - Test one explicit
git commit -Sbefore enabling automatic signing. - Maintain allowed signers outside untrusted pull-request control.
- Verify with
git verify-commitand its exit status. - Separate work, personal, bot, and release identities.
- Plan key rotation and revocation before an incident.
- Re-sign commits created by amend, rebase, or cherry-pick.
- Combine signatures with review, CI, and branch protection.