Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.jacobpevans.com/llms.txt

Use this file to discover all available pages before exploring further.

SOPS is the default for sensitive-but-not-mission-critical values that live inside a single repo. Doppler holds anything an external system could weaponize. Keychain holds anything that must never touch disk.
This page is the IaC-flavored cut. For the broader posture (every tool, every tier), see Security overview and the dedicated SOPS tool page.

When SOPS is the right answer

SOPS-encrypted files live inside a single repo and decrypt with that repo’s age recipient. The question to ask before reaching for it is blast radius: if an AI agent (or a hostile reviewer, or a careless paste into a Slack channel) decrypted this file and posted it publicly, what would actually break?
  • If the answer is “nothing meaningful” — a randomly generated local-service password, a per-host Ansible variable that names an internal interface, an LXC root password that’s already wrapped behind WireGuard plus key auth — that value is a fine fit for SOPS. Use it. Default to SOPS for as much repo-local config as possible; the encrypted-at-rest property is free once age is set up.
  • If the answer is “an attacker can now hit AWS / Doppler / a third-party SaaS” — API tokens, OAuth keys, HEC tokens, license keys, anything an external service treats as a bearer credential — that value does not belong in SOPS. It goes in Doppler.
  • If the answer is “an attacker can now move laterally inside the homelab” — the master Proxmox root password, an iDRAC admin credential, anything that grants control-plane access if exposed — it goes in Keychain, not committed at all.
The mental model is “would a one-time public leak of this exact string be a paper cut, or would it be an incident?” SOPS is for paper cuts.

The three-way split

TierHoldsMechanism
Encrypted-at-rest in git (low blast radius)Repo-local generated passwords, per-host vars, structured config the same repo consumesSOPS + age
Rotating or cross-system credentialsExternal API tokens, HEC tokens, license keys, anything a third party treats as a bearerDoppler
Local-only material (control-plane / never-on-disk)macOS-side AI tool secrets, master/admin credentials, anything that must never reach a shell historymacOS Keychain

Cross-repo: SOPS is for one repo, not many

A single secret should have exactly one source of truth. SOPS works cleanly when the value is consumed only by the repo it’s committed in — Ansible decrypts its own secrets.sops.yml, Terragrunt reads its own .sops.json, nothing else. When two repos both need the same value, do not SOPS-encrypt it twice. That’s a DRY violation — the two copies will drift the first time one repo rotates without notifying the other, and the only signal you get is a production failure. The decision tree:
  • Repo A is fully dependent on Repo B (B publishes a value, A consumes it, A never rewrites it) → SOPS in repo B is fine; repo A pulls it through B’s output, or through Doppler if the dependency is loose.
  • Repos A and B can both update the value independently → SOPS in neither. Promote the value to Doppler. Both repos read it at runtime; rotation is a single Doppler write.
  • The value is genuinely local to one repo’s CI/test/bootstrap → SOPS in that one repo. No cross-repo concern exists.

How it shows up in Ansible repos

ansible-proxmox, ansible-proxmox-apps, and ansible-splunk all follow the same pattern:
# secrets.sops.yml.example — committed
NAS_HOMEASSISTANT_SMB_PASSWORD: change-me
cp secrets.sops.yml.example secrets.sops.yml
sops --encrypt --in-place secrets.sops.yml   # encrypts in place
git add secrets.sops.yml                     # safe: values are ciphertext now
At runtime, sops exec-env secrets.sops.yml -- ansible-playbook ... decrypts into a subprocess env only; nothing persists to disk. Rotating values stay in Doppler, accessed through doppler run -- ansible-playbook .... The two wrappers compose cleanly — Doppler outermost, sops inside.

How it shows up in Terraform repos

terraform-proxmox is the canonical example: a .sops.json under each environment folder, listed in .sops.yaml with the age recipient that controls it. Terragrunt consumes it through the sops_decrypt_file data source; resolved values land in Terraform state encrypted by the state backend’s KMS key, not on disk. terraform-runs-on uses the same mechanism but keeps the encrypted file smaller — it leans on Doppler for AWS creds and reserves SOPS for bootstrap config that doesn’t change.

The .sops.yaml configuration

Each repo declares which paths to encrypt with which keys at the repo root:
creation_rules:
  - path_regex: \.sops\.json$
    age: >-
      age1aaaa... # public age recipient
Only the public half of the age key appears here. The private half lives at ~/.config/sops/age/keys.txt (local convenience) and is escrowed in Bitwarden (canonical backup). The example shown is a placeholder — real public recipients live in each repo’s .sops.yaml.

What you must never commit

Per the secrets policy: no real IPv4 addresses, no real internal hostnames, no real domain names, no AWS account IDs, no SSH keys, no user-specific paths. Use placeholders or variables for anything tied to one user; every committed value should work for any person who clones the repo right now. The committed scrubbed-value table is 192.168.0.*, example.com, example.local, your-token-here, generic role-based usernames.

Rotating the age key

The five-step dance is the same across every repo that uses SOPS:
1

Generate the new key

age-keygen -o ~/.config/sops/age/keys.txt.new. Read-only file, not a service.
2

Add the new public recipient to every .sops.yaml

Keep the old recipient until cutover so neither party loses access mid-rotation.
3

Re-encrypt every SOPS file with both recipients

sops updatekeys .sops.json per file. The encrypted file changes; the plaintext doesn’t.
4

Remove the old recipient

Re-run sops updatekeys once more. Now only the new key can decrypt.
5

Escrow + revoke

New key goes into Bitwarden the moment it’s generated. Revoke the old key in any tooling that referenced it.

See also

Security overview

Which tool for which secret, across every surface.

SOPS tool page

Tool-level docs on SOPS itself — encrypt/decrypt cycle, anti-patterns, escrow.

Doppler

Where rotating runtime secrets live. The other half of this split.

macOS Keychain

Where local-only secrets live. The third tier.