Skip to content

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:

sh
fnox set DATABASE_URL --provider age
fnox check --all
fnox exec -- npm start

Omitting 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:

bash
# macOS
brew install age

# Linux (Ubuntu/Debian)
sudo apt install age

# Or download from https://github.com/FiloSottile/age/releases

Setup ​

Option 1: generate age key ​

bash
# 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.txt

Output:

text
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:

toml
[providers]
age = { type = "age", recipients = ["age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p"] }

Or with SSH key:

toml
[providers]
age = { type = "age", recipients = ["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGQs8..."] }

Or with an explicit identity file:

toml
[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:

toml
[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:

  1. FNOX_AGE_KEY (inline identity contents).
  2. The provider's identity reference.
  3. The provider's key_file.
  4. The key-file setting (FNOX_AGE_KEY_FILE or the deprecated CLI flag).
  5. age.txt in 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 ​

bash
# Optional when the key is already in the default location
export FNOX_AGE_KEY_FILE=~/.config/fnox/age.txt

Using SSH key ​

bash
# 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' >> ~/.bashrc

Usage ​

Encrypt and store a secret ​

bash
fnox set DATABASE_URL "postgresql://localhost/mydb" --provider age

The resulting fnox.toml:

toml
[secrets]
DATABASE_URL = { provider = "age", value = "YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNjcnlwdC..." }  # ← Encrypted, safe to commit!

Decrypt and get a secret ​

bash
fnox get DATABASE_URL

Run commands with secrets ​

bash
fnox exec -- npm run dev

SSH 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 keys
  • ssh-rsa - RSA keys (2048-bit minimum, 4096-bit recommended)

Using SSH keys ​

toml
[providers.age]
type = "age"
recipients = [
  "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGQs8YqSC... alice@example.com",
  "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC5... bob@example.com"
]

Set decryption key:

bash
# Point to your SSH private key
export FNOX_AGE_KEY_FILE=~/.ssh/id_ed25519

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.

Get your SSH public key ​

bash
# Ed25519 key
cat ~/.ssh/id_ed25519.pub

# RSA key
cat ~/.ssh/id_rsa.pub

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

toml
[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:

bash
# Using age key
grep "public key:" ~/.config/fnox/age.txt

# Using SSH key
cat ~/.ssh/id_ed25519.pub

2. Add all recipients ​

toml
[providers.age]
type = "age"
recipients = [
  "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGQs...",  # alice
  "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBws...",  # bob
  "age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2el..."   # ci-bot
]

3. Encrypt secrets ​

bash
fnox set DATABASE_URL "postgresql://dev.example.com/db" --provider age
fnox set API_KEY --provider age

4. Commit to git ​

bash
git add fnox.toml
git commit -m "Add encrypted development secrets"
git push

5. Decrypt with a matching identity ​

Each team member sets their private key:

bash
# 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:

bash
fnox get DATABASE_URL  # Works for all recipients!

Adding a new team member ​

  1. New member generates/shares public key:

    bash
    cat ~/.ssh/id_ed25519.pub
  2. Admin adds to recipients:

    toml
    [providers.age]
    type = "age"
    recipients = [
      "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGQs...",  # alice
      "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBws...",  # bob
      "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIXyz..."   # charlie (NEW)
    ]
  3. Re-encrypt all secrets (necessary for new recipient):

    bash
    fnox reencrypt -p age

    Use --dry-run to preview what would be re-encrypted:

    bash
    fnox reencrypt -p age --dry-run

    For multiple profiles:

    bash
    fnox reencrypt -p age -P default -f
    fnox reencrypt -p age -P staging -f
    fnox reencrypt -p age -P prod -f
  4. Commit and push:

    bash
    git add fnox.toml
    git commit -m "Add charlie to age recipients"
    git push
  5. New member pulls and decrypts:

    bash
    git pull
    export FNOX_AGE_KEY_FILE=~/.ssh/id_ed25519
    fnox get DATABASE_URL  # Works!

CI/CD setup ​

GitHub Actions ​

yaml
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 test

Setting up the GitHub Secret:

  1. Generate a dedicated CI age key:

    bash
    age-keygen -o ci-age.txt
  2. Add CI public key to fnox.toml recipients

  3. Copy the secret key:

    bash
    grep "AGE-SECRET-KEY" ci-age.txt
  4. Add 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:

sh
fnox reencrypt --provider age

The 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:

bash
# 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_KEY or FNOX_AGE_KEY_FILE is set, or that the default key file ~/.config/fnox/age.txt exists
  • 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 ​

MIT LicenseCopyright © 2026jdx.dev