The age backend encrypts secrets using age and decrypts them on the target machine during NixOS activation. Secrets are stored encrypted in your repository and uploaded encrypted to target machines — plaintext only exists in memory on the target.
Choose the age backend when:
Choose the SOPS backend when:
The age backend uses machine keypairs with key indirection:
This means user key rotation only re-encrypts machine keys (one per machine), not every secret. Shared secrets are encrypted to all machines' public keys using age's native multi-recipient support.
The backend automatically checks these locations for your private key:
AGE_KEY environment variable (key content)AGE_KEYFILE environment variable (path to key file)~/.config/age/identities~/.config/sops/age/keys.txt~/.age/key.txtIf you don't have a key yet:
mkdir -p ~/.config/age
age-keygen -o ~/.config/age/identities Note the public key from the output — you'll need it below.
Hardware tokens (YubiKey, PicoHSM) work via age plugin identity files placed at any of the paths above.
In your clan.nix:
{
# Select the age backend
vars.settings.secretStore = "age";
# Your public key(s) as default recipients for all machines
vars.settings.recipients.default = [
"age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p"
];
# Optionally override recipients for specific machines
# vars.settings.recipients.hosts.my-machine = [
# "age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p"
# "age1another..."
# ];
} clan vars generate my-machine This will:
clan machines update my-machine The backend uploads encrypted secrets to the target, where NixOS activation scripts decrypt them on boot.
Secrets are decrypted at different points during NixOS activation, controlled by the neededFor option on each secret file:
| Phase | Decrypted to | When | Use case |
|---|---|---|---|
users | /run/user-secrets/ (tmpfs) | Before user/group creation | Secrets needed by user definitions (e.g., hashedPasswordFile) |
services | /run/secrets/ (tmpfs) | After users exist | Service credentials, API keys |
activation | In-place at upload location | During activation | Secrets for other activation scripts |
partitioning | /run/partitioning-secrets/ (tmpfs) | During partitioning | Disk encryption keys |
Secrets on tmpfs never touch disk and are lost on reboot (re-decrypted on next boot).
vars.settings.secretStoreSet to "age" to use the age backend:
vars.settings.secretStore = "age"; vars.settings.recipients.hosts.<machine>List of age public keys that can decrypt the machine's private key. Typically your admin key(s):
vars.settings.recipients.hosts.webserver = [
"age1admin1..."
"age1admin2..." # Multiple admins
]; vars.settings.recipients.defaultFallback recipients used when no host-specific recipients are configured:
vars.settings.recipients.default = [
"age1admin..."
]; Default recipients are only used if recipients.hosts.<machine> is not set. They do not combine with host-specific recipients.
clan.core.vars.age.secretLocationLocation on the target where encrypted secrets are uploaded (default: /etc/secret-vars):
clan.core.vars.age.secretLocation = "/etc/my-secrets"; your-clan/
├── clan.nix
├── secrets/
│ ├── age-keys/
│ │ └── machines/
│ │ └── my-machine/
│ │ ├── pub # Machine public key (plaintext)
│ │ └── key.age # Machine private key (encrypted to user keys)
│ └── clan-vars/
│ ├── per-machine/
│ │ └── my-machine/
│ │ └── openssh/
│ │ └── ssh.id_ed25519.age
│ └── shared/
│ └── cluster-token/
│ └── token.age
└── vars/
└── per-machine/
└── my-machine/
└── openssh/
└── ssh.id_ed25519.pub/
└── value # Public (non-secret) values Multiple recipients can all decrypt a machine's private key. This is useful for team access:
{
vars.settings.recipients.hosts.production = [
"age1admin1..." # Primary admin
"age1admin2..." # Backup admin
"age1cikey..." # CI/CD system
];
} All listed recipients can run clan vars generate and clan machines update for that machine.
When admin keys change, re-encrypt machine private keys to the new recipients:
# Update recipients in clan.nix, then:
clan vars fix my-machine This decrypts each machine key with the old identity and re-encrypts to the new recipients. Secrets themselves don't need re-encryption.
When machines are added or removed, shared secrets are automatically re-encrypted to include/exclude the machine's public key during clan vars generate.
clan vars check my-machine Verifies:
Set recipients in clan.nix:
vars.settings.recipients.hosts.my-machine = [ "age1..." ]; Place your age private key at one of the well-known paths, or set an environment variable:
# File-based
export AGE_KEYFILE=~/.config/age/identities
# Or inline
export AGE_KEY="AGE-SECRET-KEY-1..." Check the path in AGE_KEYFILE exists and is readable.
| Feature | Age Backend | SOPS Backend |
|---|---|---|
| Encryption tool | age directly | sops (wrapping age) |
| Decryption location | Target machine (activation scripts) | Target machine (sops-nix) |
| Decryption binary | age (shell scripts) | sops-install-secrets (Go) |
| Machine keys | Auto-generated, in-repo | Auto-generated, in-repo |
| Key indirection | Yes (user → machine key → secret) | Yes (similar) |
| Shared secrets | Multi-recipient age encryption | sops-nix groups |
| Hardware tokens | Via age plugins | Via sops/age plugins |