Signing git commits with a YubiKey Jump to heading

Software signing keys are only as trustworthy as the disk they live on. A ~/.ssh/id_ed25519 or an exported GPG secret key can be copied by any malware, backup agent, or careless scp with read access to your home directory — and once copied, an attacker can forge your identity on every commit until you notice. A YubiKey moves the private key into a secure element that never exports it, and binds each signature to a physical touch. This page is a focused recipe within GPG vs SSH commit signing, which covers the broader trade-offs between the two signing backends; here we take the SSH path — the simpler, better-supported option for hardware-backed signing today — and walk it end to end.

When to use this approach Jump to heading

Reach for a YubiKey-backed signing key when:

  • Your threat model includes a compromised developer laptop, and you need the signing key to survive host compromise. A resident FIDO2 key cannot be exfiltrated even from a root-level attacker.
  • You are enforcing signed commits on protected branches (see Protecting & rotating signing keys) and want the strongest possible assurance that a signature reflects a deliberate human action.
  • You work across multiple machines and want to carry one credential on your keyring rather than provisioning — and later revoking — a separate software key per host.
  • Compliance requires demonstrable proof of possession and user presence for artifact authorship.

This recipe assumes you have already decided on the SSH signing format rather than GPG. If your team is standardizing on SSH from a legacy GPG setup, pair this with migrating a team from GPG to SSH commit signing, which handles the org-wide rollout, allowed_signers distribution, and cutover sequencing.

The diagram below shows what stays on the device versus what leaves it, and where the touch gate sits in the signing flow.

YubiKey resident key signing flow with touch confirmationDiagram showing git commit invoking the SSH signer, which sends a hash to the YubiKey secure element; the private scalar never leaves the device; a physical touch releases the signature back to git; only the public key and allowed_signers list are shared with the host for verification.git commit -Shashes commitYubiKey secure elementprivate scalar (never exported)signs hash internallytouch to releasetouchsignaturesigned commitin local repohost verifiespublic key only

Step-by-step recipe Jump to heading

Step 1 — Confirm firmware and toolchain prerequisites Jump to heading

FIDO2 ed25519-sk signing needs OpenSSH 8.2 or newer on both signer and verifier, and Git 2.34+ for SSH-format signing. A resident key that survives host reprovisioning additionally wants a YubiKey 5-series device with FIDO2 enabled.

# All three must satisfy the minimums below
ssh -V                       # OpenSSH_8.2 or newer
git --version                # git version 2.34 or newer
ykman fido info              # confirms the FIDO2 applet is present

Verify the versions before spending time on key generation:

# Fails loudly if OpenSSH predates ed25519-sk support
ssh-keygen -t ed25519-sk -f /tmp/probe -N '' -C probe 2>&1 | head -1 && rm -f /tmp/probe*

If ssh-keygen reports that the type is unknown, upgrade OpenSSH before continuing — nothing downstream will work on an older build.

Step 2 — Generate a resident FIDO2 ed25519-sk key on the YubiKey Jump to heading

Generate the key with the security-key type. The -O resident option stores the credential handle on the YubiKey itself so you can later re-derive the private-key stub on any machine; -O verify-required forces a PIN in addition to touch.

# ed25519-sk: FIDO2 security-key backed Ed25519
# -O resident:        credential lives on the device, re-exportable
# -O verify-required: require the FIDO2 PIN, not just a touch
# -O application:     namespace so multiple keys can coexist on one YubiKey
ssh-keygen -t ed25519-sk -O resident -O verify-required \
  -O application=ssh:git-signing \
  -C "$(git config user.email) yubikey" \
  -f ~/.ssh/id_ed25519_sk_git

ssh-keygen writes two files: a private-key handle (id_ed25519_sk_git, useless without the physical device) and the public key (id_ed25519_sk_git.pub). Verify both exist and that the public key carries the sk-ssh-ed25519 prefix:

# The public key line must begin with [email protected]
cat ~/.ssh/id_ed25519_sk_git.pub

Step 3 — Point Git at the SSH signing format and key Jump to heading

Tell Git to use the SSH backend, name the YubiKey public key as your signing key, and turn on signing for every commit.

# Use the SSH signer instead of GPG
git config --global gpg.format ssh
# The PUBLIC key file identifies which key to sign with
git config --global user.signingkey ~/.ssh/id_ed25519_sk_git.pub
# Sign every commit and annotated tag automatically
git config --global commit.gpgSign true
git config --global tag.gpgSign true

Confirm the three values resolved as expected:

# Expect: ssh, the .pub path, and true
git config --get gpg.format
git config --get user.signingkey
git config --get commit.gpgSign

Step 4 — Register the public key in allowed_signers and on your host Jump to heading

Local git log --show-signature verification needs an allowed_signers file mapping your email to your public key. This is the SSH equivalent of a trust database.

# Create the allowed_signers file, one principal per line
mkdir -p ~/.config/git
printf '%s %s\n' "$(git config user.email)" "$(cat ~/.ssh/id_ed25519_sk_git.pub)" \
  >> ~/.config/git/allowed_signers
# Point Git at it
git config --global gpg.ssh.allowedSignersFile ~/.config/git/allowed_signers

Then upload the same public key to your Git host as a signing key (on GitHub: Settings → SSH and GPG keys → New SSH key → Key type: Signing Key). Authentication keys and signing keys are distinct roles — adding it only as an auth key will not make the host mark commits verified.

# Copy the public key to paste into the host UI
cat ~/.ssh/id_ed25519_sk_git.pub

Verify the local trust file parses and contains exactly one entry for your email:

# Should print your email followed by the sk-ssh-ed25519 key
grep "$(git config user.email)" ~/.config/git/allowed_signers

Step 5 — Sign a commit and confirm with touch Jump to heading

Make a commit. Because commit.gpgSign is true, Git invokes the SSH signer, the YubiKey blinks, and the signature is not produced until you touch it (and enter the PIN if you set verify-required).

# The YubiKey will blink — touch it to release the signature
git commit --allow-empty -m "chore: verify yubikey signing"

If the commit completes without any prompt or blink, the signer silently fell back to a software key — stop and recheck user.signingkey and gpg.format from Step 3 rather than assuming it worked.

SAFETY WARNING: A resident FIDO2 key exists in exactly one place — the YubiKey. If you lose or destroy the device without a backup, you permanently lose the ability to create new signatures with that key (existing signed history stays valid). Generate a second ed25519-sk key on a backup YubiKey during Step 2 and register its public key too, so a lost primary is a rotation, not a lockout: ssh-keygen -t ed25519-sk -O resident -f ~/.ssh/id_ed25519_sk_backup then add its .pub to allowed_signers and the host.

Step 6 — Verify the signature locally and export the resident key Jump to heading

Confirm the signature validates against allowed_signers:

# "Good \"git\" signature" plus your email = success
git log --show-signature -1

A verified commit prints a Good "git" signature for <email> line. If you see No signature or no principal matched, the allowed_signers mapping is wrong or the commit was not actually signed — revisit Steps 4 and 5.

To reconstruct the private-key handle on a new machine straight from the device (the point of -O resident), download it from the YubiKey rather than copying files around:

# Writes id_ed25519_sk_... handle files into the current directory
# from the credentials stored on the inserted YubiKey
cd ~/.ssh && ssh-keygen -K

This regenerates the local stub files on any host with the physical key, so provisioning a second laptop needs no file transfer at all.

The GPG-on-YubiKey alternative Jump to heading

If your ecosystem still requires GPG signatures — for example, tools or hosts that only understand gpg.format openpgp — a YubiKey can also act as an OpenPGP smartcard. You generate or move a GPG signing subkey onto the device’s OpenPGP applet (via gpg --card-edit or keytocard), set git config user.signingkey <GPG-KEY-ID> with gpg.format openpgp, and the daily flow is the same: the YubiKey holds the private key and requires a touch to sign. The trade-off is operational weight — you manage a gpg-agent, a full keyring, expiry dates, and subkey hierarchy rather than a single .pub file. For most teams standardizing today, the SSH path above is materially simpler and is the recommended default; reserve the GPG-on-YubiKey route for environments with an existing OpenPGP trust web. The full comparison lives in GPG vs SSH commit signing.

Validation checklist Jump to heading

Before relying on this setup, confirm each item:

Frequently asked questions Jump to heading

What is the difference between ed25519-sk and a plain ed25519 SSH key? Jump to heading

An ed25519-sk key is a FIDO2 security-key credential: the private scalar never leaves the YubiKey’s secure element, and each signature requires a physical touch (and optionally a PIN). A plain ed25519 key stores its private half as a file on disk, so it can be copied, leaked, or used silently by any process with read access to your home directory. The -sk variant trades a small amount of setup friction for hardware-bound, phishing-resistant, presence-verified signing — which is the entire point of using a hardware token.

Do I need to touch the YubiKey for every commit? Jump to heading

Yes, by default. FIDO2 signing enforces a user-presence check, so each git commit -S — or every commit once commit.gpgSign is true — will blink the YubiKey and wait for a touch. You can generate the key with the no-touch-required option to drop the presence check, but that discards the guarantee that a human deliberately authorized each signature and is not recommended for supply-chain use. If per-commit touches are disruptive, batch your work into fewer, larger commits rather than weakening the key.

What happens to my signed history if I lose the YubiKey? Jump to heading

Existing signatures stay valid, because verification only needs the public key, which is already in allowed_signers and on your Git host. You lose only the ability to create new signatures. Recover by generating a fresh ed25519-sk key on a backup YubiKey, adding its public key to allowed_signers and the host, and removing the lost key’s public entry so a found device cannot sign as you. Treat this as routine key rotation — the mechanics are covered in Protecting & rotating signing keys.