Setting up gopass with a YubiKey on macOS
I have set up gopass as a work password store, backed by age encryption with a YubiKey holding the private key, and a git remote for sync. This is my notes about how I set the whole thing up
The goal
- Password store managed by
gopass - Secrets encrypted with
age, private key on a YubiKey (viaage-plugin-yubikey) - Store committed to git, synced to a remote repo
- PIN + touch required to decrypt, nothing else
Architecture, briefly
There are two layers of encryption in this stack, and conflating them is the source of every confusion in this thread:
- Your secrets (
foo.agefiles in the store) are encrypted withageto a YubiKey recipient (age1yubikey1...). The matching private key lives on the YubiKey hardware. Decryption requires PIN + touch. - The gopass keyring file (
~/.config/gopass/age/identities) holds the YubiKey identity stub (AGE-PLUGIN-YUBIKEY-...), which is just a pointer to a slot on a specific YubiKey serial. gopass encrypts this file with a passphrase (age scrypt mode), not with the YubiKey. The passphrase is unavoidable; macOS Keychain + Touch ID can make it invisible day-to-day.
The identity stub isn’t sensitive — useless without the physical YubiKey. The keyring encryption exists because gopass uses the same code path for plugin identities and native AGE-SECRET-KEY-... identities, and for the latter it genuinely matters.
Install
| |
Generate the YubiKey identity
Plug in the YubiKey and run:
| |
Policy choices for a password store:
- PIN policy
once— PIN required once per session - Touch policy — I use
cached— touch once, cached 15 seconds (convenient)
The command produces two strings:
- An identity (
AGE-PLUGIN-YUBIKEY-1...) — pointer to the YubiKey slot - A recipient (
age1yubikey1...) — public key, used to encrypt secrets
Get them back any time with:
| |
Backup matters
If this YubiKey breaks or is lost and it’s the only recipient, the store is unrecoverable. Before going further, decide on a backup recipient: a second physical YubiKey, or a passphrase-protected age key stored offline. Add it as an additional recipient in the next step.
Set up the gopass store
Do not redirect age-plugin-yubikey --identity into ~/.config/gopass/age/identities directly. gopass expects an encrypted keyring file there and will choke trying to “decrypt” the plaintext you wrote. Several guides online suggest this; it’s wrong for current gopass.
Instead, run:
| |
When asked, choose to enter a passphrase (recommend y over the auto-generated one). This passphrase protects the local keyring file. Remember it — there’s no recovery.
gopass setup will also generate its own internal age keypair and add it to the keyring. We’ll deal with that in the next step.
Add the YubiKey identity to the keyring
| |
It prompts for:
- Passphrase — the keyring passphrase you just set
- Age identity — paste the
AGE-PLUGIN-YUBIKEY-1...string (the single line, not the#comment lines around it) - Recipient — paste the matching
age1yubikey1...
Verify:
| |
You’ll see two identities — the auto-generated one and the YubiKey one. This is the first footgun: both are now in the keyring, and gopass silently uses every identity as a recipient when encrypting new secrets. Your “YubiKey-only” store is anything but.
The critical part: remove the auto-generated identity
Check what your store is actually encrypting to:
| |
You’ll see both keys. Remove the non-YubiKey one:
| |
To verify it actually worked, look at a secret file directly:
| |
You want to see only a -> piv-p256 stanza in the age header. If you also see -> X25519, the auto-generated key is still encrypting alongside your YubiKey — your secrets can be decrypted by a soft key sitting in your home directory, which is exactly the threat model the YubiKey is meant to defeat.
This is the step the official docs do not emphasize, and it bit me hard. gopass insert and gopass show will happily succeed even when nothing is actually using the YubiKey, because the soft key works without prompting. You can go a long time thinking your YubiKey setup is live when it isn’t.
Set up the git remote
The store is already a git repo (gitfs backend). Add the remote and push:
| |
gopass sync does pull → commit → push. With core.autosync = true (default), inserts and edits push automatically.
Verify the round-trip
| |
Expected:
- YubiKey LED blinks (touch needed, since policy is
cachedand >15s since last touch) - PIN prompt on first decrypt of the session
- Touch ID prompt (or passphrase) for the keyring — once per session if Keychain is enabled
If gopass show returns the value without the YubiKey blinking at all, go back to the verification step above and check the age header for a stray -> X25519 stanza.
Make the keyring passphrase invisible
Three options for handling the keyring passphrase:
| |
Option 1 is the most convenient on macOS — Touch ID replaces typing. Option 2 is the cross-platform answer. Since I’m only using a mac I went for option one.
Daily UX summary
After all this, day-to-day looks like:
- First
gopasscommand in a session: Touch ID prompt (keychain releases the keyring passphrase) - First decrypt of the session: YubiKey PIN prompt
- Each decrypt after 15s of inactivity: YubiKey LED blinks, touch it
- Rapid successive decrypts within 15s: no interaction needed
- Insert/edit: git push happens automatically; if SSH key is in Secretive or similar, expect one more Touch ID prompt for that
The SSH Touch ID prompt — if it appears — is from your SSH agent (Secretive in my case), not from gopass. It says “SecretAgent” and mentions ssh-needs-auth.
Lessons
- Don’t write the identities file by hand. Use
gopass age identities add. The file is meant to be encrypted; pre-populating it with plaintext is the most-recommended-online and most-broken approach. - Always inspect a real
.agefile after setup to confirm only-> piv-p256stanzas are present. The CLI’sgopass recipientsoutput is necessary but not sufficient — gopass silently includes its internal identity as a recipient if it’s in the keyring. - The keyring passphrase is structural, not optional. Accept it and offload to Keychain or the agent.
Now I have a work password store I trust, with the actual security boundary on the YubiKey where I wanted it. Next: a separate private store mounted alongside, using a different YubiKey slot — same drill, but at least now I know where the trapdoors are.