diff --git a/docs/evidence/RAILIANCE-WP-0026-T01-ops-warden-receipt.json b/docs/evidence/RAILIANCE-WP-0026-T01-ops-warden-receipt.json deleted file mode 100644 index 5674c4f..0000000 --- a/docs/evidence/RAILIANCE-WP-0026-T01-ops-warden-receipt.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "interface": "railiance.attended-login-containment-receipt", - "version": 1, - "task_id": "RAILIANCE-WP-0026-T01", - "owner": "ops-warden", - "source_repo": "ops-warden", - "source_revision": "0fae0904ce8d8694338dd53a8a79abec5fec788d", - "created_at": "2026-08-22T23:31:17Z", - "disposition": "ready_for_owner_review", - "focused_test": { - "command": "uv run pytest -q tests/test_proxy.py tests/test_plan.py", - "passed": true, - "passed_count": 42, - "failed_count": 0 - }, - "repository_verification": { - "command": "uv run pytest -q", - "passed": true, - "passed_count": 390, - "deselected_count": 4, - "lint_command": "uv run ruff check .", - "lint_passed": true - }, - "acceptance_outcomes": { - "private_helper_preflight_before_auth": true, - "read_only_home_refused_before_oidc": true, - "login_child_and_revocation_stdio_contained": true, - "unexpected_stdout_and_stderr_fail_closed": true, - "possible_issuance_triggers_contained_self_revocation": true, - "helper_cleanup_is_deterministic": true, - "persistent_login_only_handoff_refused": true - }, - "live_oidc_performed": false, - "live_drill_authorized": false, - "secret_values_observed": false, - "sensitive_material_recorded": false -} diff --git a/registry/generated/high-risk-data-paths.yaml b/registry/generated/high-risk-data-paths.yaml index 6c2c341..9ce16d4 100644 --- a/registry/generated/high-risk-data-paths.yaml +++ b/registry/generated/high-risk-data-paths.yaml @@ -10,10 +10,10 @@ # declares it, and is null where the field set has not been established -- # null means unknown, never 'one field'. -generated_at: "2026-08-22T23:31:10Z" +generated_at: "2026-08-22T18:54:51Z" source: ops-warden/registry/routing/catalog.yaml -catalog_revision: "0fae0904ce8d8694338dd53a8a79abec5fec788d" -catalog_revision_date: "2026-08-23T01:31:05+02:00" +catalog_revision: "e3b9b1620c67236d7ba2c53512e7113819ea20a9" +catalog_revision_date: "2026-08-22T20:54:45+02:00" catalog_dirty: false high_risk_lane_count: 21 concrete_path_count: 14 diff --git a/registry/routing/catalog.yaml b/registry/routing/catalog.yaml index fd8d444..f4c5382 100644 --- a/registry/routing/catalog.yaml +++ b/registry/routing/catalog.yaml @@ -147,9 +147,8 @@ entries: - id: openbao-platform-admin-login title: Attended OpenBao platform administration login # This is an identity bootstrap, not a secret value lane. The authority it - # establishes is high-risk. Safety does not rely on -no-print: Warden must - # preflight a private writable helper, contain both output streams, run only - # the reviewed child command, self-revoke, and remove the helper. + # establishes is high-risk, but the command prints no token and writes only + # to the caller's normal OpenBao token helper. risk: high workload_ref: applicability: not-applicable diff --git a/src/warden/cli.py b/src/warden/cli.py index b28d6da..cb843bc 100644 --- a/src/warden/cli.py +++ b/src/warden/cli.py @@ -1169,20 +1169,13 @@ def _access_json(entry, expanded, gate: str, domain: Optional[str]) -> dict: "ops-warden holds no token." ) elif expanded.exec_capable: - if entry.lane == "login": - payload["next_action"] = ( - "Run the attended login and reviewed operation only inside the " - "contained envelope: `warden access --exec -- " - "`. The private helper is preflighted, all output " - "is suppressed, and the session is revoked and removed afterward." - ) - else: - payload["next_action"] = ( - "ops-warden can proxy this fetch as the caller: " - "`warden access --fetch` (or `--exec -- `). " - f"Runs {entry.owner_repo}'s tool with your identity; " - "ops-warden holds no value." - ) + verb = "fetch" if entry.lane != "login" else "login" + payload["next_action"] = ( + f"ops-warden can proxy this {verb} as the caller: " + f"`warden access --fetch`" + + ("" if entry.lane == "login" else " (or `--exec -- `)") + + f". Runs {entry.owner_repo}'s tool with your identity; ops-warden holds no value." + ) else: payload["next_action"] = ( f"obtain from {entry.owner_repo} ({entry.subsystem}); " @@ -1217,7 +1210,6 @@ def _access_proxy( build_wrapped_fetch, caller_auth_present, proxy_exec, - proxy_attended_login_exec, proxy_fetch, proxy_fetch_fingerprint, proxy_fetch_to_file, @@ -1248,17 +1240,18 @@ def _access_proxy( decision_id = None if is_login: - # Login lane: the authentication and reviewed child command share one - # isolated token-helper home. No credential may persist beyond that child. - if not do_exec or not child_argv: + # Login lane: interactive auth bootstrap. No caller-auth precheck (you have no + # token yet — that's the point) and no secret-read gate (it needs an identity + # this flow establishes). --exec is meaningless here. + if do_exec: err.print( - "[red]A login lane requires --exec -- [/red] " - f"({entry.id!r} cannot create a persistent login-only handoff)." + "[red]--exec is not valid for a login lane[/red] " + f"({entry.id!r} is interactive auth). Use --fetch." ) raise typer.Exit(2) err.print( - "[dim]login lane — contained OIDC and reviewed command; private helper, " - "suppressed output, deterministic self-revocation.[/dim]" + "[dim]login lane — interactive auth bootstrap; no secret-read gate, " + "token stays in the caller's own store.[/dim]" ) else: if no_policy: @@ -1350,9 +1343,7 @@ def _access_proxy( f"(caller identity; value not persisted)[/dim]" ) try: - if is_login: - rc = proxy_attended_login_exec(resolved, child_argv=child_argv) - elif do_exec: + if do_exec: if not child_argv: err.print("[red]--exec needs a command after `--`[/red], e.g. `-- npm publish`.") raise typer.Exit(2) @@ -1533,13 +1524,8 @@ def access( console.print(f" auth : {expanded.auth_method}") if expanded.path_template: console.print(f" path : {expanded.path_template}") - if expanded.fetch_command and entry.lane != "login": + if expanded.fetch_command: console.print(f" fetch : {expanded.fetch_command}") - elif expanded.fetch_command: - console.print( - " login : [dim]internal to the contained --exec envelope; " - "do not invoke separately[/dim]" - ) if expanded.policy_ref: console.print(f" policy : {expanded.policy_ref} [dim]({gate})[/dim]") console.print(f" wiki : {entry.wiki_ref}") @@ -1558,16 +1544,12 @@ def access( console.print(f" pointer : [dim]{entry.pointer_command}[/dim]") if expanded.exec_capable: label = "fallback" if entry.has_native_exec else "proxy" - if entry.lane == "login": - console.print( - f" {label:<8} : [dim]{proxy} --exec -- [/dim] " - "[yellow](contained login + command; output suppressed)[/yellow]" - ) - else: - console.print( - f" {label:<8} : [dim]{proxy} --fetch[/dim] " - "[yellow](transparent conduit — fetches as you)[/yellow]" - ) + hint = ( + "transparent conduit — fetches as you" + if entry.lane != "login" + else "runs the interactive login as you" + ) + console.print(f" {label:<8} : [dim]{proxy} --fetch[/dim] [yellow]({hint})[/yellow]") if expanded.path_template and "<" in expanded.path_template: console.print( " note : remaining <…> placeholders are owner-confirmed names " @@ -1582,21 +1564,14 @@ def access( "conduit (runs the fetch as you, holds nothing)." ) elif expanded.exec_capable: - if entry.lane == "login": - console.print( - "\n[green]Contained attended login[/green] — " - f"[bold]{proxy} --exec -- [/bold]. The login, " - "command, and revocation use a private helper with suppressed output; " - "the session is removed afterward." - ) - else: - console.print( - "\n[green]ops-warden can fetch this for you[/green] as the caller — " - f"[bold]{proxy} --fetch[/bold] (or " - f"[bold]{proxy} --exec -- [/bold]). It runs " - f"{entry.owner_repo}'s tool with [bold]your[/bold] identity; the " - "value streams to you and ops-warden never holds, caches, or logs it." - ) + verb = "fetch this for you" if entry.lane != "login" else "run this login for you" + console.print( + f"\n[green]ops-warden can {verb}[/green] as the caller — " + f"[bold]{proxy} --fetch[/bold]" + + ("" if entry.lane == "login" else f" (or [bold]{proxy} --exec -- [/bold])") + + f". It runs {entry.owner_repo}'s tool with [bold]your[/bold] identity; the " + "value streams to you and ops-warden never holds, caches, or logs it." + ) else: console.print( f"\n[yellow]ops-warden does not hold this secret.[/yellow] " diff --git a/src/warden/plan.py b/src/warden/plan.py index 730a43a..dd0d7dd 100644 --- a/src/warden/plan.py +++ b/src/warden/plan.py @@ -142,13 +142,9 @@ def _autonomous_commands(entry: RouteEntry, domain: Optional[str]) -> List[str]: expanded = expand_handoff(entry, domain=domain) if entry.lane == "login": - cmds.append( - f"warden access {entry.id} --exec -- " - ) + cmds.append(f"warden access {entry.id} --fetch") if expanded.fetch_command: - cmds.append( - "# owner login is contained by warden; do not invoke it separately" - ) + cmds.append(f"# attended owner login: {expanded.fetch_command}") return cmds if entry.has_native_exec and entry.exec_command: cmds.append(entry.exec_command) @@ -179,20 +175,14 @@ def _autonomous_commands(entry: RouteEntry, domain: Optional[str]) -> List[str]: def _founder_for_entry(entry: RouteEntry, need: str, domain: Optional[str]) -> FounderAct: expanded = expand_handoff(entry, domain=domain) if entry.lane == "login": - contained_command = ( - f"warden access {entry.id} --exec -- " - ) return FounderAct( kind="oidc_login", summary=f"Interactive OIDC/MFA login via {entry.owner_repo}", details={ "lane_id": entry.id, "auth_method": expanded.auth_method, - "fetch_command": contained_command, - "desk_hint": ( - "warden desk --from-plan (act=oidc_login); execute only through: " - + contained_command - ), + "fetch_command": expanded.fetch_command, + "desk_hint": f"warden desk --from-plan (act=oidc_login) or: {expanded.fetch_command}", }, ) if entry.lane == "ceremony": diff --git a/src/warden/proxy.py b/src/warden/proxy.py index 0acd379..208463a 100644 --- a/src/warden/proxy.py +++ b/src/warden/proxy.py @@ -8,12 +8,11 @@ intact. Three guardrails are enforced here in code: caller's own environment. ops-warden injects no token of its own; if the caller has no credential, the underlying tool fails and we surface the auth pointer. We never add a `*_TOKEN` warden owns to the child environment. -* **G2 — bounded transports, no logging of values.** Ordinary ``proxy_fetch`` runs - the tool with inherited stdout/stderr so the value never enters warden's memory; - sanctioned exec/file transports hold it only for their bounded handoff. The - high-risk attended-login lane is stricter: it captures every client byte inside - an isolated helper session, permits no output, self-revokes, and cleans up. Audit - records are metadata only. +* **G2 — transit only, no persistence/logging of values.** ``proxy_fetch`` runs the + tool with **inherited** stdout/stderr (never a pipe), so the value streams to the + caller and never enters warden's memory. ``proxy_exec`` reads the value solely to + place it in a child process's environment (the accepted proxy tradeoff) and never + writes it to disk or log. The audit record is metadata only. * **G3 — policy gate before fetch.** The CLI runs ``check_fetch_policy`` before calling anything here; this module refuses to run an unresolved command template. @@ -25,10 +24,7 @@ import json import os import re import shlex -import shutil -import stat import subprocess -import tempfile from dataclasses import dataclass from datetime import datetime, timezone from pathlib import Path @@ -37,9 +33,6 @@ from typing import List, Optional from warden.routing.models import RouteEntry _PLACEHOLDER = re.compile(r"<[^>]+>") -_OPENBAO_TOKEN = re.compile(rb"\b(?:hvs|hvb|hvr)\.[A-Za-z0-9_-]{8,}\b") -_ATTENDED_LOGIN_ROOT = ".warden-attended-login" -_TOKEN_HELPER_NAME = ".vault-token" @dataclass(frozen=True) @@ -212,267 +205,6 @@ def proxy_fetch(resolved: ResolvedFetch) -> int: return completed.returncode -def _assert_owned_mode(path: Path, *, mode: int, directory: bool) -> None: - """Require a caller-owned, non-symlink path with an exact private mode.""" - info = path.lstat() - expected_type = stat.S_ISDIR if directory else stat.S_ISREG - if not expected_type(info.st_mode) or stat.S_ISLNK(info.st_mode): - raise ProxyError("attended login private storage has an unsafe path type") - if hasattr(os, "getuid") and info.st_uid != os.getuid(): - raise ProxyError("attended login private storage is not caller-owned") - if stat.S_IMODE(info.st_mode) != mode: - raise ProxyError("attended login private storage has an unsafe mode") - - -def _prepare_attended_login_home() -> tuple[Path, Path, bool]: - """Create and prove an isolated token-helper home before authentication.""" - home = Path.home() - try: - home_info = home.lstat() - except OSError as exc: - raise ProxyError( - "attended login requires a usable writable default home before OIDC" - ) from exc - if ( - not stat.S_ISDIR(home_info.st_mode) - or stat.S_ISLNK(home_info.st_mode) - or stat.S_IMODE(home_info.st_mode) & 0o222 == 0 - ): - raise ProxyError( - "attended login requires a usable writable default home before OIDC" - ) - - # Prove the default home itself is writable. A pre-existing writable child must - # not let a newly read-only HOME reach the OIDC process. - probe_fd = -1 - probe_path: Path | None = None - probe_cleanup_error: OSError | None = None - try: - probe_fd, probe_name = tempfile.mkstemp(prefix=".warden-home-probe-", dir=home) - probe_path = Path(probe_name) - os.write(probe_fd, b"preflight") - os.fsync(probe_fd) - except OSError as exc: - raise ProxyError( - "attended login requires a usable writable default home before OIDC" - ) from exc - finally: - if probe_fd >= 0: - os.close(probe_fd) - if probe_path is not None: - try: - probe_path.unlink() - except OSError as exc: - probe_cleanup_error = exc - if probe_cleanup_error is not None: - raise ProxyError("attended login home preflight cleanup failed") from probe_cleanup_error - - root = home / _ATTENDED_LOGIN_ROOT - root_created = False - try: - root.mkdir(mode=0o700) - root_created = True - except FileExistsError: - pass - except OSError as exc: - raise ProxyError("could not establish attended login private storage") from exc - _assert_owned_mode(root, mode=0o700, directory=True) - - try: - session = Path(tempfile.mkdtemp(prefix="session-", dir=root)) - session.chmod(0o700) - _assert_owned_mode(session, mode=0o700, directory=True) - helper = session / _TOKEN_HELPER_NAME - flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL - if hasattr(os, "O_NOFOLLOW"): - flags |= os.O_NOFOLLOW - fd = os.open(helper, flags, 0o600) - try: - # Exercise persistence before auth, then leave the helper empty for bao. - os.write(fd, b"preflight") - os.fsync(fd) - os.ftruncate(fd, 0) - finally: - os.close(fd) - _assert_owned_mode(helper, mode=0o600, directory=False) - except (OSError, ProxyError) as exc: - if "session" in locals(): - shutil.rmtree(session, ignore_errors=True) - if root_created: - try: - root.rmdir() - except OSError: - pass - if isinstance(exc, ProxyError): - raise - raise ProxyError("could not establish attended login private storage") from exc - return root, session, root_created - - -def _contained_run(argv: List[str], *, env: dict) -> subprocess.CompletedProcess: - """Run with both output streams captured and never forwarded.""" - return subprocess.run( # noqa: S603 - argv, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - stdin=None, - env=env, - check=False, - ) - - -def _output_bytes(completed: subprocess.CompletedProcess) -> bytes: - stdout = completed.stdout - stderr = completed.stderr - if isinstance(stdout, str): - stdout = stdout.encode("utf-8", errors="replace") - if isinstance(stderr, str): - stderr = stderr.encode("utf-8", errors="replace") - stdout = stdout if isinstance(stdout, bytes) else b"" - stderr = stderr if isinstance(stderr, bytes) else b"" - return stdout + b"\n" + stderr - - -def _revoke_contained( - bao_binary: str, - *, - env: dict, - possible_output: bytes, -) -> bool: - """Attempt self-revocation without exposing helper or captured output.""" - revoke_env = dict(env) - try: - first = _contained_run( - [bao_binary, "token", "revoke", "-self"], env=revoke_env - ) - except OSError: - return False - if first.returncode == 0: - return True - - # A helper-persistence failure can leave the issued token only in contained - # client output. Use it solely for immediate self-revocation, never for a log, - # return value, hash, fingerprint, file, or argv. - match = _OPENBAO_TOKEN.search(possible_output) - if match is None: - return False - token = match.group(0).decode("ascii") - revoke_env["BAO_TOKEN"] = token - revoke_env.pop("VAULT_TOKEN", None) - try: - try: - second = _contained_run( - [bao_binary, "token", "revoke", "-self"], env=revoke_env - ) - except OSError: - return False - return second.returncode == 0 - finally: - revoke_env.pop("BAO_TOKEN", None) - token = "" # noqa: F841 - best-effort release of the credential reference - - -def proxy_attended_login_exec( - resolved: ResolvedFetch, - *, - child_argv: List[str], -) -> int: - """Run an attended login and one silent child inside a private helper home. - - The default home is proven writable before the OIDC client starts. Login, - child, and revocation output are captured and discarded. Any non-empty output, - persistence defect, or non-zero result fails closed; any possibly issued token - is revoked before the isolated helper directory is removed. - """ - if not child_argv: - raise ProxyError( - "attended login requires --exec -- ; a persistent " - "login-only handoff is not permitted" - ) - if ( - resolved.argv is None - or len(resolved.argv) < 2 - or Path(resolved.argv[0]).name != "bao" - or resolved.argv[1] != "login" - ): - raise ProxyError("attended login requires a direct bao login argv") - - root, session, root_created = _prepare_attended_login_home() - helper = session / _TOKEN_HELPER_NAME - env = _caller_env() - env["HOME"] = str(session) - env.pop("BAO_TOKEN", None) - env.pop("VAULT_TOKEN", None) - for key in ( - "BAO_LOG_LEVEL", - "BAO_LOG_FORMAT", - "VAULT_LOG_LEVEL", - "VAULT_LOG_FORMAT", - ): - env.pop(key, None) - - login_argv = list(resolved.argv) - if not any(arg == "-format" or arg.startswith("-format=") for arg in login_argv): - login_argv.append("-format=json") - - try: - try: - login = _contained_run(login_argv, env=env) - except OSError as exc: - raise ProxyError("attended login client could not start before OIDC") from exc - login_output = _output_bytes(login) - helper_valid = False - try: - _assert_owned_mode(helper, mode=0o600, directory=False) - helper_valid = helper.stat().st_size > 0 - except (OSError, ProxyError): - helper_valid = False - - if login.returncode != 0 or login_output.strip() or not helper_valid: - revoked = _revoke_contained( - resolved.argv[0], env=env, possible_output=login_output - ) - status = "revoked" if revoked else "revocation could not be confirmed" - raise ProxyError( - "attended login failed closed before command handoff; any possible " - f"issued session was contained and {status}" - ) - - try: - child = _contained_run(child_argv, env=env) - except OSError as exc: - revoked = _revoke_contained( - resolved.argv[0], env=env, possible_output=b"" - ) - status = "revoked" if revoked else "revocation could not be confirmed" - raise ProxyError( - "attended command could not start; the login session was " + status - ) from exc - - child_output = _output_bytes(child) - revoked = _revoke_contained( - resolved.argv[0], env=env, possible_output=child_output - ) - if child.returncode != 0 or child_output.strip(): - status = "revoked" if revoked else "revocation could not be confirmed" - raise ProxyError( - "attended command failed closed because it returned a failure or " - f"unexpected output; the login session was {status}" - ) - if not revoked: - raise ProxyError( - "attended command completed but session revocation could not be confirmed" - ) - return 0 - finally: - try: - shutil.rmtree(session) - if root_created: - root.rmdir() - except OSError as exc: - raise ProxyError("attended login private storage cleanup failed") from exc - - def _capture_value(resolved: ResolvedFetch) -> str: """Run the fetch and return its stdout (the value) minus one trailing newline. diff --git a/src/warden/worker.py b/src/warden/worker.py index 9f54db0..e1caf01 100644 --- a/src/warden/worker.py +++ b/src/warden/worker.py @@ -588,13 +588,7 @@ def draft_route_answer(query: str) -> str: elif e.has_native_exec: parts.append(f"Primary: {e.exec_command}.") elif e.exec_capable: - if e.lane == "login": - parts.append( - f"Contained login: warden access {e.id} --exec -- " - "." - ) - else: - parts.append(f"Proxy: warden access {e.id} --fetch (as the caller).") + parts.append(f"Proxy: warden access {e.id} --fetch (as the caller).") parts.append(f"See {e.wiki_ref}.") return " ".join(parts) diff --git a/tests/test_plan.py b/tests/test_plan.py index 73fc7e9..7f6b7aa 100644 --- a/tests/test_plan.py +++ b/tests/test_plan.py @@ -65,17 +65,19 @@ def test_plan_first_time_openbao_database_admin_uses_platform_admin_login(): assert plan.founder_act.kind == "oidc_login" command = plan.founder_act.details["fetch_command"] assert command == ( - "warden access openbao-platform-admin-login --exec -- " + "bao login -no-print -method=oidc -path=netkingdom role=platform-admin" ) assert "financials" not in command assert "paste_once" not in plan.founder_act.details["desk_hint"] assert any( - item - == "warden access openbao-platform-admin-login --exec -- " + item == "warden access openbao-platform-admin-login --fetch" for item in plan.commands ) - assert not any("--fetch" in item for item in plan.commands) - assert not any("--out" in item or "--wrap" in item for item in plan.commands) + assert not any( + flag in item + for item in plan.commands + for flag in ("--exec", "--out", "--wrap") + ) def test_plan_openbao_shamir_recovery_uses_approval_ceremony_not_secret_provision(): diff --git a/tests/test_proxy.py b/tests/test_proxy.py index cac3a97..4e4857e 100644 --- a/tests/test_proxy.py +++ b/tests/test_proxy.py @@ -13,7 +13,6 @@ from warden.proxy import ( ProxyError, ResolvedFetch, caller_auth_present, - proxy_attended_login_exec, proxy_exec, proxy_fetch, resolve_fetch_command, @@ -252,143 +251,37 @@ def test_cli_proxy_rejects_retired_no_policy_bypass(monkeypatch, tmp_path): # --- T4: login lane -------------------------------------------------------- -def test_cli_login_lane_contains_login_handoff_and_revocation(monkeypatch, tmp_path): - """Login and its reviewed child share a private, silent helper session.""" +def test_cli_login_lane_runs_without_token_or_policy_ack(monkeypatch, tmp_path): + """Login lane skips the caller-auth precheck and the secret-read gate.""" _proxy_env(monkeypatch, tmp_path) monkeypatch.delenv("VAULT_TOKEN", raising=False) monkeypatch.delenv("BAO_TOKEN", raising=False) monkeypatch.setattr(Path, "home", lambda: tmp_path) # no ~/.vault-token - calls = [] + ran = {} def fake_run(argv, **kw): - calls.append((argv, kw)) - assert kw["stdout"] is subprocess.PIPE - assert kw["stderr"] is subprocess.PIPE - private_home = Path(kw["env"]["HOME"]) - assert private_home != tmp_path - assert oct(private_home.stat().st_mode & 0o777) == "0o700" - helper = private_home / ".vault-token" - assert oct(helper.stat().st_mode & 0o777) == "0o600" - if argv[:2] == ["bao", "login"]: - helper.write_bytes(b"non-production-test-double") - helper.chmod(0o600) - return subprocess.CompletedProcess(argv, 0, stdout=b"", stderr=b"") + ran["argv"] = argv + ran["stdout"] = kw.get("stdout") + return subprocess.CompletedProcess(argv, 0) monkeypatch.setattr("warden.proxy.subprocess.run", fake_run) - r = runner.invoke( - app, - [ - "access", "login oidc", "--domain", "coulomb_social", - "--exec", "--", "true", - ], - ) + r = runner.invoke(app, ["access", "login oidc", "--domain", "coulomb_social", "--fetch"]) assert r.exit_code == 0 - assert [call[0][:2] for call in calls] == [ - ["bao", "login"], - ["true"], - ["bao", "token"], - ] - assert not (tmp_path / ".warden-attended-login").exists() - assert "non-production-test-double" not in r.output - audit = (tmp_path / "state" / "access-audit.log").read_text() - assert "non-production-test-double" not in audit + assert ran["argv"][:2] == ["bao", "login"] # interactive login ran + assert ran["stdout"] is None # inherited stdio — token not captured -def test_cli_login_lane_rejects_persistent_fetch(monkeypatch, tmp_path): +def test_cli_login_lane_rejects_exec(monkeypatch, tmp_path): _proxy_env(monkeypatch, tmp_path) monkeypatch.setattr( "warden.proxy.subprocess.run", lambda *a, **k: (_ for _ in ()).throw(AssertionError("should not run")), ) r = runner.invoke( - app, ["access", "login oidc", "--domain", "coulomb_social", "--fetch"] + app, ["access", "login oidc", "--domain", "coulomb_social", "--exec", "--", "true"] ) assert r.exit_code == 2 - assert "requires --exec" in r.output - - -def test_attended_login_refuses_read_only_home_before_auth(monkeypatch, tmp_path): - monkeypatch.setattr(Path, "home", lambda: tmp_path) - monkeypatch.setattr( - "warden.proxy.subprocess.run", - lambda *a, **k: (_ for _ in ()).throw(AssertionError("OIDC started")), - ) - tmp_path.chmod(0o555) - try: - with pytest.raises(ProxyError, match="writable default home"): - proxy_attended_login_exec( - ResolvedFetch(argv=["bao", "login", "-no-print"]), - child_argv=["true"], - ) - finally: - tmp_path.chmod(0o700) - - -def test_attended_login_persistence_failure_revokes_and_cleans(monkeypatch, tmp_path): - monkeypatch.setattr(Path, "home", lambda: tmp_path) - calls = [] - - def fake_run(argv, **kw): - calls.append(argv) - # Login succeeds but the pre-created helper remains empty: persistence failed. - return subprocess.CompletedProcess(argv, 0, stdout=b"", stderr=b"") - - monkeypatch.setattr("warden.proxy.subprocess.run", fake_run) - with pytest.raises(ProxyError, match="failed closed before command handoff"): - proxy_attended_login_exec( - ResolvedFetch(argv=["bao", "login", "-no-print"]), - child_argv=["should-not-run"], - ) - assert calls == [ - ["bao", "login", "-no-print", "-format=json"], - ["bao", "token", "revoke", "-self"], - ] - assert not (tmp_path / ".warden-attended-login").exists() - - -@pytest.mark.parametrize("stream", ["stdout", "stderr"]) -def test_attended_login_unexpected_output_is_contained_revoked_and_cleaned( - monkeypatch, tmp_path, capsys, stream -): - monkeypatch.setattr(Path, "home", lambda: tmp_path) - sentinel = "hvs.NONPRODUCTION_SENTINEL" - calls = [] - - def fake_run(argv, **kw): - calls.append((argv, dict(kw["env"]))) - if argv[:2] == ["bao", "login"]: - output = sentinel.encode() - return subprocess.CompletedProcess( - argv, - 0, - stdout=output if stream == "stdout" else b"", - stderr=output if stream == "stderr" else b"", - ) - if argv[:3] == ["bao", "token", "revoke"]: - # The helper is empty. The second contained attempt uses the captured - # value only through BAO_TOKEN, never argv or visible output. - return subprocess.CompletedProcess( - argv, - 0 if kw["env"].get("BAO_TOKEN") == sentinel else 1, - stdout=b"", - stderr=b"", - ) - raise AssertionError("reviewed child ran after unexpected login output") - - monkeypatch.setattr("warden.proxy.subprocess.run", fake_run) - with pytest.raises(ProxyError, match="failed closed before command handoff") as exc: - proxy_attended_login_exec( - ResolvedFetch(argv=["bao", "login", "-no-print"]), - child_argv=["should-not-run"], - ) - captured = capsys.readouterr() - assert sentinel not in str(exc.value) - assert sentinel not in captured.out - assert sentinel not in captured.err - assert all(sentinel not in " ".join(argv) for argv, _ in calls) - assert calls[-1][1]["BAO_TOKEN"] == sentinel - assert not (tmp_path / ".warden-attended-login").exists() def test_real_catalog_login_entry_is_login_lane(): diff --git a/tests/test_routing.py b/tests/test_routing.py index 6226080..209e54c 100644 --- a/tests/test_routing.py +++ b/tests/test_routing.py @@ -962,10 +962,11 @@ def test_cli_route_gaps_fail_on_stale_exits_3(repo_catalog_env): assert result.exit_code == 3 rows = json.loads(result.stdout) assert any(r["stale"] for r in rows) - # An asked-and-waiting lane stays stale until it is verified, regardless of - # how many calendar days have elapsed since the request. + # A freshly reviewed lane can still be stale because it was never verified; + # asked-and-waiting must not silently pass the gate as the calendar moves. assert any( r["stale"] + and r["days_since_review"] <= 1 and r["verified"] == "asked-and-waiting" for r in rows ) diff --git a/wiki/playbooks/openbao-platform-admin-login.md b/wiki/playbooks/openbao-platform-admin-login.md index c1c86ae..a5c5322 100644 --- a/wiki/playbooks/openbao-platform-admin-login.md +++ b/wiki/playbooks/openbao-platform-admin-login.md @@ -18,35 +18,27 @@ workload KV-read lane and it does not provision a secret value. `openbao-api-key`, a workload role, paste-once provisioning, or root, stop and report a routing defect. -2. The operator performs the identity act and the separately reviewed owner - command through one contained envelope: +2. The operator performs the one identity act through KeyCape OIDC/MFA: ```bash - warden access openbao-platform-admin-login --exec -- + bao login -no-print -method=oidc -path=netkingdom role=platform-admin ``` - Warden refuses a login-only `--fetch`. Before OIDC it proves the caller's - default home is usable, creates a caller-owned `0700` isolated home and a - `0600` token helper, and then runs `bao login -no-print` with both stdout and - stderr captured. The reviewed command runs in the same contained home with - both streams captured; it must persist any permitted metadata evidence itself - and remain silent. Warden self-revokes the session and removes the helper on - every success or failure path. - - Safety does not rely on `-no-print`. Any client or child output, helper - persistence defect, non-zero exit, or revocation/cleanup defect fails closed. - Captured bytes are never returned, logged, excerpted, hashed, or fingerprinted. - Do not paste a token into chat, State Hub, a shell argument, or a handoff file. - Root is offline break-glass authority, not a fallback for failure. + `-no-print` is mandatory. Do not paste a token into chat, State Hub, a shell + argument, or a temporary handoff file. Root is offline break-glass authority, + not a fallback for an OIDC or callback failure. 3. Verify authority using metadata or capabilities only, never by reading a secret value. Then run only the separately reviewed owner procedure. For the database engine this procedure lives in `rapp-postgres`; the login does not itself approve configuration changes. -4. Confirm the contained command exits successfully. Warden performs and checks - `bao token revoke -self` inside the contained environment before cleanup; do - not retain or reuse the helper. +4. Revoke the attended token when the reviewed operation and its non-secret + verification are complete: + + ```bash + bao token revoke -self + ``` If browser login fails before authentication, confirm the `netkingdom` auth mount, `platform-admin` role, and allowed callback with `railiance-platform` and diff --git a/workplans/WARDEN-WP-0027-credential-governance-lockdown.md b/workplans/WARDEN-WP-0027-credential-governance-lockdown.md index 5a2a55d..717ed37 100644 --- a/workplans/WARDEN-WP-0027-credential-governance-lockdown.md +++ b/workplans/WARDEN-WP-0027-credential-governance-lockdown.md @@ -189,29 +189,6 @@ matrix is in platform snapshot/driver and master quorum contracts remain required before the fully parameterized read-only preflight. -**Live attempt terminal NO-GO 2026-08-23.** All owner receipts and the final -read-only preflight became green, and the operator opened the exact bounded -window with decision `449a697a-5303-4582-aa9e-b0bc8b35ab2d`. The platform -driver stopped before seal when attended OIDC succeeded but the read-only agent -home prevented token-helper persistence and the underlying client emitted the -short-lived credential into captured output despite `-no-print`. The credential -was immediately self-revoked. Independent metadata confirmed OpenBao remained -initialized and unsealed with zero unseal progress; no seal, unseal, reboot, -restore, policy, PVC, or workload mutation occurred. NO-GO decision -`85724c0c-e70f-4e2f-a8c6-a9cb1ea6331b` supersedes and consumes the GO; the -scenario and its receipts cannot be reused. - -The direct remediation interface is `RAILIANCE-WP-0026-T01`. Ops-warden commit -`0fae0904ce8d8694338dd53a8a79abec5fec788d` replaces the persistent login-only -handoff with a contained login-plus-reviewed-command envelope. It proves a -private writable helper before OIDC, captures login/child/revocation stdout and -stderr, fails closed on any output or persistence defect, self-revokes, and -removes the helper. No live OIDC was used for verification. The value-safe -owner receipt is -`docs/evidence/RAILIANCE-WP-0026-T01-ops-warden-receipt.json`; a future drill -requires owner acceptance, fresh receipts, a new scenario id, and a new human -decision. - ## Task: Tamper-evident policy governance + reconcile ```task