Commit Graph
2 Commits
Author SHA1 Message Date
notplantsandClaude 6c56c1953e refactor(secrets): store lives at /secrets, not /srv/secrets
/srv is the projects tree — agents grep, find and `ls -R` it constantly, so a store
under it turns up in ordinary searches and risks being read (or pasted) by accident.
/secrets sits outside that blast radius: nothing routinely walks it, and it is still
0700 loops, still not a repo, still ciphertext at rest.

Override with AO_SECRETS_STORE if a host puts it elsewhere.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-01 16:54:03 +00:00
notplantsandClaude 7605efe624 feat(secrets): one sops+age store for the host, documented for every project
Credentials were scattered in plaintext: a gitea password baked into six git remote
URLs (`git remote -v` prints those), API keys in .env files, an incus client key at
0644. Anything living in a repo is one `git add -A` from being pushed.

So: ONE encrypted file outside every git tree, and a helper each project uses.

  /srv/secrets/store.yaml       sops+age ciphertext, 0600, not a repo, no remote
  ~/.config/sops/age/keys.txt   the only plaintext secret on disk, 0600

secrets.py is stdlib + the sops binary: get("group.key"), get_group("group"), and
materialize() for consumers that must read a fixed path (systemd EnvironmentFile,
ssh IdentityFile, nix authKeyFile) — those keep their file, but the store is the
source of truth, so a materialized file is never hand-edited.

`list` prints names only, never values, so it is safe in a transcript.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-01 16:51:36 +00:00