Add castle secret CLI and fail-loud unresolved-secret apply gate
Writing a secret by hand meant knowing the active backend and its layout;
getting it wrong (a file write on an OpenBao fleet) left the value where the
resolver never reads it, so ${secret:NAME} silently degraded to the literal
<MISSING_SECRET:NAME> placeholder that a service then used as its credential.
- `castle secret {list|set|get|rm}` reads/writes the ACTIVE backend, so there's
no wrong store to pick. `set NAME` with no value reads a hidden prompt / stdin.
- `castle apply` (and --plan) now refuses to converge any deployment whose
${secret:...} refs don't resolve in the active backend: exits non-zero, writes
nothing, and names each deployment + secret + the `castle secret set` fix.
- New helpers in core/config.py: active_secret_backend(), active_backend_name(),
secret_refs(). Gate impl: deploy._unresolved_secrets() + ApplyResult.blocked.
- Tests: test_deploy_secret_gate.py, TestSecretRefs, test_secret.py. Docs: AGENTS.md.
This commit is contained in:
14
AGENTS.md
14
AGENTS.md
@@ -75,6 +75,12 @@ castle job <list|info|delete|restart|logs> ...
|
||||
castle tool list [--json] # each tool's executable + description + install state
|
||||
castle tool info <name> [--json]
|
||||
|
||||
# Secrets — read/write the ACTIVE backend (file or openbao); never hand-write the store
|
||||
castle secret list # names in the active backend
|
||||
castle secret set <NAME> [VALUE] # VALUE omitted → hidden prompt / stdin
|
||||
castle secret get <NAME> # print a value
|
||||
castle secret rm <NAME> [-y] # delete
|
||||
|
||||
# Platform-wide
|
||||
castle apply [name] [--plan] # converge runtime to config — the workhorse
|
||||
castle list [--kind ...] [--stack ...] [--json] # all deployments
|
||||
@@ -271,6 +277,14 @@ secrets:
|
||||
via a least-privilege `castle-read` token. Only the bootstrap tier stays as
|
||||
files (`OPENBAO_ROOT_TOKEN`/`OPENBAO_UNSEAL_KEY`, the `cloudflared` creds dir).
|
||||
|
||||
**Write secrets with `castle secret set NAME`, never by hand.** It targets the
|
||||
*active* backend, so a value can't land in a store the resolver never reads — the
|
||||
failure mode where a file-written secret is silently ignored on an openbao fleet
|
||||
and `${secret:NAME}` degrades to a literal `<MISSING_SECRET:NAME>` that a service
|
||||
then uses as its credential. `castle apply` **refuses to converge** (exits
|
||||
non-zero, writes nothing) any deployment whose `${secret:…}` refs don't resolve in
|
||||
the active backend, naming the fix.
|
||||
|
||||
**Never** put secret *values* in `castle.yaml` or project dirs — use `${secret:…}`.
|
||||
Roots: **`CASTLE_HOME`** (config/code/artifacts/secrets, default `~/.castle`,
|
||||
env-only — it *contains* castle.yaml) and **program data** (base of `${data_dir}`,
|
||||
|
||||
Reference in New Issue
Block a user