
I use hardware security keys to access my servers and sign Git commits.
With this setup all private key material is stored on my YubiKeys in a way that prevents it from ever being extracted — not even by me. This makes it impossible for software to exfiltrate those secrets.
This is typically a tricky thing to set up correctly and consistently, so I created a series of scripts that do all the hard work for me and ensure a consistent setup. Those scripts are published on git.xo.codes.
I keep several YubiKeys all provisioned identically. I keep a key on me, at each of my personal computers and another stored securely in another location.
| Secret | Secrecy | Storage |
|---|---|---|
| SSH private key | Highest | YubiKey only (cannot be exported). |
| SSH public key | None | SSH servers, Git servers, anywhere. |
| SSH stub (ecdsa-sk) | None | ~/.ssh/yubikey-<serial> on each PC; manually copied to new machines. |
| SSH stub (ed25519-sk) | None | YubiKey; regenerated to any PC on demand with ssh-keygen -K. |
master-secret-key.asc | Highest | Offline storage. |
secret-subkeys.asc | Highest | YubiKeys + offline storage. |
gpg-signing-key.asc | High | Offline storage. |
revocation.asc | High | Offline storage. |
public-key.asc | None | Git server, anywhere. |
| FIDO2 PIN | High | Your brain + YubiKeys. |
| OpenPGP user PIN | High | Your brain + YubiKeys. |
| OpenPGP admin PIN | Higher | Your brain + YubiKeys. |
https://git.xo.codes/xo/yubikey-config
I use a series of scripts that set up my keys for me. The process is interactive and scripted to make the process easy, repeatable and sharable.
After provisioning your first key, the backup directory will contain:
| File | Contents |
|---|---|
master-secret-key.asc | The offline master key – guard this carefully |
secret-subkeys.asc | The signing subkey (also on your YubiKeys) |
public-key.asc | Your public key – safe to distribute |
gpg-signing-key.asc | Public signing subkey – for importing on new machines |
revocation.asc | Revocation certificate – lets you invalidate the key if needed |
Keep this directory somewhere secure and offline. I don't go all-out backing this up across multiple media types and locations, but I do keep a USB key somewhere safe. Worst case scenario I can always re-provision my keys again with a few headaches – but it is possible.
The keytocard GPG command that moves the signing subkey onto a YubiKey is
destructive – it removes the key from your local keyring when it's transferred.
To provision additional keys, the script reimports the subkey from your
backup before each transfer. This is why the backup is created in step 3,
before the transfer in step 7.
YubiKey firmware can't be updated – whatever firmware your key has you are stuck with. The firmware version determines what kind of SSH key the YubiKey can generate:
ed25519-sk resident keys.
Key material lives entirely on the hardware and nothing is written to disk.
ecdsa-sk non-resident keys. A stub
file is written to disk during provisioning.The scripts handle both automatically. The difference between ecdsa-sk and ed25519-sk isn't where the private key lives – that is always resident (on the YubiKeys). The difference is where the key handle lives. For ed25519-sk the key handle is also in resident storage. For ecdsa-sk the key handle is on disk.
With a couple of YubiKeys and a USB drive for backup I run
00-provision-keys-colors.bat
and follow the prompts. This involves setting PIN codes and touching my YubiKey
a few times.
Once my keys are provisioned I access whatever servers I want to use these keys
on and I copy the contents of backup_drive/authorized_keys to the
authorized_keys of any SSH user/server I want to access.
I also copy the signing public key to my Git server so it can verify the authenticity of my commits and I can get a little green checkmark next to commits.
On a typical day I have my YubiKey plugged into the computer I'm using. When I
want to SSH into a server I just type ssh myserver in a terminal and I'm
prompted to enter a PIN and touch my key.
Similarly when I run git commit or push I am prompted to enter a PIN and touch my YubiKey.
With a YubiKey I want to use on this new machine and my backup drive:
gpg --import gpg-signing-key.asc
gpg --card-status. GPG detects the card and
links it to the imported key automatically.
gpg --edit-key <fingerprint>
Type trust, select 5, then quit.
git config --global gpg.program "C:\Program Files\GnuPG\bin\gpg.exe"
git config --global commit.gpgsign true
git config --global user.signingkey <fingerprint>
The value you set for gpg.program should match the top result from
where gpg. If it doesn't, update gpg.program to match.
GPG records the serial number of the last card it used. When you swap in a different YubiKey, GPG can't match it and signing fails. The fix is one command:
gpg --card-statusRun that after inserting a different key, then retry your commit.
If you find yourself doing this very often you could always create a gpg wrapper
script and point git's gpg.program config value to the wrapper script. A
wrapper might look something like this:
@echo off
gpg --card-status 2> nul
gpg %*
I use a password manager for passkeys, time-based one-time passwords, and generated text-based passwords.
My password manager is secured with a strong text-based password and a time-based one-time password which is also stored on my YubiKeys.
I don't store passkeys or time-based one-time passwords (other than of my vault) on YubiKeys because there's a relatively low number of those that can be stored on YubiKeys and I don't want to have to remember which key goes with which account.
This article isn't about password managers though, but it's sort of related. So there you are.

xo © 2026 all rights reserved — attribution