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>
This commit is contained in:
@@ -363,11 +363,11 @@ Run it by hand with `engine/agents.py up --config agents.toml`.
|
|||||||
secret anywhere else** — not in a git remote URL, not in a project `.env`, not in a prompt.
|
secret anywhere else** — not in a git remote URL, not in a project `.env`, not in a prompt.
|
||||||
|
|
||||||
```
|
```
|
||||||
/srv/secrets/store.yaml the store: sops+age ciphertext, mode 0600
|
/secrets/store.yaml the store: sops+age ciphertext, mode 0600
|
||||||
~/.config/sops/age/keys.txt the age private key — the ONE plaintext secret, mode 0600
|
~/.config/sops/age/keys.txt the age private key — the ONE plaintext secret, mode 0600
|
||||||
```
|
```
|
||||||
|
|
||||||
`/srv/secrets/` is deliberately **not a git repo and has no remote**, so there is no path by
|
`/secrets/` is deliberately **not a git repo and has no remote**, so there is no path by
|
||||||
which a `git add`/`git push` can leak it; the store is ciphertext at rest anyway.
|
which a `git add`/`git push` can leak it; the store is ciphertext at rest anyway.
|
||||||
|
|
||||||
Read it with `engine/secrets.py` (stdlib + the `sops` binary, no Python deps):
|
Read it with `engine/secrets.py` (stdlib + the `sops` binary, no Python deps):
|
||||||
@@ -382,7 +382,7 @@ env = get_group("cc_ci_testenv") # a whole group as a dict
|
|||||||
python3 engine/secrets.py list # group/key NAMES only — never prints values
|
python3 engine/secrets.py list # group/key NAMES only — never prints values
|
||||||
python3 engine/secrets.py get tangled.cookie # one value on stdout
|
python3 engine/secrets.py get tangled.cookie # one value on stdout
|
||||||
python3 engine/secrets.py materialize tangled-session # write a runtime file from the store
|
python3 engine/secrets.py materialize tangled-session # write a runtime file from the store
|
||||||
sops /srv/secrets/store.yaml # add/edit: decrypts to $EDITOR, re-encrypts on save
|
sops /secrets/store.yaml # add/edit: decrypts to $EDITOR, re-encrypts on save
|
||||||
```
|
```
|
||||||
|
|
||||||
**Materialized files.** Some consumers read a fixed path and can't be taught otherwise (a systemd
|
**Materialized files.** Some consumers read a fixed path and can't be taught otherwise (a systemd
|
||||||
|
|||||||
+5
-5
@@ -6,9 +6,9 @@ remote URLs (`https://user:pass@host/...`, which `git remote -v` happily prints)
|
|||||||
in .env files, a private key at mode 0644. Anything in a repo is one `git add -A` away from
|
in .env files, a private key at mode 0644. Anything in a repo is one `git add -A` away from
|
||||||
a push. So: ONE encrypted file, OUTSIDE every git tree, and a helper every project uses.
|
a push. So: ONE encrypted file, OUTSIDE every git tree, and a helper every project uses.
|
||||||
|
|
||||||
store: /srv/secrets/store.yaml sops+age ciphertext, mode 0600
|
store: /secrets/store.yaml sops+age ciphertext, mode 0600
|
||||||
age key: ~/.config/sops/age/keys.txt the ONLY plaintext secret on disk, 0600
|
age key: ~/.config/sops/age/keys.txt the ONLY plaintext secret on disk, 0600
|
||||||
outside git by construction — /srv/secrets is not a repo and has no remote.
|
outside git by construction — /secrets is not a repo and has no remote.
|
||||||
|
|
||||||
USAGE (library):
|
USAGE (library):
|
||||||
from secrets import get, get_group
|
from secrets import get, get_group
|
||||||
@@ -23,14 +23,14 @@ USAGE (CLI):
|
|||||||
MATERIALIZED FILES: some consumers read a fixed path and cannot be taught otherwise (a
|
MATERIALIZED FILES: some consumers read a fixed path and cannot be taught otherwise (a
|
||||||
systemd EnvironmentFile, an ssh IdentityFile, `nix`'s authKeyFile). Those files still exist
|
systemd EnvironmentFile, an ssh IdentityFile, `nix`'s authKeyFile). Those files still exist
|
||||||
on disk at 0600, but the STORE IS THE SOURCE OF TRUTH — `materialize` rewrites them from it.
|
on disk at 0600, but the STORE IS THE SOURCE OF TRUTH — `materialize` rewrites them from it.
|
||||||
Never edit a materialized file by hand; edit the store (`sops /srv/secrets/store.yaml`) and
|
Never edit a materialized file by hand; edit the store (`sops /secrets/store.yaml`) and
|
||||||
re-materialize, or the two silently drift.
|
re-materialize, or the two silently drift.
|
||||||
|
|
||||||
ADDING A SECRET: sops /srv/secrets/store.yaml (opens decrypted in $EDITOR, re-encrypts on save)
|
ADDING A SECRET: sops /secrets/store.yaml (opens decrypted in $EDITOR, re-encrypts on save)
|
||||||
"""
|
"""
|
||||||
import json, os, subprocess, sys, pathlib
|
import json, os, subprocess, sys, pathlib
|
||||||
|
|
||||||
STORE = os.environ.get("AO_SECRETS_STORE", "/srv/secrets/store.yaml")
|
STORE = os.environ.get("AO_SECRETS_STORE", "/secrets/store.yaml")
|
||||||
AGE_KEY = os.environ.get("SOPS_AGE_KEY_FILE", os.path.expanduser("~/.config/sops/age/keys.txt"))
|
AGE_KEY = os.environ.get("SOPS_AGE_KEY_FILE", os.path.expanduser("~/.config/sops/age/keys.txt"))
|
||||||
|
|
||||||
# name -> (path, mode). Files a consumer reads from a fixed location; see MATERIALIZED FILES.
|
# name -> (path, mode). Files a consumer reads from a fixed location; see MATERIALIZED FILES.
|
||||||
|
|||||||
Reference in New Issue
Block a user