agenix (comparison)

Updated

July 30, 2026

agenix (comparison)

Goal: Implement the same lab secret once with agenix, compare trade-offs to sops-nix, and declare a house default (this book: sops-nix primary).

Note

Comparison chapter ≠ dual-stack forever. After the lab, remove or disable the non-default tool so you maintain one system.

Why this chapter exists

Both sops-nix and agenix solve “encrypted secrets in git + decrypt on activation.” Teams pick based on:

Factor Why it matters
File format UX YAML multi-secret files vs one age file per secret
Tooling sops editor vs agenix CLI
Ecosystem fit Docs you read; modules you copy
Complexity Bootstrap and key distribution

You should not choose from social-media vibes alone. Touch both once.


Theory 1 — agenix mental model

agenix (and compatible community usage) roughly:

Concept Meaning
secrets.nix Maps secret files → public keys allowed to decrypt
*.age files Ciphertext in repo
age identities Private keys on machines/admins
NixOS module age.secrets.<name>.file = ./secret.age; → runtime path

Flow

echo 'PLACEHOLDER' | agenix -e secret.age
git add secret.age secrets.nix
nixos-rebuild → decrypt to /run/agenix/... (path scheme per version)
service reads path

Theory 2 — Flake input sketch

inputs = {
  nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";
  agenix.url = "github:ryantm/agenix";
  agenix.inputs.nixpkgs.follows = "nixpkgs";
};

# modules list:
# inputs.agenix.nixosModules.default

Pin and follows like any other input (lock discipline).

nix flake update agenix

Theory 3 — secrets.nix shape

# secrets/secrets.nix — public keys only
let
  # replace with your real age public keys
  admin = "age1AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
  host  = "age1BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
in
{
  "lab-demo-token.age".publicKeys = [ admin host ];
}

No private keys in this file.

Identity reuse

Reuse public age keys from the sops-nix setup when possible so you are not juggling unrelated key material during comparison.


Theory 4 — NixOS declaration

{ config, inputs, ... }:
{
  imports = [ inputs.agenix.nixosModules.default ];

  age.secrets.lab-demo-token = {
    file = ../../secrets/lab-demo-token.age;
    # mode / owner as needed
  };

  # config.age.secrets.lab-demo-token.path → runtime path
}

Consumer pattern matches sops-nix: path at runtime, never embed content in derivations.

systemd.services.agenix-demo = {
  wantedBy = [ "multi-user.target" ];
  serviceConfig = {
    Type = "oneshot";
    RemainAfterExit = true;
    ExecStart = "/bin/sh -c 'test -r ${config.age.secrets.lab-demo-token.path}'";
  };
};

Theory 5 — Side-by-side comparison

Dimension sops-nix agenix
Multi-key structured file Natural (YAML tree) Usually one file per secret
Edit UX sops file in-place decrypt editor agenix -e
Creation rules .sops.yaml secrets.nix publicKeys
HM integration Strong story Available patterns
Learning transfer SOPS used outside Nix too Age-centric, Nix-flavored
House default (this book) Primary Compare once

When agenix might still win for you

  • You want the simplest possible “one secret, one file” model
  • Your team already standardized on agenix
  • You dislike SOPS YAML tooling

When sops-nix wins for this curriculum

  • Multiple secrets per environment file
  • Same SOPS skills in non-Nix contexts
  • Book consistency for later examples

Theory 6 — Dual-running risks

Running both modules on one host:

  • Two key bootstraps
  • Two runtime directories
  • Cognitive load on incident response

Acceptable for one comparison lab. Not acceptable as permanent undefined state—pick a default before the next service chapters pile on.


Theory 7 — Declaring house default

Write in docs/secrets.md:

## House default

**sops-nix** for all new secrets.

agenix: lab comparison only; module removed after comparison
(or: we chose agenix because … — only if you override the book).

If you override the book, do so explicitly so later sops-shaped examples are translated consciously.


Theory 8 — Re-encryption and rotation

Event sops-nix agenix
Add recipient update .sops.yaml, re-encrypt update secrets.nix, re-encrypt
Rotate value edit via tool re-encrypt file
Rotate keys re-encrypt all files re-encrypt all .age

