Hardware keys
YubiKey hardware keys provide tamper-resistant cryptographic key storage for SSH authentication and age encryption. Unlike software keys stored as files on disk, hardware key material never leaves the YubiKey's secure element — the operating system can only request cryptographic operations, not extract the private key. This page covers YubiKey setup for SSH and age, SSH agent configuration, and key inventory management.
For encrypting and managing NixOS secrets with agenix, see Secrets management.
Threat model
A typical agenix setup begins with age-keygen, which creates a software key file at ~/.config/age/keys.txt. This file serves as the master decryption key. Anyone who can read it can decrypt every secret in the repository. Full-disk encryption protects the file while the machine is powered off, but on a running system the key is an ordinary file accessible to any process running as the user.
A YubiKey eliminates this class of vulnerability. Cryptographic keys are generated inside the YubiKey's tamper-resistant secure element and never leave it. The operating system cannot extract the private key; it can only send data to the YubiKey and receive signed or decrypted results. Every cryptographic operation requires physical touch on the device.
Two YubiKey capabilities apply to NixOS secrets management:
- FIDO2 (for SSH) -- The YubiKey generates an
ed25519-skSSH key pair. The-sksuffix indicates a security key; the private key stays on the device. The file on disk is a handle, useless without the physical YubiKey. With the-O residentflag, the key is stored on the YubiKey itself, allowing it to be loaded on any machine withssh-add -K. - PIV (for age encryption) -- The
age-plugin-yubikeytool creates an age identity backed by the YubiKey's PIV applet. Encrypting and decrypting age-encrypted data requires the YubiKey to be plugged in and touched.
For disk encryption keys managed by TPM and LUKS, see Disk encryption.
The trust chain
YubiKey, age, agenix, and NixOS each handle one layer of the secrets stack. Together they form a complete trust chain from hardware to running services:
YubiKey Hardware
|
+-- FIDO2: ed25519-sk SSH key
| +-- Authenticates to remote hosts and GitHub
|
+-- PIV: age-plugin-yubikey identity
+-- Encrypts/decrypts age-encrypted data
+-- agenix uses age to manage NixOS secrets
+-- NixOS activation decrypts to /run/agenix/ (tmpfs)
+-- Services read secrets from known pathsEach layer is independent. Age can be used without agenix. Agenix can be used with a software age key instead of a YubiKey. A YubiKey can be used for SSH without touching age at all. Composed together, however, the stack provides hardware-rooted trust for the entire secrets workflow.
YubiKey SSH key setup
Prerequisites
A YubiKey 5 series with firmware 5.2.3 or later is required for Ed25519-SK resident keys. Earlier firmware supports ECDSA-SK but not Ed25519-SK. Check the firmware version:
ykman infoYubiKey firmware cannot be upgraded. If the firmware is below 5.2.3, substitute ecdsa-sk for ed25519-sk in the commands below.
The required software packages are yubikey-manager, age-plugin-yubikey, and openssh. If using Keystone, enable the hardware key module to install all packages automatically:
keystone.hardwareKey.enable = true;Otherwise, start a temporary shell with the required tools:
nix-shell -p yubikey-manager age-plugin-yubikey opensshGenerate SSH key on YubiKey
Set a FIDO2 PIN on the YubiKey if one has not been set:
ykman fido access change-pinGenerate a resident Ed25519-SK key. The -O resident flag stores the key on the YubiKey itself:
ssh-keygen -t ed25519-sk -O resident -O application=ssh:mynameThe YubiKey will require a touch and the FIDO2 PIN. This creates a public key and a private key handle. The handle is safe to store in version control; it is useless without the physical YubiKey.
Verify the credential was stored:
ykman fido credentials listLoad the resident key into the SSH agent:
ssh-add -KExport the public key for use in NixOS configurations:
ssh-add -L | grep ed25519-sk > ~/.ssh/id_ed25519_sk.pubThe key is now portable. On any new machine, plug in the YubiKey and run ssh-add -K to load it.
YubiKey age identity setup
Generate age identity on YubiKey
The age-plugin-yubikey tool creates an age encryption identity backed by the YubiKey:
age-plugin-yubikeyThe interactive setup prompts for:
- Slot -- Select slot 1 unless there is a reason to use another.
- PIN policy -- Choose
once(enter PIN once per session) oralways. - Touch policy -- Choose
alwaysto require physical touch for every operation.
After setup, the tool prints the age public key:
age1yubikey1q...Save this public key. It goes into secrets.nix. The private key never leaves the YubiKey.
Verify the identity:
age-plugin-yubikey --listDeclarative identity management
Keystone provides the ageYubikey home-manager module to write a combined identity file and set AGE_IDENTITIES_FILE automatically:
keystone.terminal.ageYubikey = {
enable = true;
identities = [
"AGE-PLUGIN-YUBIKEY-17DDRYQ5ZFMHALWQJTKHAV" # Serial: 36854515, Slot: 1
];
};For multiple YubiKeys (primary and backup):
keystone.terminal.ageYubikey = {
enable = true;
identities = [
"AGE-PLUGIN-YUBIKEY-17DDRYQ5ZFMHALWQJTKHAV" # primary
"AGE-PLUGIN-YUBIKEY-1A2B3C4D5E6F7G8H9I0JAB" # backup
];
};When home-manager activates, the module writes the configured identity strings to ~/.age/yubikey-identity.txt and sets AGE_IDENTITIES_FILE to point at it. The identity string is not a secret; it is a reference containing the YubiKey serial number and PIV slot. The actual private key stays inside the YubiKey hardware.
With AGE_IDENTITIES_FILE set, agenix commands use the YubiKey identity automatically without requiring -i flags:
agenix -e secrets/my-secret.age # touch YubiKey when prompted
agenix -d secrets/my-secret.age # touch YubiKey when prompted
agenix -r # touch YubiKey once per secret| Option | Type | Default | Description |
| -------------- | ------------ | ----------------------------- | ---------------------------------------------------------------------------- |
| enable | bool | false | Enable age-plugin-yubikey identity file management |
| identities | listOf str | [] | Age-plugin-yubikey identity strings (each starts with AGE-PLUGIN-YUBIKEY-) |
| identityPath | str | ~/.age/yubikey-identity.txt | Path where the combined identity file is written |
Hardware key inventory
Maintain an inventory of YubiKeys in the nixos-config or agenix-secrets repository. If a key is lost or compromised, the inventory identifies exactly which keys and secrets to revoke and re-key.
| Serial | Model | Role | Deployed | SSH Fingerprint | Age Public Key |
| -------- | ------------- | --------------------- | ---------- | ---------------- | ---------------- |
| 12345678 | YubiKey 5 NFC | Primary - daily carry | 2026-02-23 | SHA256:abc123... | age1yubikey1q... |
| 87654321 | YubiKey 5C | Backup - home safe | 2026-02-23 | SHA256:def456... | age1yubikey1q... |Record for each key: serial number, model and form factor, role (primary, backup, offsite), date deployed, SSH public key fingerprint, and age public key.
Backup key workflow: Initialize a backup YubiKey with the same steps (generate SSH key, generate age identity), add both keys' public keys to secrets.nix, and re-key with agenix -r. Store the backup in a secure offsite location. If the primary key is lost, the backup can decrypt and re-key all secrets.
SSH agent configuration
An SSH agent holds private keys in memory so passphrases do not need to be re-entered for every connection. Only one agent should manage SSH_AUTH_SOCK at a time.
OpenSSH agent (recommended default)
The simplest option, running as a systemd user service:
{
programs.ssh = {
enable = true;
addKeysToAgent = "yes";
};
services.ssh-agent.enable = true;
}This starts ssh-agent as a systemd user service and sets SSH_AUTH_SOCK automatically. Keys are added to the agent on first use after entering the passphrase once.
For YubiKey resident keys, add automatic loading to the shell initialization:
programs.zsh.initExtra = ''
if command -v ssh-add &> /dev/null && [ -n "$SSH_AUTH_SOCK" ]; then
ssh-add -K 2>/dev/null
fi
'';GNOME Keyring
GNOME Keyring integrates with the desktop login session, unlocking keys automatically at login:
{
services.gnome.gnome-keyring.enable = true;
security.pam.services.greetd.enableGnomeKeyring = true;
}GPG agent with SSH support
If GPG keys are stored on a YubiKey via the OpenPGP applet, GPG agent can act as the SSH agent. Keystone enables this with:
keystone.hardwareKey.enable = true;GPG agent with SSH support and GNOME Keyring's SSH component cannot run simultaneously, as both set SSH_AUTH_SOCK. Choose one or the other.
SSH configuration tips
Regardless of which agent is in use, these SSH configuration patterns are useful:
{
programs.ssh = {
enable = true;
addKeysToAgent = "yes";
matchBlocks = {
"github.com" = {
identityFile = "~/.ssh/id_ed25519";
identitiesOnly = true;
};
"prod-*" = {
identityFile = "~/.ssh/id_ed25519_sk";
identitiesOnly = true;
};
};
};
}Setting identitiesOnly = true prevents the agent from offering every loaded key to every host; it sends only the key specified in identityFile.
Troubleshooting
YubiKey not detected
systemctl status pcscd
lsusb | grep -i yubi
sudo systemctl restart pcscdIf pcscd is not installed, ensure keystone.hardwareKey.enable = true is set or add services.pcscd.enable = true to the NixOS configuration.
AGE_IDENTITIES_FILE not set
Session variables require a new login shell after the first home-manager activation. Log out and back in, then verify:
echo $AGE_IDENTITIES_FILEMultiple agents competing for SSH_AUTH_SOCK
echo $SSH_AUTH_SOCK
ls -la $SSH_AUTH_SOCKCommon paths indicate which agent is active:
/run/user/1000/keyring/ssh-- GNOME Keyring/run/user/1000/gnupg/S.gpg-agent.ssh-- GPG Agent/run/user/1000/ssh-agent-- OpenSSH Agent
Disable agents that are not in use. Only one should own the socket.
See also
- Secrets management -- Agenix-based encrypted secrets for NixOS
- Disk encryption -- LUKS and TPM-based key management for storage encryption
- Architecture -- Module system overview and security model context
- Getting started -- Secrets configuration during initial deployment
- age-plugin-yubikey -- Age encryption plugin for YubiKey
- YubiKey Manager -- CLI tool for YubiKey configuration