From 9dc7e140b865439f237ec75c6dd6718f9f2da635 Mon Sep 17 00:00:00 2001 From: tegwick Date: Tue, 26 May 2026 01:50:57 +0200 Subject: [PATCH] Refine OpenBao taint resolution --- .../security_bootstrap_console.py | 41 +++++++++++++++---- ...-custody-and-openbao-identity-bootstrap.md | 6 +++ 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/tools/security-bootstrap-console/security_bootstrap_console.py b/tools/security-bootstrap-console/security_bootstrap_console.py index d228b55..bc90326 100755 --- a/tools/security-bootstrap-console/security_bootstrap_console.py +++ b/tools/security-bootstrap-console/security_bootstrap_console.py @@ -1029,10 +1029,33 @@ def state_value(ok: bool, set_value: bool = False, err: bool = False) -> str: return "nil" -def openbao_trial_taint(data: dict[str, Any], relation: str = "downstream") -> dict[str, Any]: +def openbao_trial_taint( + data: dict[str, Any], + relation: str = "downstream", + material: str = "general", +) -> dict[str, Any]: if not yes(data, "openbao_trial_material_exposed") or yes(data, "openbao_compromise_response_complete"): return {} + unseal_resolved = yes(data, "openbao_unseal_keys_rotated") + root_resolved = data.get("root_token_disposition") == "revoked" + if material == "unseal" and unseal_resolved: + return {} + if material == "root-token" and root_resolved: + return {} + relation_text = "Directly marked" if relation == "direct" else "Downstream" + unresolved = [] + if not unseal_resolved: + unresolved.append("exposed unseal shares need rotation") + if not root_resolved: + unresolved.append("exposed initial root token needs revocation") + if unresolved: + resolution = " Remaining: " + "; ".join(unresolved) + "." + else: + resolution = ( + " Exposed unseal shares are rotated and the exposed initial root token is revoked; " + "only residual downstream review remains before marking the compromise response complete." + ) return { "tainted": True, "taint_source": "Trial key material exposed", @@ -1040,7 +1063,8 @@ def openbao_trial_taint(data: dict[str, Any], relation: str = "downstream") -> d "taint_reason": ( f"{relation_text} from recorded OpenBao trial key-material exposure. " "Operator may proceed, but resulting evidence and work should be treated as tainted " - "until rotation, reset, or another compromise response is recorded." + "until rotation, revocation, reset, or another compromise response is recorded." + + resolution ), } @@ -1232,7 +1256,8 @@ def artifact_payloads(data: dict[str, Any]) -> list[dict[str, str]]: state = bootstrap_secret_state() root_disposition = str(data.get("root_token_disposition") or "") init_output = yes(data, "openbao_init_output_produced") - openbao_direct_taint = openbao_trial_taint(data, "direct") + openbao_unseal_taint = openbao_trial_taint(data, "direct", "unseal") + openbao_root_taint = openbao_trial_taint(data, "direct", "root-token") return [ { "name": "platform-root", @@ -1334,7 +1359,7 @@ def artifact_payloads(data: dict[str, Any]) -> list[dict[str, str]]: "location": "created during attended init; not stored here", "state": state_value(yes(data, "openbao_unseal_keys_rotated"), init_output or yes(data, "openbao_initialized")), }, - openbao_direct_taint, + openbao_unseal_taint, ), add_taint( { @@ -1346,7 +1371,7 @@ def artifact_payloads(data: dict[str, Any]) -> list[dict[str, str]]: "location": "created during attended init; never pasted here", "state": state_value(root_disposition in {"revoked", "offline-sealed"}, init_output or yes(data, "openbao_initialized")), }, - openbao_direct_taint, + openbao_root_taint, ), ] @@ -1449,8 +1474,10 @@ def runbook_payloads(data: dict[str, Any]) -> list[dict[str, str]]: initial_config_applied = yes(data, "openbao_initial_config_applied") trial_exposed = yes(data, "openbao_trial_material_exposed") response_complete = yes(data, "openbao_compromise_response_complete") + keys_rotated = yes(data, "openbao_unseal_keys_rotated") openbao_direct_taint = openbao_trial_taint(data, "direct") openbao_downstream_taint = openbao_trial_taint(data, "downstream") + openbao_unseal_taint = openbao_trial_taint(data, "downstream", "unseal") key_compromise_location = "Template: record the exposure, choose reset versus rotation, inspect affected paths, and record only the non-secret outcome." if trial_exposed and not response_complete: @@ -1493,7 +1520,7 @@ def runbook_payloads(data: dict[str, Any]) -> list[dict[str, str]]: "location": rotate_location, "state": "template", }, - openbao_downstream_taint if trial_exposed and not response_complete else {}, + openbao_unseal_taint if trial_exposed and not response_complete and not keys_rotated else {}, ), add_taint( { @@ -2753,7 +2780,7 @@ def ui_html() -> str:
- +
diff --git a/workplans/NET-WP-0015-platform-root-custody-and-openbao-identity-bootstrap.md b/workplans/NET-WP-0015-platform-root-custody-and-openbao-identity-bootstrap.md index 61d1147..c674d59 100644 --- a/workplans/NET-WP-0015-platform-root-custody-and-openbao-identity-bootstrap.md +++ b/workplans/NET-WP-0015-platform-root-custody-and-openbao-identity-bootstrap.md @@ -309,6 +309,12 @@ Integration with gates for the dedicated KeyCape OpenBao client, OpenBao OIDC/JWT auth configuration, and MFA-backed OpenBao admin login verification; cleanup and reopening move to S5/S6. +**2026-05-26:** Refined the OpenBao trial-exposure taint model so direct +unseal-share taint clears after confirmed unseal-key rotation, and direct +initial-root-token taint clears after the exposed OpenBao root token is +revoked. Downstream work remains visibly tainted until derived access paths +are reviewed and the compromise response is explicitly recorded complete. + **2026-05-24:** Stepped back from ad hoc secret rollout and added the custodian age-key bootstrap model to the control surface. The UI now records the custodian public age recipient, a derived fingerprint, and a non-secret