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.
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