RPF-WP-0015-T02: contained-login only, single silent attended window

Harness used the retracted public OpenBao URL and a token file; lane used a
standing token file. Both now run only inside the contained ops-warden login
(tunnel, helper token), and t02-attended-window.py runs project, exercise and
cleanup under one login without printing (warden fails closed on child output).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 680842@bnt-lap001
Assistant-Session: 54eb9e8e-7d6b-4f0c-b7a6-a6db62075883
This commit is contained in:
codex 2026-10-14 14:23:46 +02:00
parent badb91c71a
commit 311aa86eef
10 changed files with 773 additions and 109 deletions

View file

@ -85,46 +85,86 @@ driver as `AUDIT_T02_SOURCE`/`AUDIT_T02_TENANT`; the confirm string is
`platform/workloads/audit-core/senders` → ExternalSecret `audit-core-senders`
→ env `AUDIT_CORE_SENDERS`, so both projection and cleanup restart
`deploy/audit-core`. The restarts must stay outside the restart-free proof:
project → restart → T02 exercise → evidence → cleanup → restart.
1. `make audit-synthetic-sender-preflight` — read-only; reads registry metadata
only (add `AUDIT_SYNTHETIC_SENDER_PREFLIGHT_ARGS=--inspect-registry` for an
attended in-memory conflict check).
2. `make audit-synthetic-sender-project` — only within the first 3 minutes of
the window and with ≥10 minutes of identity lifetime left. Writes the
bearer to a new mode-`0600` file (`AUDIT_T02_TOKEN_FILE`, absolute, in an
operator-owned `0700` directory outside every Git work tree; never stdout),
adds the one identity, forces `audit-core-senders` sync and runs
`rollout restart deploy/audit-core`, then waits for one Ready restarted pod
and prints its UID. Any failure removes the identity (restarting again) and
shreds the bearer file.
3. The T02 exercise (`make audit-database-lease-recovery-exercise`). Its
baseline must capture the post-projection pod UID; never project while the
exercise is running.
4. Record the exercise's JSON result (it is the evidence file).
5. `make audit-synthetic-sender-cleanup AUDIT_T02_EVIDENCE=<result.json>` —
refuses unless that evidence names this `approval_id`, or an explicit
`AUDIT_SYNTHETIC_SENDER_ABORT_REASON` is given for an aborted run. Removes
only the exact named identity (refusing if the name carries a different
scope), restarts audit-core only if something was removed, shreds the
bearer file, and confirms 1/1 Ready. Re-running is a no-op.
**Contained OpenBao access only.** Every live command (lane `project` and
`cleanup`, harness `exercise`) requires `--contained` and refuses unless it runs
as the child of `scripts/openbao-attended-exec.py`, i.e. inside `warden access
openbao-platform-admin-login --exec`: `WARDEN_ROUTING_CATALOG` must be set,
`BAO_ADDR` must be the operator tunnel `http://127.0.0.1:18200`, and
`BAO_TOKEN`/`VAULT_TOKEN`/`OPENBAO_TOKEN` must be absent; `bao` authenticates
through Warden's isolated token helper. There is no token-file path any more:
the harness's former default `https://bao.coulomb.social` was retracted on
2026-09-15 (RMASTER-WP-0020-T09), and a standing 0600 platform-admin token file
is outside the accepted custody path. Outside the login, the lane's `preflight`
runs the cluster checks only; `--contained` adds registry metadata and
`--inspect-registry` (attended, in-memory) adds the conflict check.
**One login per window.** `scripts/t02-attended-window.py` is the single reviewed
child of the contained login, so the ≤15-minute window needs one MFA login:
```bash
make audit-t02-attended-window \
AUDIT_DATABASE_LEASE_APPROVAL=/path/to/approved-receipt.json \
AUDIT_DATABASE_LEASE_DRIVER=/path/to/audit-core/scripts/t02_synthetic_load_driver.py \
AUDIT_SYNTHETIC_SENDER_CONFIRM=audit-core-synthetic-sender:<approval_id>:attended \
AUDIT_T02_TOKEN_FILE=/abs/0700-dir/t02.bearer \
AUDIT_T02_EVIDENCE_DIR=/abs/0700-dir/evidence
```
It runs, in order:
1. lane `project` — only within the first 3 minutes of the window and with
≥10 minutes of identity lifetime left. Writes the bearer to a new mode-`0600`
file (absolute, operator-owned `0700` directory outside every Git work tree;
never stdout), adds the one identity, forces `audit-core-senders` sync, runs
`rollout restart deploy/audit-core`, and waits for one Ready restarted pod.
Any failure removes the identity (restarting again) and shreds the file.
2. port-forward — the lane and harness reach the cluster only via
`ssh railiance01 kubectl`, so the window uses the same path: `ssh -L
127.0.0.1:<port>:127.0.0.1:<port> railiance01 timeout 900 kubectl -n
audit-core port-forward --address 127.0.0.1 deploy/audit-core <port>:8080`
(default port 18081, `AUDIT_T02_PORT` to override). No local kubeconfig is
needed; the remote forward is bounded by `timeout` even if SSH is cut.
audit-core's NetworkPolicies admit only named namespaces, which is why the
operator-side driver needs the forward.
3. harness `exercise --contained` with the registered driver; the driver env is
derived from the receipt: `AUDIT_T02_BASE_URL=http://127.0.0.1:<port>`,
`AUDIT_T02_TOKEN_FILE`, `AUDIT_T02_SOURCE`/`AUDIT_T02_TENANT` from
`synthetic_sender`, `AUDIT_T02_FIXTURE_ID=t02-<approval_id>`,
`AUDIT_T02_OCCURRED_AT=window.start`. Its baseline captures the
post-projection pod UID.
4. evidence — the exercise JSON is written `0600` as
`<evidence-dir>/<approval_id>-exercise.json`; a value-safe window summary is
written as `<approval_id>-window.json` on success and on abort.
5. lane `cleanup` with that evidence — refuses unless it names this
`approval_id`. Removes only the exact named identity (refusing if the name
carries a different scope), restarts audit-core only if something was
removed, shreds the bearer file, and confirms 1/1 Ready.
The port-forward is stopped before cleanup and on every exit. Any failure after
projection was attempted (including a failed `project`, whose registry write may
already have reached OpenBao) runs `cleanup --abort-reason` before the window
script exits non-zero. Children's stdout is captured and only their value-safe
JSON is re-emitted.
The single-step targets (`audit-synthetic-sender-project`,
`audit-database-lease-recovery-exercise`, `audit-synthetic-sender-cleanup`)
remain for the abort path; each costs its own contained login. A missed cleanup
is run as `make audit-synthetic-sender-cleanup
AUDIT_SYNTHETIC_SENDER_ABORT_REASON=...` and is a no-op if nothing remains.
If cleanup is missed, audit-core's `expires_at` enforcement makes the identity
inert at window end; the attended cleanup is still required to remove it.
Projection and cleanup each restart audit-core once (Recreate, no surge), so
production senders see two short retryable gaps per window; announce them with
the window.
The driver runs operator-side. audit-core's NetworkPolicies admit only named
namespaces, so reach the pod with `kubectl -n audit-core port-forward
deploy/audit-core <local-port>:8080` and set `AUDIT_T02_BASE_URL` to that local
URL; record the base URL and the driver's source/tenant (which must equal the
receipt's `synthetic_sender` block) in the window notes. Projection and cleanup
each restart audit-core once (Recreate, no surge), so production senders see two
short retryable gaps per window; announce them with the window.
If `project` fails after the registry write may already have reached OpenBao (for
example a transport error on the write itself), treat the identity as possibly
present: run `cleanup --abort-reason "<reason>"` before leaving the window.
The lane's `--token-file` is the attended operator token for the registry write;
it must come from the contained ops-warden attended login accepted under RPF-WP-0017
(isolated 0600 helper state, removed afterwards), not from a long-lived token file.
The contained login discards child output and fails closed (exit 12, token
revoked) on any byte the child prints. The window script therefore prints
nothing: its summary, exercise evidence or `window-error-*.json` land only in the
evidence directory. Single-step targets require `AUDIT_T02_RESULT_FILE` so the
lane/harness write their JSON there instead of stdout.
## Attended sequence
@ -165,13 +205,11 @@ bodies and clear all transient JSON from memory or mode-`0600` temporary files.
(which restarts audit-core after the evidence is captured). The replacement remains managed by ESO
and expires through its normal database role TTL.
Only inside the approved window, with the receipt and reviewed driver:
```bash
make audit-database-lease-recovery-exercise \
AUDIT_DATABASE_LEASE_APPROVAL=/path/to/approved-receipt.json \
AUDIT_DATABASE_LEASE_DRIVER=/path/to/approved-load-driver
```
Only inside the approved window, with the receipt and reviewed driver, run the
whole window through `make audit-t02-attended-window` (see the synthetic sender
section above); the exercise step alone is
`make audit-database-lease-recovery-exercise`, which also runs inside the
contained login.
The exact confirmation string is embedded in the target. The harness rechecks
the Secret/pod/lease immediately before revocation, requires at least five

