SECRETS-WP-0015-T04: phase B client-secret custody proposal and ISRG trust status
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: sonnet Assistant-Process: 4515@bnt-lap001 Assistant-Session: 0bf96127-6e04-41be-bfda-d11717c3f354
This commit is contained in:
parent
9d3de49000
commit
65a3dbe103
1 changed files with 58 additions and 0 deletions
|
|
@ -233,3 +233,61 @@ Next owner action: railiance-platform, with rapp-openbao, accepts or declines a
|
|||
OpenBao restore with the secrets-engine JWT role and KV path, as a dependency of this task. The
|
||||
consumer-continuity run (plan claim 3) stays a separately admitted attempt after that. Status stays `wait`.
|
||||
No selector, service_auth.py, checker or founder_admission.py file was edited, so the NK49 pins stand.
|
||||
|
||||
2026-10-27 proposal for NK-WP-0049-T43 / phase B (message 765f6d7b from net-kingdom; proposal only, no live action,
|
||||
no secret value read, nothing authorized; founder decision 2026-10-27: this workload consumer satisfies T18 scope,
|
||||
no Authelia in phase B).
|
||||
|
||||
Why the ordinary path cannot be used. `login_service_jwt` takes the client secret from
|
||||
`OpenBaoOidcClientSecretSource`, an attended OIDC operator login to OpenBao followed by an in-memory KV read. Phase B
|
||||
has no Authelia/OIDC operator, and the plan forbids injecting the recovered issuer secret into ordinary
|
||||
`login_service_jwt` or creating a persistent consumer copy. `client_secret_file` is the approval-profile path and
|
||||
needs a 0600 file; phase B must not use it. So phase B needs a new, separately reviewed secret source.
|
||||
|
||||
Proposed custody: founder-attended pipe into an inherited descriptor, in-memory only.
|
||||
1. New file, not an edit of any pinned one (`service_auth.py`, selector, checker and `founder_admission.py` stay
|
||||
byte-identical, so the NK49 pins stand): `tools/phase-b-consumer-run.py` (name suggested) plus tests. It builds a
|
||||
`KeyCapeServiceAuthProvider(kcfg, secret_source=<callable>)` itself; the callable reads fd N once.
|
||||
2. The founder starts the runner in their own terminal. The secret is produced by the founder's pipeline from the
|
||||
backup's SOPS material and reaches the runner only as a pipe: `<founder decrypt of the one field> | runner
|
||||
--secret-fd 0`-style, or an explicitly opened fd 3 (`3< <(...)`) so stdin stays free for the founder's own
|
||||
prompts. The value never appears in argv, environment, a file, a tmpfs path or shell history, and the runner
|
||||
refuses `--secret` / `--secret-file` / `KEYCAPE_*_SECRET` env if present. The agent never holds the terminal,
|
||||
the pipeline or the output; it only receives the value-free result.
|
||||
3. Runner reads at most 4 KiB, until EOF, requires one non-empty token without whitespace (same rule as
|
||||
`read_client_secret_file`), closes the fd, uses the value for exactly one client-credentials exchange, then
|
||||
zeroes its buffer reference and does not retry or fall back. fd must be a pipe (S_ISFIFO), not a regular file or
|
||||
tty, so a file copy or an echoed prompt is refused. Core dumps disabled (RLIMIT_CORE 0, PR_SET_DUMPABLE 0), no
|
||||
swap-visible logging, no `repr` of the value.
|
||||
4. Runner runs inside the admitted network (member of the internal network, no default route), only after
|
||||
`check-keycape-recovery-selector.py --admission` passes for the founder-signed attempt for this exact case; the
|
||||
run is single use inside the signed window.
|
||||
5. Result is value-free JSON only: exchange accepted, subject/tenant/roles/scope claims match, `openbao:login`
|
||||
accepted, `openbao:admin` refused, wrong-secret refusal (founder supplies a second unrelated token through the
|
||||
same fd mechanism), session `revoke-self` attempted and succeeded, secret source name
|
||||
`founder_attended_pipe_fd`, cleanup facts.
|
||||
6. The decrypt step is the founder's: SOPS/age key custody is NetKingdom's and the founder's. Which exact backup field
|
||||
and the decrypt command are for NetKingdom to name; secrets-engine will not name a path to the value and will not
|
||||
run the decrypt. Not a bypass of the existing "no persistent consumer copy" rule: nothing persists.
|
||||
7. Alternative considered and not preferred: the issuer's own `service-token` command mints a token in the issuer
|
||||
tmpfs and hands only that token over private stdin to the runner (plan claim 2). That needs a new NK49 source
|
||||
closure and review and hands a token, not the client secret, so it does not exercise the client's own secret and
|
||||
would not prove the secret in the backup is the one the consumer uses. Offered as a fallback only.
|
||||
|
||||
Gaps and owner items: a new reviewed runner file (this task, after founder approval of the design and the
|
||||
SECRETS-WP-0015 attempt admission for phase B); NK49 names the backup field and decrypt command; the founder signs a
|
||||
new admission, since the 2026-10-23 window ended 2026-10-24T05:00Z. Status stays `wait`.
|
||||
|
||||
ISRG Root X1 trust of restored OpenBao for /jwks: UNPROVEN in this repository. What the evidence does show: the
|
||||
KeyCape side served a production letsencrypt-prod chain to ISRG Root X1 (NK49 T18 verifier, receipt 16d72f7c) and
|
||||
the restored OpenBao's `keycape-services` JWT config and role were present and equal to source (RPF-WP-0064-T07,
|
||||
receipt 4e048dee), with no data read and no JWKS fetch, because both ran `--network none`. No rehearsal here ever
|
||||
made OpenBao fetch `/jwks`; the only JWKS fetches in this repo are the consumer-side Python tests
|
||||
(`tools/exercise_approval_identity.py`, synthetic CA via SSL_CERT_FILE), which say nothing about OpenBao's trust
|
||||
store. Whether it trusts the chain depends on facts only the OpenBao owner can read: the OpenBao image's system CA
|
||||
bundle, and whether the restored JWT config sets `jwks_ca_pem` / `oidc_discovery_ca_pem` (a pinned CA would
|
||||
override or restrict system roots). Requested of railiance-platform in its phase B rehearsal: show those two
|
||||
settings as present/absent (value-free), and a bounded probe from inside the restored OpenBao network namespace
|
||||
that OpenBao-side validation of a JWT against `https://kc.coulomb.social/jwks` succeeds, using the production
|
||||
chain and not LE staging (staging roots are not ISRG Root X1). The first live login attempt is the real proof; it
|
||||
should be preceded by that probe so a trust failure is not misread as a consumer failure.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue