age
The age provider encrypts values into fnox.toml with age. Anyone with a matching identity (an age key, a supported SSH private key, or an age plugin identity) can decrypt them, offline and without a cloud account. fnox set writes new ciphertext.
| Config type | age |
| Storage | Encrypted in config |
Writes (fnox set) | Yes |
| Requires | An age identity; age-plugin-* on PATH for plugin recipients |
Quick start
The quick start walks through a complete first setup. In short, generate a key with the age CLI and print its public recipient:
mkdir -p ~/.config/fnox
age-keygen -o ~/.config/fnox/age.txt
age-keygen -y ~/.config/fnox/age.txtAdd the recipient (it starts with age1) to fnox.toml:
[providers.age]
type = "age"
recipients = ["age1..."]Store a value through the hidden prompt, then use it:
fnox set DATABASE_URL --provider age
fnox provider test age
fnox exec -- npm startrecipients takes public keys only. Never paste a private key (AGE-SECRET-KEY-...) into it.
Decryption identity
Encryption needs only the public recipients. To decrypt, fnox uses the first identity it finds, in this order:
FNOX_AGE_KEY(inline identity contents).- The provider's
identityreference. - The provider's
key_file. - The key-file setting (
FNOX_AGE_KEY_FILEor the deprecated--age-key-fileflag). age.txtin the fnox configuration directory (~/.config/fnox/age.txtby default; seeFNOX_CONFIG_DIR).
An exported FNOX_AGE_KEY overrides every provider-specific setting.
Local development
Keep your key at the default path, or point the provider at it with key_file. To decrypt with an SSH key instead, set the key-file path:
export FNOX_AGE_KEY_FILE=~/.ssh/id_ed25519CI
Generate a dedicated key for CI, add its public recipient to recipients, run fnox reencrypt -p age, and store the private key in your CI secret store as FNOX_AGE_KEY:
- name: Run tests with secrets
env:
FNOX_AGE_KEY: ${{ secrets.FNOX_AGE_KEY }}
run: fnox exec -- npm testSee CI/CD for non-interactive runs and pull requests from forks.
Configuration
| Field | Required | Default | Description |
|---|---|---|---|
recipients | Yes | None | Public recipients to encrypt to: age1... keys, ssh-ed25519 or ssh-rsa public keys, or age plugin recipients. |
key_file | No | None | Path to an identity file: an age key, SSH private key, or plugin identity. Accepts { secret = "NAME" }. |
identity | No | None | A { provider = "...", value = "..." } reference that resolves to the identity contents through another provider. |
Relative key_file paths are resolved from the config file that declares the provider. Paths beginning with ~ expand to your home directory, and absolute paths are used unchanged.
Every provider also accepts auth_command and daemon_cache.
Store the identity in another provider, such as the OS keychain, so it never sits in a file:
[providers.keychain]
type = "keychain"
service = "fnox"
[providers.age]
type = "age"
recipients = ["age1..."]
identity = { provider = "keychain", value = "age-key" }The identity reference cannot point at the age provider that uses it.
How values are stored
fnox set encrypts the value to every recipient and stores the base64-encoded age file as the secret's value:
[secrets]
DATABASE_URL = { provider = "age", value = "YWdlLWVuY3J5cHRpb24ub3Jn..." } # ciphertext, abbreviatedThe ciphertext is safe to commit. Typing a plaintext string into value by hand does not encrypt it; let fnox set generate the ciphertext.
fnox reencrypt and fnox sync encrypt each group of secrets with one shared key, which is itself encrypted to the recipients. Values in this form start with fnox-age-batch-v1:. Resolving them through fnox exec unlocks the shared key once, so a hardware-backed identity asks for one approval instead of one per secret. To consolidate separately encrypted values, including secrets added with fnox set, run:
fnox reencrypt --provider ageThe command uses the provider's current recipients and preserves each secret's source file and profile. Decrypting the existing values during re-encryption can still require multiple hardware approvals. Key or --filter arguments consolidate only the selected secrets.
SSH keys
Use an existing SSH public key as a recipient and the matching private key to decrypt. Supported key types are ssh-ed25519 and ssh-rsa.
[providers.age]
type = "age"
recipients = [
"ssh-ed25519 AAAAC3Nza... alice@example.com",
"ssh-rsa AAAAB3Nza... bob@example.com",
]Point fnox at the private key with key_file or FNOX_AGE_KEY_FILE.
Password-protected SSH keys
Password-protected SSH keys are not supported by this integration. Generate a dedicated age identity or use a supported age plugin instead of removing the passphrase from your SSH key.
Plugin support
Age plugins add hardware-backed and alternative identities, such as age-plugin-yubikey (YubiKey PIV) or age-plugin-se (Apple Secure Enclave). fnox works with any age plugin: it runs the matching age-plugin-<name> binary from your PATH to encrypt and decrypt.
Plugin recipients usually carry the plugin name in their prefix (age1yubikey1...), though not always; current age-plugin-tpm releases produce age1tag1... recipients, for example.
[providers.age]
type = "age"
recipients = ["age1yubikey1..."]
key_file = "~/.config/fnox/yubikey-identity.txt"Within one command, fnox decrypts each ciphertext once, so a PIN or touch prompt is not repeated when several secrets or providers share a value. Refer to each plugin's documentation for setup. Syncing secrets locally has walkthroughs for Secure Enclave, YubiKey, TPM, and FIDO2 plugins.
Team workflow
Encrypt each secret to every teammate's public key. Anyone whose recipient was included can decrypt with their own private key.
Each person shares their public recipient:
shage-keygen -y ~/.config/fnox/age.txt # age key cat ~/.ssh/id_ed25519.pub # SSH keyList every recipient on the provider:
toml[providers.age] type = "age" recipients = [ "ssh-ed25519 AAAAC3Nza...", # alice "ssh-ed25519 AAAAC3Nza...", # bob "age1...", # ci ]Encrypt secrets with
fnox set KEY --provider ageand commitfnox.toml.
Each teammate decrypts with an identity from the selection order above. See Teams for onboarding and offboarding across providers.
Adding a new team member
Adding a recipient does not update existing ciphertext. After adding the new public key to recipients, re-encrypt:
fnox reencrypt -p age --dry-run # preview
fnox reencrypt -p agereencrypt works on the selected profile. Repeat it for each profile that has age secrets, with -f to skip the confirmation:
fnox reencrypt -p age -P staging -f
fnox reencrypt -p age -P production -fCommit the updated fnox.toml. The new member can decrypt after pulling.
Removing a team member
Remove their recipient and run fnox reencrypt for each affected profile. Re-encryption cannot revoke access to old ciphertext in git history; rotate the underlying secret if that person's access must end.
Troubleshooting
no identity matched any of the recipients
Your identity does not match any recipient the value was encrypted to. Compare your public key with the provider's recipients, then ask a teammate who can decrypt to add you and run fnox reencrypt:
age-keygen -y ~/.config/fnox/age.txt
grep -A5 recipients fnox.tomlIdentity file not found
No identity was found in the selection order. Set FNOX_AGE_KEY, key_file, or FNOX_AGE_KEY_FILE, or create age.txt in the fnox configuration directory, and check that the file is readable.
SSH key not working
Check that the key type is ssh-ed25519 or ssh-rsa, that the path points at the private key (not the .pub file), and that the key is not password-protected.
Age plugin not found
Plugin recipients and identities need the age-plugin-<name> binary on PATH. Run which age-plugin-yubikey (or the plugin you use) in the same shell as fnox.
See also
- Quick start: create a key and store your first secret.
- Teams: onboard and offboard people across providers.
- Syncing secrets locally: cache remote secrets under a personal age key.
fnox reencrypt: re-encrypt after recipient changes.