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 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. 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 ## Pre-seal hold point
- An approved, bounded window names the accountable platform driver and a - An approved, bounded window names the accountable platform driver and a

View file

@ -171,6 +171,29 @@ entries:
exec_capable: true exec_capable: true
lane: login 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 - id: whynot-design-npm-publish
title: whynot-design npm publish token (@whynot/design → coulomb Gitea registry) 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). # 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}", "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): if _APPROVAL_SIGNS.search(need):
return FounderAct( return FounderAct(
kind="approve", kind="approve",
@ -293,8 +303,9 @@ def build_plan(
domain=domain, domain=domain,
) )
# Login lanes always need a human identity act # Login and ceremony lanes always need a human act. A ceremony is a pure
if entry.lane == "login": # 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) act = _founder_for_entry(entry, need, domain)
return AccessPlan( return AccessPlan(
need=need, need=need,
@ -308,7 +319,11 @@ def build_plan(
founder_act=act, founder_act=act,
catalog=freshness, catalog=freshness,
candidates=candidates, 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, actor=actor,
domain=domain, domain=domain,
) )

View file

@ -69,7 +69,7 @@ _REQUIRED_FIELDS = (
"workload_ref", "workload_ref",
) )
_VALID_STATUS = ("active", "draft") _VALID_STATUS = ("active", "draft")
_VALID_LANES = ("secret", "login") _VALID_LANES = ("secret", "login", "ceremony")
_VALID_ROTATION_METHODS = ("rotate", "re-establish") _VALID_ROTATION_METHODS = ("rotate", "re-establish")
# Default review cadence for a catalog pointer — "is this still the right owner # 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 # "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), # no identity yet), no caller-auth precheck (the point is to get one),
# run interactively as the caller; warden never captures the token. # 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" lane: str = "secret"
# Owner-native exec front door (WP-0019). When `exec_owner` is set, that subsystem # 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 # (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(): def test_plan_unroutable():
# Zero keyword overlap with catalog (avoid tokens like secret/key/token) # Zero keyword overlap with catalog (avoid tokens like secret/key/token)
plan = build_plan("xyzzy-plugh-fnord-qqq-zzzz") 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" 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(): def test_find_exact_id_wins_over_keyword_collision():
catalog = load_catalog(_repo_catalog()) catalog = load_catalog(_repo_catalog())
# "npm" alone collides with openbao-api-key; the exact id must resolve uniquely. # "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) | | `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-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-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 | | `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 | | `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 | | `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 approved ops-bridge unattended-signing design may re-evaluate the narrow
AppRole under its own workplan; that is service access, not recovery authority. 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: Tamper-evident policy governance + reconcile
```task ```task