View file

@ -25,10 +25,15 @@ Runbook: `docs/audit-core-database-lease-recovery.md`.
`RAILIANCE-WP-0024-T02-SYNTHETIC-01` with driver revision
`sha256:941ba251…` (audit-core `8c8bcf4`), and the `synthetic_sender` block.
3. Announce two short audit-core restart gaps (projection and cleanup).
4. In the window: attended ops-warden login → `make audit-synthetic-sender-project`
→ port-forward audit-core → `make audit-database-lease-recovery-exercise` →
record evidence → `make audit-synthetic-sender-cleanup`.
5. Abort: stop load, `cleanup --abort-reason`, never restart the pod by hand.
4. In the window: one contained ops-warden login runs the whole sequence —
`make audit-t02-attended-window` (approval receipt, driver path, confirm
string, bearer file and evidence dir outside Git). Through
`scripts/openbao-attended-exec.py` it runs lane project (restart #1) →
ssh-bounded port-forward → contained T02 exercise → 0600 evidence JSON →
lane cleanup with that evidence (restart #2). No token file is used.
5. Abort: the window script stops the port-forward and runs `cleanup --abort-reason`
itself; if it could not, run `make audit-synthetic-sender-cleanup
AUDIT_SYNTHETIC_SENDER_ABORT_REASON=...` (one more login). Never restart the pod by hand.
## T03 — coordinated railiance01 reboot