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:
tegwick 2026-08-22 20:54:45 +02:00
parent fff76ef089
commit e3b9b1620c
10 changed files with 131 additions and 4 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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.

View file

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