~/blog/git-signing-setup

SSH Git Commit Signing for Busy Engineers

8 min read

Most commit signing guides still drag GPG into the story first. I don't think that should be the default anymore. In practice, it usually means a second key system to maintain, agent drama at the worst possible time, and the classic gpg: signing failed: Inappropriate ioctl for device detour when all you wanted was a verified commit on GitHub.

If your goal is simple provenance for personal repositories or team codebases, SSH signing is the better default. You get the same Verified badge on GitHub, local signature verification in Git, and a setup most engineers can understand in one pass.

This guide is opinionated on purpose. It assumes macOS, Git 2.34+, and two SSH keys: one for authentication and one for signing.

GitHub explicitly supports reusing your authentication key as a signing key. That is a valid choice. I still prefer separation because it keeps workstation policy cleaner and key rotation less coupled.

Why I Default to SSH Signing

For most engineers, SSH signing solves the actual problem: proving that a commit came from a key you control without introducing another identity stack you now have to babysit.

It gives you verified commits and tags on GitHub, local verification with git log --show-signature, and a toolchain you already have on the machine. For most day to day engineering work, that's enough.

That doesn't make GPG obsolete. GPG still has a richer trust model, it fits some open source distribution workflows better, and some teams genuinely want its expiry and revocation semantics.

That last point is worth calling out. GPG keys can expire or be revoked, which gives you another security control if you want tighter lifecycle management. Once a GPG key expires or is revoked, it is no longer valid for signing new commits.

SSH keys do not have that built in lifecycle. On GitHub, once an SSH key has been verified for signing, commits signed with that key remain verified indefinitely. If you want expiry and revocation to be part of the model itself, GPG still has the edge.

But for personal projects, internal repos, and everyday engineering work, SSH signing is the practical default.

Why I Still Split Auth and Signing

GitHub's docs are clear: you can reuse your existing authentication key as a signing key. If you want the shortest possible setup, that is a valid choice.

I still recommend a dedicated signing key for three reasons.

First, operational separation. Your authentication key is used for remote access. Your signing key is used only to produce local signatures. Keeping those roles separate makes the workstation easier to reason about.

Second, lower blast radius. A signing key that never authenticates to remotes is easier to scope mentally and operationally. It also makes the namespaces="git" restriction in your local allowed signers file more meaningful.

Third, cleaner rotation. GitHub preserves verified status after a commit has been verified, so this is not about saving your GitHub history. It is about avoiding unnecessary coupling when you rotate an authentication key because of a new laptop, policy change, or suspected compromise.

The extra cost is about two minutes. For me, that trade is worth it.

Prerequisites

Before you run anything, check three things. git --version should report 2.34 or later. Your Git commit email should match a verified email on your GitHub account. And the commands below are specific to macOS anywhere they use pbcopy, UseKeychain, or --apple-use-keychain.

If you are on Linux or Windows, the signing model is the same, but the agent and clipboard steps will differ.

Setup

A macOS setup script

Save this as setup-git-signing.sh, read it once, then run it. It generates both keys, configures Git for SSH signing, writes the local allowed signers file, and verifies the setup with a throwaway commit.

#!/usr/bin/env bash
set -euo pipefail
 
GIT_EMAIL="${GIT_EMAIL:-$(git config --global user.email 2>/dev/null || echo "")}"
GIT_NAME="${GIT_NAME:-$(git config --global user.name 2>/dev/null || echo "")}"
 
AUTH_KEY="$HOME/.ssh/id_ed25519"
SIGNING_KEY="$HOME/.ssh/id_ed25519_signing"
ALLOWED_SIGNERS="$HOME/.config/git/allowed_signers"
 
if [[ -z "$GIT_EMAIL" ]]; then
  echo "Error: set GIT_EMAIL before running this script."
  echo "  export GIT_EMAIL=you@example.com"
  exit 1
fi
 
if [[ -z "$GIT_NAME" ]]; then
  echo "Error: set GIT_NAME before running this script."
  echo "  export GIT_NAME='Your Name'"
  exit 1
fi
 
echo "Setting up Git signing for: $GIT_NAME <$GIT_EMAIL>"
echo ""
 
mkdir -p "$HOME/.ssh" "$(dirname "$ALLOWED_SIGNERS")"
 
if [[ ! -f "$AUTH_KEY" ]]; then
  echo "Generating authentication key: $AUTH_KEY"
  ssh-keygen -t ed25519 -C "$GIT_EMAIL" -f "$AUTH_KEY" -N ""
else
  echo "Authentication key already exists: $AUTH_KEY"
fi
 
if [[ ! -f "$SIGNING_KEY" ]]; then
  echo "Generating signing key: $SIGNING_KEY"
  ssh-keygen -t ed25519 -C "${GIT_EMAIL}_signing" -f "$SIGNING_KEY" -N ""
else
  echo "Signing key already exists: $SIGNING_KEY"
fi
 
