fix: route OpenBao recovery ceremonies safely
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a0290b-3241-74c3-b868-6049545af836
This commit is contained in:
parent
fff76ef089
commit
e3b9b1620c
10 changed files with 131 additions and 4 deletions
|
|
@ -37,6 +37,11 @@ Hard lockdown is not an agent command. The irreversible hold point is the
|
|||
platform owner's attended seal action. All prerequisites below must be true
|
||||
before that hold point; repository access alone grants no authority to cross it.
|
||||
|
||||
Route planning must select `openbao-shamir-recovery-ceremony` and return one
|
||||
`founder_required` approval act. A result that selects `openbao-api-key`, asks
|
||||
for paste-once provisioning, or offers any raw-value transport is a routing
|
||||
failure and must not be executed.
|
||||
|
||||
## Pre-seal hold point
|
||||
|
||||
- An approved, bounded window names the accountable platform driver and a
|
||||
|
|
|
|||
|
|
@ -171,6 +171,29 @@ entries:
|
|||
exec_capable: true
|
||||
lane: login
|
||||
|
||||
- id: openbao-shamir-recovery-ceremony
|
||||
title: Attended OpenBao Shamir seal and unseal recovery ceremony
|
||||
# A ceremony pointer, not a credential-value lane. Approval coordinates
|
||||
# existing out-of-band custodians; Warden never requests or transports a share.
|
||||
risk: high
|
||||
workload_ref:
|
||||
applicability: not-applicable
|
||||
reason: "Attended platform trust-root ceremony; no workload credential is retrieved."
|
||||
need_keywords: [openbao, shamir, seal, unseal, sealed, emergency, recovery, break-glass, quorum, share, shares, custodian, ceremony, raft, snapshot, provider-console, abort, attended]
|
||||
owner_repo: railiance-platform
|
||||
subsystem: OpenBao operator recovery
|
||||
warden_executes: false
|
||||
wiki_ref: wiki/playbooks/openbao-shamir-recovery-ceremony.md#worker-checklist
|
||||
canon_ref: railiance-platform/docs/railiance01-coordinated-reboot.md
|
||||
reviewed: "2026-08-22"
|
||||
status: active
|
||||
delegation:
|
||||
mode: native
|
||||
intended_owner: railiance-platform
|
||||
reviewed: "2026-08-22"
|
||||
verified: source-read
|
||||
lane: ceremony
|
||||
|
||||
- id: whynot-design-npm-publish
|
||||
title: whynot-design npm publish token (@whynot/design → coulomb Gitea registry)
|
||||
# Publish rights to the package registry — a leaked token is a supply-chain write, not a read (WARDEN-WP-0032-T05).
|
||||
|
|
|
|||
|
|
@ -185,6 +185,16 @@ def _founder_for_entry(entry: RouteEntry, need: str, domain: Optional[str]) -> F
|
|||
"desk_hint": f"warden desk --from-plan (act=oidc_login) or: {expanded.fetch_command}",
|
||||
},
|
||||
)
|
||||
if entry.lane == "ceremony":
|
||||
return FounderAct(
|
||||
kind="approve",
|
||||
summary=f"Attended owner ceremony approval required for {entry.id}",
|
||||
details={
|
||||
"lane_id": entry.id,
|
||||
"wiki_ref": entry.wiki_ref,
|
||||
"desk_hint": "warden desk --act approve --lane " + entry.id,
|
||||
},
|
||||
)
|
||||
if _APPROVAL_SIGNS.search(need):
|
||||
return FounderAct(
|
||||
kind="approve",
|
||||
|
|
@ -293,8 +303,9 @@ def build_plan(
|
|||
domain=domain,
|
||||
)
|
||||
|
||||
# Login lanes always need a human identity act
|
||||
if entry.lane == "login":
|
||||
# Login and ceremony lanes always need a human act. A ceremony is a pure
|
||||
# owner pointer: it must never fall through to secret paste-once mechanics.
|
||||
if entry.lane in ("login", "ceremony"):
|
||||
act = _founder_for_entry(entry, need, domain)
|
||||
return AccessPlan(
|
||||
need=need,
|
||||
|
|
@ -308,7 +319,11 @@ def build_plan(
|
|||
founder_act=act,
|
||||
catalog=freshness,
|
||||
candidates=candidates,
|
||||
reasons=["login lane requires interactive founder/operator identity act"],
|
||||
reasons=[
|
||||
"login lane requires interactive founder/operator identity act"
|
||||
if entry.lane == "login"
|
||||
else "ceremony lane requires attended owner approval"
|
||||
],
|
||||
actor=actor,
|
||||
domain=domain,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ _REQUIRED_FIELDS = (
|
|||
"workload_ref",
|
||||
)
|
||||
_VALID_STATUS = ("active", "draft")
|
||||
_VALID_LANES = ("secret", "login")
|
||||
_VALID_LANES = ("secret", "login", "ceremony")
|
||||
_VALID_ROTATION_METHODS = ("rotate", "re-establish")
|
||||
|
||||
# Default review cadence for a catalog pointer — "is this still the right owner
|
||||
|
|
|
|||
|
|
@ -174,6 +174,8 @@ class RouteEntry:
|
|||
# "login" — interactive auth bootstrap (OIDC/MFA). No secret-read gate (you have
|
||||
# no identity yet), no caller-auth precheck (the point is to get one),
|
||||
# run interactively as the caller; warden never captures the token.
|
||||
# "ceremony" — attended owner operation such as Shamir seal/unseal. It is a
|
||||
# pointer plus approval boundary, never an executable access lane.
|
||||
lane: str = "secret"
|
||||
# Owner-native exec front door (WP-0019). When `exec_owner` is set, that subsystem
|
||||
# (e.g. secrets-engine) provides the PRIMARY way to run a secret-backed command; the
|
||||
|
|
|
|||
|
|
@ -80,6 +80,23 @@ def test_plan_first_time_openbao_database_admin_uses_platform_admin_login():
|
|||
)
|
||||
|
||||
|
||||
def test_plan_openbao_shamir_recovery_uses_approval_ceremony_not_secret_provision():
|
||||
plan = build_plan(
|
||||
"coordinate one attended production OpenBao emergency seal/unseal drill "
|
||||
"with a fresh encrypted off-host Raft snapshot receipt, verified "
|
||||
"provider-console access, two-of-three Shamir custodian quorum, named "
|
||||
"driver and abort operator, without exposing credential values"
|
||||
)
|
||||
assert plan.verdict == "founder_required"
|
||||
assert plan.lane_id == "openbao-shamir-recovery-ceremony"
|
||||
assert plan.founder_act is not None
|
||||
assert plan.founder_act.kind == "approve"
|
||||
assert "openbao-shamir-recovery-ceremony" in plan.founder_act.details["desk_hint"]
|
||||
assert "paste_once" not in plan.founder_act.details["desk_hint"]
|
||||
assert all("warden access" not in item for item in plan.commands)
|
||||
assert any("openbao-shamir-recovery-ceremony" in item for item in plan.commands)
|
||||
|
||||
|
||||
def test_plan_unroutable():
|
||||
# Zero keyword overlap with catalog (avoid tokens like secret/key/token)
|
||||
plan = build_plan("xyzzy-plugh-fnord-qqq-zzzz")
|
||||
|
|
|
|||
|
|
@ -170,6 +170,18 @@ def test_platform_admin_login_lane_is_exact_and_non_value_bearing():
|
|||
assert entry.workload_ref.resolution == "not-applicable"
|
||||
|
||||
|
||||
def test_openbao_recovery_ceremony_is_non_value_bearing_owner_pointer():
|
||||
entry = load_catalog(_repo_catalog()).get("openbao-shamir-recovery-ceremony")
|
||||
assert entry.lane == "ceremony"
|
||||
assert entry.risk == "high"
|
||||
assert entry.owner_repo == "railiance-platform"
|
||||
assert entry.warden_executes is False
|
||||
assert entry.exec_capable is False
|
||||
assert entry.has_handoff is False
|
||||
assert entry.vends_secret is False
|
||||
assert entry.workload_ref.resolution == "not-applicable"
|
||||
|
||||
|
||||
def test_find_exact_id_wins_over_keyword_collision():
|
||||
catalog = load_catalog(_repo_catalog())
|
||||
# "npm" alone collides with openbao-api-key; the exact id must resolve uniquely.
|
||||
|
|
|
|||
|
|
@ -91,6 +91,7 @@ lists the interim set — WARDEN-WP-0030).
|
|||
| `ops-warden-warden-sign-token` | "railiance-platform broker owns the `warden-sign` lease — use `credential exec`" | `railiance-platform/scripts/credential.py exec --grant ops-warden/warden-sign` (see playbook) |
|
||||
| `openbao-api-key` | "OpenBao owns this — here is the path/command shape" | Call OpenBao directly, or use `warden access --fetch/--exec` as yourself when the lane is `exec_capable` |
|
||||
| `openbao-platform-admin-login` | "This is an attended OpenBao administration identity act, not workload secret retrieval" | Use KeyCape-backed OIDC/MFA at `auth/netkingdom`, role `platform-admin`; never substitute a workload role or root token |
|
||||
| `openbao-shamir-recovery-ceremony` | "This is an attended trust-root ceremony, not secret provisioning" | Obtain one explicit approval and follow the railiance-platform recovery checklist with the existing out-of-band custodians; never send shares through Warden |
|
||||
| `flex-auth-policy-check` | "flex-auth decides — here is the policy doc" | Query flex-auth / embed the PEP |
|
||||
| `key-cape-oidc-login` | "key-cape / Keycloak owns identity" | Authenticate via IAM Profile, or use the `warden access` login lane as yourself |
|
||||
| `ops-bridge-tunnel` | "ops-bridge owns transport — supply a `cert_command`" | Open the tunnel with ops-bridge |
|
||||
|
|
|
|||
37
wiki/playbooks/openbao-shamir-recovery-ceremony.md
Normal file
37
wiki/playbooks/openbao-shamir-recovery-ceremony.md
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# OpenBao Shamir recovery ceremony
|
||||
|
||||
This lane routes an attended production seal/unseal or trust-root recovery need
|
||||
to `railiance-platform`. It does not retrieve, provision, proxy, or transport an
|
||||
unseal share, recovery value, OpenBao token, snapshot, or console credential.
|
||||
|
||||
## Worker checklist
|
||||
|
||||
1. Plan the need before drafting an operator step:
|
||||
|
||||
```bash
|
||||
warden plan "attended OpenBao Shamir emergency seal/unseal recovery ceremony" --json
|
||||
```
|
||||
|
||||
The result must select `openbao-shamir-recovery-ceremony`, return
|
||||
`founder_required`, and name one `approve` act. If it selects
|
||||
`openbao-api-key`, proposes paste-once provisioning, or requests a raw share,
|
||||
stop without executing the proposed act.
|
||||
|
||||
2. Treat approval as coordination, not secret delivery. The platform owner uses
|
||||
the existing out-of-band share custody and provider-console paths. Warden
|
||||
Desk must never receive a share or console credential.
|
||||
|
||||
3. Follow the authoritative owner checklist in
|
||||
`railiance-platform/docs/railiance01-coordinated-reboot.md` and the OpenBao
|
||||
recovery section of `railiance-platform/docs/openbao.md`. The local consumer
|
||||
boundary and evidence requirements are in
|
||||
`docs/credential-governance-break-glass.md`.
|
||||
|
||||
4. Stop before the live hold point unless the owner preflight reports
|
||||
`ready_for_live_execution: true` and the operator gives an explicit go/no-go
|
||||
for that bounded window.
|
||||
|
||||
Allowed records are non-secret approval ids, role attestations, timestamps,
|
||||
seal-state booleans, hashes, and verification outcomes. Never place shares,
|
||||
tokens, credentials, Secret data, or decrypted snapshots in Git, State Hub,
|
||||
logs, shell history, or chat.
|
||||
|
|
@ -145,6 +145,21 @@ that boundary and still would not recover a sealed OpenBao. A separately
|
|||
approved ops-bridge unattended-signing design may re-evaluate the narrow
|
||||
AppRole under its own workplan; that is service access, not recovery authority.
|
||||
|
||||
**Read-only readiness 2026-08-22.** The owner `node-reboot` preflight passed
|
||||
every automated check without observing a secret value: node/k3s, platform-pg
|
||||
and continuous archiving, a 16.37-hour successful backup, initialized/unsealed
|
||||
OpenBao with `shares=3` and `threshold=2`, ESO stores/projections, and the
|
||||
reviewed audit-core digest were healthy. `ready_for_live_execution` correctly
|
||||
remained false because no approved window, current encrypted off-host snapshot
|
||||
receipt, provider-console attestation, quorum attestation, owner-ack flag, or
|
||||
named abort operator was supplied.
|
||||
|
||||
That preflight also exposed a Warden safety defect: the ceremony request matched
|
||||
the generic `openbao-api-key` template and proposed paste-once provisioning.
|
||||
`openbao-shamir-recovery-ceremony` now distinguishes the owner-operated approval
|
||||
ceremony from secret retrieval. It is a non-value-bearing pointer and can never
|
||||
offer `--fetch`, `--exec`, `--out`, `--wrap`, or paste-once share transport.
|
||||
|
||||
## Task: Tamper-evident policy governance + reconcile
|
||||
|
||||
```task
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue