Refine OpenBao taint resolution

This commit is contained in:
2026-05-26 01:50:57 +02:00
parent 500e616202
commit 9dc7e140b8
2 changed files with 40 additions and 7 deletions

View File

@@ -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:
<div id="runbooks-records" class="record-list"></div>
<div class="choice-list">
<label class="choice"><input id="openbao_trial_material_exposed" type="checkbox"><span><strong>Trial key material exposed</strong><span>Init output, unseal shares, or root-token material escaped the custody boundary during a trial.</span></span></label>
<label class="choice"><input id="openbao_compromise_response_complete" type="checkbox"><span><strong>Compromise response complete</strong><span>Exposed material was rotated or the trial environment was reset. No secret values are recorded here.</span></span></label>
<label class="choice"><input id="openbao_compromise_response_complete" type="checkbox"><span><strong>Compromise response complete</strong><span>Unseal shares, root token, and derived access paths were rotated, revoked, reset, or accepted as residual risk. No secret values are recorded here.</span></span></label>
<label class="choice"><input id="openbao_unseal_keys_rotated" type="checkbox"><span><strong>New unseal keys generated</strong><span>OpenBao generated replacement unseal shares under the current runbook.</span></span></label>
<label class="choice"><input id="openbao_emergency_lockdown_drilled" type="checkbox"><span><strong>Emergency lock-down drill recorded</strong><span>Railiance OpenBao was sealed and status-confirmed during a drill or real lock-down. No token or share is recorded here.</span></span></label>
</div>

View File

@@ -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