echo "Writing allowed signers file: $ALLOWED_SIGNERS"
if [[ -f "$ALLOWED_SIGNERS" ]]; then
  grep -v -F "$GIT_EMAIL namespaces=\"git\"" "$ALLOWED_SIGNERS" > "${ALLOWED_SIGNERS}.tmp" || true
  mv "${ALLOWED_SIGNERS}.tmp" "$ALLOWED_SIGNERS"
fi
 
printf '%s namespaces="git" %s\n' "$GIT_EMAIL" "$(cat "${SIGNING_KEY}.pub")" >> "$ALLOWED_SIGNERS"
 
echo "Configuring ~/.gitconfig"
git config --global user.name "$GIT_NAME"
git config --global user.email "$GIT_EMAIL"
git config --global gpg.format ssh
git config --global user.signingkey "${SIGNING_KEY}.pub"
git config --global gpg.ssh.allowedSignersFile "$ALLOWED_SIGNERS"
git config --global commit.gpgsign true
git config --global tag.gpgsign true
 
echo ""
echo "Verifying with a test commit..."
TMPDIR=$(mktemp -d)
git init "$TMPDIR" -q
git -C "$TMPDIR" commit --allow-empty -m "test signing" -q
git -C "$TMPDIR" log --show-signature -1 | grep 'Good "git" signature'
rm -rf "$TMPDIR"
 
echo ""
echo "Done."
echo ""
echo "Next steps:"
echo "1. Add the authentication key to GitHub as an Authentication Key"
echo "2. Add the signing key to GitHub as a Signing Key"
echo "3. Optionally load the auth key into the macOS keychain:"
echo "   /usr/bin/ssh-add --apple-use-keychain \"$AUTH_KEY\""
echo ""
echo "Authentication key:"
cat "${AUTH_KEY}.pub"
echo ""
echo "Signing key:"
cat "${SIGNING_KEY}.pub"
#!/usr/bin/env bash
set -euo pipefail
 
GIT_EMAIL="${GIT_EMAIL:-$(git config --global user.email 2>/dev/null || echo "")}"
GIT_NAME="${GIT_NAME:-$(git config --global user.name 2>/dev/null || echo "")}"
 
AUTH_KEY="$HOME/.ssh/id_ed25519"
SIGNING_KEY="$HOME/.ssh/id_ed25519_signing"
ALLOWED_SIGNERS="$HOME/.config/git/allowed_signers"
 
if [[ -z "$GIT_EMAIL" ]]; then
  echo "Error: set GIT_EMAIL before running this script."
  echo "  export GIT_EMAIL=you@example.com"
  exit 1
fi
 
if [[ -z "$GIT_NAME" ]]; then
  echo "Error: set GIT_NAME before running this script."
  echo "  export GIT_NAME='Your Name'"
  exit 1
fi
 
echo "Setting up Git signing for: $GIT_NAME <$GIT_EMAIL>"
echo ""
 
mkdir -p "$HOME/.ssh" "$(dirname "$ALLOWED_SIGNERS")"
 
if [[ ! -f "$AUTH_KEY" ]]; then
  echo "Generating authentication key: $AUTH_KEY"
  ssh-keygen -t ed25519 -C "$GIT_EMAIL" -f "$AUTH_KEY" -N ""
else
  echo "Authentication key already exists: $AUTH_KEY"
fi
 
if [[ ! -f "$SIGNING_KEY" ]]; then
  echo "Generating signing key: $SIGNING_KEY"
  ssh-keygen -t ed25519 -C "${GIT_EMAIL}_signing" -f "$SIGNING_KEY" -N ""
else
  echo "Signing key already exists: $SIGNING_KEY"
fi
 
echo "Writing allowed signers file: $ALLOWED_SIGNERS"
if [[ -f "$ALLOWED_SIGNERS" ]]; then
  grep -v -F "$GIT_EMAIL namespaces=\"git\"" "$ALLOWED_SIGNERS" > "${ALLOWED_SIGNERS}.tmp" || true
  mv "${ALLOWED_SIGNERS}.tmp" "$ALLOWED_SIGNERS"
fi
 
printf '%s namespaces="git" %s\n' "$GIT_EMAIL" "$(cat "${SIGNING_KEY}.pub")" >> "$ALLOWED_SIGNERS"
 
echo "Configuring ~/.gitconfig"
git config --global user.name "$GIT_NAME"
git config --global user.email "$GIT_EMAIL"
git config --global gpg.format ssh
git config --global user.signingkey "${SIGNING_KEY}.pub"
git config --global gpg.ssh.allowedSignersFile "$ALLOWED_SIGNERS"
git config --global commit.gpgsign true
git config --global tag.gpgsign true
 
echo ""
echo "Verifying with a test commit..."
TMPDIR=$(mktemp -d)
git init "$TMPDIR" -q
git -C "$TMPDIR" commit --allow-empty -m "test signing" -q
git -C "$TMPDIR" log --show-signature -1 | grep 'Good "git" signature'
rm -rf "$TMPDIR"
 