Practice rotation on a placeholder secret, not production.


Theory 9 — Path API discipline (both tools)

# GOOD: path reference
environment.etc."demo-path".text = config.age.secrets.lab-demo-token.path;
# (still careful: only the path string)

# BAD: content into store
# environment.etc."demo".text = builtins.readFile config.age.secrets....path;

Same rule as sops: runtime consumers, not eval-time content embedding.


Worked example — Same placeholder secret both ways

Tool Artifact
sops secrets/lab.yaml key lab.demo_token
agenix secrets/lab-demo-token.age

Same placeholder value during local edit; different ciphertext formats in git. Runtime paths differ; consumer scripts should use module-provided .path.


Exercises

Exercise 1 — Add agenix input

# flake input + lock update agenix only
nix flake update agenix
nix flake metadata

Exercise 2 — Public keys and secrets.nix

Reuse public age keys from sops-nix if possible. Write secrets/secrets.nix with publicKeys only.

Exercise 3 — Encrypt one age secret

Using current agenix/age CLI docs for your pin:

nix shell github:ryantm/agenix#agenix nixpkgs#age
# agenix -e secrets/lab-demo-token.age

Put only a lab placeholder value inside.

Exercise 4 — NixOS module + rebuild

Declare age.secrets.…, rebuild, list runtime secrets dir for agenix:

sudo nixos-rebuild switch --flake .#lab
sudo ls -la /run/agenix 2>/dev/null || sudo ls -la /run/secrets 2>/dev/null || true

Confirm path exists; do not paste secret contents into notes.

Exercise 5 — Consumer parity

Point a oneshot at config.age.secrets.lab-demo-token.path similar to sops-nix. Prove readable by the intended user.

Exercise 6 — Comparison write-up

One page in the journal:

  1. Bootstrap friction: sops vs agenix
  2. Day-2 secret add friction (ongoing maintenance)
  3. Multi-secret ergonomics
  4. Failure modes observed
  5. Decision: house default = …

Exercise 7 — Decommission the non-default

If house default is sops-nix:

  1. Remove agenix module from active host imports
  2. Optionally leave encrypted .age file as historical artifact or delete
  3. Remove agenix input or keep input unused (prefer remove to simplify lock)
  4. Rebuild clean

Opposite steps if you chose agenix (then translate later chapters).

Exercise 8 — docs/secrets.md house default section

Commit the decision explicitly.

Exercise 9 — follows check

Confirm agenix input used follows while present; clean lock after removal.

Exercise 10 — Incident note

Add one paragraph: how rotation differs if dual tools had been left enabled.

Exercise 11 — Team rationale

Write two sentences you would tell a teammate who prefers the other tool.

Exercise 12 — Path-only audit

rg -n 'age\.secrets|sops\.secrets' modules/ || true

Ensure consumers use .path.

Exercise 13 — Rebuild with single stack

sudo nixos-rebuild switch --flake .#lab
nix flake metadata  # no unexpected dual inputs if removed

Exercise 14 — Future secret list

Map upcoming secrets (DB, proxy DNS token, Grafana) to the chosen tool only.


Common gotchas

Symptom / mistake What to do
Private key in secrets.nix Remove; rotate
Wrong public key list Re-encrypt with correct recipients
Both tools half-configured Finish decommission exercise
Assuming identical paths Always use .path from the module
Editing ciphertext as plaintext Use tool entrypoints only
Forgetting follows on agenix input Fix lock discipline
Leaving dual forever “for flexibility” Pick one; document override
Comparison without write-up Decision is the deliverable
Production secrets in comparison Placeholders only

Checkpoint

  • agenix encrypt + decrypt-on-activate worked once
  • Comparison table completed in your words
  • House default declared in docs/secrets.md
  • Non-default tool removed or clearly disabled
  • Still no plaintext secrets in git
  • Rebuild succeeds with chosen stack only
  • Future secrets mapped to one tool
  • follows/lock cleaned up

Journal (optional)

Write three personal gotchas before continuing.