railiance-platform/scripts/t02-attended-window.py
codex dd003c9429 RPF-WP-0015-T03: post-reboot synthetic E2 mode in the reviewed sender lane
One contained login: project one synthetic sender, event accepted, replay
must be duplicate (exactly once), cleanup; no lease is touched.

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
2026-10-14 20:30:11 +02:00

278 lines
12 KiB
Python
Executable file

#!/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")
e2 = getattr(args, "mode", "t02") == "e2"
exercise = runner(
[sys.executable, str(HARNESS_SCRIPT), "synthetic-e2" if e2 else "exercise",
"--approval", str(args.approval),
"--remote", args.remote, "--contained", "--load-driver", str(args.load_driver),
"--confirm", LANE.LEASE.E2_CONFIRM if e2 else HARNESS_CONFIRM],
label="T03 synthetic E2" if e2 else "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)
parser.add_argument("--mode", choices=["t02", "e2"], default="t02",
help="e2: post-reboot synthetic event accepted exactly once; no lease revocation")
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())