echo ""
echo "Done."
echo ""
echo "Next steps:"
echo "1. Add the authentication key to GitHub as an Authentication Key"
echo "2. Add the signing key to GitHub as a Signing Key"
echo "3. Optionally load the auth key into the macOS keychain:"
echo "   /usr/bin/ssh-add --apple-use-keychain \"$AUTH_KEY\""
echo ""
echo "Authentication key:"
cat "${AUTH_KEY}.pub"
echo ""
echo "Signing key:"
cat "${SIGNING_KEY}.pub"

Run it:

export GIT_EMAIL='you@example.com'
export GIT_NAME='Your Name'
chmod +x setup-git-signing.sh
./setup-git-signing.sh
export GIT_EMAIL='you@example.com'
export GIT_NAME='Your Name'
chmod +x setup-git-signing.sh
./setup-git-signing.sh

What the script actually does

id_ed25519 becomes your authentication key. id_ed25519_signing becomes your signing key.

gpg.format ssh tells Git to use SSH signatures, and commit.gpgsign true plus tag.gpgsign true make signing the default.

allowed_signers is the local trust file. It is what lets git log --show-signature tell you a signature is good and trusted on your machine.

The test repository is there for one reason: if it prints Good "git" signature, the local configuration works.

One subtle but important point: allowed_signers is for local verification, not GitHub verification. GitHub checks signatures against the keys on your account when you push.

Adding the Keys to GitHub

The script prints both public keys at the end. Add each one as a separate entry in Settings -> SSH and GPG keys.

Authentication key: add ~/.ssh/id_ed25519.pub as an Authentication Key.

Signing key: add ~/.ssh/id_ed25519_signing.pub as a Signing Key.

# Copy auth key
pbcopy < ~/.ssh/id_ed25519.pub
 
# Copy signing key
pbcopy < ~/.ssh/id_ed25519_signing.pub
# Copy auth key
pbcopy < ~/.ssh/id_ed25519.pub
 
# Copy signing key
pbcopy < ~/.ssh/id_ed25519_signing.pub

Once both are added, newly pushed commits signed with that key should show the Verified badge on GitHub.

What ~/.gitconfig Should Look Like

After the script runs, the part of your global config that matters should look like this:

[user]
    name = Your Name
    email = you@example.com
    signingkey = ~/.ssh/id_ed25519_signing.pub
 
[gpg]
    format = ssh
 
[gpg "ssh"]
    allowedSignersFile = ~/.config/git/allowed_signers
 
[commit]
    gpgsign = true
 
[tag]
    gpgsign = true
[user]
    name = Your Name
    email = you@example.com
    signingkey = ~/.ssh/id_ed25519_signing.pub
 
[gpg]
    format = ssh
 
[gpg "ssh"]
    allowedSignersFile = ~/.config/git/allowed_signers
 
[commit]
    gpgsign = true
 
[tag]
    gpgsign = true

That is the whole point of this setup: small surface area, easy to reason about, and no second signing toolchain to babysit.

Verifying Signatures

Check the latest commit in the current repository:

git log --show-signature -1
git log --show-signature -1

Expected output looks like this:

commit abc123def456...
Good "git" signature for you@example.com with ED25519 key SHA256:xxxxxxxxxxx
Author: Your Name <you@example.com>
Date:   Sun Mar 22 09:00:00 2026 +0000

    your commit message
commit abc123def456...
Good "git" signature for you@example.com with ED25519 key SHA256:xxxxxxxxxxx
Author: Your Name <you@example.com>
Date:   Sun Mar 22 09:00:00 2026 +0000

    your commit message

Check a specific commit:

git verify-commit abc123
git verify-commit abc123

Check a tag:

git verify-tag v1.0.0
git verify-tag v1.0.0

If you see No principal matched, the commit is signed but your local allowed signers file does not trust that key for that identity yet.

If you see BAD signature, stop and investigate before you push anything else.

SSH Config for the Authentication Key

The signing script intentionally does not manage your SSH agent. That is a different concern from Git signing, and trying to do it inside a standalone shell script usually creates more confusion than it removes.

Instead, make your SSH config explicit:

Host github.com
  HostName github.com
  User git
  IdentityFile ~/.ssh/id_ed25519
  IdentitiesOnly yes
  AddKeysToAgent yes
  IgnoreUnknown UseKeychain
  UseKeychain yes
Host github.com
  HostName github.com
  User git
  IdentityFile ~/.ssh/id_ed25519
  IdentitiesOnly yes
  AddKeysToAgent yes
  IgnoreUnknown UseKeychain
  UseKeychain yes

IdentitiesOnly yes matters when you carry multiple keys. It tells SSH to offer the key you configured for GitHub instead of spraying every key loaded into the agent.

Test it:

ssh -T git@github.com
# Hi USERNAME! You've successfully authenticated...
ssh -T git@github.com
# Hi USERNAME! You've successfully authenticated...

Conclusion

If all you want is verified commits without babysitting GPG, SSH signing is the pragmatic choice.

Use one key if you want the absolute minimum setup. Use two keys if you care about cleaner operational boundaries on your workstation. Either way, the win is the same: verified provenance, less ceremony, and a setup you can explain to the rest of the team in five minutes.