Your Backup Encryption Has a Dirty Secret
You set up encrypted backups. You used GPG. You generated a 4096-bit RSA key, uploaded it to a keyserver, printed the fingerprint on paper, and felt extremely secure. Then you forgot the passphrase six months later and realised your “secure” private key was sitting in ~/.gnupg/ on a laptop that anyone with physical access could image in under five minutes.
Here’s a better story: your private key lives on a piece of hardware the size of a house key. It never leaves. Not to RAM, not to disk, not to a temp file. You touch the button, it signs. You unplug it, it’s gone. No key material on the machine, ever.
That’s what age + age-plugin-yubikey gets you. And unlike the GPG version of this story, setup takes about fifteen minutes instead of an afternoon of reading RFCs.
age in Thirty Seconds (Prereq Recap)
If you’ve read the age vs GPG piece on this site, skip this section. If not, the thirty-second version:
age is a modern file encryption tool by Filippo Valsorda. It does one thing: encrypt files, and does it with X25519 key exchange, ChaCha20-Poly1305 AEAD, and a dead-simple recipient model. You encrypt to one or more recipients (public keys), any of the corresponding private keys can decrypt.
No web of trust. No subkeys. No “what does --armor do again?” No keyservers. Just:
age -r age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p secrets.tar.gz > secrets.tar.gz.ageage -d -i ~/.age/identity.txt secrets.tar.gz.age > secrets.tar.gzThe magic is that recipients are composable: you can encrypt to multiple keys at once. Whoever holds any of those keys can decrypt. This matters a lot when we get to the YubiKey recovery story.
Enter age-plugin-yubikey
age-plugin-yubikey is a plugin that lets age use a YubiKey’s PIV applet as the backend for key generation and decryption. The private key is generated on the YubiKey itself, stored in a PIV slot, and the hardware enforces that it never leaves.
What’s PIV? It’s a smart card standard that YubiKeys implement. Your key has four main PIV slots (9a, 9c, 9d, 9e) plus 20 retired key management slots (82 to 95). For encryption purposes you’ll use one of the standard slots; 9d (Key Management) is the conventional choice, but any slot works.
Install it
# Arch / Manjarosudo pacman -S age age-plugin-yubikey
# Ubuntu / Debian (age from apt, plugin from GitHub releases)sudo apt install agewget https://github.com/str4d/age-plugin-yubikey/releases/latest/download/age-plugin-yubikey-linux-amd64.tar.gztar xzf age-plugin-yubikey-linux-amd64.tar.gzsudo install age-plugin-yubikey /usr/local/bin/
# macOSbrew install age age-plugin-yubikeyVerify both are in your PATH:
age --versionage-plugin-yubikey --versionYou’ll also want pcscd running, the PC/SC daemon that talks to smart cards:
sudo systemctl enable --now pcscdGenerating Your YubiKey Identity
This is where it gets interesting. You’re not generating a key on your computer and moving it to the YubiKey. You’re asking the YubiKey to generate the key internally. It never exists anywhere else.
age-plugin-yubikey --generate --slot 9dYou’ll get a wizard that asks for:
- A PIN policy (never / once / always): use
onceunless you enjoy typing your PIN for every file - A touch policy (never / always / cached): use
alwaysfor the good stuff; you want the physical confirmation
After generation it prints two things:
# Recipient (public key) — share this:age1yubikey1qwt8ypll5m6g...
# Identity (plugin stub) — keep this:AGE-PLUGIN-YUBIKEY-1...The recipient is a normal age public key; you can print it on a post-it, put it in a README, whatever. The identity stub is a reference to the hardware key, not the key material itself. It’s safe to store, but without the physical YubiKey it’s useless.
Save the identity stub somewhere you can find it:
mkdir -p ~/.ageage-plugin-yubikey --identity --slot 9d > ~/.age/yubikey-identity.txtEncrypting and Decrypting Files
Encryption is identical to regular age: you just use the YubiKey recipient:
# Encrypt a single fileage -r age1yubikey1qwt8ypll5m6g... secrets.env > secrets.env.age
# Encrypt a whole directory as a tarballtar czf - ~/Documents/private/ | age -r age1yubikey1qwt8ypll5m6g... > private.tar.gz.ageDecryption:
age -d -i ~/.age/yubikey-identity.txt secrets.env.age > secrets.envIf you set touch policy to always, your YubiKey’s LED will start blinking. Touch it. That’s the physical proof of presence: without your actual hand on the hardware, decryption fails. A remote attacker who somehow gets your identity stub and your encrypted files still can’t do anything.
The Recovery Problem (And How to Not Get Locked Out)
Hardware-backed keys have a critical property: if you lose the YubiKey, the private key is gone. Forever. That’s by design, but it means you need a recovery plan before you encrypt anything you care about.
The solution is age’s multi-recipient model. Encrypt to multiple recipients simultaneously:
# Primary: YubiKeyYUBIKEY_PRIMARY="age1yubikey1qwt8ypll5m6g..."
# Backup: second YubiKey (different slot, or a spare key)YUBIKEY_BACKUP="age1yubikey1q9p3kzlm4n7r..."
# Emergency: a passphrase-protected software key (locked in your password manager)SOFTWARE_BACKUP="age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p"
age -r "$YUBIKEY_PRIMARY" -r "$YUBIKEY_BACKUP" -r "$SOFTWARE_BACKUP" \ secrets.tar.gz > secrets.tar.gz.ageAny one of those three keys can decrypt the file. Your primary YubiKey is the everyday driver. Your backup YubiKey lives in a fireproof safe. Your software key is a passphrase-protected identity in your Bitwarden vault. Now losing one key is a minor inconvenience, not a catastrophe.
For convenience, put your recipients in a file:
# Primary YubiKey (slot 9d, home desktop)age1yubikey1qwt8ypll5m6g...
# Backup YubiKey (spare, safe)age1yubikey1q9p3kzlm4n7r...
# Software emergency keyage1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8pThen:
age -R ~/.age/recipients.txt secrets.tar.gz > secrets.tar.gz.agerage: The Rust Implementation
rage is a Rust reimplementation of age, fully compatible, slightly faster, and supports the same plugin system. If you’re building scripts or want better error messages, it’s worth using:
# Install ragecargo install rage# or on Arch: sudo pacman -S rage
# Works identicallyrage -R ~/.age/recipients.txt secrets.tar.gz > secrets.tar.gz.agerage -d -i ~/.age/yubikey-identity.txt secrets.tar.gz.age > secrets.tar.gzThe plugin protocol is the same, so age-plugin-yubikey works with both.
Integrations Worth Knowing About
chezmoi
chezmoi manages dotfiles with built-in age encryption support. Point it at your YubiKey identity and your dotfiles repo becomes a secure secrets store that only decrypts on machines with your key plugged in:
[encryption] tool = "age" [encryption.age] identity = "~/.age/yubikey-identity.txt" recipient = "age1yubikey1qwt8ypll5m6g..."Now chezmoi add --encrypt ~/.ssh/id_ed25519 encrypts your SSH key in the repo, and chezmoi apply prompts for a YubiKey touch to decrypt it.
sops
sops (now a CNCF project, formerly Mozilla’s) handles structured secret files (YAML, JSON, ENV) and has native age support. For your Kubernetes secrets or Ansible vault replacements:
# .sops.yaml in your repo rootcreation_rules: - path_regex: secrets/.* age: >- age1yubikey1qwt8ypll5m6g..., age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
# EncryptSOPS_AGE_KEY_FILE=~/.age/yubikey-identity.txt sops --encrypt secrets/db.yaml > secrets/db.enc.yamlThe YubiKey touch requirement kicks in every time sops needs to decrypt. Your 2 AM deploy will require a literal hand on the hardware. This is either reassuring or annoying depending on how you feel about 2 AM deploys.
pass with tomb
If you’re a pass user and want YubiKey-protected password storage, pass-age (a pass backend using age) combined with age-plugin-yubikey gives you touch-required password access. It’s a bit of a setup adventure, but the result is a password vault where every pass show physically requires you to tap a key.
Trade-offs and Gotchas
PIV slot count: YubiKey 5 series gives you 4 main slots + 20 retired management slots. That’s plenty. Just document which slot you used; age-plugin-yubikey --list shows all configured slots.
Touch policy is forever: You can’t change the touch policy after key generation without overwriting the slot (and losing that key). Decide upfront. always is more secure. cached (touch required once per session) is the compromise for heavy use.
YubiKey PIN is separate from your system PIN: Default PIV PIN is 123456. Change it. Seriously, change it right now:
ykman piv access change-pinSet the PUK too (the PIN unblock key). Lose both and you’re locked out of PIV forever.
Key backup: You cannot export a hardware-generated PIV key. This is by design. Your recovery strategy is the multi-recipient approach above, not a key export. There is no “I’ll just restore from backup.” The hardware key is the backup.
Age file format is not GPG: Files encrypted with age cannot be decrypted by GPG and vice versa. If you’re migrating an existing encrypted backup system, re-encrypt everything in a single session before decommissioning the old setup.
The Practical Setup Checklist
Get this right once and you’re set:
[ ] Install age + age-plugin-yubikey + pcscd[ ] Generate key: age-plugin-yubikey --generate --slot 9d[ ] Save identity stub to ~/.age/yubikey-identity.txt[ ] Change YubiKey PIV PIN from default (ykman piv access change-pin)[ ] Generate software backup key: age-keygen -o ~/.age/emergency-identity.txt[ ] Build recipients.txt with primary YubiKey + backup YubiKey + software key[ ] Test: encrypt a test file, decrypt with each recipient separately[ ] Store emergency-identity.txt passphrase in password manager[ ] Put backup YubiKey somewhere physically secureTest the full recovery path before you encrypt anything important. Yes, actually test it. Plug in the backup key. Decrypt the test file. Make sure it works. Your future self, the one at 2 AM with a dead YubiKey and an encrypted database, will appreciate it enormously.
Hardware-Backed or Hardware-Blocked?
The pitch for hardware-backed encryption is simple: the attack surface collapses. An attacker needs your encrypted files and your physical YubiKey and your PIN and your physical presence (if touch is enabled). Remote compromise of your machine gets them nothing useful.
The cost is that you’re on the hook for hardware lifecycle management. YubiKeys die. They get lost. They get confiscated at borders. The multi-recipient setup handles all of this, but only if you set it up before you need it.
GPG with a YubiKey PIV applet exists too, and it’s fine if you’re already in the GPG ecosystem. But if you’re starting fresh or building a new backup system, age is the better foundation. Less ceremony, better defaults, and a plugin ecosystem that’s growing faster than GPG’s.
Your private key should cost more to steal than the files it protects. Hardware-backed encryption gets you there. Now go change that default PIV PIN.
Related Reading
- systemd-homed: Portable Encrypted Home Directories
- Tang & Clevis: LUKS Auto-Unlock Without a Typed Passphrase
- Advanced UFW Techniques: Enhancing Firewall Security
- AppArmor vs SELinux: Mandatory Access Control Without the Existential Dread
- Auditd & Audit Logging: Know Exactly Who Touched What on Your Server