There’s a moment when everything clicks. You plug in your YubiKey, type your PIN once, and then everything just works. SSH to servers? No password. Sign git commits? Automatic. Get a password from pass? Touch the key and done.

That moment took me about three evenings of frustration to reach. But now that it works, I never want to go back.

Why This Setup?

I had a problem: too many authentication methods.

  • SSH keys on disk (encrypted, but still)
  • GPG keys for git signing (different key, different passphrase)
  • pass for passwords (again that GPG key, again that passphrase)
  • 2FA codes in an authenticator app (grab phone, type code)

Every time I wanted to do something, I had to type a password or grab my phone. That’s friction. And friction means I’m going to take security shortcuts.

The solution: one YubiKey that does everything.

What’s the Stack?

Quick overview of the components:

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 magic is in gpg-agent. It can function as an SSH agent too. So your GPG key on your YubiKey also becomes your SSH key.

The Setup

1. Put GPG Key on 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 doesn’t copy. Make a backup of your .gnupg directory first.

2. Configure GPG-Agent

This is where the magic happens. 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

4. Configure Pass

pass is simple — it encrypts passwords with GPG.

# 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 physical confirmation.

The Daily Workflow

This is what my day looks like:

# 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 PIN, unlimited for touch).

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'

Touch Policy

YubiKey has different touch policies:

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

I use “On” for signing and authentication. Every git commit and SSH login requires a physical touch. That’s intentional — I want to know when my key is being used.

# 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

Backup Strategy

Your YubiKey can break or get lost. Plan for that:

  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? Then your YubiKey is unusable for those keys. That’s why: backup YubiKey.

Why This Works for Me

Security and convenience are normally at odds. More security = more friction. But this setup flips that:

  • Fewer passwords to type: PIN once per session, then just touch
  • Impossible to phish: Physical key required, no password that can be stolen
  • Unified workflow: One key for everything, no context switching between tools
  • Works offline: No internet needed, no cloud dependency

Most importantly: I actually use it. A security setup that’s annoying gets bypassed. This one doesn’t.

Minimum Viable Setup

If you want to try this but don’t want to configure everything at once:

  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

After that you can add pass, configure git signing, and tweak the finer details.

Conclusion

The initial setup isn’t trivial. You need to understand GPG, configure gpg-agent, and understand how the pieces work together. That takes time.

But once it works? Then you have a setup where your YubiKey really becomes the center of your authentication. One physical key, one PIN, and everything works.

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.