Skip to content
Go back

YubiKey + age: Hardware-Backed Encryption Without GPG

By SumGuy 9 min read
YubiKey + age: Hardware-Backed Encryption Without GPG
Contents

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:

Terminal window
age -r age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p secrets.tar.gz > secrets.tar.gz.age
age -d -i ~/.age/identity.txt secrets.tar.gz.age > secrets.tar.gz

The 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

Terminal window
# Arch / Manjaro
sudo pacman -S age age-plugin-yubikey
# Ubuntu / Debian (age from apt, plugin from GitHub releases)
sudo apt install age
wget https://github.com/str4d/age-plugin-yubikey/releases/latest/download/age-plugin-yubikey-linux-amd64.tar.gz
tar xzf age-plugin-yubikey-linux-amd64.tar.gz
sudo install age-plugin-yubikey /usr/local/bin/
# macOS
brew install age age-plugin-yubikey

Verify both are in your PATH:

Terminal window
age --version
age-plugin-yubikey --version

You’ll also want pcscd running, the PC/SC daemon that talks to smart cards:

Terminal window
sudo systemctl enable --now pcscd

Generating 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.

Terminal window
age-plugin-yubikey --generate --slot 9d

You’ll get a wizard that asks for:

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:

Terminal window
mkdir -p ~/.age
age-plugin-yubikey --identity --slot 9d > ~/.age/yubikey-identity.txt

Encrypting and Decrypting Files

Encryption is identical to regular age: you just use the YubiKey recipient:

Terminal window
# Encrypt a single file
age -r age1yubikey1qwt8ypll5m6g... secrets.env > secrets.env.age
# Encrypt a whole directory as a tarball
tar czf - ~/Documents/private/ | age -r age1yubikey1qwt8ypll5m6g... > private.tar.gz.age

Decryption:

Terminal window
age -d -i ~/.age/yubikey-identity.txt secrets.env.age > secrets.env

If 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:

Terminal window
# Primary: YubiKey
YUBIKEY_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.age

Any 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:

~/.age/recipients.txt
# Primary YubiKey (slot 9d, home desktop)
age1yubikey1qwt8ypll5m6g...
# Backup YubiKey (spare, safe)
age1yubikey1q9p3kzlm4n7r...
# Software emergency key
age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p

Then:

Terminal window
age -R ~/.age/recipients.txt secrets.tar.gz > secrets.tar.gz.age

rage: 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:

Terminal window
# Install rage
cargo install rage
# or on Arch: sudo pacman -S rage
# Works identically
rage -R ~/.age/recipients.txt secrets.tar.gz > secrets.tar.gz.age
rage -d -i ~/.age/yubikey-identity.txt secrets.tar.gz.age > secrets.tar.gz

The 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:

~/.config/chezmoi/chezmoi.toml
[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:

Terminal window
# .sops.yaml in your repo root
creation_rules:
- path_regex: secrets/.*
age: >-
age1yubikey1qwt8ypll5m6g...,
age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
# Encrypt
SOPS_AGE_KEY_FILE=~/.age/yubikey-identity.txt sops --encrypt secrets/db.yaml > secrets/db.enc.yaml

The 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:

Terminal window
ykman piv access change-pin

Set 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 secure

Test 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.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Next Post
K9s vs Lens vs Headlamp: Cluster UIs

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts