OS keychain
Store secrets in your operating system's native secure storage.
Supported platforms
- macOS: Keychain Access (built-in)
- Windows: Credential Manager (built-in)
- Linux: Secret Service over D-Bus (GNOME Keyring, KWallet)
Quick start
# Linux only: make sure a Secret Service daemon is running
sudo apt-get install gnome-keyring # Ubuntu/DebianAdd these definitions to fnox.toml. Merge them into any existing tables with the same names:
[providers]
keychain = { type = "keychain", service = "fnox" }# Store a secret in OS keychain
fnox set DATABASE_URL "postgresql://localhost/mydb" --provider keychain
# Retrieve from keychain
fnox get DATABASE_URLLinux setup
On Linux, fnox talks to the Secret Service API directly over D-Bus, so you need a Secret Service implementation such as GNOME Keyring or KWallet running (no libsecret packages are required):
sudo apt-get install gnome-keyringsudo dnf install gnome-keyringsudo pacman -S gnome-keyringmacOS and Windows have built-in support—no installation needed.
Configuration
[providers]
keychain = { type = "keychain", service = "fnox", prefix = "myapp/" } # Prefix is optionalService name
The service acts as a namespace to isolate fnox secrets from other applications:
[providers]
keychain = { type = "keychain", service = "myapp" }Prefix
Optional prefix prepended to secret names:
[providers]
keychain = { type = "keychain", service = "fnox", prefix = "myapp/" } # "database-url" becomes "myapp/database-url"How it works
- Storage: Secrets are stored in the OS credential manager (encrypted by OS)
- Config:
fnox.tomlcontains only the secret name, not the value - Retrieval: fnox queries the OS keychain API
- Service: Acts as a namespace (isolates fnox secrets from other apps)
- Prefix: Additional namespacing within the service
Usage
Store a secret
fnox set DATABASE_URL "postgresql://localhost/mydb" --provider keychainYour fnox.toml:
[secrets]
DATABASE_URL = { provider = "keychain", value = "DATABASE_URL" } # ← Keychain entry name, not the actual secretThe actual secret is stored in the OS keychain, encrypted.
Retrieve a secret
fnox get DATABASE_URLRun commands
fnox exec -- npm run devRecommended: use with age, not as bulk storage
On macOS, access controls can prompt separately for each keychain item. If those prompts interrupt a project with many secrets, store one age identity in the keychain and use it to decrypt the rest.
The pattern that scales much better is to store a single age private key in the keychain and encrypt all your secrets with age:
[providers]
keychain = { type = "keychain", service = "fnox" }
age = { type = "age", recipients = ["age1..."], identity = { provider = "keychain", value = "age-key" } }
[secrets]
# Many secrets, all encrypted with age — only one keychain access (the age key)
DATABASE_URL = { provider = "age", value = "encrypted..." }
API_KEY = { provider = "age", value = "encrypted..." }
STRIPE_KEY = { provider = "age", value = "encrypted..." }
# ...This way:
- One keychain item for the identity. Access prompts depend on the keychain's application permissions.
- Adding more secrets is free — they go into the encrypted config, not into the keychain.
- Loss of the keychain item is recoverable from any other machine that holds the same age identity.
Reach for direct provider = "keychain" only for the handful of bootstrap secrets that don't have anything else to decrypt them (e.g., the age key itself, a 1Password service account token).
Bootstrap pattern
A common pattern is to store provider tokens in the keychain:
[providers]
keychain = { type = "keychain", service = "fnox" }
age = { type = "age", recipients = ["age1..."] }
[secrets]
OP_SERVICE_ACCOUNT_TOKEN = { provider = "keychain", value = "op-token" } # Store 1Password token in keychain
DATABASE_URL = { provider = "age", value = "encrypted..." } # Other secrets encrypted with ageThen bootstrap:
export OP_SERVICE_ACCOUNT_TOKEN=$(fnox get OP_SERVICE_ACCOUNT_TOKEN)
# Now can access 1Password secrets
fnox exec -- ./start.shExample configurations
Personal project
[providers]
app-keychain = { type = "keychain", service = "myapp" }
[secrets]
DATABASE_URL = { provider = "keychain", value = "database-url" }
API_KEY = { provider = "keychain", value = "api-key" }Bootstrap tokens
[providers]
keychain = { type = "keychain", service = "fnox-tokens" }
[secrets]
GITHUB_TOKEN = { provider = "keychain", value = "github" }
NPM_TOKEN = { provider = "keychain", value = "npm" }Machine-specific secrets
# fnox.local.toml (gitignored)
[providers]
keychain = { type = "keychain", service = "fnox-local" }
[secrets]
LAPTOP_DB_URL = { provider = "keychain", value = "laptop-db" }Platform details
macOS keychain
Secrets stored in:
- Login Keychain (default)
- System Keychain (requires admin)
View in Keychain Access app:
- Open Keychain Access
- Search for service name (e.g., "fnox")
- Double-click to view/edit
Windows Credential Manager
Secrets stored in Windows Credential Manager.
View in Control Panel:
- Control Panel → User Accounts → Credential Manager
- Windows Credentials
- Look for fnox entries
Linux Secret Service
Secrets stored in:
- GNOME Keyring (GNOME desktop)
- KWallet (KDE desktop)
- Other Secret Service implementations
View with Seahorse (GNOME):
sudo apt install seahorse
seahorseUsage notes
Access depends on the current user and an available, unlocked credential store. macOS may prompt for access to individual items. Linux needs a Secret Service implementation on D-Bus; headless setups must provide one explicitly.
Limitations
Headless environments
The keychain provider needs an unlocked OS keychain or Secret Service session, which is usually unavailable in:
- CI/CD (GitHub Actions, GitLab CI, etc.)
- Docker containers (without X11/Wayland)
- SSH sessions (without forwarding)
- Headless servers
No desktop session is required, though — only an unlocked Secret Service on the bus. If you provision one yourself, the provider works headlessly; fnox's own Linux CI does this with gnome-keyring-daemon --unlock --components=secrets --daemonize. That means keeping the unlock password in CI, which is the secret you were trying to protect, so for CI/CD prefer age encryption or a cloud provider.
Tests auto-skip in CI
fnox's keychain tests skip automatically on macOS CI runners (where they hang) and on platforms other than macOS and Linux. On Linux CI they run against a headless gnome-keyring-daemon. Set SKIP_KEYCHAIN_TESTS=1 to skip them everywhere:
# Runs locally
mise run test:bats -- test/keychain.bats
# Skip the keychain tests
SKIP_KEYCHAIN_TESTS=1 mise run test:batsSecurity
- Encryption: Managed by the platform credential store
- Access control: OS enforces access (user/session isolation)
- Keyring unlock: May require password entry on first access
- Resolved values: fnox and the receiving process still handle plaintext in memory
Troubleshooting
"Keyring is locked"
Unlock your keyring:
macOS:
- Keyring unlocks automatically on login
Linux (GNOME):
# Unlock manually
gnome-keyring-daemon --unlockWindows:
- Credential Manager unlocks on login
"Access denied"
Check that the process has access:
- macOS: May prompt for Keychain Access permission
- Linux: Ensure Secret Service is running
- Windows: Check User Account Control settings
"Service not available" (Linux)
Install and start Secret Service:
# Ubuntu/Debian
sudo apt-get install gnome-keyring
gnome-keyring-daemon --start
# Or use KWallet
sudo apt-get install kwalletmanagerNext steps
- Age Encryption - Team-friendly alternative
- Hierarchical Config - Per-machine configuration with fnox.local.toml
- 1Password - Team password manager