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:
| Tool | Function |
|---|---|
| YubiKey | Hardware security key with smartcard functionality |
| GPG | Encryption and signing |
| pass | CLI password manager (uses GPG) |
| gpg-agent | Agent that caches GPG keys and handles SSH auth |
| SSH | You 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:
| Policy | Meaning |
|---|---|
| Off | No touch needed |
| On | Touch for every operation |
| Fixed | Touch required, cannot be changed |
| Cached | Touch 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:
- Backup YubiKey: Put the same keys on a second YubiKey, store it safely
- Paper backup: Print your master key and store it offline
- 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:
- Buy a YubiKey 5 (the NFC variant is handy for phone)
- Generate a GPG key with authentication subkey
- Put the keys on the YubiKey
- Configure gpg-agent for SSH
- 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.
