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:
parent
badb91c71a
commit
311aa86eef
10 changed files with 773 additions and 109 deletions
|
|
@ -2,7 +2,7 @@
|
|||
# Custodian Brief — railiance-platform
|
||||
|
||||
**Domain:** financials
|
||||
**Last synced:** 2026-10-11 21:24 UTC
|
||||
**Last synced:** 2026-10-14 10:58 UTC
|
||||
**State Hub:** http://127.0.0.1:8000 *(adjust if running on a remote machine)*
|
||||
|
||||
## Active Workstreams
|
||||
|
|
@ -25,14 +25,14 @@ Progress: 4/6 done | workplan_id: `b2c25a01-4a80-55c1-90cf-8538000f7e0e`
|
|||
- ! T06 — Publish the Railiance/OpenBao custody handoff `3b9748c4`
|
||||
*(wait: Owner work first: the OpenBao owner confirms mount/path/fields and writer/reader authority and publishes the non-secret routing receipt; the handoff enters the attended window only when a command exists.)*
|
||||
|
||||
### Onboard rapp-policy-nexus to the ArgoCD production lane
|
||||
Progress: 3/5 done | workplan_id: `ec41a4bd-df18-5b07-9b63-ccb80d9f001c`
|
||||
### Admit the Playwright pilot and establish scoped consumer group access
|
||||
Progress: 4/6 done | workplan_id: `31933d08-c877-5fda-aff9-6220c3705c92`
|
||||
|
||||
**Open tasks:**
|
||||
- ! Adopt policy-nexus (live, founder go-ahead) `8638fa73`
|
||||
*(wait: Merge approved by D2; runs once T03 diff and root automation (RPF-WP-0044-T07) are proven.)*
|
||||
- ! Prepare the Application and the dry-run evidence `2afb1aac`
|
||||
*(wait: Steps 1 and 3 done 2026-10-11 (docs/evidence/2026-10-11-policy-nexus-adoption-readonly-precheck.json, no stop condition). Step 2 render diff needs the railiance-apps values path from RAPP-POLICY-NEXUS-WP-0002-T02; agents can run it read-only via the k3s tunnel.)*
|
||||
- ! Connect platform identities and prove managed consumer operation `a82762eb`
|
||||
*(wait: User admitted all identified platform humans and coding agents; their supported Kubernetes authentication/group-mapping route remains unimplemented.)*
|
||||
- ! Obtain Custodian coordination of self-service consumer delivery `ef6c21bc`
|
||||
*(wait: Await Custodian coordination acceptance, accountable coordinator and source task links for the existing cross-repository delivery.)*
|
||||
|
||||
### ArgoCD phase B: adopt the four existing Applications on railiance01
|
||||
Progress: 5/8 done | workplan_id: `98140775-3b9a-5cf9-9af6-722502d487dc`
|
||||
|
|
@ -44,11 +44,21 @@ Progress: 5/8 done | workplan_id: `98140775-3b9a-5cf9-9af6-722502d487dc`
|
|||
- ! Turn the root back to the production lane `844ff115`
|
||||
*(wait: Restore prune/selfHeal once T03-T06 have automated proof; no further founder go-ahead (D2).)*
|
||||
|
||||
### Onboard rapp-policy-nexus to the ArgoCD production lane
|
||||
Progress: 3/5 done | workplan_id: `ec41a4bd-df18-5b07-9b63-ccb80d9f001c`
|
||||
|
||||
**Open tasks:**
|
||||
- ! Adopt policy-nexus (live, founder go-ahead) `8638fa73`
|
||||
*(wait: Merge approved by D2; runs once T03 diff and root automation (RPF-WP-0044-T07) are proven.)*
|
||||
- ! Prepare the Application and the dry-run evidence `2afb1aac`
|
||||
*(wait: Steps 1 and 3 done 2026-10-11 (docs/evidence/2026-10-11-policy-nexus-adoption-readonly-precheck.json, no stop condition). Step 2 render diff needs the railiance-apps values path from RAPP-POLICY-NEXUS-WP-0002-T02; agents can run it read-only via the k3s tunnel.)*
|
||||
|
||||
### Close Forgejo primary backup coverage on Scaleway
|
||||
Progress: 3/4 done | workplan_id: `7beec1a7-aa82-5a36-9a66-6b60008a2455`
|
||||
|
||||
**Open tasks:**
|
||||
- · Establish primary full-archive delivery and application recovery `a4807df0`
|
||||
- ! Establish primary full-archive delivery and application recovery `a4807df0`
|
||||
*(wait: Durable scheduled caller, verified inventory and quota/retention gates remain open; September 15 prohibits expiration and scheduled secondary cutover.)*
|
||||
|
||||
### Close S3 service assurance and ownership gaps
|
||||
Progress: 4/7 done | workplan_id: `ca639c3d-3a87-5fa4-ad13-6f2e014b0c84`
|
||||
|
|
|
|||
67
Makefile
67
Makefile
|
|
@ -526,41 +526,60 @@ audit-database-lease-approval-check: ## Validate an approved T02 receipt without
|
|||
python3 scripts/audit-core-database-lease-recovery.py validate-approval \
|
||||
--approval "$(AUDIT_DATABASE_LEASE_APPROVAL)"
|
||||
|
||||
audit-database-lease-recovery-exercise: ## Run the separately approved attended T02 exercise
|
||||
@test -n "$(AUDIT_DATABASE_LEASE_APPROVAL)" || { echo 'set AUDIT_DATABASE_LEASE_APPROVAL=/path/to/approved-receipt.json' >&2; exit 2; }
|
||||
@test -n "$(AUDIT_DATABASE_LEASE_DRIVER)" || { echo 'set AUDIT_DATABASE_LEASE_DRIVER=/path/to/approved-load-driver' >&2; exit 2; }
|
||||
python3 scripts/audit-core-database-lease-recovery.py exercise \
|
||||
--approval "$(AUDIT_DATABASE_LEASE_APPROVAL)" \
|
||||
--load-driver "$(AUDIT_DATABASE_LEASE_DRIVER)" \
|
||||
ATTENDED_EXEC = python3 scripts/openbao-attended-exec.py --
|
||||
AUDIT_T02_NEED_APPROVAL = @test -n "$(AUDIT_DATABASE_LEASE_APPROVAL)" || { echo 'set AUDIT_DATABASE_LEASE_APPROVAL=/path/to/approved-receipt.json' >&2; exit 2; }
|
||||
AUDIT_T02_NEED_DRIVER = @test -n "$(AUDIT_DATABASE_LEASE_DRIVER)" || { echo 'set AUDIT_DATABASE_LEASE_DRIVER=/path/to/approved-load-driver' >&2; exit 2; }
|
||||
AUDIT_SYNTHETIC_SENDER_NEED_CONFIRM = @test -n "$(AUDIT_SYNTHETIC_SENDER_CONFIRM)" || { echo 'set AUDIT_SYNTHETIC_SENDER_CONFIRM to the confirm string printed by preflight' >&2; exit 2; }
|
||||
AUDIT_T02_NEED_BEARER = @test -n "$(AUDIT_T02_TOKEN_FILE)" || { echo 'set AUDIT_T02_TOKEN_FILE=/abs/path/outside/git (0700 dir)' >&2; exit 2; }
|
||||
|
||||
audit-t02-attended-window: ## One contained login: project, port-forward, T02 exercise, evidence, cleanup
|
||||
$(AUDIT_T02_NEED_APPROVAL)
|
||||
$(AUDIT_T02_NEED_DRIVER)
|
||||
$(AUDIT_SYNTHETIC_SENDER_NEED_CONFIRM)
|
||||
$(AUDIT_T02_NEED_BEARER)
|
||||
@test -n "$(AUDIT_T02_EVIDENCE_DIR)" || { echo 'set AUDIT_T02_EVIDENCE_DIR=/abs/dir/outside/git (0700)' >&2; exit 2; }
|
||||
$(ATTENDED_EXEC) scripts/t02-attended-window.py \
|
||||
--approval "$(abspath $(AUDIT_DATABASE_LEASE_APPROVAL))" \
|
||||
--load-driver "$(abspath $(AUDIT_DATABASE_LEASE_DRIVER))" \
|
||||
--bearer-file "$(AUDIT_T02_TOKEN_FILE)" --evidence-dir "$(AUDIT_T02_EVIDENCE_DIR)" \
|
||||
--confirm "$(AUDIT_SYNTHETIC_SENDER_CONFIRM)" $(if $(AUDIT_T02_PORT),--port $(AUDIT_T02_PORT))
|
||||
|
||||
audit-database-lease-recovery-exercise: ## Exercise step alone, inside its own contained login (prefer audit-t02-attended-window)
|
||||
$(AUDIT_T02_NEED_APPROVAL)
|
||||
$(AUDIT_T02_NEED_DRIVER)
|
||||
@test -n "$(AUDIT_T02_RESULT_FILE)" || { echo 'set AUDIT_T02_RESULT_FILE=/abs/new/file outside Git (the contained child must stay silent)' >&2; exit 2; }
|
||||
$(ATTENDED_EXEC) scripts/audit-core-database-lease-recovery.py exercise --contained --result-file "$(AUDIT_T02_RESULT_FILE)" \
|
||||
--approval "$(abspath $(AUDIT_DATABASE_LEASE_APPROVAL))" \
|
||||
--load-driver "$(abspath $(AUDIT_DATABASE_LEASE_DRIVER))" \
|
||||
--confirm RAILIANCE-WP-0024-T02:attended
|
||||
|
||||
AUDIT_SYNTHETIC_SENDER_LANE = python3 scripts/audit_core_synthetic_sender_lane.py
|
||||
AUDIT_SYNTHETIC_SENDER_NEED_APPROVAL = @test -n "$(AUDIT_DATABASE_LEASE_APPROVAL)" || { echo 'set AUDIT_DATABASE_LEASE_APPROVAL=/path/to/approved-receipt.json' >&2; exit 2; }
|
||||
AUDIT_SYNTHETIC_SENDER_NEED_CONFIRM = @test -n "$(AUDIT_SYNTHETIC_SENDER_CONFIRM)" || { echo 'set AUDIT_SYNTHETIC_SENDER_CONFIRM to the confirm string printed by preflight' >&2; exit 2; }
|
||||
audit-synthetic-sender-test: ## Unit tests for the synthetic sender lane and T02 window (no network)
|
||||
PYTHONDONTWRITEBYTECODE=1 python3 -m unittest tests.test_audit_core_synthetic_sender_lane tests.test_t02_attended_window
|
||||
|
||||
audit-synthetic-sender-test: ## Unit tests for the synthetic sender lane (no network)
|
||||
PYTHONDONTWRITEBYTECODE=1 python3 -m unittest tests.test_audit_core_synthetic_sender_lane
|
||||
|
||||
audit-synthetic-sender-preflight: ## Read-only gate for the T02 synthetic sender (no registry values read)
|
||||
$(AUDIT_SYNTHETIC_SENDER_NEED_APPROVAL)
|
||||
$(AUDIT_SYNTHETIC_SENDER_LANE) preflight --approval "$(AUDIT_DATABASE_LEASE_APPROVAL)" \
|
||||
audit-synthetic-sender-preflight: ## Read-only T02 sender gate; cluster checks only (registry metadata needs --contained)
|
||||
$(AUDIT_T02_NEED_APPROVAL)
|
||||
python3 scripts/audit_core_synthetic_sender_lane.py preflight --approval "$(AUDIT_DATABASE_LEASE_APPROVAL)" \
|
||||
$(if $(AUDIT_T02_TOKEN_FILE),--bearer-file "$(AUDIT_T02_TOKEN_FILE)") $(AUDIT_SYNTHETIC_SENDER_PREFLIGHT_ARGS)
|
||||
|
||||
audit-synthetic-sender-project: ## Attended: add the one T02 identity, write bearer file, restart audit-core
|
||||
$(AUDIT_SYNTHETIC_SENDER_NEED_APPROVAL)
|
||||
audit-synthetic-sender-project: ## Projection step alone, inside its own contained login (prefer audit-t02-attended-window)
|
||||
$(AUDIT_T02_NEED_APPROVAL)
|
||||
$(AUDIT_SYNTHETIC_SENDER_NEED_CONFIRM)
|
||||
@test -n "$(AUDIT_T02_TOKEN_FILE)" || { echo 'set AUDIT_T02_TOKEN_FILE=/abs/path/outside/git (0700 dir)' >&2; exit 2; }
|
||||
$(AUDIT_SYNTHETIC_SENDER_LANE) project --approval "$(AUDIT_DATABASE_LEASE_APPROVAL)" \
|
||||
$(AUDIT_T02_NEED_BEARER)
|
||||
@test -n "$(AUDIT_T02_RESULT_FILE)" || { echo 'set AUDIT_T02_RESULT_FILE=/abs/new/file outside Git (the contained child must stay silent)' >&2; exit 2; }
|
||||
$(ATTENDED_EXEC) scripts/audit_core_synthetic_sender_lane.py project --contained --result-file "$(AUDIT_T02_RESULT_FILE)" \
|
||||
--approval "$(abspath $(AUDIT_DATABASE_LEASE_APPROVAL))" \
|
||||
--bearer-file "$(AUDIT_T02_TOKEN_FILE)" --confirm "$(AUDIT_SYNTHETIC_SENDER_CONFIRM)"
|
||||
|
||||
audit-synthetic-sender-cleanup: ## Attended, idempotent: remove the T02 identity after evidence, restart audit-core
|
||||
$(AUDIT_SYNTHETIC_SENDER_NEED_APPROVAL)
|
||||
audit-synthetic-sender-cleanup: ## Idempotent cleanup inside a contained login (evidence or abort reason required)
|
||||
$(AUDIT_T02_NEED_APPROVAL)
|
||||
$(AUDIT_SYNTHETIC_SENDER_NEED_CONFIRM)
|
||||
@test -n "$(AUDIT_T02_EVIDENCE)$(AUDIT_SYNTHETIC_SENDER_ABORT_REASON)" || { echo 'set AUDIT_T02_EVIDENCE=/path/to/exercise-result.json or AUDIT_SYNTHETIC_SENDER_ABORT_REASON=...' >&2; exit 2; }
|
||||
$(AUDIT_SYNTHETIC_SENDER_LANE) cleanup --approval "$(AUDIT_DATABASE_LEASE_APPROVAL)" \
|
||||
@test -n "$(AUDIT_T02_RESULT_FILE)" || { echo 'set AUDIT_T02_RESULT_FILE=/abs/new/file outside Git (the contained child must stay silent)' >&2; exit 2; }
|
||||
$(ATTENDED_EXEC) scripts/audit_core_synthetic_sender_lane.py cleanup --contained --result-file "$(AUDIT_T02_RESULT_FILE)" \
|
||||
--approval "$(abspath $(AUDIT_DATABASE_LEASE_APPROVAL))" \
|
||||
--confirm "$(AUDIT_SYNTHETIC_SENDER_CONFIRM)" \
|
||||
$(if $(AUDIT_T02_TOKEN_FILE),--bearer-file "$(AUDIT_T02_TOKEN_FILE)") \
|
||||
$(if $(AUDIT_T02_EVIDENCE),--exercise-evidence "$(AUDIT_T02_EVIDENCE)") \
|
||||
$(if $(AUDIT_T02_EVIDENCE),--exercise-evidence "$(abspath $(AUDIT_T02_EVIDENCE))") \
|
||||
$(if $(AUDIT_SYNTHETIC_SENDER_ABORT_REASON),--abort-reason "$(AUDIT_SYNTHETIC_SENDER_ABORT_REASON)")
|
||||
|
||||
railiance01-reboot-preflight: ## Read-only gate for the coordinated single-node reboot
|
||||
|
|
@ -589,7 +608,7 @@ wp0024-t02-driver-register: ## Register committed audit-core driver source witho
|
|||
@python3 scripts/wp0024-t02-driver-candidate.py register \
|
||||
--repo "$(abspath ../audit-core)" --driver "$(DRIVER)" --reviewer "$(REVIEWER)"
|
||||
|
||||
.PHONY: audit-synthetic-sender-test audit-synthetic-sender-preflight audit-synthetic-sender-project audit-synthetic-sender-cleanup audit-whitehat-e2-preflight audit-whitehat-e2-project audit-whitehat-e2-cleanup audit-database-lease-recovery-preflight audit-database-lease-approval-check audit-database-lease-recovery-exercise railiance01-reboot-preflight wp0024-owner-show wp0024-owner-verify wp0024-owner-status wp0024-t02-driver-show wp0024-t02-driver-status wp0024-t02-driver-register apps-pg-isolation-probe
|
||||
.PHONY: audit-t02-attended-window audit-synthetic-sender-test audit-synthetic-sender-preflight audit-synthetic-sender-project audit-synthetic-sender-cleanup audit-whitehat-e2-preflight audit-whitehat-e2-project audit-whitehat-e2-cleanup audit-database-lease-recovery-preflight audit-database-lease-approval-check audit-database-lease-recovery-exercise railiance01-reboot-preflight wp0024-owner-show wp0024-owner-verify wp0024-owner-status wp0024-t02-driver-show wp0024-t02-driver-status wp0024-t02-driver-register apps-pg-isolation-probe
|
||||
.PHONY: db-deploy db-status db-shell db-logs forgejo-db-deploy forgejo-db-status forgejo-db-shell apps-pg-deploy apps-pg-status apps-pg-shell apps-pg-logs net-kingdom-pg-inter-hub-networkpolicy-deploy pg-deploy pg-status pg-pgpool-check valkey-deploy valkey-status openbao-repo openbao-dry-run openbao-overlay-apply openbao-verify-login-overlay openbao-deploy openbao-status openbao-verify openbao-verify-post-unseal openbao-configure-initial openbao-configure-ssh openbao-verify-ssh openbao-verify-authenticated openbao-configure-external-secrets-issue-core openbao-configure-external-secrets-activity-core openbao-configure-external-secrets-forgejo openbao-validate-restore-evidence openbao-validate-emergency-evidence credential-grants-validate credential-change-applier-dry-run credential-change-applier-apply-plan credential-change-applier-apply credential-change-runbook credential-change-record-evidence credential-change-lifecycle-plan credential-change-lifecycle-event credential-change-import-inventory agent-high-risk-boundary-check openbao-credential-change-appliers-dry-run openbao-configure-credential-change-appliers openbao-token-grants-dry-run openbao-configure-token-grants openbao-verify-token-grants-dry-run openbao-verify-token-grants openbao-verify-token-grants-smoke credential-helper-dry-run credential-tests credential-exec-ops-warden-smoke argocd-bootstrap-dry-run argocd-bootstrap-deploy argocd-repo-apply argocd-status backup forgejo-backup forgejo-backup-dry-run forgejo-backup-status forgejo-package-prune forgejo-package-prune-dry-run reef-deployables apps-pg-evidence consumption-preflight help
|
||||
|
||||
##@ S3 service assurance (read-only)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -34,7 +34,8 @@
|
|||
"docs/railiance01-coordinated-reboot.md",
|
||||
"scripts/audit-core-recovery-preflight.py",
|
||||
"scripts/audit-core-database-lease-recovery.py",
|
||||
"scripts/audit_core_synthetic_sender_lane.py"
|
||||
"scripts/audit_core_synthetic_sender_lane.py",
|
||||
"scripts/t02-attended-window.py"
|
||||
],
|
||||
"assertions": [
|
||||
"The database-lease exercise uses only synthetic load and proves health 200, readiness and requests fail retryably with 503, then recover without changing the audit-core pod UID or restart count.",
|
||||
|
|
@ -57,7 +58,8 @@
|
|||
"docs/railiance01-coordinated-reboot.md",
|
||||
"scripts/audit-core-recovery-preflight.py",
|
||||
"scripts/audit-core-database-lease-recovery.py",
|
||||
"docs/audit-core-database-lease-approval.example.json"
|
||||
"docs/audit-core-database-lease-approval.example.json",
|
||||
"scripts/t02-attended-window.py"
|
||||
],
|
||||
"assertions": [
|
||||
"Lease selection aborts unless exactly one live handle exists below database/creds/audit-core-runtime and its issue time coheres with the ExternalSecret refresh.",
|
||||
|
|
@ -122,7 +124,8 @@
|
|||
"scripts/audit-core-database-lease-recovery.py",
|
||||
"docs/audit-core-database-lease-approval.example.json",
|
||||
"docs/openbao-reboot-snapshot-receipt.example.json",
|
||||
"scripts/audit_core_synthetic_sender_lane.py"
|
||||
"scripts/audit_core_synthetic_sender_lane.py",
|
||||
"scripts/t02-attended-window.py"
|
||||
],
|
||||
"assertions": [
|
||||
"T02 remains gated on a named window, abort operator, exact owner receipts and a revision-pinned synthetic-load driver; repository access alone cannot revoke a lease.",
|
||||
|
|
@ -139,11 +142,12 @@
|
|||
},
|
||||
"artifact_sha256": {
|
||||
"docs/audit-core-database-lease-approval.example.json": "87e57e3290074508c6517938acf8c7fc1a75d977b8ff630f247a4ff5d2ee45c6",
|
||||
"docs/audit-core-database-lease-recovery.md": "3544abde993505c1d84a5cfc882ce1619ca2e115b1cb74fdb1262a42984ee0be",
|
||||
"docs/audit-core-database-lease-recovery.md": "ae9e6b0d6cf60514cfabbd1fd0a9818d33dbbac9291a94539a4e049492c48f21",
|
||||
"docs/openbao-reboot-snapshot-receipt.example.json": "b95903a3e1aacb607eecc3f0440f08e62480d09da47bd7ab96c3eb8814f2eaee",
|
||||
"docs/railiance01-coordinated-reboot.md": "6fd6301f808449ad6519764d994b05aee41a96be879b917ee1d18d8a65d5f6fd",
|
||||
"scripts/audit-core-database-lease-recovery.py": "1de02f13dcd4e87e65de1c97ae14567f2b8e4a51aa0423e3fca78a224cf374b3",
|
||||
"scripts/audit-core-database-lease-recovery.py": "3066718d06ce29b98fa8ee513bd967e0f48c47db2d66b5083c591f5f1c2bddd6",
|
||||
"scripts/audit-core-recovery-preflight.py": "05e89ae9d0b9a636b67d9a8d0c203ac746c97208e32061cb23f1ca785eda8e5c",
|
||||
"scripts/audit_core_synthetic_sender_lane.py": "e71b3b3d56472a1cc91fc093d327a72116d89d07c1676f44bc24a8da36154bd4"
|
||||
"scripts/audit_core_synthetic_sender_lane.py": "608145f43b5adea110c4c73a2f567370fcfb603ef73ca8230775de9bf7687fa6",
|
||||
"scripts/t02-attended-window.py": "1faca370d14de9a55a3f04e55f6046105b5d3a606a4d79dadbe72c495ab75077"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import hashlib
|
|||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
|
@ -122,14 +121,41 @@ def safe_run(
|
|||
return completed
|
||||
|
||||
|
||||
# Operator OpenBao access is the SSH tunnel; the public endpoint was retracted
|
||||
# 2026-09-15 (RMASTER-WP-0020-T09).
|
||||
TUNNEL_ADDR = "http://127.0.0.1:18200"
|
||||
CONTAINMENT_MARKER = "WARDEN_ROUTING_CATALOG"
|
||||
TOKEN_ENV = ("BAO_TOKEN", "VAULT_TOKEN", "OPENBAO_TOKEN")
|
||||
|
||||
|
||||
def contained_bao_env(environ: dict[str, str] | None = None) -> dict[str, str]:
|
||||
"""Environment for ``bao`` inside the contained attended login only.
|
||||
|
||||
``scripts/openbao-attended-exec.py`` runs ``warden access
|
||||
openbao-platform-admin-login --exec`` with BAO_ADDR pinned to the tunnel,
|
||||
no token variable, and the Warden routing marker; ``bao`` then
|
||||
authenticates through Warden's isolated token helper. Anything else --
|
||||
a token file, a token variable, or another address -- is refused.
|
||||
"""
|
||||
env = dict(os.environ if environ is None else environ)
|
||||
if not env.get(CONTAINMENT_MARKER):
|
||||
raise ProcedureError(
|
||||
"live OpenBao access requires the contained attended login "
|
||||
"(scripts/openbao-attended-exec.py)"
|
||||
)
|
||||
if env.get("BAO_ADDR", TUNNEL_ADDR) != TUNNEL_ADDR:
|
||||
raise ProcedureError(f"BAO_ADDR must be the operator tunnel {TUNNEL_ADDR}")
|
||||
if any(env.get(name) for name in TOKEN_ENV):
|
||||
raise ProcedureError("token variables must be unset inside the contained login")
|
||||
for name in TOKEN_ENV:
|
||||
env.pop(name, None)
|
||||
env["BAO_ADDR"] = TUNNEL_ADDR
|
||||
return env
|
||||
|
||||
|
||||
class Authority:
|
||||
def __init__(self, token_file: Path) -> None:
|
||||
if not token_file.is_file() or stat.S_IMODE(token_file.stat().st_mode) != 0o600:
|
||||
raise ProcedureError("OpenBao token file must exist with mode 0600")
|
||||
token = token_file.read_text(encoding="utf-8").splitlines()[0].strip()
|
||||
if not token:
|
||||
raise ProcedureError("OpenBao token file is empty")
|
||||
self.env = dict(os.environ, BAO_ADDR="https://bao.coulomb.social", BAO_TOKEN=token)
|
||||
def __init__(self) -> None:
|
||||
self.env = contained_bao_env()
|
||||
|
||||
def bao(self, args: list[str], *, label: str) -> str:
|
||||
return safe_run(["bao", *args], label=label, env=self.env).stdout.strip()
|
||||
|
|
@ -333,7 +359,7 @@ def exercise(args: argparse.Namespace) -> dict[str, Any]:
|
|||
if args.confirm != CONFIRM:
|
||||
raise ProcedureError(f"live exercise requires --confirm {CONFIRM}")
|
||||
remote = PREFLIGHT.Remote(args.remote)
|
||||
authority = Authority(args.token_file)
|
||||
authority = Authority()
|
||||
driver = LoadDriver(
|
||||
args.load_driver,
|
||||
approval["synthetic_load"]["contract_id"],
|
||||
|
|
@ -450,18 +476,30 @@ def exercise(args: argparse.Namespace) -> dict[str, Any]:
|
|||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
|
||||
def emit_result(result: dict, result_file: "Path | None") -> None:
|
||||
"""Under the contained login the child must stay silent: write, never print."""
|
||||
if result_file is None:
|
||||
print(json.dumps(result, indent=2, sort_keys=True))
|
||||
return
|
||||
fd = os.open(result_file, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
||||
json.dump(result, handle, indent=2, sort_keys=True)
|
||||
handle.write("\n")
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("command", choices=["validate-approval", "exercise"])
|
||||
parser.add_argument("--approval", type=Path, required=True)
|
||||
parser.add_argument("--remote", default="railiance01")
|
||||
parser.add_argument(
|
||||
"--token-file", type=Path,
|
||||
default=Path.home() / ".local/openbao/platform-admin.token",
|
||||
"--contained", action="store_true",
|
||||
help="required for exercise: run inside the contained attended OpenBao login",
|
||||
)
|
||||
parser.add_argument("--load-driver", type=Path)
|
||||
parser.add_argument("--confirm")
|
||||
args = parser.parse_args()
|
||||
parser.add_argument("--result-file", type=Path, help="write the JSON result here (0600, new) and print nothing")
|
||||
args = parser.parse_args(argv)
|
||||
try:
|
||||
if args.command == "validate-approval":
|
||||
approval = load_approval(args.approval, datetime.now(UTC), require_open_window=False)
|
||||
|
|
@ -472,13 +510,18 @@ def main() -> int:
|
|||
"secret_values_observed": False,
|
||||
}
|
||||
else:
|
||||
if not args.contained:
|
||||
raise ProcedureError("live exercise requires --contained (scripts/openbao-attended-exec.py)")
|
||||
if args.load_driver is None:
|
||||
raise ProcedureError("live exercise requires --load-driver")
|
||||
result = exercise(args)
|
||||
except (OSError, IndexError, ProcedureError, PREFLIGHT.PreflightError) as exc:
|
||||
print(f"database lease recovery failed: {exc}", file=sys.stderr)
|
||||
if args.result_file is None:
|
||||
print(f"database lease recovery failed: {exc}", file=sys.stderr)
|
||||
else:
|
||||
emit_result({"status": "failed", "error": str(exc), "secret_values_observed": False}, args.result_file)
|
||||
return 1
|
||||
print(json.dumps(result, indent=2, sort_keys=True))
|
||||
emit_result(result, args.result_file)
|
||||
return 0
|
||||
|
||||
|
||||
|
|
|
|||
54
scripts/audit_core_synthetic_sender_lane.py
Normal file → Executable file
54
scripts/audit_core_synthetic_sender_lane.py
Normal file → Executable file
|
|
@ -262,6 +262,24 @@ def settled_state(remote: Any, *, previous_uid: str | None, timeout: float = 90)
|
|||
time.sleep(3)
|
||||
|
||||
|
||||
class ContainedOperator(CREDS.Operator):
|
||||
"""The -02 registry/kubectl helpers, authenticated only by the contained login.
|
||||
|
||||
No token file and no token variable: ``bao`` runs with the tunnel address
|
||||
and Warden's isolated token helper (see ``LEASE.contained_bao_env``).
|
||||
"""
|
||||
|
||||
def __init__(self, remote: str) -> None: # noqa: D107 - deliberately no super().__init__
|
||||
self.remote = remote
|
||||
self.bao_env = LEASE.contained_bao_env()
|
||||
|
||||
|
||||
def contained_operator(args: argparse.Namespace) -> Any:
|
||||
if not args.contained:
|
||||
raise LaneError(f"{args.command} requires --contained (scripts/openbao-attended-exec.py)")
|
||||
return ContainedOperator(args.remote)
|
||||
|
||||
|
||||
def registry_metadata(operator: Any) -> dict[str, Any]:
|
||||
raw = operator.bao(["kv", "metadata", "get", "-format=json", REGISTRY_PATH], label="read sender registry metadata").stdout
|
||||
try:
|
||||
|
|
@ -303,9 +321,16 @@ def preflight(args: argparse.Namespace) -> dict[str, Any]:
|
|||
if args.offline:
|
||||
result["live_checks"] = "skipped"
|
||||
return result
|
||||
if args.inspect_registry and not args.contained:
|
||||
raise LaneError("--inspect-registry requires --contained")
|
||||
remote = PREFLIGHT.Remote(args.remote)
|
||||
result["audit_core"] = cluster_state(remote)
|
||||
operator = CREDS.Operator(args.remote, args.token_file)
|
||||
result["automated_checks_passed"] = result["audit_core"]["automated_checks_passed"]
|
||||
if not args.contained:
|
||||
# Outside the attended login there is no OpenBao credential at all.
|
||||
result["sender_registry_metadata"] = "skipped-not-contained"
|
||||
return result
|
||||
operator = contained_operator(args)
|
||||
result["sender_registry_metadata"] = registry_metadata(operator)
|
||||
if args.inspect_registry:
|
||||
# Attended only: values are held in memory and never emitted.
|
||||
|
|
@ -316,7 +341,6 @@ def preflight(args: argparse.Namespace) -> dict[str, Any]:
|
|||
)
|
||||
result["registry_conflicts"] = registry_conflicts(registry, spec)
|
||||
registry.clear()
|
||||
result["automated_checks_passed"] = result["audit_core"]["automated_checks_passed"]
|
||||
return result
|
||||
|
||||
|
||||
|
|
@ -334,6 +358,7 @@ def project(args: argparse.Namespace) -> dict[str, Any]:
|
|||
approval, spec = load_receipt(args.approval, now, require_open_window=True)
|
||||
if args.confirm != confirm_token(approval):
|
||||
raise LaneError(f"project requires --confirm {confirm_token(approval)}")
|
||||
operator = contained_operator(args)
|
||||
assert_projection_time(approval, spec, now)
|
||||
if args.bearer_file is None:
|
||||
raise LaneError("project requires --bearer-file outside Git")
|
||||
|
|
@ -343,7 +368,6 @@ def project(args: argparse.Namespace) -> dict[str, Any]:
|
|||
if not before["automated_checks_passed"]:
|
||||
raise LaneError("audit-core recovery preflight is not green; refusing to project")
|
||||
LEASE.attest_chain_state(remote)
|
||||
operator = CREDS.Operator(args.remote, args.token_file)
|
||||
registry = operator.registry()
|
||||
conflicts = registry_conflicts(registry, spec)
|
||||
if conflicts:
|
||||
|
|
@ -384,8 +408,8 @@ def cleanup(args: argparse.Namespace) -> dict[str, Any]:
|
|||
approval, spec = load_receipt(args.approval, utc_now(), require_open_window=False)
|
||||
if args.confirm != confirm_token(approval):
|
||||
raise LaneError(f"cleanup requires --confirm {confirm_token(approval)}")
|
||||
operator = contained_operator(args)
|
||||
gate = assert_evidence(args.exercise_evidence, args.abort_reason, approval)
|
||||
operator = CREDS.Operator(args.remote, args.token_file)
|
||||
removed = _cleanup_registry(operator, spec)
|
||||
remaining = any(
|
||||
isinstance(entry, dict) and entry.get("name") == spec["name"] for entry in operator.registry()
|
||||
|
|
@ -412,26 +436,42 @@ def cleanup(args: argparse.Namespace) -> dict[str, Any]:
|
|||
}
|
||||
|
||||
|
||||
|
||||
def emit_result(result: dict, result_file: "Path | None") -> None:
|
||||
"""Under the contained login the child must stay silent: write, never print."""
|
||||
if result_file is None:
|
||||
print(json.dumps(result, indent=2, sort_keys=True))
|
||||
return
|
||||
fd = os.open(result_file, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
||||
json.dump(result, handle, indent=2, sort_keys=True)
|
||||
handle.write("\n")
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||
parser.add_argument("command", choices=["preflight", "project", "cleanup"])
|
||||
parser.add_argument("--approval", type=Path, required=True, help="approved T02 window receipt")
|
||||
parser.add_argument("--bearer-file", type=Path, help="absolute path outside Git (mode-0700 directory)")
|
||||
parser.add_argument("--remote", default="railiance01")
|
||||
parser.add_argument("--token-file", type=Path, default=Path.home() / ".local/openbao/platform-admin.token")
|
||||
parser.add_argument("--contained", action="store_true",
|
||||
help="run inside the contained attended OpenBao login (required for project/cleanup)")
|
||||
parser.add_argument("--confirm")
|
||||
parser.add_argument("--offline", action="store_true", help="preflight: validate receipt/path only")
|
||||
parser.add_argument("--inspect-registry", action="store_true", help="preflight: attended registry conflict check")
|
||||
parser.add_argument("--exercise-evidence", type=Path, help="cleanup: recorded T02 exercise result JSON")
|
||||
parser.add_argument("--abort-reason", help="cleanup: abort path when no exercise evidence exists")
|
||||
parser.add_argument("--result-file", type=Path, help="write the JSON result here (0600, new) and print nothing")
|
||||
args = parser.parse_args(argv)
|
||||
try:
|
||||
handler = {"preflight": preflight, "project": project, "cleanup": cleanup}[args.command]
|
||||
result = handler(args)
|
||||
except ERRORS as exc:
|
||||
print(f"synthetic sender lane failed: {exc}", file=sys.stderr)
|
||||
if args.result_file is None:
|
||||
print(f"synthetic sender lane failed: {exc}", file=sys.stderr)
|
||||
else:
|
||||
emit_result({"status": "failed", "error": str(exc), "secret_values_observed": False}, args.result_file)
|
||||
return 1
|
||||
print(json.dumps(result, indent=2, sort_keys=True))
|
||||
emit_result(result, args.result_file)
|
||||
return 0
|
||||
|
||||
|
||||
|
|
|
|||
274
scripts/t02-attended-window.py
Executable file
274
scripts/t02-attended-window.py
Executable file
|
|
@ -0,0 +1,274 @@
|
|||
#!/usr/bin/env python3
|
||||
"""One contained attended window for RAILIANCE-WP-0024-T02.
|
||||
|
||||
Run only as the single child of the contained attended login, so the whole
|
||||
window costs one MFA login:
|
||||
|
||||
python3 scripts/openbao-attended-exec.py -- scripts/t02-attended-window.py ...
|
||||
|
||||
Order (each step a reviewed command; this file adds no OpenBao call of its own):
|
||||
|
||||
1. lane ``project`` -- one temporary sender, bearer to a 0600 file, audit-core restart
|
||||
2. port-forward -- ``ssh -L`` to ``kubectl -n audit-core port-forward`` on the remote
|
||||
3. harness ``exercise`` with the registered load driver
|
||||
4. evidence -- exercise JSON written 0600 outside Git
|
||||
5. lane ``cleanup`` -- with that evidence (restart #2)
|
||||
|
||||
Any failure after projection was attempted runs ``cleanup --abort-reason``.
|
||||
The port-forward is always stopped. Children's stdout is captured and only
|
||||
their value-safe JSON is re-emitted; nothing here reads the bearer.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
LANE_SCRIPT = ROOT / "scripts" / "audit_core_synthetic_sender_lane.py"
|
||||
HARNESS_SCRIPT = ROOT / "scripts" / "audit-core-database-lease-recovery.py"
|
||||
HARNESS_CONFIRM = "RAILIANCE-WP-0024-T02:attended"
|
||||
REMOTE_PF_TIMEOUT_SECONDS = 15 * 60
|
||||
CONTAINER_PORT = 8080
|
||||
|
||||
|
||||
class WindowError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def _load_lane() -> Any:
|
||||
spec = importlib.util.spec_from_file_location("audit_core_synthetic_sender_lane", LANE_SCRIPT)
|
||||
if not spec or not spec.loader:
|
||||
raise WindowError("cannot load synthetic sender lane")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
LANE = _load_lane()
|
||||
|
||||
|
||||
def stamp(value: datetime) -> str:
|
||||
return value.astimezone(UTC).replace(microsecond=0).isoformat().replace("+00:00", "Z")
|
||||
|
||||
|
||||
def run_json(command: list[str], *, label: str, env: dict[str, str] | None = None) -> dict[str, Any]:
|
||||
"""Run a value-safe reviewed child; capture both streams (warden fails on any output)."""
|
||||
completed = subprocess.run(
|
||||
command, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env, check=False
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
# Reviewed lane/harness errors are fixed value-safe strings; keep one bounded line.
|
||||
reason = (completed.stderr.strip().splitlines() or [""])[-1][:200]
|
||||
raise WindowError(f"{label} failed (exit {completed.returncode}): {reason}")
|
||||
try:
|
||||
payload = json.loads(completed.stdout)
|
||||
except json.JSONDecodeError as exc:
|
||||
raise WindowError(f"{label} returned invalid JSON") from exc
|
||||
if not isinstance(payload, dict) or payload.get("secret_values_observed") is not False:
|
||||
raise WindowError(f"{label} did not attest value safety")
|
||||
return payload
|
||||
|
||||
|
||||
def private_dir(path: Path) -> Path:
|
||||
"""An operator-owned 0700 directory outside every Git work tree."""
|
||||
path = path.resolve()
|
||||
LANE.check_bearer_path(path / "probe", must_exist=False)
|
||||
return path
|
||||
|
||||
|
||||
def write_private_json(path: Path, document: dict[str, Any]) -> None:
|
||||
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
||||
json.dump(document, handle, indent=2, sort_keys=True)
|
||||
handle.write("\n")
|
||||
|
||||
|
||||
def slug(value: str) -> str:
|
||||
cleaned = re.sub(r"[^a-z0-9-]+", "-", value.lower()).strip("-")
|
||||
if not cleaned:
|
||||
raise WindowError("approval id cannot form a fixture id")
|
||||
return cleaned[:48]
|
||||
|
||||
|
||||
def driver_env(spec: dict[str, Any], approval: dict[str, Any], bearer: Path, port: int) -> dict[str, str]:
|
||||
env = dict(os.environ)
|
||||
env.update({
|
||||
"AUDIT_T02_BASE_URL": f"http://127.0.0.1:{port}",
|
||||
"AUDIT_T02_TOKEN_FILE": str(bearer),
|
||||
"AUDIT_T02_SOURCE": spec["source"],
|
||||
"AUDIT_T02_TENANT": spec["tenant"],
|
||||
"AUDIT_T02_FIXTURE_ID": f"t02-{slug(approval['approval_id'])}",
|
||||
"AUDIT_T02_OCCURRED_AT": approval["window"]["start"],
|
||||
})
|
||||
return env
|
||||
|
||||
|
||||
class PortForward:
|
||||
"""``ssh -L`` to a remote, time-bounded ``kubectl port-forward`` on loopback.
|
||||
|
||||
The lane and harness reach the cluster only through ``ssh <remote> kubectl``;
|
||||
the port-forward uses the same path so the window needs no local kubeconfig.
|
||||
"""
|
||||
|
||||
def __init__(self, remote: str, port: int) -> None:
|
||||
self.remote = remote
|
||||
self.port = port
|
||||
self.process: subprocess.Popen[bytes] | None = None
|
||||
|
||||
def command(self) -> list[str]:
|
||||
remote_cmd = (
|
||||
f"timeout {REMOTE_PF_TIMEOUT_SECONDS} kubectl -n audit-core port-forward "
|
||||
f"--address 127.0.0.1 deploy/audit-core {self.port}:{CONTAINER_PORT}"
|
||||
)
|
||||
return [
|
||||
"ssh", "-o", "BatchMode=yes", "-o", "ExitOnForwardFailure=yes",
|
||||
"-L", f"127.0.0.1:{self.port}:127.0.0.1:{self.port}",
|
||||
self.remote, remote_cmd,
|
||||
]
|
||||
|
||||
def start(self, *, timeout: float = 30) -> None:
|
||||
self.process = subprocess.Popen(
|
||||
self.command(), stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
|
||||
)
|
||||
deadline = time.monotonic() + timeout
|
||||
while time.monotonic() < deadline:
|
||||
if self.process.poll() is not None:
|
||||
raise WindowError("audit-core port-forward exited early")
|
||||
# A local ssh listener accepts before the remote forward is ready, so
|
||||
# require a real /healthz answer through the whole chain.
|
||||
try:
|
||||
with urllib.request.urlopen(f"http://127.0.0.1:{self.port}/healthz", timeout=2) as response:
|
||||
if response.status == 200:
|
||||
return
|
||||
except (OSError, urllib.error.URLError):
|
||||
pass
|
||||
time.sleep(1)
|
||||
raise WindowError("audit-core port-forward did not become reachable")
|
||||
|
||||
def stop(self) -> None:
|
||||
if self.process is None or self.process.poll() is not None:
|
||||
return
|
||||
self.process.terminate()
|
||||
try:
|
||||
self.process.wait(timeout=10)
|
||||
except subprocess.TimeoutExpired:
|
||||
self.process.kill()
|
||||
self.process.wait(timeout=5)
|
||||
|
||||
|
||||
def window(args: argparse.Namespace, *, runner: Callable[..., dict[str, Any]] = run_json,
|
||||
forward_factory: Callable[[str, int], Any] = PortForward) -> dict[str, Any]:
|
||||
if not args.bearer_file.is_absolute() or not args.evidence_dir.is_absolute():
|
||||
raise WindowError("--bearer-file and --evidence-dir must be absolute")
|
||||
LANE.LEASE.contained_bao_env() # refuse before any step outside the contained login
|
||||
approval, spec = LANE.load_receipt(args.approval, datetime.now(UTC), require_open_window=True)
|
||||
if args.confirm != LANE.confirm_token(approval):
|
||||
raise WindowError(f"window requires --confirm {LANE.confirm_token(approval)}")
|
||||
evidence_dir = private_dir(args.evidence_dir)
|
||||
base = f"{slug(approval['approval_id'])}"
|
||||
exercise_path = evidence_dir / f"{base}-exercise.json"
|
||||
summary_path = evidence_dir / f"{base}-window.json"
|
||||
for path in (exercise_path, summary_path):
|
||||
if path.exists():
|
||||
raise WindowError(f"evidence file already exists: {path.name}")
|
||||
lane = [sys.executable, str(LANE_SCRIPT)]
|
||||
common = ["--approval", str(args.approval), "--remote", args.remote, "--contained",
|
||||
"--confirm", args.confirm]
|
||||
summary: dict[str, Any] = {
|
||||
"procedure": "t02-attended-window",
|
||||
"approval_id": approval["approval_id"],
|
||||
"identity": LANE.identity_view(spec),
|
||||
"started_at": stamp(datetime.now(UTC)),
|
||||
"steps": [],
|
||||
"secret_values_observed": False,
|
||||
}
|
||||
projection_attempted = False
|
||||
forward = forward_factory(args.remote, args.port)
|
||||
try:
|
||||
projection_attempted = True
|
||||
summary["project"] = runner(
|
||||
[*lane, "project", *common, "--bearer-file", str(args.bearer_file)], label="lane project")
|
||||
summary["steps"].append("project")
|
||||
forward.start()
|
||||
summary["steps"].append("port-forward")
|
||||
exercise = runner(
|
||||
[sys.executable, str(HARNESS_SCRIPT), "exercise", "--approval", str(args.approval),
|
||||
"--remote", args.remote, "--contained", "--load-driver", str(args.load_driver),
|
||||
"--confirm", HARNESS_CONFIRM],
|
||||
label="T02 exercise",
|
||||
env=driver_env(spec, approval, args.bearer_file, args.port),
|
||||
)
|
||||
summary["steps"].append("exercise")
|
||||
write_private_json(exercise_path, exercise)
|
||||
summary["exercise_evidence"] = exercise_path.name
|
||||
summary["steps"].append("evidence")
|
||||
forward.stop()
|
||||
summary["cleanup"] = runner(
|
||||
[*lane, "cleanup", *common, "--bearer-file", str(args.bearer_file),
|
||||
"--exercise-evidence", str(exercise_path)], label="lane cleanup")
|
||||
summary["steps"].append("cleanup")
|
||||
summary["outcome"] = "completed"
|
||||
except BaseException as exc:
|
||||
forward.stop()
|
||||
summary["outcome"] = "aborted"
|
||||
summary["abort_reason"] = str(exc)[:200] if isinstance(exc, WindowError) else type(exc).__name__
|
||||
if projection_attempted and "cleanup" not in summary["steps"]:
|
||||
try:
|
||||
summary["cleanup"] = runner(
|
||||
[*lane, "cleanup", *common, "--bearer-file", str(args.bearer_file),
|
||||
"--abort-reason", f"t02 window aborted: {summary['abort_reason']}"],
|
||||
label="lane abort cleanup")
|
||||
summary["steps"].append("abort-cleanup")
|
||||
except BaseException as cleanup_exc: # keep the first failure; flag cleanup loudly
|
||||
summary["abort_cleanup_failed"] = str(cleanup_exc)[:200]
|
||||
summary["finished_at"] = stamp(datetime.now(UTC))
|
||||
write_private_json(summary_path, summary)
|
||||
raise
|
||||
finally:
|
||||
forward.stop()
|
||||
summary["finished_at"] = stamp(datetime.now(UTC))
|
||||
write_private_json(summary_path, summary)
|
||||
return summary
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||
parser.add_argument("--approval", type=Path, required=True)
|
||||
parser.add_argument("--load-driver", type=Path, required=True)
|
||||
parser.add_argument("--bearer-file", type=Path, required=True)
|
||||
parser.add_argument("--evidence-dir", type=Path, required=True)
|
||||
parser.add_argument("--confirm", required=True)
|
||||
parser.add_argument("--remote", default="railiance01")
|
||||
parser.add_argument("--port", type=int, default=18081)
|
||||
args = parser.parse_args(argv)
|
||||
# The contained login discards child output and fails closed on any byte,
|
||||
# so this script never prints; its outcome lives only in the evidence dir.
|
||||
try:
|
||||
window(args)
|
||||
except (OSError, KeyError, WindowError, LANE.LaneError, LANE.LEASE.ProcedureError) as exc:
|
||||
try:
|
||||
LANE.emit_result(
|
||||
{"status": "failed", "error": str(exc), "secret_values_observed": False},
|
||||
args.evidence_dir / f"window-error-{int(time.time())}.json",
|
||||
)
|
||||
except OSError:
|
||||
pass
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -200,15 +200,15 @@ class SyntheticSenderLaneTests(unittest.TestCase):
|
|||
bearer = self.dir / "t02.bearer"
|
||||
operator = FakeOperator([{"name": "user-engine", "tokens": ["p"], "sources": ["user-engine"], "tenants": ["*"]}])
|
||||
args = argparse.Namespace(
|
||||
approval=approval_path, confirm=lane.confirm_token(approval), bearer_file=bearer,
|
||||
remote="railiance01", token_file=self.dir / "unused",
|
||||
command="project", approval=approval_path, confirm=lane.confirm_token(approval),
|
||||
bearer_file=bearer, remote="railiance01", contained=True,
|
||||
)
|
||||
green = {"automated_checks_passed": True, "ready": "1/1", "pod_uid": "uid-1"}
|
||||
restart = mock.Mock(side_effect=[lane.CREDS.ProcedureError("rollout failed"), None])
|
||||
with mock.patch.object(lane.PREFLIGHT, "Remote"), \
|
||||
mock.patch.object(lane, "cluster_state", return_value=green), \
|
||||
mock.patch.object(lane.LEASE, "attest_chain_state"), \
|
||||
mock.patch.object(lane.CREDS, "Operator", return_value=operator), \
|
||||
mock.patch.object(lane, "contained_operator", return_value=operator), \
|
||||
mock.patch.object(lane.CREDS, "force_sender_sync_and_restart", restart):
|
||||
with self.assertRaises(lane.CREDS.ProcedureError):
|
||||
lane.project(args)
|
||||
|
|
@ -226,6 +226,45 @@ class SyntheticSenderLaneTests(unittest.TestCase):
|
|||
self.assertIn("--confirm audit-core-synthetic-sender:t02-window-1:attended", stderr.getvalue())
|
||||
self.assertEqual("", stdout.getvalue())
|
||||
|
||||
def test_live_commands_refuse_outside_contained_login(self) -> None:
|
||||
approval_path = self.write_receipt(receipt(self.now))
|
||||
approval = json.loads(approval_path.read_text())
|
||||
base = dict(approval=approval_path, confirm=lane.confirm_token(approval),
|
||||
bearer_file=self.dir / "t02.bearer", remote="railiance01",
|
||||
exercise_evidence=None, abort_reason="test")
|
||||
for command in ("project", "cleanup"):
|
||||
with self.subTest(command=command):
|
||||
with self.assertRaisesRegex(lane.LaneError, "--contained"):
|
||||
getattr(lane, command)(argparse.Namespace(command=command, contained=False, **base))
|
||||
contained = {"WARDEN_ROUTING_CATALOG": "/catalog.yaml", "BAO_ADDR": lane.LEASE.TUNNEL_ADDR}
|
||||
refusals = [
|
||||
{},
|
||||
{**contained, "WARDEN_ROUTING_CATALOG": ""},
|
||||
{**contained, "BAO_ADDR": "https://bao.coulomb.social"},
|
||||
{**contained, "BAO_TOKEN": "x"},
|
||||
{**contained, "VAULT_TOKEN": "x"},
|
||||
]
|
||||
for env in refusals:
|
||||
with self.subTest(env=sorted(env)):
|
||||
with mock.patch.dict(os.environ, env, clear=True):
|
||||
with self.assertRaises(lane.LEASE.ProcedureError):
|
||||
lane.ContainedOperator("railiance01")
|
||||
with mock.patch.dict(os.environ, contained, clear=True):
|
||||
operator = lane.ContainedOperator("railiance01")
|
||||
self.assertEqual(lane.LEASE.TUNNEL_ADDR, operator.bao_env["BAO_ADDR"])
|
||||
self.assertNotIn("BAO_TOKEN", operator.bao_env)
|
||||
|
||||
def test_harness_exercise_requires_contained(self) -> None:
|
||||
stderr = io.StringIO()
|
||||
with redirect_stderr(stderr):
|
||||
code = lane.LEASE.main(["exercise", "--approval", str(self.write_receipt(receipt(self.now))),
|
||||
"--load-driver", "/bin/true", "--confirm", lane.LEASE.CONFIRM])
|
||||
self.assertEqual(1, code)
|
||||
self.assertIn("--contained", stderr.getvalue())
|
||||
with mock.patch.dict(os.environ, {"BAO_ADDR": lane.LEASE.TUNNEL_ADDR}, clear=True):
|
||||
with self.assertRaises(lane.LEASE.ProcedureError):
|
||||
lane.LEASE.Authority()
|
||||
|
||||
def test_offline_preflight_is_value_safe(self) -> None:
|
||||
stdout = io.StringIO()
|
||||
with redirect_stdout(stdout):
|
||||
|
|
|
|||
192
tests/test_t02_attended_window.py
Normal file
192
tests/test_t02_attended_window.py
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SPEC = importlib.util.spec_from_file_location("t02_attended_window", ROOT / "scripts" / "t02-attended-window.py")
|
||||
assert SPEC and SPEC.loader
|
||||
window = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(window)
|
||||
|
||||
TEST_SPEC = importlib.util.spec_from_file_location(
|
||||
"test_audit_core_synthetic_sender_lane", ROOT / "tests" / "test_audit_core_synthetic_sender_lane.py")
|
||||
assert TEST_SPEC and TEST_SPEC.loader
|
||||
lane_tests = importlib.util.module_from_spec(TEST_SPEC)
|
||||
TEST_SPEC.loader.exec_module(lane_tests)
|
||||
|
||||
CONTAINED = {"WARDEN_ROUTING_CATALOG": "/catalog.yaml", "BAO_ADDR": "http://127.0.0.1:18200", "PATH": os.environ.get("PATH", "")}
|
||||
|
||||
|
||||
class FakeForward:
|
||||
def __init__(self, log: list[str], fail: bool = False) -> None:
|
||||
self.log, self.fail = log, fail
|
||||
|
||||
def start(self) -> None:
|
||||
self.log.append("port-forward-start")
|
||||
if self.fail:
|
||||
raise window.WindowError("audit-core port-forward exited early")
|
||||
|
||||
def stop(self) -> None:
|
||||
self.log.append("port-forward-stop")
|
||||
|
||||
|
||||
class Runner:
|
||||
"""Records the step of each reviewed child; optionally fails one step."""
|
||||
|
||||
def __init__(self, log: list[str], fail_on: str | None = None) -> None:
|
||||
self.log, self.fail_on, self.envs = log, fail_on, {}
|
||||
|
||||
def __call__(self, command: list[str], *, label: str, env: dict | None = None) -> dict:
|
||||
step = "exercise" if "exercise" in command else command[2]
|
||||
if step == "cleanup" and "--abort-reason" in command:
|
||||
step = "abort-cleanup"
|
||||
self.log.append(step)
|
||||
self.envs[step] = env
|
||||
assert "--contained" in command
|
||||
if step == self.fail_on:
|
||||
raise window.WindowError(f"{label} failed (exit 1)")
|
||||
if step == "exercise":
|
||||
return {"procedure": window.LANE.LEASE.PROCEDURE, "approval_id": "t02-window-1",
|
||||
"completed_at": "2026-10-14T00:00:00Z", "secret_values_observed": False}
|
||||
return {"secret_values_observed": False}
|
||||
|
||||
|
||||
class AttendedWindowTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.temp = tempfile.TemporaryDirectory()
|
||||
self.dir = Path(self.temp.name)
|
||||
os.chmod(self.dir, 0o700)
|
||||
self.now = datetime.now(UTC)
|
||||
document = lane_tests.receipt(self.now)
|
||||
self.approval = self.dir / "receipt.json"
|
||||
self.approval.write_text(json.dumps(document), encoding="utf-8")
|
||||
self.evidence = self.dir / "evidence"
|
||||
self.evidence.mkdir(mode=0o700)
|
||||
self.args = argparse.Namespace(
|
||||
approval=self.approval, load_driver=Path("/opt/driver"), bearer_file=self.dir / "t02.bearer",
|
||||
evidence_dir=self.evidence, confirm=window.LANE.confirm_token(document),
|
||||
remote="railiance01", port=18081,
|
||||
)
|
||||
|
||||
def tearDown(self) -> None:
|
||||
self.temp.cleanup()
|
||||
|
||||
def run_window(self, runner: Runner, forward: FakeForward) -> dict:
|
||||
with mock.patch.dict(os.environ, CONTAINED, clear=True):
|
||||
return window.window(self.args, runner=runner, forward_factory=lambda remote, port: forward)
|
||||
|
||||
def test_main_never_prints_and_records_failure_in_evidence_dir(self) -> None:
|
||||
import contextlib
|
||||
import io
|
||||
|
||||
out, err = io.StringIO(), io.StringIO()
|
||||
with tempfile.TemporaryDirectory() as tmp, contextlib.redirect_stdout(out), contextlib.redirect_stderr(err):
|
||||
evidence = Path(tmp)
|
||||
with mock.patch.object(window, "window", side_effect=window.WindowError("project failed (exit 1): x")):
|
||||
code = window.main(
|
||||
["--approval", "/a", "--load-driver", "/d", "--bearer-file", "/b",
|
||||
"--evidence-dir", str(evidence), "--confirm", "c"]
|
||||
)
|
||||
records = list(evidence.glob("window-error-*.json"))
|
||||
self.assertEqual(1, code)
|
||||
self.assertEqual(1, len(records))
|
||||
self.assertEqual(0o600, records[0].stat().st_mode & 0o777)
|
||||
self.assertEqual("failed", json.loads(records[0].read_text())["status"])
|
||||
with mock.patch.object(window, "window", return_value={"status": "ok"}):
|
||||
self.assertEqual(0, window.main(
|
||||
["--approval", "/a", "--load-driver", "/d", "--bearer-file", "/b",
|
||||
"--evidence-dir", str(evidence), "--confirm", "c"]
|
||||
))
|
||||
self.assertEqual("", out.getvalue())
|
||||
self.assertEqual("", err.getvalue())
|
||||
|
||||
def test_result_file_is_new_private_and_suppresses_stdout(self) -> None:
|
||||
import contextlib
|
||||
import io
|
||||
|
||||
out = io.StringIO()
|
||||
with tempfile.TemporaryDirectory() as tmp, contextlib.redirect_stdout(out):
|
||||
target = Path(tmp) / "r.json"
|
||||
window.LANE.emit_result({"secret_values_observed": False}, target)
|
||||
self.assertEqual(0o600, target.stat().st_mode & 0o777)
|
||||
with self.assertRaises(FileExistsError):
|
||||
window.LANE.emit_result({"secret_values_observed": False}, target)
|
||||
self.assertEqual("", out.getvalue())
|
||||
|
||||
def test_happy_path_order_and_evidence(self) -> None:
|
||||
log: list[str] = []
|
||||
runner = Runner(log)
|
||||
summary = self.run_window(runner, FakeForward(log))
|
||||
self.assertEqual(
|
||||
["project", "port-forward-start", "exercise", "port-forward-stop", "cleanup", "port-forward-stop"], log)
|
||||
self.assertEqual("completed", summary["outcome"])
|
||||
exercise = self.evidence / "t02-window-1-exercise.json"
|
||||
self.assertEqual(0o600, exercise.stat().st_mode & 0o777)
|
||||
self.assertTrue((self.evidence / "t02-window-1-window.json").exists())
|
||||
env = runner.envs["exercise"]
|
||||
self.assertEqual("http://127.0.0.1:18081", env["AUDIT_T02_BASE_URL"])
|
||||
self.assertEqual(str(self.args.bearer_file), env["AUDIT_T02_TOKEN_FILE"])
|
||||
self.assertEqual("railiance-t02-synthetic", env["AUDIT_T02_SOURCE"])
|
||||
self.assertEqual("tenant:trial:t02-synthetic-1", env["AUDIT_T02_TENANT"])
|
||||
|
||||
def test_failures_after_projection_run_abort_cleanup(self) -> None:
|
||||
for fail_on in ("project", "exercise", "cleanup"):
|
||||
with self.subTest(fail_on=fail_on):
|
||||
for path in self.evidence.iterdir():
|
||||
path.unlink()
|
||||
log: list[str] = []
|
||||
with self.assertRaises(window.WindowError):
|
||||
self.run_window(Runner(log, fail_on), FakeForward(log))
|
||||
self.assertEqual("abort-cleanup", log[-2] if log[-1] == "port-forward-stop" else log[-1])
|
||||
self.assertIn("port-forward-stop", log)
|
||||
summary = json.loads((self.evidence / "t02-window-1-window.json").read_text())
|
||||
self.assertEqual("aborted", summary["outcome"])
|
||||
self.assertFalse(summary["secret_values_observed"])
|
||||
|
||||
def test_port_forward_failure_aborts_without_exercise(self) -> None:
|
||||
log: list[str] = []
|
||||
with self.assertRaises(window.WindowError):
|
||||
self.run_window(Runner(log), FakeForward(log, fail=True))
|
||||
self.assertNotIn("exercise", log)
|
||||
self.assertIn("abort-cleanup", log)
|
||||
|
||||
def test_refuses_outside_contained_login_before_any_step(self) -> None:
|
||||
log: list[str] = []
|
||||
with mock.patch.dict(os.environ, {"BAO_TOKEN": "x", **CONTAINED}, clear=True):
|
||||
with self.assertRaises(window.LANE.LEASE.ProcedureError):
|
||||
window.window(self.args, runner=Runner(log), forward_factory=lambda r, p: FakeForward(log))
|
||||
with mock.patch.dict(os.environ, {"PATH": "/usr/bin"}, clear=True):
|
||||
with self.assertRaises(window.LANE.LEASE.ProcedureError):
|
||||
window.window(self.args, runner=Runner(log), forward_factory=lambda r, p: FakeForward(log))
|
||||
self.assertEqual([], log)
|
||||
|
||||
def test_wrong_confirm_and_git_evidence_dir_refused_before_any_step(self) -> None:
|
||||
log: list[str] = []
|
||||
self.args.confirm = "wrong"
|
||||
with self.assertRaises(window.WindowError):
|
||||
self.run_window(Runner(log), FakeForward(log))
|
||||
self.args.confirm = window.LANE.confirm_token(json.loads(self.approval.read_text()))
|
||||
self.args.evidence_dir = ROOT / "docs"
|
||||
with self.assertRaises(window.LANE.LaneError):
|
||||
self.run_window(Runner(log), FakeForward(log))
|
||||
self.assertEqual([], log)
|
||||
|
||||
def test_port_forward_uses_ssh_remote_loopback_and_bounded_kubectl(self) -> None:
|
||||
command = window.PortForward("railiance01", 18081).command()
|
||||
self.assertIn("ExitOnForwardFailure=yes", command)
|
||||
self.assertIn("127.0.0.1:18081:127.0.0.1:18081", command)
|
||||
self.assertTrue(command[-1].startswith("timeout 900 kubectl -n audit-core port-forward --address 127.0.0.1"))
|
||||
self.assertTrue(command[-1].endswith("deploy/audit-core 18081:8080"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue