Age encryption
The age provider encrypts values into fnox.toml. Decryption uses an age identity, a supported SSH private key, or an age plugin. Standard age keys work offline and do not require a cloud account.
Quick start
Follow the age quick start for a complete setup. If you already have a key and a configured provider:
fnox set DATABASE_URL --provider age
fnox check --all
fnox exec -- npm startOmitting the value from fnox set prompts with hidden input. Never paste a private key into recipients: that field takes public recipients only.
Installation
Install the age CLI:
# macOS
brew install age
# Linux (Ubuntu/Debian)
sudo apt install age
# Or download from https://github.com/FiloSottile/age/releasesSetup
Option 1: generate age key
# Create config directory
mkdir -p ~/.config/fnox
# Generate age key
age-keygen -o ~/.config/fnox/age.txt
# Print only the public recipient
age-keygen -y ~/.config/fnox/age.txtOutput:
age1...Option 2: use SSH key
Age has first-class SSH key support — no key generation needed. Your existing SSH public key becomes the recipient and your private key decrypts; see SSH Key Support below.
Configuration
Add age provider to fnox.toml:
[providers]
age = { type = "age", recipients = ["age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p"] }Or with SSH key:
[providers]
age = { type = "age", recipients = ["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGQs8..."] }Or with an explicit identity file:
[providers]
age = { type = "age", recipients = ["age1..."], key_file = "./age.txt" }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.
Or store the age identity in another provider, such as the OS keychain:
[providers]
keychain = { type = "keychain", service = "fnox" }
age = { type = "age", recipients = ["age1..."], identity = { provider = "keychain", value = "age-key" } }Set decryption key
fnox selects an identity 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 CLI flag). age.txtin the fnox configuration directory.
For local use, prefer key_file or the default file. FNOX_AGE_KEY is useful when CI supplies the identity directly. An exported inline key overrides the provider-specific settings.
Using age key
# Optional when the key is already in the default location
export FNOX_AGE_KEY_FILE=~/.config/fnox/age.txtUsing SSH key
# Point to SSH private key
export FNOX_AGE_KEY_FILE=~/.ssh/id_ed25519
# Add to shell profile
echo 'export FNOX_AGE_KEY_FILE=~/.ssh/id_ed25519' >> ~/.bashrcUsage
Encrypt and store a secret
fnox set DATABASE_URL "postgresql://localhost/mydb" --provider ageThe resulting fnox.toml:
[secrets]
DATABASE_URL = { provider = "age", value = "YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNjcnlwdC..." } # ← Encrypted, safe to commit!Decrypt and get a secret
fnox get DATABASE_URLRun commands with secrets
fnox exec -- npm run devSSH key support
Use a supported SSH public key as a recipient and the matching private key for decryption.
Supported SSH key types
ssh-ed25519- Ed25519 keysssh-rsa- RSA keys (2048-bit minimum, 4096-bit recommended)
Using SSH keys
[providers.age]
type = "age"
recipients = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGQs8YqSC... alice@example.com",
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC5... bob@example.com"
]Set decryption key:
# Point to your SSH private key
export FNOX_AGE_KEY_FILE=~/.ssh/id_ed25519Password-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.
Get your SSH public key
# Ed25519 key
cat ~/.ssh/id_ed25519.pub
# RSA key
cat ~/.ssh/id_rsa.pubPlugin support
Age plugins extend age with hardware-backed and alternative keys. fnox supports any age plugin, for example age-plugin-yubikey (YubiKey / PIV) or age-plugin-se (Apple's Secure Enclave).
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 = ["age1yubikey1qwla8v7cu3mx6mp79asgrh5ad2h52flwln7c66ydcyy50lg5uh0gxh4kmaz"]Refer to each plugin's docs for setup instructions. The sync guide also has full hardware-backed decryption walkthroughs for Secure Enclave, YubiKey, TPM, and FIDO2.
Team workflow
1. Collect public keys
Each team member shares their public key:
# Using age key
grep "public key:" ~/.config/fnox/age.txt
# Using SSH key
cat ~/.ssh/id_ed25519.pub2. Add all recipients
[providers.age]
type = "age"
recipients = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGQs...", # alice
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBws...", # bob
"age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2el..." # ci-bot
]3. Encrypt secrets
fnox set DATABASE_URL "postgresql://dev.example.com/db" --provider age
fnox set API_KEY --provider age4. Commit to git
git add fnox.toml
git commit -m "Add encrypted development secrets"
git push5. Decrypt with a matching identity
Each team member sets their private key:
# Alice (SSH key)
export FNOX_AGE_KEY_FILE=~/.ssh/id_ed25519
# Bob (SSH key)
export FNOX_AGE_KEY_FILE=~/.ssh/id_ed25519
# CI bot (age key)
export FNOX_AGE_KEY="AGE-SECRET-KEY-1..."A teammate whose public recipient was included when the secret was encrypted can now decrypt:
fnox get DATABASE_URL # Works for all recipients!Adding a new team member
New member generates/shares public key:
bashcat ~/.ssh/id_ed25519.pubAdmin adds to recipients:
toml[providers.age] type = "age" recipients = [ "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGQs...", # alice "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBws...", # bob "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIXyz..." # charlie (NEW) ]Re-encrypt all secrets (necessary for new recipient):
bashfnox reencrypt -p ageUse
--dry-runto preview what would be re-encrypted:bashfnox reencrypt -p age --dry-runFor multiple profiles:
bashfnox reencrypt -p age -P default -f fnox reencrypt -p age -P staging -f fnox reencrypt -p age -P prod -fCommit and push:
bashgit add fnox.toml git commit -m "Add charlie to age recipients" git pushNew member pulls and decrypts:
bashgit pull export FNOX_AGE_KEY_FILE=~/.ssh/id_ed25519 fnox get DATABASE_URL # Works!
CI/CD setup
GitHub Actions
name: CI
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup fnox age key
env:
FNOX_AGE_KEY: ${{ secrets.FNOX_AGE_KEY }}
run: |
# Key is already set via environment variable
echo "Age key configured"
- name: Run tests
run: |
fnox exec -- npm testSetting up the GitHub Secret:
Generate a dedicated CI age key:
bashage-keygen -o ci-age.txtAdd CI public key to
fnox.tomlrecipientsCopy the secret key:
bashgrep "AGE-SECRET-KEY" ci-age.txtAdd to GitHub Secrets as
FNOX_AGE_KEY
Usage notes
Age decrypts locally with a matching identity. Changing recipients does not update existing ciphertext: run fnox reencrypt for each affected profile. Removing a recipient cannot revoke that person's access to old ciphertext in git history; rotate the underlying secret if access must end.
reencrypt groups selected secrets by provider. Age encrypts each group with one shared wrapped key, so batch resolution through fnox exec needs to unlock that key only once. 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 regex filters consolidate only the selected secrets; other values remain unchanged.
Troubleshooting
"no identity matched any of the recipients"
Your private key doesn't match any of the recipients. Check:
# Verify your public key matches a recipient
cat ~/.config/fnox/age.txt # Check public key
cat ~/.ssh/id_ed25519.pub # Check SSH public key
# Compare with fnox.toml recipients
grep recipients fnox.toml"failed to decrypt"
- Check that
FNOX_AGE_KEYorFNOX_AGE_KEY_FILEis set, or that the default key file~/.config/fnox/age.txtexists - Verify the key file exists and is readable
- Ensure you're using the correct private key
SSH key not working
- Verify SSH key type is supported (ed25519 or rsa)
- Check that the private key file path is correct
- Ensure the private key is NOT password-protected
Next steps
- Real-World Example - Complete project setup with age
- Profiles - Multi-environment configuration
- AWS KMS - Alternative with AWS-managed keys