Secrets Management with Age / Rage
This document outlines basic "Hello, World" usage for encrypting and decrypting secrets using rage (a Rust implementation of age) with various identity and recipient types.
Key Terminology
To avoid confusion when using age and rage:
- Identity File (
-i <file>): Represents the Identity / key reference payload (*-identity.txtorprivate-key.txt). Used for decryption. - Recipient File (
-R <file>) or Recipient Address (-r <address>): Contains one or more Public Keys (tpm-public-key.txt,yubikey-public-key.txt). Used for encryption.
Standard Identity File Location (
~/.config/age/identities):rageandageCLI tools require passing explicit-i /path/to/identityarguments for custom key locations during decryption. For details on hownixfileshandles default identity loading via theragenixwrapper andragedFish alias while keepingrageunaliased, see Secrets Documentation - User Identities.
SSH Host Keys & age vs rage
When using SSH host keys (/etc/ssh/ssh_host_ed25519_key) as decryption identities:
age(Go implementation): Supports raw OpenSSH public keys (ssh-ed25519 AAAAC3...) directly as recipients (-r "ssh-ed25519 AAAAC3...") and decrypts natively with-i /etc/ssh/ssh_host_ed25519_key.ragenix/agenix: Configuressecrets.nixwith rawssh-ed25519 AAAAC3...public key strings. During system activation,ragenixusesage/rageto decrypt/run/secrets/directly using the system host key.ssh-to-age: Converts an OpenSSH public key into a native X25519age1...address. Note that converting toage1...recipient format requires converting the private key withssh-to-age -kfor manual decryption viarage -d. Using rawssh-ed25519 AAAAC3...strings insecrets.nixis the standard convention for NixOS activation.
1. Passphrase Encryption
Passphrase-based encryption prompts for a password interactively during encryption and decryption. No key files or hardware tokens are required.
Note on
pinentry& Terminal Passphrase Prompts: Whenrage -pruns, it invokespinentryby default. In terminal environments (such as GNOME Console, Kitty, or TMUX),pinentry-cursesmay hang or fail to capture keyboard input.To bypass
pinentryand enter the passphrase directly in your terminal, setPINENTRY_PROGRAM="":PINENTRY_PROGRAM="" rage -p -o secret.txt.age secret.txt
Encrypt
Create a test file and encrypt it:
echo 'hello, secret' > secret.txt
rage -p -o secret.txt.age secret.txt
Decrypt
rage -d secret.txt.age
2. Private Key File
Generate a standard X25519 key pair stored in local files.
Note on Post-Quantum (PQ) Encryption: Standard
rage-keygengenerates X25519 key pairs (Curve25519 ECC), which are not post-quantum safe. Native post-quantum support (-pq) is being developed in unreleasedrageupstream PRs (#590).
Key Generation
Generate the private key file and derive the public recipient file:
rage-keygen -o private-key.txt
rage-keygen -y private-key.txt > public-key.txt
Encrypt
Encrypt using the recipient public key file (-R):
rage -R public-key.txt -o secret.txt.age secret.txt
Decrypt
Decrypt using the private key file (-i):
rage -d -i private-key.txt secret.txt.age
NB: The private-key.txt is not protected by any passphrase. We therefore do not generally recommend this, and instead encourage the use of TPM (and YK, if available).
3. TPM (Trusted Platform Module)
age-plugin-tpm stores key material sealed inside the local system's TPM 2.0 hardware module.
Note on Post-Quantum Safety: TPM 2.0 ECC (P-256 / RSA) key storage is not post-quantum safe.
Key Generation
Generate the TPM identity file sealed with a PIN/passphrase (-g -p), append it directly to ~/.config/age/identities, and convert it to a recipient public key (-y reading from input file -o):
mkdir -p ~/.config/age
age-plugin-tpm -g -p >> ~/.config/age/identities
# Export recipient in age1tpm1... format (required for ragenix due to https://github.com/yaxitech/ragenix/issues/170)
age-plugin-tpm --tpm-recipient -y -o tpm-public-key.txt ~/.config/age/identities
Important Note for
ragenix/secrets.nix: When generating TPM recipient public keys for use insecrets.nix, always useage-plugin-tpm --tpm-recipient -y ...to export the recipient inage1tpm1...format. The defaultage1tag1...(p256tag) format is currently not handled correctly duringragenixrekeying (yaxitech/ragenix#170).PIN / Passphrase Security Requirement: Always supply the
-p/--pinflag when generating a TPM key. Without-p, any unprivileged process running under your user shell could silently ask the TPM chip to unseal and decrypt age secrets. With-p,age-plugin-tpmtriggers a graphical/terminalpinentryprompt requiring human confirmation before the TPM releases the key.
How identity files work & System Wipes:
~/.config/age/identitiescontains sealed key blob payloads bound to your system's TPM 2.0.- If your machine is completely wiped and NixOS is re-installed, as long as you retain
~/.config/age/identitiesand the hardware TPM chip was not reset in BIOS/UEFI, your identity can still be unsealed and decrypted on the fresh NixOS installation. - If the TPM chip is cleared/reset in BIOS/UEFI, the TPM identity becomes permanently unrecoverable.
Encrypt
Using the generated public recipient file (-R):
rage -R tpm-public-key.txt -o secret.txt.age secret.txt
Decrypt
rage -d -i ~/.config/age/identities secret.txt.age
4. YubiKey
age-plugin-yubikey manages age identity keys stored inside a physical YubiKey's PIV slot.
Note on Post-Quantum Safety: YubiKey PIV slots use RSA / ECC (P-256, Ed25519) and are not post-quantum safe.
Key Generation
Insert your YubiKey and generate a new identity key, then print its recipient address:
$ age-plugin-yubikey --generate
🎲 Generating key...
Enter PIN for YubiKey with serial 9599730 (default is 123456): [hidden]
✨ Your YubiKey is using the default PIN. Let's change it!
✨ We'll also set the PUK equal to the PIN.
🔐 The PIN can be numbers, letters, or symbols. Not just numbers!
📏 The PIN must be at least 6 and at most 8 characters in length.
❌ Your keys will be lost if the PIN and PUK are locked after 3 incorrect tries.
Enter current PUK (default is 12345678): [hidden]
Choose a new PIN/PUK: [hidden]
✨ Your YubiKey is using the default management key.
✨ We'll migrate it to a PIN-protected management key.
... Success!
🔏 Generating certificate...
👆 Please touch the YubiKey
# Serial: 1234567, Slot: 1
# Name: age identity abcdefgh
# Created: Sat, 08 Aug 2026 22:09:57 +0000
# PIN policy: Once (A PIN is required once per session, if set)
# Touch policy: Always (A physical touch is required for every decryption)
# Recipient: age1yubikey1q...
AGE-PLUGIN-YUBIKEY-...
$ mkdir -p ~/.config/age
$ age-plugin-yubikey --identity >> ~/.config/age/identities
$ age-plugin-yubikey --list > yubikey-public-key.txt
How identity files work & System Wipes:
~/.config/age/identitiescontains stub references (serial number, slot, public key) pointing to secret keys stored securely inside physical YubiKeys.- If your OS is wiped and re-installed, retaining
~/.config/age/identities(or re-runningage-plugin-yubikey --identity >> ~/.config/age/identities) will allow you to decrypt files on the new installation using the same physical YubiKey.
Encrypt
age-plugin-yubikey outputs age recipient addresses (age1yubikey1...). Encrypt using -r with the recipient address (or -R with a recipient file containing age1yubikey... addresses):
rage -e -r $(cat yubikey-public-key.txt) -o secret.txt.age secret.txt
Decrypt
rage -d -i ~/.config/age/identities secret.txt.age
When decrypting, rage will prompt for your YubiKey PIN and require physical touch on the YubiKey hardware button.
Troubleshooting
PC/SC Security Violation Error with YubiKey over SSH
When running age-plugin-yubikey -l or decrypting with a YubiKey over an SSH session, you may encounter an error such as:
$ age-plugin-yubikey -l
Error: Error while communicating with YubiKey: PC/SC error: Access was denied because of a security violation
Cause: Access was denied because of a security violation
Cause:
Smart card readers and CCID daemons (pcscd) rely on polkit and systemd-logind session authorization for access control.
- When logged into a local desktop/terminal session (
seat0),systemd-logindclassifies your session asRemote=no. Polkit's default policy (org.debian.pcsc-lite.access_pcsc/access_card) automatically allows active local users access to hardware tokens. - When connected over SSH,
systemd-logindclassifies the session asRemote=yes. Polkit denies PC/SC access to remote sessions by default to prevent remote SSH users from unauthorized access to physically plugged-in YubiKeys.
Verification:
Verify how systemd-logind classifies your current session:
loginctl show-session $XDG_SESSION_ID
- Local terminal:
Remote=no,Seat=seat0 - SSH terminal:
Remote=yes,Seat=
No Output from age-plugin-yubikey -l
If age-plugin-yubikey -l completes without printing any identities or errors, no age key has been generated on the YubiKey yet.
Resolution:
Generate a new identity key on your YubiKey:
age-plugin-yubikey --generate
age-plugin-yubikey --generate Timeout or Hanging (Disabled PIV Interface)
When running age-plugin-yubikey --generate, if it hangs on ⏳ Please insert the YubiKey. even though your YubiKey is plugged in, and eventually fails with:
Error: Timed out while waiting for a YubiKey to be inserted.
Cause:
The PIV interface on your YubiKey may be disabled over USB.
Verification:
Check enabled USB applications using ykman:
ykman info
Check if PIV is listed under enabled USB applications.
Resolution:
Enable the PIV application over USB:
ykman config usb --enable PIV
Related: str4d/age-plugin-yubikey#238