generated from coulomb/repo-seed
NET-WP-0020 finished: attended-ceremony + auto-unseal-transit profiles, greenfield init/unseal proof
T2: greenfield live proof against a fresh uninitialized OpenBao 2.5.5 — caught and fixed 'bao operator unseal -' not reading stdin (now 'bao write sys/unseal key=-'); init and reseal-replay paths proven. T3: attended-ceremony selectable — runbook, non-secret ceremony-record template + validator, and a lab/production deployment profile that blocks sops-held-automation in console selection, gates, and the init script. T4: console gate + evidence flags for auto-unseal-transit (Helm seal stanza prepared in railiance-platform). Also: SCOPE.md refreshed to current repo state; adhoc fix for the broken check-secrets Make target (unescaped $). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
21
Makefile
21
Makefile
@@ -106,7 +106,7 @@ check-secrets: ## Fail if any file under secrets/ is not SOPS-encrypted
|
||||
fi; \
|
||||
case "$$f" in *.age|*.gpg) continue ;; esac; \
|
||||
echo " UNENCRYPTED: $$f"; bad=1; \
|
||||
done < <(git ls-files --others --cached 'secrets' 2>/dev/null | grep -v '/$'); \
|
||||
done < <(git ls-files --others --cached 'secrets' 2>/dev/null | grep -v '/$$'); \
|
||||
if [[ "$$bad" -ne 0 ]]; then \
|
||||
echo ""; \
|
||||
echo "ERROR: Unencrypted secret(s) detected. Encrypt with: sops --encrypt --in-place <file>"; \
|
||||
@@ -311,6 +311,20 @@ security-bootstrap-select-openbao-unseal-custody-model: security-bootstrap-metad
|
||||
select-openbao-unseal-custody-model \
|
||||
--model "$(if $(MODEL),$(MODEL),sops-held-automation)"
|
||||
|
||||
security-bootstrap-select-deployment-profile: security-bootstrap-metadata-init ## Select deployment profile (production blocks sops-held-automation): make ... PROFILE=production
|
||||
@[[ -n "$(PROFILE)" ]] || (echo "Usage: make security-bootstrap-select-deployment-profile PROFILE=lab|production"; exit 1)
|
||||
python3 tools/security-bootstrap-console/security_bootstrap_console.py \
|
||||
--metadata "$(SECURITY_BOOTSTRAP_METADATA)" \
|
||||
select-deployment-profile --profile "$(PROFILE)"
|
||||
|
||||
security-bootstrap-openbao-ceremony-record-template: ## Print non-secret attended OpenBao ceremony record template
|
||||
python3 tools/security-bootstrap-console/security_bootstrap_console.py openbao-ceremony-record-template
|
||||
|
||||
security-bootstrap-validate-openbao-ceremony-record: ## Validate non-secret attended ceremony record: make ... [EVIDENCE=.local/openbao-ceremony-record.json]
|
||||
python3 tools/security-bootstrap-console/security_bootstrap_console.py \
|
||||
validate-openbao-ceremony-record \
|
||||
--evidence "$(if $(EVIDENCE),$(EVIDENCE),.local/openbao-ceremony-record.json)"
|
||||
|
||||
security-bootstrap-metadata-init: ## Create durable local non-secret bootstrap metadata if missing
|
||||
@mkdir -p "$$(dirname "$(SECURITY_BOOTSTRAP_METADATA)")"
|
||||
@if [[ -f "$(SECURITY_BOOTSTRAP_METADATA)" ]]; then \
|
||||
@@ -355,6 +369,11 @@ security-bootstrap-ui: security-bootstrap-metadata-init ## Serve local custody a
|
||||
security-bootstrap-sign-custody-roster \
|
||||
security-bootstrap-approve-custody \
|
||||
security-bootstrap-custody-packet security-bootstrap-openbao-preflight \
|
||||
security-bootstrap-openbao-unseal-custody-models \
|
||||
security-bootstrap-select-openbao-unseal-custody-model \
|
||||
security-bootstrap-select-deployment-profile \
|
||||
security-bootstrap-openbao-ceremony-record-template \
|
||||
security-bootstrap-validate-openbao-ceremony-record \
|
||||
security-bootstrap-metadata-init security-bootstrap-ui \
|
||||
security-bootstrap-console-test security-bootstrap-scripts-syntax \
|
||||
security-bootstrap-validate-keycape-client
|
||||
|
||||
49
SCOPE.md
49
SCOPE.md
@@ -22,13 +22,21 @@ NetKingdom is a self-optimizing security platform for Kubernetes-based IT infras
|
||||
|
||||
- NetKingdom IAM Profile specification (versioned OIDC/PKCE contract;
|
||||
canonical spec: `canon/standards/iam-profile_v0.2.md`)
|
||||
- SSO/MFA Platform: Keycloak with LDAP/Entra federation, enterprise identity (NK-WP-0001)
|
||||
- Local Identity: file-based user store + minimal OIDC server for bootstrap phase (NK-WP-0002)
|
||||
- SSO/MFA Platform: Keycloak with LDAP/Entra federation, enterprise identity (NK-WP-0001, finished)
|
||||
- Local Identity: file-based user store + minimal OIDC server for bootstrap phase (NK-WP-0002, finished)
|
||||
- User Engine Boundary Contract: source-of-truth, membership,
|
||||
application-onboarding, projection, authorization, and audit contracts for
|
||||
`user-engine` integration (`canon/standards/user-engine-boundary-contract_v0.1.md`)
|
||||
- Security bootstrapping: credential management, SOPS/age integration,
|
||||
platform-root custody, OpenBao runtime secret authority
|
||||
- OpenBao init/unseal custody models (NET-WP-0020): `sops-held-automation`
|
||||
(lab, unattended greenfield rebuilds via `creds-bootstrap-agent` Phase 7b),
|
||||
`attended-ceremony` (production, runbook + non-secret evidence records), and
|
||||
`auto-unseal-transit` (production HA; seal stanza lives in
|
||||
railiance-platform) — all gated by the security bootstrap console and a
|
||||
lab/production deployment profile
|
||||
- Security bootstrap console (`tools/security-bootstrap-console/`): custody
|
||||
gates, roster, evidence validators, refuse-live-init boundary
|
||||
- Architectural decisions (DECISIONS.md): identity source, secrets, GitOps, bootstrap user store
|
||||
|
||||
---
|
||||
@@ -62,9 +70,17 @@ NetKingdom is a self-optimizing security platform for Kubernetes-based IT infras
|
||||
|
||||
## Current State
|
||||
|
||||
- Status: active (design phase complete, implementation ongoing)
|
||||
- Implementation: emerging — NK-WP-0001 (SSO/MFA) and NK-WP-0002 (local identity) both in active development
|
||||
- Stability: evolving
|
||||
- Status: active — core identity and bootstrap phases delivered; follow-on work proposed
|
||||
- Implementation: NK-WP-0001 (SSO/MFA), NK-WP-0002 (local identity), the
|
||||
security bootstrap arc (NET-WP-0015–0017, 0019), the IAM Profile spec
|
||||
(NK-WP-0012), user-engine boundary contracts (NK-WP-0014), and OpenBao
|
||||
unseal custody + SSH automation (NET-WP-0020) are all finished — see
|
||||
`workplans/archived/`
|
||||
- Open: NK-WP-0009 (security pattern tutorials) and NK-WP-0011 (enterprise
|
||||
federation / SAML) are proposed, not yet started
|
||||
- Stability: stabilizing — bootstrap/custody tooling is live-proven (greenfield
|
||||
OpenBao init/unseal proof 2026-07-02); production custody models are gated
|
||||
by evidence
|
||||
- Usage: foundational authentication layer for all NetKingdom deployments
|
||||
|
||||
---
|
||||
@@ -108,6 +124,13 @@ description: Enterprise-grade Keycloak-based SSO with LDAP/Entra federation, MFA
|
||||
keywords: [sso, mfa, keycloak, ldap, entra, federation, oidc, enterprise]
|
||||
```
|
||||
|
||||
```capability
|
||||
type: security
|
||||
title: OpenBao unseal custody models and bootstrap automation
|
||||
description: Three gated init/unseal custody models — SOPS-held automation for unattended lab rebuilds (greenfield-proven), attended ceremony with non-secret evidence records for production, and transit/KMS auto-unseal for production HA — enforced by the security bootstrap console and a lab/production deployment profile.
|
||||
keywords: [openbao, unseal, custody, bootstrap, sops, age, ceremony, transit, auto-unseal, console]
|
||||
```
|
||||
|
||||
```capability
|
||||
type: security
|
||||
title: Bootstrap local identity service
|
||||
@@ -121,9 +144,12 @@ keywords: [bootstrap, local-identity, oidc, minimal, dev, sandbox]
|
||||
|
||||
- Start with: `wiki/` (specifications and decisions), `DECISIONS.md` (key architectural choices D1–D5)
|
||||
- Key files / directories: `docs/platform-root-custody.md`, `sso-mfa/`
|
||||
(NK-WP-0001 active workplan), `local-identity/` (NK-WP-0002),
|
||||
`workplans/`
|
||||
- Entry points: `workplans/NK-WP-0001-sso-mfa-platform.md` and `NK-WP-0002-local-identity.md` for current work
|
||||
(SSO/MFA platform + bootstrap scripts), `local-identity/`,
|
||||
`tools/security-bootstrap-console/`, `workplans/` (finished plans in
|
||||
`workplans/archived/`)
|
||||
- Entry points: `workplans/NK-WP-0009-netkingdom-security-pattern-tutorials.md`
|
||||
and `workplans/NK-WP-0011-enterprise-federation-saml.md` (proposed next
|
||||
work); finished context in `workplans/archived/`
|
||||
- User-domain boundary contract:
|
||||
`canon/standards/user-engine-boundary-contract_v0.1.md`
|
||||
- User-engine integration assessment (intent/scope fit, gaps, and recommendations):
|
||||
@@ -131,5 +157,8 @@ keywords: [bootstrap, local-identity, oidc, minimal, dev, sandbox]
|
||||
- Bootstrap/custody entry points:
|
||||
`docs/platform-root-custody.md`,
|
||||
`docs/security-bootstrap-use-cases.md`,
|
||||
`workplans/NET-WP-0015-platform-root-custody-and-openbao-identity-bootstrap.md`,
|
||||
and `workplans/NET-WP-0016-guided-security-bootstrap-experience.md`
|
||||
`docs/openbao-unseal-custody-models.md` (three custody models + deployment
|
||||
profile), and `docs/openbao-attended-ceremony-runbook.md` (production
|
||||
ceremony); history of the custody/bootstrap arc in `workplans/archived/`
|
||||
(NET-WP-0015–0017, 0019) and
|
||||
`workplans/NET-WP-0020-openbao-unseal-custody-and-ssh-automation.md`
|
||||
|
||||
82
docs/openbao-attended-ceremony-runbook.md
Normal file
82
docs/openbao-attended-ceremony-runbook.md
Normal file
@@ -0,0 +1,82 @@
|
||||
# OpenBao Attended Ceremony Runbook
|
||||
|
||||
Date: 2026-07-02
|
||||
Status: active — production custody model (`attended-ceremony`, NET-WP-0020 T3)
|
||||
|
||||
Human-attended OpenBao init/unseal for production trust posture. The security
|
||||
bootstrap console **never runs `bao operator init`** (refuse-live-init
|
||||
boundary) — this runbook is executed by the `openbao-ceremony-operator` role
|
||||
and evidenced by a **non-secret** ceremony record.
|
||||
|
||||
Contrast with `sops-held-automation` (lab posture): there, root token and
|
||||
unseal shares live together in one SOPS bundle for unattended rebuild loops.
|
||||
The attended ceremony keeps unseal shares **out of band** and retires the root
|
||||
token, so no single artifact can open the platform.
|
||||
|
||||
---
|
||||
|
||||
## Preconditions
|
||||
|
||||
1. Console gates green up to the init ceremony:
|
||||
`make security-bootstrap-console` — custody strategy approved, preflight
|
||||
passed.
|
||||
2. Select profile and model (production blocks the lab default):
|
||||
|
||||
```bash
|
||||
python3 tools/security-bootstrap-console/security_bootstrap_console.py \
|
||||
--metadata .local/security-bootstrap.json \
|
||||
select-deployment-profile --profile production
|
||||
|
||||
python3 tools/security-bootstrap-console/security_bootstrap_console.py \
|
||||
--metadata .local/security-bootstrap.json \
|
||||
select-openbao-unseal-custody-model --model attended-ceremony
|
||||
```
|
||||
|
||||
3. OpenBao deployed and reachable:
|
||||
`make -C ../railiance-platform openbao-status` (expect uninitialized/sealed
|
||||
on greenfield).
|
||||
4. Two people present (operator + witness) where the custody roster requires
|
||||
it; unseal-share escrow destinations agreed per
|
||||
`docs/platform-root-custody.md` (signed custody roster).
|
||||
|
||||
## Ceremony
|
||||
|
||||
Follow `railiance-platform/docs/openbao.md` for the exact commands. Outline:
|
||||
|
||||
1. **Init** — operator runs `bao operator init` (3 shares, threshold 2)
|
||||
directly against the pod from a terminal. Output goes only to the
|
||||
operator's screen — never into the console, chat, State Hub, or files
|
||||
inside a Git checkout.
|
||||
2. **Escrow** — each unseal share is transcribed to its escrow destination
|
||||
(offline packet / password safe per roster). No two shares in the same
|
||||
custody location.
|
||||
3. **Unseal** — replay threshold shares; verify
|
||||
`initialized=true sealed=false`.
|
||||
4. **Root retirement** — use the root token only for the initial
|
||||
configuration handoff (`make -C ../railiance-platform
|
||||
openbao-configure-initial`), then revoke it (`bao token revoke -self`) or
|
||||
escrow it per roster; record the disposition.
|
||||
|
||||
## Evidence
|
||||
|
||||
Record the ceremony in a non-secret JSON record and validate it:
|
||||
|
||||
```bash
|
||||
python3 tools/security-bootstrap-console/security_bootstrap_console.py \
|
||||
openbao-ceremony-record-template > .local/openbao-ceremony-record.json
|
||||
# edit: dispositions, dates, roles — NEVER shares, tokens, or key material
|
||||
|
||||
make security-bootstrap-validate-openbao-ceremony-record
|
||||
```
|
||||
|
||||
The validator refuses records containing secret-looking markers (tokens, key
|
||||
blocks, otpauth URIs) or leftover template placeholders. After a valid
|
||||
record, set the console flags (`openbao_initialized`,
|
||||
`openbao_post_unseal_verified`) in the metadata so the gates advance.
|
||||
|
||||
## Related
|
||||
|
||||
- `docs/openbao-unseal-custody-models.md` — model framework
|
||||
- `docs/platform-root-custody.md` — custody roster and share holders
|
||||
- `docs/security-bootstrap-openbao-ceremony-ux.md` — operator UX notes
|
||||
- `railiance-platform/docs/openbao.md` — deploy + command-level ceremony
|
||||
@@ -1,7 +1,8 @@
|
||||
# OpenBao Unseal Custody Models
|
||||
|
||||
Date: 2026-06-17
|
||||
Status: framework — automation path active; production paths planned
|
||||
Date: 2026-06-17 (updated 2026-07-02)
|
||||
Status: all three models implemented — automation path proven greenfield;
|
||||
production models gated by deployment profile and evidence
|
||||
|
||||
NetKingdom bootstrap must support **three** OpenBao init/unseal custody models.
|
||||
Development starts with **maximum automation** for fast test cycles, then adds
|
||||
@@ -18,9 +19,9 @@ during bootstrap and rebuild.
|
||||
|
||||
| Model ID | Label | Custody strength | Automation | Status |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `sops-held-automation` | SOPS-held unseal | Lab / fast iteration | High | **Implemented** (console + creds agent path) |
|
||||
| `attended-ceremony` | Attended ceremony | Production | Low | Planned |
|
||||
| `auto-unseal-transit` | Auto-unseal (transit/KMS) | Production HA | High | Planned |
|
||||
| `sops-held-automation` | SOPS-held unseal | Lab / fast iteration | High | **Implemented** (console + creds agent path; greenfield-proven 2026-07-02) |
|
||||
| `attended-ceremony` | Attended ceremony | Production | Low | **Implemented** (runbook + ceremony-record validator) |
|
||||
| `auto-unseal-transit` | Auto-unseal (transit/KMS) | Production HA | High | **Implemented** (Helm seal stanza prepared; gate blocked until seal configured + verified) |
|
||||
|
||||
### `sops-held-automation` (default for greenfield dev)
|
||||
|
||||
@@ -37,32 +38,42 @@ during bootstrap and rebuild.
|
||||
- **Not** production trust posture — use to prove S1→S3→SSH engine automation,
|
||||
then graduate to stronger models.
|
||||
|
||||
### `attended-ceremony` (production target)
|
||||
### `attended-ceremony` (production)
|
||||
|
||||
- Human-attended `bao operator init`, out-of-band unseal share escrow, root token
|
||||
retirement — per `railiance-platform/docs/openbao.md`.
|
||||
retirement — runbook: `docs/openbao-attended-ceremony-runbook.md`
|
||||
(command detail in `railiance-platform/docs/openbao.md`).
|
||||
- Matches first successful NetKingdom bootstrap (NET-WP-0015–0017).
|
||||
- Console keeps **refuse-live-init** boundary; ceremony runbooks only.
|
||||
- Console keeps the **refuse-live-init** boundary; the ceremony is evidenced by
|
||||
a non-secret record validated with `validate-openbao-ceremony-record`
|
||||
(`make security-bootstrap-validate-openbao-ceremony-record`).
|
||||
|
||||
### `auto-unseal-transit` (production HA target)
|
||||
### `auto-unseal-transit` (production HA)
|
||||
|
||||
- OpenBao seal configuration uses **transit** or cloud KMS auto-unseal.
|
||||
- Pod restart without manual unseal threshold ceremony.
|
||||
- Requires `railiance-platform` Helm seal stanza + KMS provisioning.
|
||||
- Seal stanza prepared (disabled by default) in
|
||||
`railiance-platform/helm/openbao-values.yaml`; enable + migrate per
|
||||
`railiance-platform/docs/openbao.md` "Auto-Unseal via Transit Seal".
|
||||
- Console init gate stays **blocked** until `openbao_transit_seal_configured`
|
||||
and `openbao_auto_unseal_verified` are set in the non-secret metadata.
|
||||
|
||||
---
|
||||
|
||||
## Development strategy
|
||||
## Deployment profile
|
||||
|
||||
```text
|
||||
1. Implement automation path (sops-held-automation)
|
||||
→ SSH engine, warden sign, host CA trust, 3-node rebuild loops
|
||||
2. Add attended-ceremony gates (block automation defaults in production profile)
|
||||
3. Add auto-unseal-transit for HA ThreePhoenix rebuilds
|
||||
The console metadata carries a `deployment_profile` (`lab` default,
|
||||
`production`). The **production profile blocks `sops-held-automation`** — its
|
||||
SOPS bundle holds root token and unseal shares together, which is lab posture
|
||||
only. Selection, status gates, and `openbao-init-unseal.sh` all enforce the
|
||||
block:
|
||||
|
||||
```bash
|
||||
make security-bootstrap-select-deployment-profile PROFILE=production
|
||||
```
|
||||
|
||||
Each model is selectable in the **security bootstrap console**. Unimplemented
|
||||
models are **blocked** with a hint pointing to the active automation path.
|
||||
Each model is selectable in the **security bootstrap console**; gates express
|
||||
what evidence is still missing for the selected model.
|
||||
|
||||
---
|
||||
|
||||
|
||||
40
history/2026-07-02-openbao-greenfield-init-unseal-proof.md
Normal file
40
history/2026-07-02-openbao-greenfield-init-unseal-proof.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# OpenBao greenfield init/unseal proof (NET-WP-0020 T2)
|
||||
|
||||
Date: 2026-07-02
|
||||
Scope: `sso-mfa/bootstrap/openbao-init-unseal.sh` proven live against a
|
||||
genuinely uninitialized OpenBao 2.5.5 instance.
|
||||
|
||||
## Method
|
||||
|
||||
A fresh local `bao server` (file storage, throwaway scratchpad dir) was started
|
||||
uninitialized and sealed. A `kubectl` shim forwarded the script's
|
||||
`kubectl -n openbao exec openbao-0 -- bao …` calls to the local instance, so
|
||||
the script ran byte-for-byte unmodified logic against real OpenBao. Console
|
||||
metadata lived in a scratch file with `sops-held-automation` selected.
|
||||
|
||||
This substitutes for the "rebuild slate" run: it exercises the exact
|
||||
init/unseal/verify code paths on a greenfield instance. The first 3-node
|
||||
rebuild will re-run the same script through `creds-bootstrap-agent.sh`
|
||||
Phase 7b.
|
||||
|
||||
## Results
|
||||
|
||||
1. **Custody gate refusal** — with no model selected, the script refused
|
||||
(`unseal custody model is 'unselected'`). Matches earlier negative tests.
|
||||
2. **Bug found and fixed** — `bao operator unseal -` does **not** read the
|
||||
share from stdin (OpenBao treats `-` as the literal key: *"'key' must be a
|
||||
valid hex or base64 string"*; without an argument it demands a TTY). The
|
||||
live cluster never hit this because it was already unsealed. Fixed to
|
||||
`bao write sys/unseal key=-`, which reads the value from stdin — shares
|
||||
still never touch argv or logs.
|
||||
3. **Greenfield init path** — uninitialized → `operator init` (3 shares,
|
||||
threshold 2) → init.json written 0600 into the age-custody secrets dir →
|
||||
share replay stopped at threshold (share 2 of 3) → post-unseal verified.
|
||||
Evidence: `openbao_did_init_this_run=true`, `openbao_initialized=true`,
|
||||
`openbao_post_unseal_verified=true`.
|
||||
4. **Restart/reseal path** — server restarted (initialized + sealed) →
|
||||
script skipped init, replayed SOPS-held shares to threshold → verified.
|
||||
Evidence: `openbao_did_init_this_run=false`, both flags true.
|
||||
|
||||
Test server, storage, and init material were destroyed after the proof
|
||||
(init.json shredded).
|
||||
@@ -78,8 +78,15 @@ PY
|
||||
# Only sops-held-automation may be automated. attended-ceremony and
|
||||
# auto-unseal-transit must never reach `bao operator init/unseal` from here.
|
||||
MODEL=""
|
||||
PROFILE=""
|
||||
if [[ -f "$CONSOLE_METADATA" ]]; then
|
||||
MODEL=$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get("openbao_unseal_custody_model") or "")' "$CONSOLE_METADATA")
|
||||
PROFILE=$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get("deployment_profile") or "")' "$CONSOLE_METADATA")
|
||||
fi
|
||||
if [[ "$PROFILE" == "production" ]]; then
|
||||
die "deployment profile is 'production' — sops-held automation is blocked.
|
||||
Use the attended ceremony (docs/openbao-attended-ceremony-runbook.md) or
|
||||
auto-unseal-transit, or reselect: select-deployment-profile --profile lab"
|
||||
fi
|
||||
if [[ "$MODEL" != "sops-held-automation" ]]; then
|
||||
die "unseal custody model is '${MODEL:-unselected}' — automation requires 'sops-held-automation'.
|
||||
@@ -145,8 +152,10 @@ if [[ "$SEALED" == "true" ]]; then
|
||||
log "replaying unseal shares (have $SHARE_COUNT)..."
|
||||
for idx in $(seq 0 $((SHARE_COUNT - 1))); do
|
||||
# Share travels stdin→stdin; never argv, never logs.
|
||||
# `bao operator unseal -` does not read stdin (needs a TTY or the
|
||||
# share in argv), so use the sys/unseal API with key=- instead.
|
||||
python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["unseal_keys_b64"][int(sys.argv[2])])' "$INIT_FILE" "$idx" \
|
||||
| kubectl -n "$NAMESPACE" exec -i "$POD" -- bao operator unseal - >/dev/null
|
||||
| kubectl -n "$NAMESPACE" exec -i "$POD" -- bao write sys/unseal key=- >/dev/null
|
||||
STATUS_JSON="$(bao_status)"
|
||||
SEALED=$(python3 -c 'import json,sys; print(str(json.loads(sys.stdin.read()).get("sealed", True)).lower())' <<<"$STATUS_JSON")
|
||||
log "unseal share $((idx + 1)) applied (sealed=$SEALED)"
|
||||
|
||||
@@ -64,30 +64,28 @@ OPENBAO_UNSEAL_CUSTODY_MODEL_SPECS: dict[str, dict[str, str]] = {
|
||||
},
|
||||
"attended-ceremony": {
|
||||
"label": "Attended ceremony (production custody)",
|
||||
"implementation": "planned",
|
||||
"implementation": "implemented",
|
||||
"summary": (
|
||||
"Human-attended init, out-of-band unseal escrow, root retirement — "
|
||||
"railiance-platform/docs/openbao.md ceremony."
|
||||
),
|
||||
"blocked_hint": (
|
||||
"Not yet implemented in the automation path. Use sops-held-automation for "
|
||||
"fast bootstrap test cycles; attended ceremony will gate production trust."
|
||||
"railiance-platform/docs/openbao.md ceremony. Console never runs init; "
|
||||
"runbook + evidence validation only."
|
||||
),
|
||||
"runbook_entry": "docs/openbao-attended-ceremony-runbook.md",
|
||||
"custody_strength": "production",
|
||||
},
|
||||
"auto-unseal-transit": {
|
||||
"label": "Auto-unseal (transit/KMS seal)",
|
||||
"implementation": "planned",
|
||||
"implementation": "implemented",
|
||||
"summary": (
|
||||
"Seal config uses transit or cloud KMS; pod restart without manual unseal."
|
||||
),
|
||||
"blocked_hint": (
|
||||
"Not yet implemented. Requires railiance-platform Helm seal stanza and "
|
||||
"KMS/transit provisioning. Use sops-held-automation until available."
|
||||
"Seal config uses transit or cloud KMS; pod restart without manual unseal. "
|
||||
"Gate stays blocked until the seal stanza is applied and auto-unseal is verified."
|
||||
),
|
||||
"config_entry": "railiance-platform/helm/openbao-values.yaml (seal stanza)",
|
||||
"custody_strength": "production-ha",
|
||||
},
|
||||
}
|
||||
VALID_DEPLOYMENT_PROFILES = ("lab", "production")
|
||||
DEFAULT_DEPLOYMENT_PROFILE = "lab"
|
||||
VALID_OPENBAO_UNSEAL_CUSTODY_MODELS = frozenset(OPENBAO_UNSEAL_CUSTODY_MODEL_SPECS)
|
||||
DEFAULT_OPENBAO_UNSEAL_CUSTODY_MODEL = "sops-held-automation"
|
||||
IMPLEMENTED_OPENBAO_UNSEAL_CUSTODY_MODELS = frozenset(
|
||||
@@ -461,14 +459,38 @@ def openbao_unseal_custody_model_implemented(model: str) -> bool:
|
||||
return model in IMPLEMENTED_OPENBAO_UNSEAL_CUSTODY_MODELS
|
||||
|
||||
|
||||
def resolve_deployment_profile(data: dict[str, Any]) -> str:
|
||||
profile = str(data.get("deployment_profile") or "").strip()
|
||||
if profile in VALID_DEPLOYMENT_PROFILES:
|
||||
return profile
|
||||
return DEFAULT_DEPLOYMENT_PROFILE
|
||||
|
||||
|
||||
def openbao_unseal_custody_model_entry(spec: dict[str, str]) -> str:
|
||||
for key in ("automation_entry", "runbook_entry", "config_entry"):
|
||||
if spec.get(key):
|
||||
return spec[key]
|
||||
return "n/a"
|
||||
|
||||
|
||||
def openbao_unseal_custody_model_gate(data: dict[str, Any]) -> Gate:
|
||||
model = resolve_openbao_unseal_custody_model(data)
|
||||
spec = OPENBAO_UNSEAL_CUSTODY_MODEL_SPECS[model]
|
||||
if resolve_deployment_profile(data) == "production" and model == "sops-held-automation":
|
||||
return Gate(
|
||||
"OpenBao unseal custody model",
|
||||
"blocked",
|
||||
(
|
||||
"Production profile blocks sops-held-automation (root token and unseal "
|
||||
"shares in one SOPS bundle is lab posture). Select attended-ceremony or "
|
||||
"auto-unseal-transit."
|
||||
),
|
||||
)
|
||||
if openbao_unseal_custody_model_implemented(model):
|
||||
return Gate(
|
||||
"OpenBao unseal custody model",
|
||||
"done",
|
||||
f"{spec['label']} selected — automation entry: {spec.get('automation_entry', 'n/a')}",
|
||||
f"{spec['label']} selected — entry: {openbao_unseal_custody_model_entry(spec)}",
|
||||
)
|
||||
return Gate(
|
||||
"OpenBao unseal custody model",
|
||||
@@ -489,6 +511,12 @@ def openbao_init_ceremony_gate(data: dict[str, Any]) -> Gate:
|
||||
spec.get("blocked_hint", "Selected unseal custody model is not implemented."),
|
||||
)
|
||||
if model == "sops-held-automation":
|
||||
if resolve_deployment_profile(data) == "production":
|
||||
return Gate(
|
||||
"OpenBao init ceremony",
|
||||
"blocked",
|
||||
"Production profile blocks sops-held-automation. Select a production model.",
|
||||
)
|
||||
entry = OPENBAO_UNSEAL_CUSTODY_MODEL_SPECS[model].get("automation_entry", "")
|
||||
return Gate(
|
||||
"OpenBao init ceremony",
|
||||
@@ -498,10 +526,44 @@ def openbao_init_ceremony_gate(data: dict[str, Any]) -> Gate:
|
||||
f"({entry}). Console will not run init."
|
||||
),
|
||||
)
|
||||
if model == "auto-unseal-transit":
|
||||
if not yes(data, "openbao_transit_seal_configured"):
|
||||
return Gate(
|
||||
"OpenBao init ceremony",
|
||||
"blocked",
|
||||
(
|
||||
"Transit/KMS seal not configured yet — enable the seal stanza in "
|
||||
"railiance-platform/helm/openbao-values.yaml and provision the "
|
||||
"transit/KMS backend, then set openbao_transit_seal_configured."
|
||||
),
|
||||
)
|
||||
if not yes(data, "openbao_auto_unseal_verified"):
|
||||
return Gate(
|
||||
"OpenBao init ceremony",
|
||||
"blocked",
|
||||
(
|
||||
"Auto-unseal not verified — restart the OpenBao pod, confirm it "
|
||||
"unseals without shares, then set openbao_auto_unseal_verified."
|
||||
),
|
||||
)
|
||||
return Gate(
|
||||
"OpenBao init ceremony",
|
||||
"human",
|
||||
(
|
||||
"Transit seal active. Attended `bao operator init` still required once "
|
||||
"(recovery keys, root retirement) — follow "
|
||||
"docs/openbao-attended-ceremony-runbook.md. Console will not run init."
|
||||
),
|
||||
)
|
||||
runbook = OPENBAO_UNSEAL_CUSTODY_MODEL_SPECS[model].get("runbook_entry", "")
|
||||
return Gate(
|
||||
"OpenBao init ceremony",
|
||||
"human",
|
||||
"Human-attended ceremony only. This console will not run init.",
|
||||
(
|
||||
"Human-attended ceremony only. This console will not run init. "
|
||||
f"Follow {runbook} and validate the non-secret record with "
|
||||
"validate-openbao-ceremony-record."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -740,12 +802,14 @@ def next_action(
|
||||
if gate.name == "OpenBao preflight":
|
||||
return "Run OpenBao preflight"
|
||||
if gate.name == "OpenBao unseal custody model":
|
||||
return (
|
||||
"Select openbao-unseal-custody-model sops-held-automation "
|
||||
"(other models not yet implemented)"
|
||||
)
|
||||
if data and resolve_deployment_profile(data) == "production":
|
||||
return (
|
||||
"Select a production unseal custody model "
|
||||
"(attended-ceremony or auto-unseal-transit)"
|
||||
)
|
||||
return "Select an implemented openbao-unseal-custody-model"
|
||||
if gate.name == "OpenBao init ceremony":
|
||||
return "Select an implemented unseal custody model first"
|
||||
return "Resolve the OpenBao init ceremony gate (see gate reason)"
|
||||
if gate.name == "KeyCape OpenBao client definition":
|
||||
return "Ship KeyCape OpenBao client definition"
|
||||
if gate.name == "KeyCape OpenBao client deployed":
|
||||
@@ -821,6 +885,9 @@ def print_status(data: dict[str, Any]) -> None:
|
||||
print("17. select-openbao-unseal-custody-model")
|
||||
print("18. web-ui")
|
||||
print("19. validate-keycape-client (T08: example of validator-driven gate in UI state model)")
|
||||
print("20. select-deployment-profile")
|
||||
print("21. openbao-ceremony-record-template")
|
||||
print("22. validate-openbao-ceremony-record")
|
||||
print("")
|
||||
print("Refusal boundary")
|
||||
print("This console will not run bao operator init or collect secret values.")
|
||||
@@ -1934,6 +2001,10 @@ def metadata_template() -> dict[str, Any]:
|
||||
"progress_scope": "",
|
||||
"openbao_unseal_custody_model": DEFAULT_OPENBAO_UNSEAL_CUSTODY_MODEL,
|
||||
"openbao_unseal_custody_model_selected_at": "",
|
||||
"deployment_profile": DEFAULT_DEPLOYMENT_PROFILE,
|
||||
"deployment_profile_selected_at": "",
|
||||
"openbao_transit_seal_configured": False,
|
||||
"openbao_auto_unseal_verified": False,
|
||||
"openbao_preflight_passed": False,
|
||||
"openbao_init_output_produced": False,
|
||||
"openbao_initialized": False,
|
||||
@@ -2002,7 +2073,7 @@ def print_openbao_unseal_custody_models() -> int:
|
||||
if status != "implemented":
|
||||
print(f" blocked_hint: {spec.get('blocked_hint', '')}")
|
||||
else:
|
||||
print(f" automation_entry: {spec.get('automation_entry', '')}")
|
||||
print(f" entry: {openbao_unseal_custody_model_entry(spec)}")
|
||||
print("")
|
||||
return 0
|
||||
|
||||
@@ -2031,6 +2102,18 @@ def print_select_openbao_unseal_custody_model(args: argparse.Namespace, data: di
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 2
|
||||
if model == "sops-held-automation" and resolve_deployment_profile(data) == "production":
|
||||
print("OPENBAO UNSEAL CUSTODY MODEL NOT SELECTABLE")
|
||||
print("")
|
||||
print(f"Model: {model} ({spec.get('label', '')})")
|
||||
print("Deployment profile: production")
|
||||
print("")
|
||||
print(
|
||||
"Production profile blocks sops-held-automation — root token and unseal "
|
||||
"shares in one SOPS bundle is lab posture. Select attended-ceremony or "
|
||||
"auto-unseal-transit, or switch back with: select-deployment-profile --profile lab"
|
||||
)
|
||||
return 1
|
||||
merged = metadata_template()
|
||||
merged.update(data)
|
||||
merged["openbao_unseal_custody_model"] = model
|
||||
@@ -2042,10 +2125,100 @@ def print_select_openbao_unseal_custody_model(args: argparse.Namespace, data: di
|
||||
print(f"Metadata: {args.metadata}")
|
||||
print(f"Model: {model}")
|
||||
print(f"Label: {spec.get('label', '')}")
|
||||
print(f"Automation entry: {spec.get('automation_entry', '')}")
|
||||
print(f"Entry: {openbao_unseal_custody_model_entry(spec)}")
|
||||
return 0
|
||||
|
||||
|
||||
def print_select_deployment_profile(args: argparse.Namespace, data: dict[str, Any]) -> int:
|
||||
profile = args.profile
|
||||
if args.metadata is None:
|
||||
print(
|
||||
"ERROR: select-deployment-profile requires --metadata /path/to/non-secret.json",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 2
|
||||
merged = metadata_template()
|
||||
merged.update(data)
|
||||
merged["deployment_profile"] = profile
|
||||
merged["deployment_profile_selected_at"] = utc_now()
|
||||
merged["metadata_updated_at"] = utc_now()
|
||||
write_metadata(args.metadata, merged)
|
||||
print("DEPLOYMENT PROFILE SELECTED")
|
||||
print("")
|
||||
print(f"Metadata: {args.metadata}")
|
||||
print(f"Profile: {profile}")
|
||||
if profile == "production":
|
||||
model = resolve_openbao_unseal_custody_model(merged)
|
||||
print("")
|
||||
print("Production profile blocks sops-held-automation for OpenBao init/unseal.")
|
||||
if model == "sops-held-automation":
|
||||
print(
|
||||
f"WARNING: current unseal custody model is {model!r} — now blocked. "
|
||||
"Select attended-ceremony or auto-unseal-transit."
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
def openbao_ceremony_record_template() -> dict[str, Any]:
|
||||
return {
|
||||
"record_version": "v1",
|
||||
"evidence_date": "YYYY-MM-DD",
|
||||
"operator": "openbao-ceremony-operator",
|
||||
"runbook_reference": "docs/openbao-attended-ceremony-runbook.md",
|
||||
"ceremony_scope": "Attended OpenBao operator init on <cluster>, unseal share escrow, root token retirement.",
|
||||
"key_shares": 3,
|
||||
"key_threshold": 2,
|
||||
"unseal_share_escrow_disposition": "Describe where each share went (holder role + storage class) — never the shares themselves.",
|
||||
"root_token_disposition": "revoked-or-escrowed: describe.",
|
||||
"witness": "role or name of second attendee, or 'none' with justification",
|
||||
"attended_init_completed": False,
|
||||
"unseal_shares_escrowed_out_of_band": False,
|
||||
"root_token_retired_or_escrowed": False,
|
||||
"post_unseal_verified": False,
|
||||
"no_secret_material_recorded": False,
|
||||
}
|
||||
|
||||
|
||||
def print_openbao_ceremony_record_template() -> None:
|
||||
print(json.dumps(openbao_ceremony_record_template(), indent=2))
|
||||
|
||||
|
||||
def print_validate_openbao_ceremony_record(args: argparse.Namespace) -> int:
|
||||
evidence_path = resolve_cli_path(args.evidence)
|
||||
evidence, errors = load_evidence_json(evidence_path, "openbao-ceremony")
|
||||
if evidence is not None:
|
||||
errors.extend(
|
||||
require_evidence_fields(
|
||||
evidence,
|
||||
required_strings=(
|
||||
"evidence_date",
|
||||
"operator",
|
||||
"runbook_reference",
|
||||
"ceremony_scope",
|
||||
"unseal_share_escrow_disposition",
|
||||
"root_token_disposition",
|
||||
"witness",
|
||||
),
|
||||
required_true=(
|
||||
"attended_init_completed",
|
||||
"unseal_shares_escrowed_out_of_band",
|
||||
"root_token_retired_or_escrowed",
|
||||
"post_unseal_verified",
|
||||
"no_secret_material_recorded",
|
||||
),
|
||||
)
|
||||
)
|
||||
return print_validation_result(
|
||||
"OPENBAO ATTENDED CEREMONY RECORD VALIDATION",
|
||||
errors,
|
||||
[
|
||||
f"ceremony record valid: {evidence_path}",
|
||||
"record is non-secret (no secret-looking markers found)",
|
||||
"next: set openbao_initialized / openbao_post_unseal_verified in console metadata",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def gate_payload(gate: Gate) -> dict[str, str]:
|
||||
return {
|
||||
"name": gate.name,
|
||||
@@ -4974,6 +5147,29 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
default=DEFAULT_OPENBAO_UNSEAL_CUSTODY_MODEL,
|
||||
help="Unseal custody model id.",
|
||||
)
|
||||
select_profile = sub.add_parser(
|
||||
"select-deployment-profile",
|
||||
help="Select deployment profile (production blocks sops-held-automation).",
|
||||
)
|
||||
select_profile.add_argument(
|
||||
"--profile",
|
||||
choices=list(VALID_DEPLOYMENT_PROFILES),
|
||||
required=True,
|
||||
help="Deployment profile id.",
|
||||
)
|
||||
sub.add_parser(
|
||||
"openbao-ceremony-record-template",
|
||||
help="Print non-secret attended OpenBao ceremony record JSON template.",
|
||||
)
|
||||
validate_ceremony = sub.add_parser(
|
||||
"validate-openbao-ceremony-record",
|
||||
help="Validate a non-secret attended OpenBao ceremony record.",
|
||||
)
|
||||
validate_ceremony.add_argument(
|
||||
"--evidence",
|
||||
default=".local/openbao-ceremony-record.json",
|
||||
help="Path to the non-secret ceremony record JSON.",
|
||||
)
|
||||
sub.add_parser("refuse-live-init", help="Explain why live OpenBao init is refused.")
|
||||
web = sub.add_parser("web-ui", help="Serve a local custody approval UI.")
|
||||
web.add_argument("--host", default="127.0.0.1", help="Bind host. Defaults to localhost.")
|
||||
@@ -5002,6 +5198,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
"validate-cleanup",
|
||||
"approve-custody-mode",
|
||||
"select-openbao-unseal-custody-model",
|
||||
"select-deployment-profile",
|
||||
"web-ui",
|
||||
}
|
||||
if args.command in metadata_commands and args.metadata is None:
|
||||
@@ -5099,6 +5296,13 @@ def main(argv: list[str] | None = None) -> int:
|
||||
return print_openbao_preflight(args)
|
||||
if args.command == "openbao-unseal-custody-models":
|
||||
return print_openbao_unseal_custody_models()
|
||||
if args.command == "select-deployment-profile":
|
||||
return print_select_deployment_profile(args, data)
|
||||
if args.command == "openbao-ceremony-record-template":
|
||||
print_openbao_ceremony_record_template()
|
||||
return 0
|
||||
if args.command == "validate-openbao-ceremony-record":
|
||||
return print_validate_openbao_ceremony_record(args)
|
||||
if args.command == "select-openbao-unseal-custody-model":
|
||||
return print_select_openbao_unseal_custody_model(args, data)
|
||||
if args.command == "web-ui":
|
||||
|
||||
@@ -36,15 +36,82 @@ def test_openbao_unseal_custody_model_gate_automation_default():
|
||||
assert init_gate.status == "automation"
|
||||
|
||||
|
||||
def test_openbao_unseal_custody_planned_models_blocked():
|
||||
def test_openbao_unseal_custody_production_models_selectable():
|
||||
for model in ("attended-ceremony", "auto-unseal-transit"):
|
||||
data = console.metadata_template()
|
||||
data["openbao_unseal_custody_model"] = model
|
||||
gate = console.openbao_unseal_custody_model_gate(data)
|
||||
assert gate.status == "blocked"
|
||||
assert "not yet implemented" in gate.reason.lower()
|
||||
init_gate = console.openbao_init_ceremony_gate(data)
|
||||
assert init_gate.status == "blocked"
|
||||
assert gate.status == "done"
|
||||
|
||||
|
||||
def test_attended_ceremony_init_gate_is_human_with_runbook():
|
||||
data = console.metadata_template()
|
||||
data["openbao_unseal_custody_model"] = "attended-ceremony"
|
||||
init_gate = console.openbao_init_ceremony_gate(data)
|
||||
assert init_gate.status == "human"
|
||||
assert "openbao-attended-ceremony-runbook" in init_gate.reason
|
||||
|
||||
|
||||
def test_auto_unseal_transit_init_gate_requires_evidence():
|
||||
data = console.metadata_template()
|
||||
data["openbao_unseal_custody_model"] = "auto-unseal-transit"
|
||||
gate = console.openbao_init_ceremony_gate(data)
|
||||
assert gate.status == "blocked"
|
||||
assert "openbao_transit_seal_configured" in gate.reason
|
||||
data["openbao_transit_seal_configured"] = True
|
||||
gate = console.openbao_init_ceremony_gate(data)
|
||||
assert gate.status == "blocked"
|
||||
assert "openbao_auto_unseal_verified" in gate.reason
|
||||
data["openbao_auto_unseal_verified"] = True
|
||||
gate = console.openbao_init_ceremony_gate(data)
|
||||
assert gate.status == "human"
|
||||
|
||||
|
||||
def test_production_profile_blocks_sops_held_automation():
|
||||
data = console.metadata_template()
|
||||
data["deployment_profile"] = "production"
|
||||
gate = console.openbao_unseal_custody_model_gate(data)
|
||||
assert gate.status == "blocked"
|
||||
assert "production profile" in gate.reason.lower()
|
||||
init_gate = console.openbao_init_ceremony_gate(data)
|
||||
assert init_gate.status == "blocked"
|
||||
data["openbao_unseal_custody_model"] = "attended-ceremony"
|
||||
assert console.openbao_unseal_custody_model_gate(data).status == "done"
|
||||
|
||||
|
||||
def test_openbao_ceremony_record_template_and_validation(tmp_path):
|
||||
tmpl = console.openbao_ceremony_record_template()
|
||||
for key in (
|
||||
"attended_init_completed",
|
||||
"unseal_shares_escrowed_out_of_band",
|
||||
"root_token_retired_or_escrowed",
|
||||
"post_unseal_verified",
|
||||
"no_secret_material_recorded",
|
||||
):
|
||||
assert key in tmpl
|
||||
# a filled-in, non-secret record validates cleanly
|
||||
record = dict(tmpl)
|
||||
record.update(
|
||||
evidence_date="2026-07-02",
|
||||
ceremony_scope="Attended init on Railiance ThreePhoenix.",
|
||||
unseal_share_escrow_disposition="Shares to roster holders, offline packets.",
|
||||
root_token_disposition="revoked after configure-initial",
|
||||
witness="recovery-custodian",
|
||||
attended_init_completed=True,
|
||||
unseal_shares_escrowed_out_of_band=True,
|
||||
root_token_retired_or_escrowed=True,
|
||||
post_unseal_verified=True,
|
||||
no_secret_material_recorded=True,
|
||||
)
|
||||
path = tmp_path / "record.json"
|
||||
path.write_text(json.dumps(record))
|
||||
_, errors = console.load_evidence_json(path, "openbao-ceremony")
|
||||
assert errors == []
|
||||
# a record leaking a token marker is refused
|
||||
record["root_token_disposition"] = "hvs.deadbeef"
|
||||
path.write_text(json.dumps(record))
|
||||
_, errors = console.load_evidence_json(path, "openbao-ceremony")
|
||||
assert any("secret-looking" in e for e in errors)
|
||||
|
||||
def test_onboarding_dry_run_template_has_required_fields():
|
||||
tmpl = console.onboarding_dry_run_template()
|
||||
|
||||
@@ -33,3 +33,18 @@ recipient and a clear notice instead of dying; live runs without a key still
|
||||
fail hard. Verified: full `--dry-run` now traverses Phase 0 through Phase 10
|
||||
including the new Phase 7b OpenBao hook (NET-WP-0020-T02) on a machine with
|
||||
no age key.
|
||||
|
||||
## Fix broken check-secrets Make target (unescaped `$`)
|
||||
|
||||
```task
|
||||
id: ADHOC-2026-07-02-T02
|
||||
status: done
|
||||
priority: medium
|
||||
```
|
||||
|
||||
`make check-secrets` failed with a bash parse error ("unexpected EOF while
|
||||
looking for matching `'`"): the trailing `grep -v '/$'` used a single `$`,
|
||||
which make expanded before bash saw it. Escaped to `$$`. Verified:
|
||||
`make check-secrets` passes again ("All secrets/ files appear SOPS-encrypted").
|
||||
Pre-existing bug, unrelated to NET-WP-0020; found while running the final
|
||||
checks for that workplan.
|
||||
|
||||
@@ -4,7 +4,7 @@ type: workplan
|
||||
title: "OpenBao Unseal Custody Models and SSH Automation Path"
|
||||
domain: infotech
|
||||
repo: net-kingdom
|
||||
status: active
|
||||
status: finished
|
||||
owner: codex
|
||||
topic_slug: net-kingdom
|
||||
created: "2026-06-17"
|
||||
@@ -44,7 +44,7 @@ state_hub_task_id: "7040f347-d54a-42ba-a14f-5b0a7e691786"
|
||||
|
||||
```task
|
||||
id: NET-WP-0020-T02
|
||||
status: progress
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "65407eb1-9d89-4158-aed5-4987badd83fc"
|
||||
```
|
||||
@@ -61,10 +61,14 @@ state_hub_task_id: "65407eb1-9d89-4158-aed5-4987badd83fc"
|
||||
`creds-state.yaml` flags, encrypts + commits new init material, and a
|
||||
custody-gate refusal warns without aborting the SSO/MFA bootstrap —
|
||||
dry-run/skip/refusal paths harness-tested)
|
||||
- [ ] Greenfield live proof: run against a sealed/uninitialized OpenBao on a
|
||||
rebuild slate (current cluster is already initialized+unsealed, so only the
|
||||
status/verify path was live-smoked on 2026-07-02; custody-gate refusal was
|
||||
proven for `unselected` and `attended-ceremony`)
|
||||
- [x] Greenfield live proof: full init→unseal→verify path proven 2026-07-02
|
||||
against a genuinely uninitialized local OpenBao 2.5.5 (kubectl-exec shim,
|
||||
script logic unmodified) — see
|
||||
`history/2026-07-02-openbao-greenfield-init-unseal-proof.md`. The proof
|
||||
caught and fixed a real bug: `bao operator unseal -` does not read stdin;
|
||||
now `bao write sys/unseal key=-` (shares still never in argv/logs). Restart/
|
||||
reseal replay path proven too. The first 3-node rebuild slate re-runs the
|
||||
same script via Phase 7b.
|
||||
|
||||
**2026-07-02 (later):** Bernd reviewed the helper design (five safety
|
||||
properties incl. the root-token-in-bundle caveat of the sops-held model) and
|
||||
@@ -84,27 +88,42 @@ never appear in argv or logs.
|
||||
|
||||
```task
|
||||
id: NET-WP-0020-T03
|
||||
status: wait
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "34f3d979-a040-49ca-bfcb-35cf17473a06"
|
||||
```
|
||||
|
||||
- [ ] Implement `attended-ceremony` selection path (runbooks + evidence validators)
|
||||
- [ ] Production profile blocks `sops-held-automation` default
|
||||
- [x] Implement `attended-ceremony` selection path (runbooks + evidence validators)
|
||||
- [x] Production profile blocks `sops-held-automation` default
|
||||
|
||||
**Blocked until:** T2 automation path proven on greenfield rebuild.
|
||||
**2026-07-02:** Model selectable in the console; runbook at
|
||||
`docs/openbao-attended-ceremony-runbook.md`; non-secret ceremony record
|
||||
template + `validate-openbao-ceremony-record` validator (refuses secret
|
||||
markers/placeholders). New `deployment_profile` metadata (`lab`/`production`,
|
||||
`select-deployment-profile`): production blocks `sops-held-automation` in
|
||||
selection, status gates, and `openbao-init-unseal.sh`. Console refuse-live-init
|
||||
boundary unchanged. Covered by console test suite (14 passing) + CLI smoke.
|
||||
|
||||
### T4 — Auto-unseal transit profile
|
||||
|
||||
```task
|
||||
id: NET-WP-0020-T04
|
||||
status: wait
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "54ab6505-c13b-4f63-8c94-07dd202de90a"
|
||||
```
|
||||
|
||||
- [ ] `railiance-platform` Helm seal stanza for transit/KMS
|
||||
- [ ] Console gate + evidence for `auto-unseal-transit`
|
||||
- [x] `railiance-platform` Helm seal stanza for transit/KMS
|
||||
- [x] Console gate + evidence for `auto-unseal-transit`
|
||||
|
||||
**2026-07-02:** Commented-out `seal "transit"` stanza (disabled by default;
|
||||
token via `extraSecretEnvironmentVars`, never Git) in
|
||||
`railiance-platform/helm/openbao-values.yaml` plus an "Auto-Unseal via Transit
|
||||
Seal" section in `railiance-platform/docs/openbao.md` (enable → `-migrate` →
|
||||
pod-restart proof). Console: model selectable; init gate stays blocked until
|
||||
`openbao_transit_seal_configured` and `openbao_auto_unseal_verified` are set.
|
||||
Live transit/KMS provisioning is future ops work on the HA rebuild, gated by
|
||||
that evidence.
|
||||
|
||||
### T5 — SSH engine + host CA automation (cross-repo)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user