diff --git a/src/warden/proxy.py b/src/warden/proxy.py index 0acd379..1fd3e2a 100644 --- a/src/warden/proxy.py +++ b/src/warden/proxy.py @@ -12,8 +12,9 @@ intact. Three guardrails are enforced here in code: 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. + an isolated helper session, never returns that output, requires successful + persistence to a private token helper, self-revokes, and cleans up. Audit records + are 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. @@ -380,9 +381,11 @@ def proxy_attended_login_exec( """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. + child, and revocation output are captured and discarded. A successful login + may return client output only after the private helper has been populated; + persistence defects and non-zero results fail closed. The reviewed child must + remain silent. Any possibly issued token is revoked before the isolated helper + directory is removed. """ if not child_argv: raise ProxyError( @@ -428,7 +431,7 @@ def proxy_attended_login_exec( except (OSError, ProxyError): helper_valid = False - if login.returncode != 0 or login_output.strip() or not helper_valid: + if login.returncode != 0 or not helper_valid: revoked = _revoke_contained( resolved.argv[0], env=env, possible_output=login_output ) diff --git a/tests/test_proxy.py b/tests/test_proxy.py index cac3a97..e049b16 100644 --- a/tests/test_proxy.py +++ b/tests/test_proxy.py @@ -391,6 +391,42 @@ def test_attended_login_unexpected_output_is_contained_revoked_and_cleaned( assert not (tmp_path / ".warden-attended-login").exists() +def test_attended_login_contained_success_output_never_escapes(monkeypatch, tmp_path, capsys): + monkeypatch.setattr(Path, "home", lambda: tmp_path) + sentinel = "hvs.NONPRODUCTION_CONTAINED_LOGIN" + child_ran = False + + def fake_run(argv, **kw): + nonlocal child_ran + helper = Path(kw["env"]["HOME"]) / ".vault-token" + if argv[:2] == ["bao", "login"]: + helper.write_text(sentinel) + helper.chmod(0o600) + return subprocess.CompletedProcess( + argv, + 0, + stdout=json.dumps({"auth": {"client_token": sentinel}}).encode(), + stderr=b"", + ) + if argv == ["reviewed-child"]: + child_ran = True + return subprocess.CompletedProcess(argv, 0, stdout=b"", stderr=b"") + if argv[:3] == ["bao", "token", "revoke"]: + return subprocess.CompletedProcess(argv, 0, stdout=b"", stderr=b"") + raise AssertionError(argv) + + monkeypatch.setattr("warden.proxy.subprocess.run", fake_run) + assert proxy_attended_login_exec( + ResolvedFetch(argv=["bao", "login", "-no-print"]), + child_argv=["reviewed-child"], + ) == 0 + captured = capsys.readouterr() + assert child_ran is True + assert sentinel not in captured.out + assert sentinel not in captured.err + assert not (tmp_path / ".warden-attended-login").exists() + + def test_real_catalog_login_entry_is_login_lane(): from warden.routing import load_catalog e = load_catalog(_repo_catalog()).get("key-cape-oidc-login") diff --git a/workplans/WARDEN-WP-0034-attended-login-openbao-output.md b/workplans/WARDEN-WP-0034-attended-login-openbao-output.md new file mode 100644 index 0000000..0b616b8 --- /dev/null +++ b/workplans/WARDEN-WP-0034-attended-login-openbao-output.md @@ -0,0 +1,36 @@ +--- +id: WARDEN-WP-0034 +type: workplan +title: "Accept contained OpenBao login output only after helper persistence" +domain: infotech +repo: ops-warden +status: active +owner: codex +topic_slug: attended-login-openbao-output +created: "2026-09-01" +updated: "2026-09-01" +--- + +## Repair attended-login handoff + +```task +id: WARDEN-WP-0034-T01 +status: done +priority: high +``` + +Allow a successful OpenBao login to proceed when its output is fully contained +and the private mode-0600 token helper is populated. Continue failing closed on +non-zero login, missing persistence, child output, revocation failure, or cleanup +failure. + +## Verify live contained operation + +```task +id: WARDEN-WP-0034-T02 +status: progress +priority: high +``` + +Run the proxy regression suite, reinstall the CLI, and complete one governed +OpenBao platform-admin operation with deterministic self-revocation.