Agenix Secrets — Encrypted Agent Credentials
Source:
docs/04-agenix-secrets.mdat2081f31a099e· docs version 0.4.0
Security-profile boundary: agenix keeps plaintext out of Git and the Nix store, but passing a decrypted file into an agent still gives that agent the real credential.
balancedandstrictrejectenvFileandagenixFile. The direct-agent examples below are retained only for the explicitdevcompatibility profile. Secure profiles use agenix for host-side brokers, Grafana credentials, Git auto-push, and backup credentials; the agent should receive only a scoped, revocable virtual broker key.
For a balanced agent, declare the decrypted provider value as a single secret file and point only the LLM broker at it:
age.secrets.openai-key = { file = ./secrets/openai-key.age; owner = "root"; group = "root"; mode = "0400";};
tentaflake.broker.agents.hermes-coding = { enable = true; subnet = "10.203.20.0/30"; gateway = "10.203.20.1"; llm = { enable = true; upstreamBaseUrl = "https://api.openai.com/v1/"; providerCredentialFile = config.age.secrets.openai-key.path; allowedModels = [ { name = "gpt-5-mini"; inputMicrousdPerMillion = 250000; outputMicrousdPerMillion = 2000000; } ]; };};systemd copies the value into the broker’s private credential directory. The
agent receives only its random virtual key. The longer direct-container
environment examples below apply only to dev.
This guide covers encrypting agent API keys and tokens — for either the Hermes or ZeroClaw runtime — with agenix so they can be committed to Git safely and decrypted only at NixOS activation time.
Why Agenix?
Section titled “Why Agenix?”| Approach | Secrets in Git? | Secrets in Nix Store? | Complexity |
|---|---|---|---|
Plain .env files |
❌ (must gitignore) | ❌ (if mounted correctly) | Low |
| Agenix | ✅ (encrypted .age files) |
❌ (decrypted at runtime) | Medium |
builtins.readFile |
❌ | ❌ SECRETS IN STORE | Low (dangerous) |
| External vault (Vault, Doppler) | N/A | N/A | High |
Agenix gives you the best balance: secrets encrypted in Git, decrypted only at activation, never in the Nix store, and no external vault dependency.
Dev-only direct-container architecture
Section titled “Dev-only direct-container architecture”┌─────────────────────────────────────────────────────────────┐│ Git Repo (public/private) ││ ┌──────────────────────────────────────────────────┐ ││ │ secrets/hermes-coding.env.age (encrypted) │ ││ │ secrets/hermes-research.env.age (encrypted) │ ││ │ secrets.nix (recipients) │ ││ └──────────────────────────────────────────────────┘ │└─────────────────────────────────────────────────────────────┘ │ │ nixos-rebuild switch ▼┌─────────────────────────────────────────────────────────────┐│ Nix Store (world-readable) ││ ┌──────────────────────────────────────────────────┐ ││ │ /nix/store/...-hermes-coding.env.age (copied) │ ││ │ /nix/store/...-hermes-research.env.age (copied) │ ││ │ ↑ STILL ENCRYPTED ↑ │ ││ └──────────────────────────────────────────────────┘ │└─────────────────────────────────────────────────────────────┘ │ │ agenix activation script ▼┌─────────────────────────────────────────────────────────────┐│ Runtime (root-only, tmpfs) ││ ┌──────────────────────────────────────────────────┐ ││ │ /run/agenix/hermes-coding-env (plaintext, 0600) │ ││ │ /run/agenix/hermes-research-env (plaintext, 0600) │ ││ │ ↑ NEVER IN STORE ↑ │ ││ └──────────────────────────────────────────────────┘ │└─────────────────────────────────────────────────────────────┘ │ │ Docker --env-file mount ▼┌─────────────────────────────────────────────────────────────┐│ Docker Container (isolated) ││ │ OPENROUTER_API_KEY=sk-or-... ││ │ TELEGRAM_BOT_TOKEN=... │└─────────────────────────────────────────────────────────────┘Step-by-Step Setup
Section titled “Step-by-Step Setup”1. Enable Agenix in flake.nix
Section titled “1. Enable Agenix in flake.nix”Uncomment the agenix input and module import:
{ inputs = { # ... agenix = { url = "github:ryantm/agenix"; inputs.nixpkgs.follows = "nixpkgs"; }; };
outputs = { self, nixpkgs, agenix, ... }@inputs: # ... nixosConfigurations.tentaflake = nixpkgs.lib.nixosSystem { modules = [ inputs.agenix.nixosModules.age # <-- add this ./configuration.nix ]; };}2. Create secrets.nix (recipients + secret declarations)
Section titled “2. Create secrets.nix (recipients + secret declarations)”Copy secrets.nix.example to secrets.nix and edit:
cp secrets.nix.example secrets.nixEdit secrets.nix to set your agent names and SSH public keys:
{ config, lib, pkgs, ... }:
let # Your agent names (match mkHermesAgent / mkZeroClawAgent calls) agentNames = [ "hermes-coding" "hermes-research" "zeroclaw-assistant" ];
# SSH public keys of machines that can decrypt. # NEVER put private keys here. recipients = [ "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA..." # Your dev machine # Host key auto-detected on NixOS — add others as needed ];
mkSecret = name: { "${name}-env" = { file = ./secrets/${name}.env.age; owner = name; group = name; mode = "0600"; }; };in{ age.secrets = lib.mergeAttrsList (map mkSecret agentNames);}Import it in configuration.nix:
{ imports = [ ./modules/... ./secrets.nix # <-- add this ];}3. Install Agenix CLI
Section titled “3. Install Agenix CLI”# One-time:nix profile install nixpkgs#agenix
# Or use ad-hoc:nix shell nixpkgs#agenix -c agenix --help4. Create Encrypted .age Files
Section titled “4. Create Encrypted .age Files”# Create the secrets directorymkdir -p secrets
# Create encrypted secret filesecho "OPENROUTER_API_KEY=sk-or-..." | agenix -e secrets/hermes-coding.env.age --stdinecho "OPENROUTER_API_KEY=sk-or-..." | agenix -e secrets/hermes-research.env.age --stdin
# Or edit interactively:agenix -e secrets/hermes-coding.env.ageEach .age file contains the environment variables for one agent:
OPENROUTER_API_KEY=sk-or-v1-abc123...TELEGRAM_BOT_TOKEN=123456:ABC-DEF1234...The same works for a ZeroClaw agent — see zeroclaw.env.example for its
ZEROCLAW_<section>__<sub>__<key> env-var convention (double underscores):
echo "ZEROCLAW_providers__models__openrouter__default__api_key=sk-or-..." \ | agenix -e secrets/zeroclaw-assistant.env.age --stdin5. Wire Agenix Secrets to Agents
Section titled “5. Wire Agenix Secrets to Agents”This section applies only when the host explicitly selects:
tentaflake.security.profile = "dev";In my-agents.nix, use agenixFile instead of envFile. Both runtimes accept
it the same way — mkHermesAgent and mkZeroClawAgent:
{ mkHermesAgent, mkZeroClawAgent }:let hermesAgents = [ { name = "coding"; agenixFile = "/run/agenix/hermes-coding-env"; # ← matches secrets.nix path settings = { model.default = "openrouter/anthropic/claude-sonnet-4"; toolsets = [ "terminal" "memory" "file" "skills" ]; }; } { name = "research"; agenixFile = "/run/agenix/hermes-research-env"; settings = { model.default = "openrouter/deepseek/deepseek-v4-flash"; toolsets = [ "terminal" "web" "memory" "file" "skills" ]; }; } ]; zeroclawAgents = [ { name = "assistant"; agenixFile = "/run/agenix/zeroclaw-assistant-env"; hostPort = 9246; servePort = 9145; # Trimmed for brevity — see the zeroclawAgents entry in # my-agents.nix.example for the full required settings # (runtime_profiles, agents.main, risk_profiles, …). settings.schema_version = 3; settings.providers.models.openrouter.default.model = "anthropic/claude-haiku-4.5"; } ];inmap mkHermesAgent hermesAgents ++ map mkZeroClawAgent zeroclawAgentsThe runtime path follows this convention:
/run/agenix/<name-from-secrets.nix> │ └─ mkSecret creates "${name}-env" → path = /run/agenix/hermes-coding-env6. Set SSH Identity for the Target Host
Section titled “6. Set SSH Identity for the Target Host”On the target NixOS machine, ensure the host’s SSH key is available:
# In your flake.nix params or configuration.nix:age.identityPaths = [ "/etc/ssh/ssh_host_ed25519_key"];This is typically auto-detected on NixOS. If you use impermanence, point to the persistent path:
age.identityPaths = [ "/persist/etc/ssh/ssh_host_ed25519_key"];7. Rebuild
Section titled “7. Rebuild”sudo nixos-rebuild switch --flake /etc/nixos#tentaflakeAgenix decrypts the .age files during activation and places plaintext at
/run/agenix/. Only the explicitly selected dev compatibility profile passes
such a file to an agent as --env-file; balanced supplies individual files to
host-side services through systemd credentials instead.
Verification
Section titled “Verification”After rebuild, verify secrets are wired without revealing contents:
# Check file exists with correct permissionsls -l /run/agenix/# Expected: -rw------- 1 hermes-coding hermes-coding ... hermes-coding-env
stat -c '%U %G %a %n' /run/agenix/hermes-coding-env# Expected: hermes-coding hermes-coding 600 .../hermes-coding-env
# Check the agent container has the env file mounteddocker inspect hermes-coding | jq '.[0].Mounts[] | select(.Source | startswith("/run/agenix"))'Never run cat /run/agenix/* or agenix -d to verify contents — use permissions and process checks instead.
Working with Multiple Contributors
Section titled “Working with Multiple Contributors”If multiple people need to edit secrets, add their SSH public keys to secrets.nix recipients and rekey:
# After adding new recipients to secrets.nix:cd secretsagenix --rekeyThe .age format supports multiple recipients — each can decrypt independently.
Key Rotation & Recovery
Section titled “Key Rotation & Recovery”Rotating a Secret Value
Section titled “Rotating a Secret Value”When an API key or token changes (provider rotation, suspected leak, expiry),
edit the .age file, rebuild, and restart the agent — the container only reads
its --env-file at start:
agenix -e secrets/hermes-coding.env.age # replace the old keysudo nixos-rebuild switch --flake /etc/nixos#tentaflaketentaflake restart coding # pick up the new env fileDon’t forget to revoke the old key at the provider (OpenRouter, Telegram, …).
Rotating or Removing a Compromised Recipient Key
Section titled “Rotating or Removing a Compromised Recipient Key”If a recipient’s SSH private key is compromised (lost laptop, leaked key):
# 1. Remove or replace the key in secrets.nix `recipients`# 2. Re-encrypt every .age file for the new recipient setagenix --rekey# 3. Commit, rebuild, and revoke the old key everywhere else it grants# access (GitHub, authorized_keys on servers, ...)Rekeying does not rewrite Git history — old ciphertext remains decryptable by the compromised key. Treat every secret value it could read as exposed and rotate those too (see above).
Recovery: Lost Host SSH Key
Section titled “Recovery: Lost Host SSH Key”Decryption needs any one listed recipient, so a reinstalled host is recoverable from any surviving recipient machine (e.g. your dev machine):
# On the surviving recipient machine:# Get the reinstalled host's public key (awk strips the leading hostname —# recipients need the bare "ssh-ed25519 AAAA..." form):ssh-keyscan -t ed25519 <new-host> | awk '{print $2, $3}'# Add it to secrets.nix `recipients`, then:agenix --rekey# Commit, then deploy to the new host as usualIf the only recipient key is lost, the secrets are unrecoverable — there is
no passphrase fallback. Always keep at least one offline recovery recipient
(e.g. an admin age key stored off-machine) in recipients.
Rotation Cadence
Section titled “Rotation Cadence”This template sets no policy — pick one in your fork. A common example: rotate agent API keys every 90 days, and rekey immediately whenever a recipient machine is decommissioned or a key is suspected compromised.
Security Checklist
Section titled “Security Checklist”-
agenixinput uncommented inflake.nix -
inputs.agenix.nixosModules.ageimported in host modules -
secrets.nixcreated fromsecrets.nix.example - No
builtins.readFile config.age.secrets.*.pathanywhere in Nix -
age.identityPathsuses runtime strings (not Nix paths) -
owner/group/modeset to restrict access per agent -
.envfiles excluded via.gitignore(secrets/*.env) -
.agefiles tracked in Git - No private keys or decrypted secrets committed to Git
- Agent containers read
agenixFilenotenvFilewhen using agenix
Troubleshooting
Section titled “Troubleshooting”| Symptom | Likely Cause | Fix |
|---|---|---|
agenix: command not found |
CLI not installed | nix shell nixpkgs#agenix |
.age file not decrypting |
Recipient key missing or wrong | Check secrets.nix recipients match ~/.ssh/id_ed25519.pub |
| Agent can’t read env file | Wrong owner/mode | Set owner = "agent-name" and mode = "0600" in secrets.nix |
/run/agenix/ empty after rebuild |
Module not imported | Verify inputs.agenix.nixosModules.age in host modules |
error: path ... is not in the Nix store |
Private key referenced as Nix path | Use string: age.identityPaths = [ "/etc/ssh/..." ] |
References
Section titled “References”- Agenix GitHub
- NixOS Wiki — Agenix
secrets.nix.example— template in this repodocs/01-quickstart.md— getting started with agent deployment