Age Encryption
Age is a modern encryption tool that's simple, secure, and works beautifully with SSH keys.
Quick Start
# 1. Generate age key
age-keygen -o ~/.config/fnox/age.txt
# 2. Get public key
grep "public key:" ~/.config/fnox/age.txt
# Output: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
# 3. Configure fnox
cat >> fnox.toml << 'EOF'
[providers.age]
type = "age"
recipients = ["age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p"]
EOF
# 4. Set private key
export FNOX_AGE_KEY=$(cat ~/.config/fnox/age.txt | grep "AGE-SECRET-KEY")
# 5. Encrypt a secret
fnox set DATABASE_URL "postgresql://localhost/mydb" --provider ageInstallation
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
# View the generated key
cat ~/.config/fnox/age.txtOutput:
# created: 2024-01-15T10:30:45-08:00
# public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
AGE-SECRET-KEY-1ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890ABCDEFGHIJKLMNOPQRSOption 2: Use SSH Key
Age has first-class SSH key support! Use your existing SSH keys:
# No key generation needed!
# Just use your SSH public key as the recipientConfiguration
Add age provider to fnox.toml:
[providers.age]
type = "age"
recipients = ["age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p"]Or with SSH key:
[providers.age]
type = "age"
recipients = ["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGQs8..."]Set Decryption Key
Using Age Key
# Export the secret key
export FNOX_AGE_KEY=$(cat ~/.config/fnox/age.txt | grep "AGE-SECRET-KEY")
# Add to shell profile
echo 'export FNOX_AGE_KEY=$(cat ~/.config/fnox/age.txt | grep "AGE-SECRET-KEY")' >> ~/.bashrcUsing 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
Age natively supports SSH keys—no conversion needed!
Supported SSH Key Types
ssh-ed25519- Ed25519 keys (recommended, most secure)ssh-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. If your SSH key has a passphrase, you must create a copy without a passphrase for use with fnox/age.
Get Your SSH Public Key
# Ed25519 key
cat ~/.ssh/id_ed25519.pub
# RSA key
cat ~/.ssh/id_rsa.pubTeam 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 "secret-key" --provider age4. Commit to Git
git add fnox.toml
git commit -m "Add encrypted development secrets"
git push5. Everyone Can Decrypt
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..."Now everyone can 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):
bash# Re-encrypt each secret with new recipient list fnox set DATABASE_URL "$(fnox get DATABASE_URL)" --provider age fnox set API_KEY "$(fnox get API_KEY)" --provider age # ... repeat for all secretsCommit 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:
bashcat ci-age.txt | grep "AGE-SECRET-KEY"Add to GitHub Secrets as
FNOX_AGE_KEY
Pros
- ✅ Secrets live in git (version control, code review)
- ✅ Works offline
- ✅ Zero runtime dependencies (after initial setup)
- ✅ Free forever
- ✅ Works with SSH keys you already have
- ✅ Simple and secure
- ✅ Team-friendly (multiple recipients)
Cons
- ❌ Key rotation requires re-encrypting all secrets
- ❌ No audit logs
- ❌ No centralized access control
- ❌ Manual key management
- ❌ Adding new team members requires re-encryption
Best Practices
- Use SSH keys when possible - Leverage keys you already have
- Add all team members as recipients - Everyone can decrypt
- Include a CI bot key - For GitHub Actions/CI pipelines
- Keep private keys secure - Never commit
FNOX_AGE_KEYor private keys - Rotate keys periodically - Generate new keys and re-encrypt secrets
- Use different keys for different environments - Separate dev, staging, prod
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
cat fnox.toml | grep recipients"failed to decrypt"
- Check that
FNOX_AGE_KEYorFNOX_AGE_KEY_FILEis set - 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
- Team Workflow Guide - Manage team secrets effectively
- AWS KMS - Alternative with AWS-managed keys