Here is the payoff before the work: I plug in my YubiKey in the morning, type one PIN, and the rest of the day my authentication just happens. SSH to a server, no password. Sign a git commit, no passphrase. Pull a secret out of pass, just touch the key. One physical thing sits in a USB port and the friction is gone.

Getting there cost me about three evenings of swearing at gpg-agent. Now that it runs, going back feels unthinkable.

Why I Bothered

The problem was a pile of authentication methods that all worked differently.

  • SSH keys on disk (encrypted, sure, but still a file someone could copy)
  • GPG keys for git signing (a different key, a different passphrase)
  • pass for passwords (that same GPG key again, that passphrase again)
  • 2FA codes in an authenticator app (grab the phone, squint, retype)

Every action started with typing a password or reaching for my phone. That is friction, and friction is where I start cutting security corners to save myself the annoyance. A setup that annoys me is a setup I will quietly sabotage. So the goal was a single key that handles everything, with the secrets living on hardware I physically control instead of in files scattered across my disk. That is the sovereignty angle: I want to know exactly where my private keys live, and I want them somewhere I can pull out of the laptop and put in my pocket.

The answer was one YubiKey doing all of it.

The Stack, Quickly

ToolFunction
YubiKeyHardware security key with smartcard functionality
GPGEncryption and signing
passCLI password manager (uses GPG)
gpg-agentAgent that caches GPG keys and handles SSH auth
SSHYou know what SSH is

The piece that makes this whole thing hang together is gpg-agent. It can pretend to be an SSH agent, which means the GPG key sitting on your YubiKey doubles as your SSH key. One key, three jobs.

The Simplest Version First

Before the full setup, here is the minimal version so you can see where this is heading. You need a GPG key with an authentication subkey, you move it onto the YubiKey, and you point SSH at gpg-agent. Three steps and you can already SSH with the hardware key. The rest of this post adds pass, git signing, touch policies, and backups on top of that core. If you stop after the SSH part, you still have something useful.

The Full Setup

1. Put the GPG Key on the YubiKey

You need a GPG key with three subkeys:

  • Sign (for git commits)
  • Encrypt (for pass)
  • Authenticate (for SSH)

If you already have a GPG key, you can add subkeys. Otherwise create a new one.

# Generate new key (or use existing)
gpg --full-generate-key

# Add subkeys
gpg --edit-key <KEY_ID>
gpg> addkey  # Sign
gpg> addkey  # Encrypt
gpg> addkey  # Authenticate (choose "set your own capabilities")
gpg> save

Now move the keys to the YubiKey:

gpg --edit-key <KEY_ID>
gpg> key 1
gpg> keytocard  # Choose slot for signing
gpg> key 1
gpg> key 2
gpg> keytocard  # Choose slot for encryption
gpg> key 2
gpg> key 3
gpg> keytocard  # Choose slot for authentication
gpg> save

Note: keytocard moves the key, it does not copy it. Back up your .gnupg directory first, or you will learn this the hard way like I did on YubiKey number one.

2. Configure gpg-agent

This is the step that does the heavy lifting. Edit ~/.gnupg/gpg-agent.conf:

# Cache timeout (in seconds)
default-cache-ttl 600
max-cache-ttl 7200

# Enable SSH support
enable-ssh-support

# Pinentry program (for PIN entry)
pinentry-program /usr/bin/pinentry-gnome3
# Or for terminal: /usr/bin/pinentry-curses
# Or for macOS: /usr/local/bin/pinentry-mac

And in your ~/.bashrc or ~/.zshrc:

# Use GPG agent as SSH agent
export GPG_TTY=$(tty)
export SSH_AUTH_SOCK=$(gpgconf --list-dirs agent-ssh-socket)

# Restart agent if needed
gpgconf --launch gpg-agent

Restart your shell or source the file:

source ~/.zshrc

3. Export SSH Key

Your GPG authentication key is now also your SSH key. Export the public key:

# SSH public key from your GPG key
gpg --export-ssh-key <KEY_ID>

Add this to ~/.ssh/authorized_keys on your servers, or to GitHub/GitLab.

Check if it works:

ssh-add -L
# Shows your GPG key as SSH key

Adding the Real-World Layers

The SSH part above is the core. Everything from here adds a practical layer on top of it: a password manager, signed commits, and the touch policy that decides how paranoid you want to be.

4. Configure pass

pass is about as simple as a password manager gets. It encrypts each password as a GPG file on disk, and because the GPG key now lives on your YubiKey, reading a password means touching the key.

# Initialize pass with your GPG key
pass init <KEY_ID>

# Or with multiple keys (handy for backup)
pass init <KEY_ID_1> <KEY_ID_2>

Adding passwords:

# New password
pass insert servers/prod-db

# Generate random password
pass generate -n 32 servers/api-key

# View password (asks for YubiKey touch)
pass servers/prod-db

5. Git Signing

Sign git commits with your GPG key:

git config --global user.signingkey <KEY_ID>
git config --global commit.gpgsign true
git config --global tag.gpgsign true

Every commit now asks for a YubiKey touch. No extra password, just a physical tap to prove the commit came from me and not from some process that picked up a key off disk.

The Daily Workflow

Here is what an actual day looks like once it is running:

# Morning: plug in YubiKey, type PIN once
gpg --card-status  # Triggers PIN prompt

# SSH to server - no password
ssh prod-server

# Get a password - touch the key
pass show infra/grafana | head -1

# Git commit - touch the key
git commit -m "Fix critical bug"

# Everything works, YubiKey stays in USB

After that first PIN, everything keeps working until the cache timeout (600 seconds by default for the PIN, while touch stays required for as long as I leave the policy on).

Useful Aliases

# Quick pass lookup with fzf
alias p='pass show $(find ~/.password-store -name "*.gpg" | sed "s|$HOME/.password-store/||;s|\.gpg$||" | fzf)'

# Copy password to clipboard
alias pc='pass show -c $(find ~/.password-store -name "*.gpg" | sed "s|$HOME/.password-store/||;s|\.gpg$||" | fzf)'

# YubiKey status
alias yk='gpg --card-status'

# Show SSH keys
alias sshkeys='ssh-add -L'

Advanced: Touch Policy

This is where you decide how much the key bugs you. YubiKey supports a few touch policies:

PolicyMeaning
OffNo touch needed
OnTouch for every operation
FixedTouch required, cannot be changed
CachedTouch valid for X seconds

I run “On” for signing and authentication. Every git commit and SSH login needs a physical touch. That is deliberate. A touch policy is my tripwire: if the key blinks and I did not ask for anything, something is using it that should not be. I trade a tiny bit of friction for the certainty of knowing when my key is in use.

# Set touch policy via ykman
ykman openpgp keys set-touch sig on
ykman openpgp keys set-touch aut on
ykman openpgp keys set-touch enc on

Advanced: Backup Strategy

A YubiKey can snap, drown, or vanish into a coat pocket you donate to charity. Resilience here means planning for the key being gone, not hoping it never happens:

  1. Backup YubiKey: Put the same keys on a second YubiKey, store it safely
  2. Paper backup: Print your master key and store it offline
  3. Revocation certificate: Generate and store this separately
# Generate revocation certificate
gpg --gen-revoke <KEY_ID> > revoke.asc
# Store this safely, separate from your key backup

For pass you can sync the entire store with git:

cd ~/.password-store
git init
git remote add origin git@github.com:user/pass-store-private.git
git push -u origin main

The passwords are encrypted, so even if the repo leaks they’re useless without your GPG key.

Troubleshooting

“No secret key”

# Check if YubiKey is visible
gpg --card-status

# Not visible? Check USB
lsusb | grep -i yubi

# GPG doesn't know where the key is? Refresh
gpg-connect-agent "scd serialno" "learn --force" /bye

SSH doesn’t work

# Check if gpg-agent is running as SSH agent
echo $SSH_AUTH_SOCK
# Should be something like: /run/user/1000/gnupg/S.gpg-agent.ssh

# Load keys
ssh-add -L
# Should show your GPG key

# Not working? Restart agent
gpgconf --kill gpg-agent
gpgconf --launch gpg-agent

PIN blocked

After three wrong PIN attempts the YubiKey locks. Reset with the Admin PIN:

gpg --edit-card
gpg/card> admin
gpg/card> passwd
# Choose option 2: unblock PIN

Forgot your Admin PIN too? Then the YubiKey is a brick for those keys, and this is exactly why you set up a backup YubiKey before you needed one.

Why This Works for Me

The usual story is that security and convenience pull against each other. More security means more friction. This setup quietly inverts that for me:

  • Fewer passwords to type: one PIN per session, then just a touch
  • Hard to phish: the physical key has to be present, and there is no reusable password to steal
  • One workflow: a single key for SSH, signing, and pass, with no jumping between tools and mental models
  • Works offline: no internet, no cloud account, no dependency on a vendor staying online

The part that matters most is that I actually use it. A security setup that fights me gets bypassed within a week. This one I never think about, which is the whole point.

The Full Picture: Minimum Viable Version

If you want to try this without configuring all of it in one sitting, here is the smallest path that still gives you the core win. It is the simple version from the top of the post, now with the context to back it up:

  1. Buy a YubiKey 5 (the NFC variant is handy for phone)
  2. Generate a GPG key with authentication subkey
  3. Put the keys on the YubiKey
  4. Configure gpg-agent for SSH
  5. Test with ssh-add -L

Once that works, you layer pass, git signing, and the touch policies on top at whatever pace suits you.

Was It Worth It

The first-time setup is not trivial. You have to understand what GPG subkeys are, get gpg-agent to behave, and hold the mental model of how the pieces fit. That is real time, and the swearing during my three evenings was real too.

But the YubiKey now sits at the centre of how I authenticate to everything, which is exactly the point I was hoping to reach when I started: my private keys live on hardware I can pocket, and using them is one tap. Plug in, PIN, and go.

# Check if everything works
gpg --card-status && ssh-add -L && pass show test/dummy

If all of that works without errors: you’re done.