fix: contain attended login diagnostics and OpenBao recovery
Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a137ca-5c77-7a71-b9ae-23ad9b0b5684
This commit is contained in:
parent
1baeb0821c
commit
d2cd3fa940
5 changed files with 185 additions and 6 deletions
|
|
@ -0,0 +1,78 @@
|
|||
{
|
||||
"id": "WARDEN-CUST81-ATTENDED-DEPENDENCY-REPAIR-R1",
|
||||
"revision": 1,
|
||||
"status": "source_preparation_verified_not_live",
|
||||
"reviewed_at": "2026-10-15T02:18:02.246091+00:00",
|
||||
"base_commit": "1baeb0821cb6304464fab78932d8c6501f8c4cc6",
|
||||
"scope": "Existing WARDEN-WP-0036 contained attended login; CUST-WP-0081-T02 dependency. No credential scope or live change approval expansion.",
|
||||
"failed_attempts": [
|
||||
{
|
||||
"at": "2026-10-15T02:07:41.774194+00:00",
|
||||
"exit_code": 10,
|
||||
"outcome": "login_failed"
|
||||
},
|
||||
{
|
||||
"at": "2026-10-15T02:10:58.943441+00:00",
|
||||
"exit_code": 10,
|
||||
"outcome": "login_failed"
|
||||
}
|
||||
],
|
||||
"browser_evidence": {
|
||||
"profile_count": 2,
|
||||
"initialization_mtimes_utc": [
|
||||
"2026-10-15T02:08:19.576345+00:00",
|
||||
"2026-10-15T02:09:28.109549+00:00"
|
||||
],
|
||||
"prefs_file_presence_only": true,
|
||||
"profile_contents_read": false
|
||||
},
|
||||
"cleanup_evidence": {
|
||||
"custody_receipt_exists": false,
|
||||
"bootstrap_receipt_exists": false,
|
||||
"new_session_directory_present": false,
|
||||
"preexisting_session_directory_mtime": "2026-10-14T22:41:38.098938+00:00",
|
||||
"private_helper_read": false,
|
||||
"server_revocation": "unconfirmed"
|
||||
},
|
||||
"findings": [
|
||||
"The reviewed owner child never ran: Warden exit10 and absent child receipts.",
|
||||
"Actual failure cause is unknown because captured login bytes were discarded. A 121-second duration is consistent with the Bao callback timeout, not proof.",
|
||||
"Bao 2.5.5 internal helper uses .vault-token; Warden helper name is correct.",
|
||||
"Standard flag parsing stops at auth data, but Bao setupEnv scans global format across argv. Trailing -format=json already chooses JSON output. Moving before role is defensive argv normalization, not a proven timeout fix.",
|
||||
"Warden recovery matcher omitted s. tokens supported by this estate. This fix affects only existing in-memory emergency self-revocation after helper persistence failure."
|
||||
],
|
||||
"changes": [
|
||||
"Insert synthesized -format=json immediately after bao login, before auth data.",
|
||||
"Recognize s. in existing recovery-token matcher; preserve exact revoke-self-only use.",
|
||||
"Expose only fixed diagnostic categories, numeric returncode, helper-present and output-shape booleans. Captured content is never surfaced."
|
||||
],
|
||||
"strict_output_allowlist_unchanged": true,
|
||||
"focused_tests": {
|
||||
"passed": 84,
|
||||
"command": "python -m pytest -q tests/test_proxy.py",
|
||||
"ruff": "passed"
|
||||
},
|
||||
"preserved_fixture_failures": [
|
||||
"Initial task fixture omitted registry:50failed31passed; fixed by copying public registry.",
|
||||
"Whole-suite attempt omitted scripts:5 collection errors; actual owner full suite required after integration."
|
||||
],
|
||||
"knowledge_orientation": {
|
||||
"package_version": "0.2.6",
|
||||
"digest": "8d1d6a9d4d07fec7b619618de7dfe2be2254c1790f85f4198c7fe19db9751c02",
|
||||
"status": "withheld; reviewed current NetKingdom SECURITY-COMPANION, gate-house INTENT, ops-warden AGENTS/INTENT and current Custodian environment source directly"
|
||||
},
|
||||
"source_references": [
|
||||
"https://raw.githubusercontent.com/openbao/openbao/v2.5.5/command/main.go",
|
||||
"https://raw.githubusercontent.com/openbao/openbao/v2.5.5/command/base.go",
|
||||
"https://raw.githubusercontent.com/openbao/openbao/v2.5.5/command/login.go",
|
||||
"https://raw.githubusercontent.com/openbao/openbao/v2.5.5/command/token/helper_internal.go"
|
||||
],
|
||||
"no_login_or_credentials": true,
|
||||
"whole_suite": {
|
||||
"status": "unverified_terminal_output_unavailable",
|
||||
"handle": 39705,
|
||||
"last_observed": "54 percent with six failures; no terminal result consumed by owner worker",
|
||||
"terminal_handle_after_user_abort": "root poll reported Unknown process id",
|
||||
"claim": "No full-suite pass is claimed; 84 focused proxy tests and Ruff passed."
|
||||
}
|
||||
}
|
||||
|
|
@ -41,7 +41,7 @@ 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")
|
||||
_OPENBAO_TOKEN = re.compile(rb"\b(?:hvs|hvb|hvr|s)\.[A-Za-z0-9_-]{8,}\b")
|
||||
_ATTENDED_LOGIN_ROOT = ".warden-attended-login"
|
||||
_TOKEN_HELPER_NAME = ".vault-token"
|
||||
|
||||
|
|
@ -544,7 +544,7 @@ def proxy_attended_login_exec(
|
|||
|
||||
login_argv = list(resolved.argv)
|
||||
if not any(arg == "-format" or arg.startswith("-format=") for arg in login_argv):
|
||||
login_argv.append("-format=json")
|
||||
login_argv.insert(2, "-format=json")
|
||||
|
||||
try:
|
||||
try:
|
||||
|
|
@ -569,9 +569,27 @@ def proxy_attended_login_exec(
|
|||
"" if output_allowed
|
||||
else " (login client produced non-allowlisted output)"
|
||||
)
|
||||
# Fixed categories and booleans only; captured bytes stay discarded.
|
||||
if login.returncode != 0:
|
||||
if b"timed out waiting for response from provider" in login_output:
|
||||
category = "callback_timeout"
|
||||
elif b"Error handling callback" in login_output:
|
||||
category = "callback_error"
|
||||
elif b"Error authenticating:" in login_output:
|
||||
category = "authentication_error"
|
||||
else:
|
||||
category = "unknown_client_failure"
|
||||
elif not helper_valid:
|
||||
category = "helper_persistence_missing"
|
||||
else:
|
||||
category = "output_shape_rejected"
|
||||
raise AttendedLoginError(
|
||||
"attended login failed closed before command handoff"
|
||||
f"{reason}; any possible issued session was contained and {status}",
|
||||
f"{reason}; category={category}, client_exit={login.returncode}, "
|
||||
f"helper_present={helper_valid}, "
|
||||
f"stdout_allowed={_login_stdout_allowed(_stream_bytes(login.stdout))}, "
|
||||
f"stderr_allowed={_login_stderr_allowed(_stream_bytes(login.stderr))}; "
|
||||
f"any possible issued session was contained and {status}",
|
||||
exit_code=10, outcome="login_failed",
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -446,7 +446,7 @@ def test_attended_login_persistence_failure_revokes_and_cleans(monkeypatch, tmp_
|
|||
child_argv=["should-not-run"],
|
||||
)
|
||||
assert calls == [
|
||||
["bao", "login", "-no-print", "-format=json"],
|
||||
["bao", "login", "-format=json", "-no-print"],
|
||||
["bao", "token", "revoke", "-self"],
|
||||
]
|
||||
assert not (tmp_path / ".warden-attended-login").exists()
|
||||
|
|
@ -716,7 +716,7 @@ def test_attended_login_non_allowlisted_output_direct_raises_value_free(
|
|||
assert (exc.value.exit_code, exc.value.outcome) == (10, "login_failed")
|
||||
assert _LOGIN_SENTINEL not in str(exc.value)
|
||||
assert calls == [
|
||||
["bao", "login", "-no-print", "-format=json"],
|
||||
["bao", "login", "-format=json", "-no-print"],
|
||||
["bao", "token", "revoke", "-self"],
|
||||
]
|
||||
|
||||
|
|
@ -948,3 +948,49 @@ def test_access_agent_high_risk_fingerprint_allowed(tmp_path, monkeypatch):
|
|||
)
|
||||
assert r.exit_code == 0, r.output
|
||||
assert "should-not-appear" not in r.output
|
||||
|
||||
|
||||
def test_attended_json_format_precedes_auth_data(monkeypatch, tmp_path):
|
||||
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
||||
calls = []
|
||||
def run(argv, **kwargs):
|
||||
calls.append(argv)
|
||||
return subprocess.CompletedProcess(argv, 1, b"", b"")
|
||||
monkeypatch.setattr("warden.proxy.subprocess.run", run)
|
||||
with pytest.raises(AttendedLoginError):
|
||||
proxy_attended_login_exec(
|
||||
ResolvedFetch(argv=["bao", "login", "-no-print", "-method=oidc", "role=platform-admin"]),
|
||||
child_argv=["must-not-run"],
|
||||
)
|
||||
assert calls[0].index("-format=json") < calls[0].index("role=platform-admin")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("shape", ["s", "hvs", "hvb", "hvr"])
|
||||
def test_contained_recovery_supports_openbao_token_shapes(monkeypatch, shape):
|
||||
from warden.proxy import _revoke_contained
|
||||
calls = []
|
||||
sentinel = shape + ".FAKE_RECOVERY_SENTINEL_12345678"
|
||||
def run(argv, **kwargs):
|
||||
calls.append((argv, dict(kwargs["env"])))
|
||||
return subprocess.CompletedProcess(argv, 1 if len(calls) == 1 else 0, b"", b"")
|
||||
monkeypatch.setattr("warden.proxy.subprocess.run", run)
|
||||
assert _revoke_contained("bao", env={}, possible_output=sentinel.encode())
|
||||
assert calls[1][1]["BAO_TOKEN"] == sentinel
|
||||
assert all(sentinel not in " ".join(argv) for argv, _ in calls)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("prefix,category", [
|
||||
("timed out waiting for response from provider", "callback_timeout"),
|
||||
("Error handling callback", "callback_error"),
|
||||
("Error authenticating:", "authentication_error"),
|
||||
("unrecognized backend error", "unknown_client_failure"),
|
||||
])
|
||||
def test_attended_failure_is_fixed_category_without_client_bytes(monkeypatch, tmp_path, prefix, category):
|
||||
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
||||
sentinel = "SECRET_CLIENT_OUTPUT_SENTINEL"
|
||||
def run(argv, **kwargs):
|
||||
return subprocess.CompletedProcess(argv, 1, b"", (prefix + " " + sentinel).encode())
|
||||
monkeypatch.setattr("warden.proxy.subprocess.run", run)
|
||||
with pytest.raises(AttendedLoginError, match="category=" + category) as exc:
|
||||
proxy_attended_login_exec(ResolvedFetch(argv=["bao", "login", "-no-print"]), child_argv=["must-not-run"])
|
||||
assert sentinel not in str(exc.value)
|
||||
|
|
|
|||
|
|
@ -89,3 +89,22 @@ messages; it should write only approved, value-free evidence to its own receipt.
|
|||
- Human identity and MFA provider: key-cape / Keycloak
|
||||
- Database-engine procedure owner: `rapp-postgres`
|
||||
- Routing decision and founder-act surface: WARDEN-WP-0029
|
||||
|
||||
## 2026-10-15 contained failure diagnostics
|
||||
|
||||
A failed login now reports only fixed categories (`callback_timeout`,
|
||||
`callback_error`, `authentication_error`, `unknown_client_failure`,
|
||||
`helper_persistence_missing`, `output_shape_rejected`), the numeric client exit
|
||||
code, helper-present and allowed-output booleans. These are observations, not
|
||||
authorization or proof of an issued session. The original captured bytes remain
|
||||
discarded, and the existing strict output allowlist is unchanged.
|
||||
|
||||
Synthesized JSON-format flags precede positional auth data. OpenBao 2.5.5 also
|
||||
scans global format flags across argv; this normalization is not a proven fix
|
||||
for the October 15 timeout. Emergency self-revocation recognizes the estate's
|
||||
`s.` service-token shape as well as `hvs.`, `hvb.` and `hvr.`. Recovery still uses
|
||||
a value only in memory for `bao token revoke -self`, never a new handoff.
|
||||
|
||||
The two CUST81 failed attempts remain failed and revocation-unconfirmed. Fresh
|
||||
Firefox profiles prove browser initialization, not callback completion. Source
|
||||
receipt: `docs/evidence/2026-10-15-cust81-attended-dependency-repair-r1.json`.
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ status: finished
|
|||
owner: codex
|
||||
topic_slug: attended-login-openbao-output
|
||||
created: "2026-09-01"
|
||||
updated: "2026-10-14"
|
||||
updated: "2026-10-15"
|
||||
state_hub_workstream_id: "d844c96e-152d-53fa-bff6-e072125ef66c"
|
||||
---
|
||||
|
||||
|
|
@ -111,3 +111,21 @@ Tightened the same day after an independent re-review (containment gap): stdout
|
|||
the stderr OIDC prompt must start at the first byte (no leading whitespace;
|
||||
trailing newlines remain fine). The fail-closed and hand-off tests now run through
|
||||
the CLI and assert sentinel absence from both `access-audit.log` and `audit.jsonl`.
|
||||
|
||||
### 2026-10-15 CUST81 dependency correction (existing T01/T02)
|
||||
|
||||
Prepared source correction after the two CUST81 attended platform-admin attempts
|
||||
failed before child handoff. The strict login-output allowlist remains unchanged.
|
||||
Synthesized format flags now precede auth data; the recovery-only matcher handles
|
||||
OpenBao `s.` tokens; failure messages expose only fixed categories, client exit
|
||||
code and helper/output-shape booleans. No credential bytes or URLs are surfaced.
|
||||
|
||||
Do not attribute the observed 121-second failure to the argv order: Bao 2.5.5
|
||||
scans global format throughout argv. Actual callback/auth failure remains unknown.
|
||||
Browser profile initialization was verified from metadata only; server revocation
|
||||
remains unconfirmed and the failed attempts are retained. No login, admin token
|
||||
read, policy application or provisioning was performed by this correction.
|
||||
|
||||
Source verification is distinct from live verification. The already-approved
|
||||
CUST81 execution scope and its open attended gate remain with CUST-WP-0081-T02.
|
||||
Owner evidence: `docs/evidence/2026-10-15-cust81-attended-dependency-repair-r1.json`.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue