Compare commits

...

73 Commits

Author SHA1 Message Date
2db4d1afe1 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-07-01:
  - update .custodian-brief.md for railiance-platform
2026-07-01 23:12:51 +02:00
268437a36d chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-07-01:
  - RAILIANCE-WP-0005-T10: progress → wait
2026-07-01 23:12:47 +02:00
0a24ab8475 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-07-01:
  - RAILIANCE-WP-0005-T09: progress → wait
2026-07-01 23:12:47 +02:00
6ed18ca709 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-07-01:
  - RAILIANCE-WP-0005-T05: progress → wait
2026-07-01 23:12:47 +02:00
8321e14b46 Unblock credential broker warden-sign pilot 2026-07-01 23:10:38 +02:00
a95236d2e5 Add credential-change delegated applier flow 2026-07-01 20:07:26 +02:00
c626bfcf15 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-06-30:
  - update .custodian-brief.md for railiance-platform
2026-06-30 11:34:38 +02:00
3e28e9ae79 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-06-30:
  - update .custodian-brief.md for railiance-platform
2026-06-30 11:21:43 +02:00
aa4c9ac492 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-06-30:
  - update .custodian-brief.md for railiance-platform
2026-06-30 11:13:20 +02:00
b59718fc83 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-06-30:
  - update .custodian-brief.md for railiance-platform
2026-06-30 01:32:29 +02:00
51b85f802d chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-06-29:
  - update .custodian-brief.md for railiance-platform#
2026-06-29 18:57:38 +02:00
efb5432eb8 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-06-29:
  - update .custodian-brief.md for railiance-platform
2026-06-29 17:36:07 +02:00
5c6a3ce95e chore(consistency): renormalize lifecycle state [auto]
Updated by fix-consistency on 2026-06-29:
  - workplan status: ready → active
2026-06-29 17:36:01 +02:00
a07f2d3d7f chore(consistency): renormalize lifecycle state [auto]
Updated by fix-consistency on 2026-06-29:
  - workplan status: ready → active
2026-06-29 17:35:59 +02:00
481e64c3f4 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-06-29:
  - update .custodian-brief.md for railiance-platform
2026-06-29 17:19:29 +02:00
8f617fcbf4 Activate whynot npm credential lane 2026-06-29 00:13:09 +02:00
e88c7829f3 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-06-29:
  - update .custodian-brief.md for railiance-platform
2026-06-29 00:12:16 +02:00
1e769c75a0 Record whynot positive fetch verification 2026-06-28 17:26:10 +02:00
2c1e76efca Record whynot identity group evidence 2026-06-28 16:05:17 +02:00
3527bc1cae Request groups scope for whynot OIDC role 2026-06-28 13:23:14 +02:00
adf865611c Mark whynot lane applied pending verification 2026-06-28 12:53:39 +02:00
271aa94642 Record whynot OpenBao lane apply evidence 2026-06-28 12:41:39 +02:00
3ef25cb787 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-06-28:
  - update .custodian-brief.md for railiance-platform
2026-06-28 12:40:16 +02:00
53f3f4ca10 Document OpenBao Browser CLI limits 2026-06-28 09:18:36 +02:00
f630d5135e Fix OpenBao role payload handoff 2026-06-28 02:33:42 +02:00
e3147b7fd5 Prepare whynot npm token handoff 2026-06-28 01:43:06 +02:00
06f2f4e315 Approve corrected whynot CCR 2026-06-28 01:27:04 +02:00
00fb93544c Add CCR decision template task 2026-06-28 01:17:41 +02:00
5e0ed95127 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-06-28:
  - update .custodian-brief.md for railiance-platform
2026-06-28 01:16:53 +02:00
6effdb80ca Link corrected whynot CCR decision 2026-06-28 01:05:43 +02:00
eb24e04b71 Correct whynot credential tenant path 2026-06-28 01:00:12 +02:00
ad47a136f7 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-06-28:
  - update .custodian-brief.md for railiance-platform
2026-06-28 00:45:16 +02:00
82d15cfea2 chore(consistency): renormalize lifecycle state [auto]
Updated by fix-consistency on 2026-06-28:
  - workplan status: proposed → active
2026-06-28 00:45:12 +02:00
0e3ea30c75 Propose OpenBao automation delegation 2026-06-28 00:44:23 +02:00
f92d07d5a1 Record whynot CCR apply blocker 2026-06-28 00:24:23 +02:00
248bc58b6a Add credential CCR operator handoff 2026-06-28 00:21:02 +02:00
a27a114491 Approve whynot credential CCR 2026-06-28 00:13:37 +02:00
3706ff703e Link CCR approval to State Hub decision 2026-06-28 00:00:02 +02:00
52687d8b3e Confirm whynot credential binding 2026-06-27 23:45:31 +02:00
aee0dcefad Add credential lane readiness proposals 2026-06-27 23:30:29 +02:00
815b124ab1 Implement credential change request review flow 2026-06-27 22:57:21 +02:00
8c1e64d5e0 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-06-27:
  - update .custodian-brief.md for railiance-platform
2026-06-27 22:55:36 +02:00
85a4278a55 Add credential approval workflow plan 2026-06-27 22:48:24 +02:00
9d42c73833 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-06-27:
  - update .custodian-brief.md for railiance-platform
2026-06-27 22:25:27 +02:00
704ee99218 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-06-27:
  - update .custodian-brief.md for railiance-platform
2026-06-27 21:56:15 +02:00
76c9661db3 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-06-27:
  - update .custodian-brief.md for railiance-platform
2026-06-27 21:35:09 +02:00
673ec46e25 feat: complete credential broker source flow 2026-06-27 00:29:53 +02:00
2268a9375e chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-06-27:
  - update .custodian-brief.md for railiance-platform
2026-06-27 00:28:42 +02:00
752cfd6f00 feat: add credential broker token helper 2026-06-27 00:06:03 +02:00
6e663dfd20 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-06-26:
  - update .custodian-brief.md for railiance-platform
2026-06-26 17:52:42 +02:00
c7393d94ab feat: add credential grant catalog foundation 2026-06-26 17:49:40 +02:00
693dc71833 Add ESO OpenBao GitOps add-ons 2026-06-25 20:08:36 +02:00
0f0b14001e chore: finalize ArgoCD workplan and add credential broker plan 2026-06-25 17:49:35 +02:00
c022cb2f83 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-06-24:
  - update .custodian-brief.md for railiance-platform
2026-06-24 18:55:31 +02:00
86eb6ea269 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-06-24:
  - update .custodian-brief.md for railiance-platform
2026-06-24 18:46:33 +02:00
d59704deef chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-06-24:
  - update .custodian-brief.md for railiance-platform
2026-06-24 18:40:26 +02:00
f39180583a chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-06-24:
  - update .custodian-brief.md for railiance-platform
2026-06-24 18:39:35 +02:00
0b384f8485 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-06-24:
  - update .custodian-brief.md for railiance-platform
2026-06-24 15:04:32 +02:00
8e6892f4bf Normalize agent instructions and workplan frontmatter (STATE-WP-0067)
- Align agent files with on-disk workplan prefixes (infer from workplan ids)
- Set workplan domain to registered domain_slug; add topic_slug where applicable
- Repair frontmatter delimiter formatting; migrate legacy task status literals
- Regenerate AGENTS.md, CLAUDE.md, and .claude/rules from State Hub templates
2026-06-22 23:16:28 +02:00
6712eed995 Human-review .repo-classification.yaml (CUST-WP-0050 follow-up) 2026-06-22 17:56:17 +02:00
a1dbb26842 Add .repo-classification.yaml (CUST-WP-0050 T11 agent first-pass) 2026-06-22 17:47:42 +02:00
50799938db fix(openbao-ui): handle OIDC callback without Ember popup flow
OpenBao's Ember UI expects OIDC to complete in a popup and postMessage to
window.opener. The standalone KeyCape login uses a full-page redirect, so the
callback now exchanges the authorization code directly, persists the UI token
in localStorage, and redirects into the vault UI. Unauthenticated /ui/ loads
also redirect to the standalone login page to avoid ?with= bounce loops.
2026-06-19 21:18:34 +02:00
520c7ea2c0 fix(openbao-ui): serve standalone KeyCape login at /ui/vault/auth
Ember's auth route bounces between ?with=netkingdom/ and ?with=token when
OIDC mounts are hidden from the unauthenticated listing. Bypass Ember on the
bare auth path with a static login page that calls auth_url directly; OIDC
callbacks still proxy to the OpenBao UI.
2026-06-19 21:13:08 +02:00
ae4d967481 Mark ArgoCD bootstrap T05 done after live cluster apply
Record bootstrap evidence on 92.205.130.254 and note issue-core sync is
blocked until the ExternalSecret CRD is installed.
2026-06-19 21:09:36 +02:00
80648a78b7 Stop OpenBao login redirect loop by removing URL rewriting
Remove redirect-bootstrap and mount polling that fought Ember's token
fallback. Keep cosmetic overlay and direct KeyCape OIDC on sign-in only.
2026-06-19 21:07:37 +02:00
64d7c18c3f Add ArgoCD GitOps bootstrap contract for railiance01
Define platform-owned AppProjects, root app-of-apps, repository registration
templates, and tenant onboarding docs so issue-core can deploy via ArgoCD.
Ignore encrypted repository secrets locally and cross-link OpenBao delivery
guidance with the new GitOps contract.
2026-06-19 21:05:12 +02:00
cb45f29fb2 Fix OpenBao login falling back to token auth
Add synchronous redirect-bootstrap, direct KeyCape OIDC on sign-in, and mount
watching so the UI no longer lands on ?with=token when netkingdom is hidden
from unauthenticated mount listing. Document listing_visibility tune helper.
2026-06-19 21:04:31 +02:00
a6a87ae282 Fix OpenBao login overlay runaway DOM loop and slow loads
Replace the MutationObserver feedback loop with bounded, idempotent apply
retries so Firefox no longer hangs on the auth page. Route static UI assets
and API calls around HTML sub_filter injection to keep bundles compressed.
2026-06-19 20:58:44 +02:00
6ddf4e56b4 Add KeyCape login overlay gateway for OpenBao browser UI
Streamline bao.coulomb.social login as "Sign in with KeyCape" via a versioned
nginx gateway that injects overlay assets and proxies to OpenBao. Disable chart
ingress in favor of the overlay ingress, wire make openbao-deploy, and add
openbao-verify-login-overlay with upstream drift detection.
2026-06-19 20:28:16 +02:00
665d43386f Add credential routing instructions for all agent runtimes
Propagate shared credential-routing section (Codex, Claude, Grok, llm-connect)
from state-hub template via scripts/propagate_credential_routing.py.
2026-06-18 22:48:39 +02:00
423eccc8e9 feat(openbao): enable bao.coulomb.social ingress and Traefik middlewares
Expose OpenBao UI via TLS ingress with rate-limit and HSTS middlewares.
Track netkingdom OIDC mount in authenticated verify checks.
2026-06-18 01:23:02 +02:00
7838df6069 fix(openbao): complete SSH apply script for OpenBao 2.5.x issuers
Generate default CA via ssh/config/ca, split composite KUBECTL for role writes,
read pubkey from config/ca, allow warden key_id in roles, prefer production kubeconfig.
2026-06-18 01:18:56 +02:00
c24956fb5a feat(openbao): add SSH engine automation for ops-warden signing
Declarative roles, warden-sign policy, apply/verify scripts, and Makefile
targets openbao-configure-ssh and openbao-verify-ssh. Document operator flow
in docs/openbao.md for NET-WP-0020 T5 / WP-0008 T2.
2026-06-18 01:06:43 +02:00
89 changed files with 12716 additions and 39 deletions

View File

@@ -0,0 +1,50 @@
# Credential and access routing
**Audience:** Codex, Claude Code, Grok, and custodian agents that call **llm-connect**
for inference. Run this check **before** requesting secrets, API keys, SSH access,
login tokens, or database passwords — in any repo, not only `ops-warden`.
ops-warden **issues SSH certificates only** (`warden sign`, `cert_command`). Every
other credential need belongs to another subsystem. **Do not** message
`ops-warden` on State Hub expecting a secret value; the reply is a pointer, not a key.
### Lookup (do this first)
```bash
warden route find "<describe your need>" --json
warden route show <catalog-id> --json
```
Requires the `warden` CLI from `~/ops-warden` (`uv tool install .` or `uv run warden`).
| Agent runtime | How to orient |
| --- | --- |
| **Codex / Grok** (shell, HTTP State Hub) | `warden route` commands above; inbox `to_agent=railiance-platform` is for coordination, not secret vending |
| **Claude Code** (MCP when available) | `get_domain_summary("custodian")` for workstreams; **still** use `warden route` for credential ownership |
| **llm-connect** (inference service) | Never put secret retrieval in prompts; route custody to OpenBao/operator paths surfaced by `warden route` |
### Quick routing table
| I need… | Owner | ops-warden executes? |
| --- | --- | --- |
| SSH cert (`adm`/`agt`/`atm`) | ops-warden | **Yes**`warden sign` |
| API key, DB password, provider token | OpenBao (`railiance-platform`) | No — route only |
| Login / OIDC / MFA | key-cape / Keycloak | No — route only |
| Authorization decision | flex-auth | No — route only |
| activity-core → issue-core emission | activity-core + issue-core | No — `warden route show activity-core-issue-sink` |
| SSH tunnel | ops-bridge (+ `cert_command` from warden) | No — route only |
### Anti-patterns (do not do these)
- `POST /messages/` to `ops-warden` asking for `ISSUE_CORE_API_KEY`, `OPENROUTER_API_KEY`, etc.
- Inventing `warden secret`, `warden login`, `warden bao`, `warden tunnel` — they do not exist
- Pasting secrets into Git, State Hub, workplans, logs, or chat
### Other capabilities (reuse-surface)
Non-credential capabilities are usually discovered through **reuse-surface** federation
(`reuse-surface` registry / `capability.*` indexes). Credential routing is inlined in
every repo's agent instructions because it is high-frequency, high-risk, and easy to
get wrong.
**Canon:** `~/ops-warden/wiki/CredentialRouting.md` · catalog `~/ops-warden/registry/routing/catalog.yaml`

View File

@@ -1,11 +1,11 @@
## First Session Protocol
Triggered when `get_domain_summary("railiance")` shows **no workstreams**.
Triggered when `get_domain_summary("financials")` shows **no workstreams**.
The project is registered but work has not yet been structured.
**Step 1 — Read, don't write**
- `~/the-custodian/canon/projects/railiance/project_charter_v0.1.md` — purpose, scope
- `~/the-custodian/canon/projects/railiance/roadmap_v0.1.md` — planned phases
- `~/the-custodian/canon/projects/financials/project_charter_v0.1.md` — purpose, scope
- `~/the-custodian/canon/projects/financials/roadmap_v0.1.md` — planned phases
- Scan repo root: README, directory structure, existing code or docs
**Step 2 — Survey in-progress work**
@@ -17,7 +17,7 @@ roadmap phase. **Wait for approval before creating.**
**Step 4 — Create workplan file first, then DB record (ADR-001)**
```
workplans/railiance-platform-WP-NNNN-<slug>.md ← write this first
workplans/RAILIANCE-WP-NNNN-<slug>.md ← write this first
```
Then register in the hub:
```
@@ -28,7 +28,7 @@ create_task(workstream_id="<id>", title="...", priority="high|medium|low")
**Step 5 — Record the setup**
```
add_progress_event(
summary="First session: structured railiance into N workstreams, M tasks",
summary="First session: structured financials into N workstreams, M tasks",
event_type="milestone",
topic_id="ca369340-a64e-442e-98f1-a4fa7dc74a38",
detail={"workstreams": [...], "tasks_created": M}

View File

@@ -1,5 +1,5 @@
**Purpose:** OAS S3 Platform Services — PostgreSQL HA, object storage, secret management, identity
**Domain:** railiance
**Domain:** financials
**Repo slug:** railiance-platform
**Topic ID:** ca369340-a64e-442e-98f1-a4fa7dc74a38

View File

@@ -1,6 +1,7 @@
## Session Protocol
State Hub: http://127.0.0.1:8000
Dev Hub (State Hub API): http://127.0.0.1:8000
MCP server name in `~/.claude.json`: `dev-hub`
**Step 1 — Orient**
@@ -10,7 +11,7 @@ cat .custodian-brief.md
```
Then call the MCP tool for richer cross-domain context when MCP tools are exposed:
```
get_domain_summary("railiance")
get_domain_summary("financials")
```
If MCP tools are unavailable in the current agent session, use the REST API:
```bash
@@ -39,11 +40,11 @@ curl -s -X PATCH "http://127.0.0.1:8000/messages/<id>/read" \
ls workplans/
```
For each file with `status: ready`, `active`, or `blocked`, note pending
`todo`/`in_progress` tasks.
`wait`/`todo`/`progress` tasks.
**Step 4 — Present brief**
1. **Active workstreams** for `railiance` — title, task counts, blocking decisions
1. **Active workstreams** for `financials` — title, task counts, blocking decisions
2. **Pending tasks** from `workplans/` + any `[repo:railiance-platform]` hub tasks
3. **Goal guidance** — if `goal_guidance` in summary:
- `needs_workplan`: surface as top action — *"Repo goal '{title}' has no workplan yet"*

View File

@@ -1,7 +1,7 @@
## Workplan Convention (ADR-001)
File location: `workplans/railiance-platform-WP-NNNN-<slug>.md`
ID prefix: `RAILIANCE-WP`
File location: `workplans/RAILIANCE-WP-NNNN-<slug>.md`
ID prefix: `RAILIANCE-WP-`
Work items originate as files in this repo **before** being registered in the hub.
@@ -12,7 +12,7 @@ repo state, and `finished` when implementation is complete. `stalled` and
`needs_review` are derived health labels, not stored statuses.
Closed workplans may be moved to `workplans/archived/` with a completion-date
prefix: `YYMMDD-railiance-platform-WP-NNNN-<slug>.md`. The frontmatter id remains
prefix: `YYMMDD-RAILIANCE-WP-NNNN-<slug>.md`. The frontmatter id remains
unchanged; the prefix is only for quick visual reference.
Small opportunistic tasks discovered during another session use **Ad Hoc Tasks**:
@@ -25,4 +25,16 @@ Ecosystem todos from other agents arrive as `[repo:railiance-platform]` hub task
visible at session start. Pick one up by creating the workplan file, then registering
the workstream.
Task blocks use this shape:
```task
id: RAILIANCE-WP-NNNN-T01
status: wait | todo | progress | done | cancel
priority: high | medium | low
state_hub_task_id: "<uuid>" # written by fix-consistency — do not edit
```
Status progression is `todo``progress``done`; use `wait` for waiting or
blocked work and `cancel` for stopped work.
<!-- Ralph Loop rules and HEUREKA sequence: ~/.claude/CLAUDE.md — do not duplicate here -->

View File

@@ -1,18 +1,58 @@
<!-- custodian-brief: generated by fix-consistency — do not edit manually -->
# Custodian Brief — railiance-platform
**Domain:** railiance
**Last synced:** 2026-05-29 00:09 UTC
**Domain:** financials
**Last synced:** 2026-07-01 21:12 UTC
**State Hub:** http://127.0.0.1:8000 *(adjust if running on a remote machine)*
## Active Workstreams
*(none — repo may need first-session setup)*
### Credential Request and Lease Broker
Progress: 6/10 done | workstream_id: `2731fece-6c49-45b8-ab8a-4ea6c04ac603`
**Open tasks:**
- ! T05 - Implement secure delivery modes `66f3cd6d`
*(wait: OpenBao live delivery verification pending)*
- ! T07 - Add flex-auth preflight authorization and State Hub request metadata `1269bb58`
*(wait: Live flex-auth/OpenBao lifecycle evidence pending)*
- ! T09 - Verification, audit, and red-team checks `78d1db83`
*(wait: Live OpenBao audit evidence pending)*
- ! T10 - Rollout and migration `44ce4082`
*(wait: Live pilot and external routing rollout pending)*
### OpenBao Approved Automation Delegation
Progress: 2/5 done | workstream_id: `671898ef-2378-4814-b8f6-066148cdad46`
**Open tasks:**
- ! T05 - Close the whynot-design pilot `18f34c95`
- ► T03 - Add non-production applier role first `ff927a19`
- ► T04 - Add production metadata applier with human approval gate `414abd65`
### Issue-Core Runtime Ingestion Credential Lane
Progress: 2/7 done | workstream_id: `b059c81d-96f1-451f-896f-a05cd73744a1`
**Open tasks:**
- ! T03 - Apply or confirm least-privilege OpenBao metadata `e8566cf4`
- ! T04 - Provision values through approved custody `4990fe6a`
- ! T05 - Verify positive and negative access `65e83572`
- ! T06 - Activate ops-warden catalog front door `0d9a02da`
- ! T07 - Record lifecycle operations `c85d1139`
### llm-connect OpenRouter Provider Key Lane
Progress: 1/7 done | workstream_id: `f364d405-a85d-4b89-b600-1964ab436cad`
**Open tasks:**
- ! T03 - Apply or confirm least-privilege OpenBao metadata `42796ef5`
- ! T04 - Provision the provider key through approved custody `651f6ec8`
- ! T05 - Verify positive and negative access `d538cfc0`
- ! T06 - Activate ops-warden catalog front door `376de3fe`
- ! T07 - Record lifecycle operations `130155a5`
- ► T01 - Review CCR scope and selector naming `307b75a6`
---
## MCP Orientation (when available)
If the state-hub MCP server is reachable, call:
`get_domain_summary("railiance")`
`get_domain_summary("financials")`
This provides richer cross-domain context.
If the MCP call fails, use this file as your orientation source.

10
.gitignore vendored
View File

@@ -3,10 +3,20 @@ helm/*.yaml
!helm/*.sops.yaml
!helm/*.yaml.template
!helm/openbao-values.yaml
!helm/openbao-middleware.yaml
!helm/openbao-ui-overlay-k8s.yaml
# Kubernetes manifests (no secrets) are safe to commit
!helm/*-cluster.yaml
!helm/*-networkpolicies.yaml
!helm/*-databases.yaml
# ArgoCD repository credentials — encrypt locally, never commit
argocd/repositories/*.repository.sops.yaml
!argocd/repositories/*.repository.sops.yaml.template
# Kubeconfig
*.kubeconfig
# Credential broker local lease/token material
.local/credential-leases/
*.openbao-token

23
.repo-classification.yaml Normal file
View File

@@ -0,0 +1,23 @@
repo_classification:
standard: Repo Classification Standard
version: '1.0'
classified_at: '2026-06-22'
classified_by: human
category: tooling
domain: financials
secondary_domains:
- infotech
capability_tags:
- platform
- operations
- configuration
- governance
business_stake:
- finance
- technology
- operations
business_mechanics:
- control
- operation
- coordination
notes: Railiance platform substrate; human corrected category project→tooling.

View File

@@ -4,7 +4,7 @@
**Purpose:** OAS S3 Platform Services — PostgreSQL HA, object storage, secret management, identity
**Domain:** railiance
**Domain:** financials
**Repo slug:** railiance-platform
**Topic ID:** `ca369340-a64e-442e-98f1-a4fa7dc74a38`
**Workplan prefix:** `RAILIANCE-WP-`
@@ -63,8 +63,8 @@ Omit `workstream_id` / `task_id` when not applicable.
```bash
curl -s -X PATCH "http://127.0.0.1:8000/tasks/<task_id>" \
-H "Content-Type: application/json" \
-d '{"status": "in_progress"}'
# values: todo | in_progress | done | blocked
-d '{"status": "progress"}'
# values: wait | todo | progress | done | cancel
```
### Flag a task for human review
@@ -83,7 +83,7 @@ curl -s -X PATCH "http://127.0.0.1:8000/tasks/<task_id>" \
1. `cat .custodian-brief.md` — domain goal and open workstreams (offline-safe)
2. Check inbox: `GET /messages/?to_agent=railiance-platform&unread_only=true`; mark read
3. Scan workplans: `ls workplans/` — note `status: ready`, `active`, or `blocked` files and open tasks
4. Check blocked tasks: `GET /tasks/?needs_human=true`
4. Check human-needed tasks: `GET /tasks/?needs_human=true`
**During work:**
- Update task statuses in workplan files as tasks progress
@@ -101,6 +101,63 @@ curl -s -X PATCH "http://127.0.0.1:8000/tasks/<task_id>" \
---
## Credential and access routing
**Audience:** Codex, Claude Code, Grok, and custodian agents that call **llm-connect**
for inference. Run this check **before** requesting secrets, API keys, SSH access,
login tokens, or database passwords — in any repo, not only `ops-warden`.
ops-warden **issues SSH certificates only** (`warden sign`, `cert_command`). Every
other credential need belongs to another subsystem. **Do not** message
`ops-warden` on State Hub expecting a secret value; the reply is a pointer, not a key.
### Lookup (do this first)
```bash
warden route find "<describe your need>" --json
warden route show <catalog-id> --json
```
Requires the `warden` CLI from `~/ops-warden` (`uv tool install .` or `uv run warden`).
| Agent runtime | How to orient |
| --- | --- |
| **Codex / Grok** (shell, HTTP State Hub) | `warden route` commands above; inbox `to_agent=railiance-platform` is for coordination, not secret vending |
| **Claude Code** (MCP when available) | `get_domain_summary("custodian")` for workstreams; **still** use `warden route` for credential ownership |
| **llm-connect** (inference service) | Never put secret retrieval in prompts; route custody to OpenBao/operator paths surfaced by `warden route` |
### Quick routing table
| I need… | Owner | ops-warden executes? |
| --- | --- | --- |
| SSH cert (`adm`/`agt`/`atm`) | ops-warden | **Yes** — `warden sign` |
| API key, DB password, provider token | OpenBao (`railiance-platform`) | No — route only |
| Login / OIDC / MFA | key-cape / Keycloak | No — route only |
| Authorization decision | flex-auth | No — route only |
| activity-core → issue-core emission | activity-core + issue-core | No — `warden route show activity-core-issue-sink` |
| SSH tunnel | ops-bridge (+ `cert_command` from warden) | No — route only |
### Anti-patterns (do not do these)
- `POST /messages/` to `ops-warden` asking for `ISSUE_CORE_API_KEY`, `OPENROUTER_API_KEY`, etc.
- Inventing `warden secret`, `warden login`, `warden bao`, `warden tunnel` — they do not exist
- Pasting secrets into Git, State Hub, workplans, logs, or chat
### Other capabilities (reuse-surface)
Non-credential capabilities are usually discovered through **reuse-surface** federation
(`reuse-surface` registry / `capability.*` indexes). Credential routing is inlined in
every repo's agent instructions because it is high-frequency, high-risk, and easy to
get wrong.
**Canon:** `~/ops-warden/wiki/CredentialRouting.md` · catalog `~/ops-warden/registry/routing/catalog.yaml`
<!-- REPO-AGENTS-EXTENSIONS -->
<!-- Append repo-specific agent instructions below this marker.
The state-hub template sync preserves content after this line. -->
---
## Workplan Convention (ADR-001)
Work items originate as files in this repo — not in the hub. The hub is a
@@ -124,7 +181,7 @@ anything needing analysis, design, approval, dependencies, or multiple phases.
id: RAILIANCE-WP-NNNN
type: workplan
title: "..."
domain: railiance
domain: financials
repo: railiance-platform
status: proposed | ready | active | blocked | backlog | finished | archived
owner: codex
@@ -146,7 +203,7 @@ derived health labels, not frontmatter statuses.
` ` `task
id: RAILIANCE-WP-NNNN-T01
status: todo | in_progress | done | blocked
status: wait | todo | progress | done | cancel
priority: high | medium | low
state_hub_task_id: "<uuid>" # written by fix-consistency — do not edit
` ` `
@@ -154,7 +211,7 @@ state_hub_task_id: "<uuid>" # written by fix-consistency — do not edit
Task description text.
```
Status progression: `todo` → `in_progress` → `done` (or `blocked`)
Status progression: `todo` → `progress` → `done`; use `wait` for waiting/blocked work and `cancel` for stopped work.
To create a new workplan:
1. Write the file following the format above

View File

@@ -8,4 +8,5 @@
@.claude/rules/stack-and-commands.md
@.claude/rules/architecture.md
@.claude/rules/repo-boundary.md
@.claude/rules/credential-routing.md
@.claude/rules/agents.md

195
Makefile
View File

@@ -1,7 +1,7 @@
SHELL := /usr/bin/env bash
.DEFAULT_GOAL := help
KUBECONFIG ?= $(firstword $(wildcard $(HOME)/.kube/config-hosteurope) $(HOME)/.kube/config)
KUBECONFIG ?= $(firstword $(wildcard $(HOME)/.kube/config) $(HOME)/.kube/config-hosteurope)
KUBECTL_BIN ?= $(firstword $(shell command -v kubectl 2>/dev/null) $(wildcard $(HOME)/.local/bin/kubectl) kubectl)
KUBECTL := $(KUBECTL_BIN) --kubeconfig=$(KUBECONFIG)
HELM := helm --kubeconfig=$(KUBECONFIG)
@@ -13,9 +13,30 @@ OPENBAO_CHART_VERSION ?= 0.28.2
OPENBAO_NAMESPACE ?= openbao
OPENBAO_RELEASE ?= openbao
OPENBAO_VALUES ?= helm/openbao-values.yaml
OPENBAO_MIDDLEWARE ?= helm/openbao-middleware.yaml
OPENBAO_UI_OVERLAY_DIR ?= helm/openbao-ui-overlay
OPENBAO_UI_OVERLAY_K8S ?= helm/openbao-ui-overlay-k8s.yaml
OPENBAO_VERIFY_AUTH_ARGS ?=
OPENBAO_RESTORE_EVIDENCE ?= /tmp/netkingdom-openbao-restore-drill/evidence.json
OPENBAO_EMERGENCY_EVIDENCE ?= /tmp/netkingdom-openbao-emergency-drill/evidence.json
EXTERNAL_SECRETS_NAMESPACE ?= external-secrets
ARGOCD_NAMESPACE ?= argocd
ARGOCD_BOOTSTRAP_DIR ?= argocd/bootstrap
ARGOCD_REPOSITORY_SECRET ?=
CREDENTIAL_GRANTS ?= credential-grants/catalog.yaml
CREDENTIAL_CHANGE ?= CCR-2026-0001
CREDENTIAL_CHANGE_EVIDENCE_ARGS ?=
CREDENTIAL_CHANGE_LIFECYCLE_ACTION ?= deactivate
CREDENTIAL_CHANGE_LIFECYCLE_ARGS ?=
CREDENTIAL_CHANGE_IMPORT_ARGS ?=
STATE_HUB_URL ?= http://127.0.0.1:8000
OPENBAO_TOKEN_GRANT_ARGS ?=
OPENBAO_CREDENTIAL_CHANGE_APPLIER_ARGS ?=
OPENBAO_WORKLOAD_KV_ARGS ?=
CREDENTIAL_HELPER_GLOBAL_ARGS ?=
CREDENTIAL_HELPER_ARGS ?=
CREDENTIAL_HELPER_CHILD_ENV ?=
CREDENTIAL_HELPER_PURPOSE ?= flex-auth-openbao-smoke
##@ CloudNative PG (cnpg) — primary database operator
@@ -102,13 +123,25 @@ openbao-dry-run: openbao-repo ## Render the OpenBao Helm release without applyin
-f $(OPENBAO_VALUES) \
--dry-run
openbao-overlay-apply: ## Apply KeyCape login overlay gateway and assets
OPENBAO_UI_OVERLAY_DIR=$(OPENBAO_UI_OVERLAY_DIR) \
OPENBAO_UI_OVERLAY_K8S=$(OPENBAO_UI_OVERLAY_K8S) \
KUBECTL='$(KUBECTL)' OPENBAO_NAMESPACE=$(OPENBAO_NAMESPACE) \
scripts/openbao-ui-overlay-apply.sh
openbao-verify-login-overlay: ## Verify public KeyCape login overlay is active
OPENBAO_UI_OVERLAY_DIR=$(OPENBAO_UI_OVERLAY_DIR) \
scripts/openbao-verify-login-overlay.sh $(OPENBAO_VERIFY_LOGIN_OVERLAY_ARGS)
openbao-deploy: openbao-repo ## Deploy / upgrade OpenBao to the openbao namespace
$(KUBECTL) create namespace $(OPENBAO_NAMESPACE) --dry-run=client -o yaml | $(KUBECTL) apply -f -
$(KUBECTL) apply -f $(OPENBAO_MIDDLEWARE)
$(HELM) upgrade --install $(OPENBAO_RELEASE) openbao/openbao \
--version $(OPENBAO_CHART_VERSION) \
--namespace $(OPENBAO_NAMESPACE) \
-f $(OPENBAO_VALUES) \
--wait --timeout 5m
$(MAKE) openbao-overlay-apply
openbao-status: ## Show OpenBao pods, services, PVCs, and seal/init status
$(KUBECTL) get pods,svc,pvc -n $(OPENBAO_NAMESPACE) \
@@ -127,10 +160,34 @@ openbao-configure-initial: ## Apply first post-unseal audit, auth, mounts, and p
KUBECTL='$(KUBECTL)' OPENBAO_NAMESPACE=$(OPENBAO_NAMESPACE) \
OPENBAO_RELEASE=$(OPENBAO_RELEASE) scripts/openbao-apply-initial-config.sh
openbao-configure-ssh: ## Enable SSH secrets engine, roles, and warden-sign policy
KUBECTL='$(KUBECTL)' OPENBAO_NAMESPACE=$(OPENBAO_NAMESPACE) \
OPENBAO_RELEASE=$(OPENBAO_RELEASE) scripts/openbao-apply-ssh-engine.sh
openbao-verify-ssh: ## Verify SSH engine mount, roles, and warden-sign policy
KUBECTL='$(KUBECTL)' OPENBAO_NAMESPACE=$(OPENBAO_NAMESPACE) \
OPENBAO_RELEASE=$(OPENBAO_RELEASE) scripts/openbao-verify-ssh-engine.sh
openbao-verify-authenticated: ## Run authenticated non-mutating OpenBao audit/auth/mount checks
KUBECTL='$(KUBECTL)' OPENBAO_NAMESPACE=$(OPENBAO_NAMESPACE) \
OPENBAO_RELEASE=$(OPENBAO_RELEASE) scripts/openbao-verify-authenticated.sh $(OPENBAO_VERIFY_AUTH_ARGS)
openbao-configure-external-secrets-issue-core: ## Configure OpenBao policy/role for issue-core ESO pilot
KUBECTL='$(KUBECTL)' OPENBAO_NAMESPACE=$(OPENBAO_NAMESPACE) \
OPENBAO_RELEASE=$(OPENBAO_RELEASE) ESO_NAMESPACE=$(EXTERNAL_SECRETS_NAMESPACE) \
scripts/openbao-apply-external-secrets-issue-core.sh
openbao-configure-external-secrets-activity-core: ## Configure OpenBao policy/role for activity-core ESO lane
KUBECTL='$(KUBECTL)' OPENBAO_NAMESPACE=$(OPENBAO_NAMESPACE) OPENBAO_RELEASE=$(OPENBAO_RELEASE) ESO_NAMESPACE=$(EXTERNAL_SECRETS_NAMESPACE) OPENBAO_ESO_ROLE=external-secrets-activity-core OPENBAO_ESO_POLICY=workload-kv-read-llm-connect-provider-secrets POLICY_FILE='$(CURDIR)/openbao/policies/workload-kv-read-llm-connect-provider-secrets.hcl' scripts/openbao-apply-external-secrets-issue-core.sh
openbao-workload-kv-lanes-dry-run: ## Dry-run OpenBao workload KV read-lane policy apply
scripts/openbao-apply-workload-kv-lanes.sh --dry-run $(OPENBAO_WORKLOAD_KV_ARGS)
openbao-configure-workload-kv-lanes: ## Configure OpenBao workload KV read-lane policies
KUBECTL='$(KUBECTL)' OPENBAO_NAMESPACE=$(OPENBAO_NAMESPACE) \
OPENBAO_RELEASE=$(OPENBAO_RELEASE) \
scripts/openbao-apply-workload-kv-lanes.sh $(OPENBAO_WORKLOAD_KV_ARGS)
openbao-validate-restore-evidence: ## Validate non-secret OpenBao restore-drill evidence JSON
OPENBAO_RESTORE_EVIDENCE='$(OPENBAO_RESTORE_EVIDENCE)' \
scripts/openbao-validate-restore-evidence.sh
@@ -139,6 +196,140 @@ openbao-validate-emergency-evidence: ## Validate non-secret OpenBao emergency se
OPENBAO_EMERGENCY_EVIDENCE='$(OPENBAO_EMERGENCY_EVIDENCE)' \
scripts/openbao-validate-emergency-drill-evidence.sh
##@ Credential broker
credential-grants-validate: ## Validate non-secret credential grant catalog
scripts/credential-grants-validate.py $(CREDENTIAL_GRANTS)
credential-change-validate: ## Validate non-secret credential change requests
scripts/credential-change.py validate
credential-change-render: ## Render a credential change request review summary
scripts/credential-change.py render $(CREDENTIAL_CHANGE)
credential-change-plan: ## Render a credential change request apply plan for review
scripts/credential-change.py plan $(CREDENTIAL_CHANGE)
credential-change-decision-templates: ## Render CCR approve/deny/needs-changes templates
scripts/credential-change.py decision-templates $(CREDENTIAL_CHANGE)
credential-change-status: ## Render credential change request readiness status
scripts/credential-change.py status $(CREDENTIAL_CHANGE)
credential-change-status-json: ## Render credential change request readiness status as JSON
scripts/credential-change.py status --json $(CREDENTIAL_CHANGE)
credential-change-sync-decision: ## Sync resolved State Hub decision back into a CCR
scripts/credential-change.py sync-decision $(CREDENTIAL_CHANGE) --state-hub-url $(STATE_HUB_URL)
credential-change-apply-plan: ## Render approved-only operator apply plan
scripts/credential-change.py apply-plan $(CREDENTIAL_CHANGE)
credential-change-operator-commands: ## Render approved-only non-secret OpenBao operator commands
scripts/credential-change.py operator-commands $(CREDENTIAL_CHANGE)
credential-change-applier-dry-run: ## Validate delegated OpenBao metadata mutations for a CCR
scripts/credential-change.py applier-dry-run $(CREDENTIAL_CHANGE)
credential-change-applier-apply-plan: ## Render delegated OpenBao metadata apply plan
scripts/credential-change.py applier-apply $(CREDENTIAL_CHANGE) --plan-only
credential-change-applier-apply: ## Apply delegated metadata; pass confirmation/actor args via CREDENTIAL_CHANGE_EVIDENCE_ARGS
scripts/credential-change.py applier-apply $(CREDENTIAL_CHANGE) $(CREDENTIAL_CHANGE_EVIDENCE_ARGS)
credential-change-runbook: ## Render the attended CCR apply/verify runbook
scripts/credential-change.py runbook $(CREDENTIAL_CHANGE)
credential-change-record-evidence: ## Record non-secret CCR evidence; pass CREDENTIAL_CHANGE_EVIDENCE_ARGS
scripts/credential-change.py record-evidence $(CREDENTIAL_CHANGE) $(CREDENTIAL_CHANGE_EVIDENCE_ARGS)
credential-change-lifecycle-plan: ## Render deactivation/rotation/compromise lifecycle guidance
scripts/credential-change.py lifecycle-plan $(CREDENTIAL_CHANGE) --action $(CREDENTIAL_CHANGE_LIFECYCLE_ACTION)
credential-change-lifecycle-event: ## Record lifecycle event; pass CREDENTIAL_CHANGE_LIFECYCLE_ARGS
scripts/credential-change.py lifecycle-event $(CREDENTIAL_CHANGE) --action $(CREDENTIAL_CHANGE_LIFECYCLE_ACTION) $(CREDENTIAL_CHANGE_LIFECYCLE_ARGS)
credential-change-import-inventory: ## Import existing lane as non-secret CCR; pass CREDENTIAL_CHANGE_IMPORT_ARGS
scripts/credential-change.py import-inventory $(CREDENTIAL_CHANGE_IMPORT_ARGS)
openbao-credential-change-appliers-dry-run: ## Dry-run credential-change applier policies/token roles
scripts/openbao-apply-credential-change-appliers.py --dry-run $(OPENBAO_CREDENTIAL_CHANGE_APPLIER_ARGS)
openbao-configure-credential-change-appliers: ## Apply credential-change applier policies/token roles
KUBECTL='$(KUBECTL)' OPENBAO_NAMESPACE=$(OPENBAO_NAMESPACE) \
OPENBAO_RELEASE=$(OPENBAO_RELEASE) \
scripts/openbao-apply-credential-change-appliers.py $(OPENBAO_CREDENTIAL_CHANGE_APPLIER_ARGS)
openbao-token-grants-dry-run: ## Dry-run OpenBao token roles and issuer policies for credential grants
scripts/openbao-apply-token-grants.py --dry-run $(OPENBAO_TOKEN_GRANT_ARGS)
openbao-configure-token-grants: ## Apply OpenBao token roles and issuer policies for credential grants
KUBECTL='$(KUBECTL)' OPENBAO_NAMESPACE=$(OPENBAO_NAMESPACE) \
OPENBAO_RELEASE=$(OPENBAO_RELEASE) \
scripts/openbao-apply-token-grants.py $(OPENBAO_TOKEN_GRANT_ARGS)
openbao-verify-token-grants-dry-run: ## Dry-run OpenBao token grant verification
scripts/openbao-verify-token-grants.py --dry-run $(OPENBAO_TOKEN_GRANT_ARGS)
openbao-verify-token-grants: ## Verify OpenBao token roles and issuer policies for credential grants
KUBECTL='$(KUBECTL)' OPENBAO_NAMESPACE=$(OPENBAO_NAMESPACE) \
OPENBAO_RELEASE=$(OPENBAO_RELEASE) \
scripts/openbao-verify-token-grants.py $(OPENBAO_TOKEN_GRANT_ARGS)
openbao-verify-token-grants-smoke: ## Mint/revoke a child token and prove bounded warden-sign capabilities
KUBECTL='$(KUBECTL)' OPENBAO_NAMESPACE=$(OPENBAO_NAMESPACE) \
OPENBAO_RELEASE=$(OPENBAO_RELEASE) \
scripts/openbao-verify-token-grants.py --issue-smoke-token $(OPENBAO_TOKEN_GRANT_ARGS)
credential-helper-dry-run: ## Dry-run credential request, exec, status, and revoke helper flows
scripts/credential.py $(CREDENTIAL_HELPER_GLOBAL_ARGS) request --dry-run \
--grant ops-warden/warden-sign --purpose $(CREDENTIAL_HELPER_PURPOSE) \
$(CREDENTIAL_HELPER_ARGS)
scripts/credential.py $(CREDENTIAL_HELPER_GLOBAL_ARGS) request --dry-run \
--grant ops-warden/warden-sign --purpose $(CREDENTIAL_HELPER_PURPOSE) \
--delivery kubernetes-auth $(CREDENTIAL_HELPER_ARGS)
scripts/credential.py $(CREDENTIAL_HELPER_GLOBAL_ARGS) exec --dry-run \
--grant ops-warden/warden-sign --purpose $(CREDENTIAL_HELPER_PURPOSE) \
$(CREDENTIAL_HELPER_ARGS) -- SMOKE_VAULT=1 /bin/true
scripts/credential.py $(CREDENTIAL_HELPER_GLOBAL_ARGS) status --dry-run example-accessor
scripts/credential.py $(CREDENTIAL_HELPER_GLOBAL_ARGS) revoke --dry-run example-accessor
credential-tests: ## Run offline credential broker unit tests
python3 -m unittest discover -s tests -p 'test_credential*.py'
credential-change-tests: ## Run credential change request unit tests
python3 -m unittest discover -s tests -p 'test_credential_change.py'
credential-exec-ops-warden-smoke: ## Run ops-warden smoke with an exec-injected warden-sign token
KUBECTL='$(KUBECTL)' OPENBAO_NAMESPACE=$(OPENBAO_NAMESPACE) \
OPENBAO_RELEASE=$(OPENBAO_RELEASE) \
scripts/credential.py $(CREDENTIAL_HELPER_GLOBAL_ARGS) exec \
--grant ops-warden/warden-sign --purpose ops-warden-production-sign-smoke \
$(CREDENTIAL_HELPER_ARGS) -- \
$(CREDENTIAL_HELPER_CHILD_ENV) \
SMOKE_VAULT=1 /home/worsch/ops-warden/scripts/policy_gate_production_smoke.sh
##@ ArgoCD GitOps bootstrap
argocd-bootstrap-dry-run: ## Server-side dry-run ArgoCD AppProjects and root Application
$(KUBECTL) apply --dry-run=server -k $(ARGOCD_BOOTSTRAP_DIR)
argocd-bootstrap-deploy: ## Apply ArgoCD AppProjects and root Application
$(KUBECTL) apply -k $(ARGOCD_BOOTSTRAP_DIR)
argocd-repo-apply: ## Apply a SOPS-encrypted ArgoCD repository Secret (set ARGOCD_REPOSITORY_SECRET)
@test -n "$(ARGOCD_REPOSITORY_SECRET)" || \
(echo "ERROR: set ARGOCD_REPOSITORY_SECRET=argocd/repositories/<repo>.repository.sops.yaml"; exit 1)
sops -d $(ARGOCD_REPOSITORY_SECRET) | $(KUBECTL) apply -f -
argocd-status: ## Show Railiance ArgoCD projects, root app, and registered repos
$(KUBECTL) get appprojects.argoproj.io -n $(ARGOCD_NAMESPACE) \
railiance-bootstrap railiance-tenants railiance-platform-addons
$(KUBECTL) get applications.argoproj.io -n $(ARGOCD_NAMESPACE) \
railiance-apps-root external-secrets openbao-secretstore issue-core
$(KUBECTL) get secrets -n $(ARGOCD_NAMESPACE) \
-l argocd.argoproj.io/secret-type=repository
##@ Backup
backup: ## Backup platform services (PostgreSQL logical dump) — age-encrypted to Nextcloud
@@ -151,4 +342,4 @@ help: ## Show this help
/^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-22s\033[0m %s\n", $$1, $$2 } \
/^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) }' $(MAKEFILE_LIST)
.PHONY: db-deploy db-status db-shell db-logs apps-pg-deploy apps-pg-status apps-pg-shell apps-pg-logs net-kingdom-pg-inter-hub-networkpolicy-deploy pg-deploy pg-status pg-pgpool-check valkey-deploy valkey-status openbao-repo openbao-dry-run openbao-deploy openbao-status openbao-verify openbao-verify-post-unseal openbao-configure-initial openbao-verify-authenticated openbao-validate-restore-evidence openbao-validate-emergency-evidence backup help
.PHONY: db-deploy db-status db-shell db-logs apps-pg-deploy apps-pg-status apps-pg-shell apps-pg-logs net-kingdom-pg-inter-hub-networkpolicy-deploy pg-deploy pg-status pg-pgpool-check valkey-deploy valkey-status openbao-repo openbao-dry-run openbao-overlay-apply openbao-verify-login-overlay openbao-deploy openbao-status openbao-verify openbao-verify-post-unseal openbao-configure-initial openbao-configure-ssh openbao-verify-ssh openbao-verify-authenticated openbao-configure-external-secrets-issue-core openbao-configure-external-secrets-activity-core openbao-validate-restore-evidence openbao-validate-emergency-evidence credential-grants-validate credential-change-applier-dry-run credential-change-applier-apply-plan credential-change-applier-apply credential-change-runbook credential-change-record-evidence credential-change-lifecycle-plan credential-change-lifecycle-event credential-change-import-inventory openbao-credential-change-appliers-dry-run openbao-configure-credential-change-appliers openbao-token-grants-dry-run openbao-configure-token-grants openbao-verify-token-grants-dry-run openbao-verify-token-grants openbao-verify-token-grants-smoke credential-helper-dry-run credential-tests credential-exec-ops-warden-smoke argocd-bootstrap-dry-run argocd-bootstrap-deploy argocd-repo-apply argocd-status backup help

View File

@@ -0,0 +1,18 @@
# Railiance ArgoCD Tenant Applications
This directory is synced by the `railiance-apps-root` ArgoCD Application.
Tenant teams author a thin ArgoCD `Application` manifest against the contract
in `docs/argocd-gitops.md`. Platform review merges that manifest here after
checking namespace, repository, sync policy, and secret-delivery shape.
Workload manifests stay in the owning tenant repo. The default source path for
tenant workloads is:
```text
k8s/railiance/
```
Do not commit Kubernetes Secret values, ArgoCD repository credentials, OpenBao
tokens, deploy keys, or API keys here.

View File

@@ -0,0 +1,35 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: external-secrets
namespace: argocd
labels:
app.kubernetes.io/part-of: railiance-gitops
railiance-platform/component: external-secrets
annotations:
argocd.argoproj.io/sync-wave: "0"
spec:
project: railiance-platform-addons
source:
repoURL: https://charts.external-secrets.io
chart: external-secrets
targetRevision: 0.16.1
helm:
releaseName: external-secrets
values: |
installCRDs: true
serviceAccount:
create: true
name: external-secrets
destination:
server: https://kubernetes.default.svc
namespace: external-secrets
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
- ServerSideApply=true
- ApplyOutOfSyncOnly=true
- PruneLast=true

View File

@@ -0,0 +1,27 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: issue-core
namespace: argocd
labels:
app.kubernetes.io/part-of: railiance-gitops
railiance.io/domain: issue-core
annotations:
argocd.argoproj.io/sync-wave: "10"
spec:
project: railiance-tenants
source:
repoURL: https://gitea.coulomb.social/coulomb/issue-core.git
targetRevision: main
path: k8s/railiance
destination:
server: https://kubernetes.default.svc
namespace: issue-core
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
- ApplyOutOfSyncOnly=true
- PruneLast=true

View File

@@ -0,0 +1,27 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: openbao-secretstore
namespace: argocd
labels:
app.kubernetes.io/part-of: railiance-gitops
railiance-platform/component: external-secrets
annotations:
argocd.argoproj.io/sync-wave: "1"
spec:
project: railiance-platform-addons
source:
repoURL: https://gitea.coulomb.social/coulomb/railiance-platform.git
targetRevision: main
path: argocd/platform-addons/openbao-secretstore
destination:
server: https://kubernetes.default.svc
namespace: external-secrets
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
- ApplyOutOfSyncOnly=true
- PruneLast=true

View File

@@ -0,0 +1,22 @@
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: railiance-bootstrap
namespace: argocd
labels:
app.kubernetes.io/part-of: railiance-gitops
railiance-platform/component: gitops
spec:
description: Platform-owned ArgoCD bootstrap project for Railiance app-of-apps.
sourceRepos:
- https://gitea.coulomb.social/coulomb/railiance-platform.git
destinations:
- server: https://kubernetes.default.svc
namespace: argocd
clusterResourceWhitelist: []
namespaceResourceWhitelist:
- group: argoproj.io
kind: Application
orphanedResources:
warn: true

View File

@@ -0,0 +1,52 @@
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: railiance-tenants
namespace: argocd
labels:
app.kubernetes.io/part-of: railiance-gitops
railiance-platform/component: gitops
spec:
description: Guardrails for Railiance tenant applications deployed by ArgoCD.
sourceRepos:
- https://gitea.coulomb.social/coulomb/*.git
destinations:
- server: https://kubernetes.default.svc
namespace: "*"
clusterResourceWhitelist:
- group: ""
kind: Namespace
namespaceResourceWhitelist:
- group: ""
kind: ConfigMap
- group: ""
kind: PersistentVolumeClaim
- group: ""
kind: Secret
- group: ""
kind: Service
- group: ""
kind: ServiceAccount
- group: apps
kind: Deployment
- group: apps
kind: StatefulSet
- group: autoscaling
kind: HorizontalPodAutoscaler
- group: batch
kind: CronJob
- group: batch
kind: Job
- group: external-secrets.io
kind: ExternalSecret
- group: networking.k8s.io
kind: Ingress
- group: networking.k8s.io
kind: NetworkPolicy
- group: traefik.io
kind: IngressRoute
- group: traefik.io
kind: Middleware
orphanedResources:
warn: true

View File

@@ -0,0 +1,48 @@
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: railiance-platform-addons
namespace: argocd
labels:
app.kubernetes.io/part-of: railiance-gitops
railiance-platform/component: gitops
spec:
description: Platform-owned cluster add-ons required by tenant workloads.
sourceRepos:
- https://gitea.coulomb.social/coulomb/railiance-platform.git
- https://charts.external-secrets.io
destinations:
- server: https://kubernetes.default.svc
namespace: "*"
clusterResourceWhitelist:
- group: ""
kind: Namespace
- group: apiextensions.k8s.io
kind: CustomResourceDefinition
- group: admissionregistration.k8s.io
kind: MutatingWebhookConfiguration
- group: admissionregistration.k8s.io
kind: ValidatingWebhookConfiguration
- group: rbac.authorization.k8s.io
kind: ClusterRole
- group: rbac.authorization.k8s.io
kind: ClusterRoleBinding
- group: external-secrets.io
kind: ClusterSecretStore
namespaceResourceWhitelist:
- group: ""
kind: ConfigMap
- group: ""
kind: Secret
- group: ""
kind: Service
- group: ""
kind: ServiceAccount
- group: apps
kind: Deployment
- group: rbac.authorization.k8s.io
kind: Role
- group: rbac.authorization.k8s.io
kind: RoleBinding
orphanedResources:
warn: true

View File

@@ -0,0 +1,26 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: railiance-apps-root
namespace: argocd
labels:
app.kubernetes.io/part-of: railiance-gitops
railiance-platform/component: gitops
spec:
project: railiance-bootstrap
source:
repoURL: https://gitea.coulomb.social/coulomb/railiance-platform.git
targetRevision: main
path: argocd/applications
destination:
server: https://kubernetes.default.svc
namespace: argocd
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=false
- ApplyOutOfSyncOnly=true
- PruneLast=true

View File

@@ -0,0 +1,8 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- 00-railiance-bootstrap-project.yaml
- 01-railiance-tenants-project.yaml
- 02-railiance-platform-addons-project.yaml
- 10-railiance-apps-root.application.yaml

View File

@@ -0,0 +1,6 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- openbao.clustersecretstore.yaml
- openbao-activity-core.clustersecretstore.yaml

View File

@@ -0,0 +1,23 @@
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
name: openbao-activity-core
labels:
app.kubernetes.io/part-of: railiance-gitops
railiance-platform/component: external-secrets
spec:
provider:
vault:
server: http://openbao.openbao.svc.cluster.local:8200
path: platform
version: v2
auth:
kubernetes:
mountPath: kubernetes
role: external-secrets-activity-core
serviceAccountRef:
name: external-secrets
namespace: external-secrets
conditions:
- namespaces:
- activity-core

View File

@@ -0,0 +1,23 @@
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
name: openbao
labels:
app.kubernetes.io/part-of: railiance-gitops
railiance-platform/component: external-secrets
spec:
provider:
vault:
server: http://openbao.openbao.svc.cluster.local:8200
path: platform
version: v2
auth:
kubernetes:
mountPath: kubernetes
role: external-secrets-issue-core
serviceAccountRef:
name: external-secrets
namespace: external-secrets
conditions:
- namespaces:
- issue-core

View File

@@ -0,0 +1,23 @@
# ArgoCD Repository Registration
ArgoCD discovers Git repositories from Kubernetes Secrets in the `argocd`
namespace with `argocd.argoproj.io/secret-type: repository`.
Use the templates in this directory to create SOPS-encrypted, non-plaintext
repository Secret files. Credentials must be sourced from the approved
operator/OpenBao path and must never be committed in plaintext.
Recommended OpenBao path:
```text
platform/operators/argocd/repositories/<repo-name>
```
After creating an encrypted file such as
`argocd/repositories/railiance-platform.repository.sops.yaml`, apply it with:
```bash
ARGOCD_REPOSITORY_SECRET=argocd/repositories/railiance-platform.repository.sops.yaml \
make argocd-repo-apply
```

View File

@@ -0,0 +1,21 @@
# Copy to issue-core.repository.sops.yaml, fill from the approved
# operator/OpenBao path, then encrypt with:
# sops -e -i argocd/repositories/issue-core.repository.sops.yaml
#
# Do not commit plaintext credentials.
apiVersion: v1
kind: Secret
metadata:
name: issue-core-repository
namespace: argocd
labels:
argocd.argoproj.io/secret-type: repository
app.kubernetes.io/part-of: railiance-gitops
railiance-platform/component: gitops
stringData:
type: git
project: railiance-tenants
url: https://gitea.coulomb.social/coulomb/issue-core.git
username: CHANGE_ME
password: CHANGE_ME

View File

@@ -0,0 +1,21 @@
# Copy to railiance-platform.repository.sops.yaml, fill from the approved
# operator/OpenBao path, then encrypt with:
# sops -e -i argocd/repositories/railiance-platform.repository.sops.yaml
#
# Do not commit plaintext credentials.
apiVersion: v1
kind: Secret
metadata:
name: railiance-platform-repository
namespace: argocd
labels:
argocd.argoproj.io/secret-type: repository
app.kubernetes.io/part-of: railiance-gitops
railiance-platform/component: gitops
stringData:
type: git
project: railiance-bootstrap
url: https://gitea.coulomb.social/coulomb/railiance-platform.git
username: CHANGE_ME
password: CHANGE_ME

View File

@@ -0,0 +1,173 @@
id: CCR-2026-0001
kind: credential-change-request
schema_version: 1
request_type: workload-kv-read
title: whynot-design npm publish token lane
status: active
created: '2026-06-27'
updated: '2026-06-29'
requester:
agent: ops-warden
message_id: fe5b1696-8956-4bd5-9d6f-dbde1901a076
reason: Allow ops-warden to proxy caller-scoped access to whynot-design's npm publish
token.
review:
required: true
required_approvers:
- platform-operator
comments:
- at: '2026-06-27T21:40:22+00:00'
reviewer: bernd.worsch
decision: binding_confirmed
comment: 'Confirmed in chat: groups=whynot-design is the intended KeyCape/NetKingdom
binding for the whynot-design npm publish lane.'
- at: '2026-06-27T22:06:18+00:00'
reviewer: human
decision: approved
comment: 'State Hub decision 250669d0-8475-4527-9624-cd072249f9a9: APPROVE: scoped
path and confirmed binding are acceptable'
- at: '2026-06-27T22:54:20+00:00'
reviewer: bernd.worsch
decision: scope_corrected_requires_review
comment: Corrected tenant from whynot-design to coulomb per operator clarification.
The previous approval covered platform/workloads/whynot-design/whynot-design/npm-publish
and must not be reused for the corrected platform/workloads/coulomb/whynot-design/npm-publish
scope.
- at: '2026-06-27T23:23:19+00:00'
reviewer: human
decision: approved
comment: 'State Hub decision e6381a56-6b04-4fd5-b2de-f3ef59cde888: APPROVE: We
fixed the path using coulomb as the org/tenant.'
target:
domain: financials
tenant: coulomb
workload: whynot-design
environment: production
purpose: npm package publishing through ops-warden caller-scoped fetch/exec
openbao:
mount: platform
kv_path: platform/workloads/coulomb/whynot-design/npm-publish
fields:
- NPM_AUTH_TOKEN
policy_name: workload-kv-read-whynot-design-npm-publish
policy_file: openbao/policies/workload-kv-read-whynot-design-npm-publish.hcl
auth:
method: oidc
mount: netkingdom
role: whynot-design-workload-kv-read
allowed_redirect_uris:
- https://bao.coulomb.social/ui/vault/auth/netkingdom/oidc/callback
- http://localhost:8250/oidc/callback
- http://127.0.0.1:8250/oidc/callback
oidc_scopes:
- openid
- profile
- email
- groups
user_claim: sub
groups_claim: groups
bound_claims:
groups:
- whynot-design
bound_claims_confirmed: true
policies:
- workload-kv-read-whynot-design-npm-publish
ttl: 15m
access_frontdoor:
type: ops-warden
catalog_id: whynot-design-npm-publish
selector: npm publish token
command: warden access whynot-design-npm-publish --exec -- npm publish
resolvable: true
readiness: ready
activation: verified-positive-and-negative-caller-verification
risk:
classification: high
notes:
- Grants read access to the credential used to publish npm packages.
- Uses a publish-specific catalog id; a future read-only npm token must use a separate
catalog id.
- The OIDC bound claim was confirmed in review; re-confirm if the claim changes.
- ops-warden must proxy the read as the caller and must not retain the token value.
verification:
positive:
- Approved whynot-design identity can fetch field NPM_AUTH_TOKEN through OpenBao
or ops-warden.
negative:
- Non-whynot identity cannot read the path or field.
activation_conditions:
- Policy applied with platform-admin/operator authority.
- OIDC role bound to confirmed whynot-design claim or approved service account.
- Secret value provisioned directly in OpenBao through approved operator custody.
- Positive and negative verification recorded with non-secret audit ids or timestamps.
evidence:
- at: '2026-06-28T10:37:42+00:00'
actor: codex
kind: non_secret_openbao_apply_check
result: passed
details:
- Policy read succeeded for workload-kv-read-whynot-design-npm-publish.
- OIDC role read showed the whynot-design bound claim, read policy, and callback URIs.
- Metadata read showed catalog-id whynot-design-npm-publish.
- Secret field presence check found NPM_AUTH_TOKEN without printing or recording the value.
- at: '2026-06-28T11:20:06+00:00'
actor: codex
kind: non_secret_oidc_role_correction
result: applied
details:
- Positive login reported missing groups claim because the role did not request the groups scope.
- Updated auth/netkingdom/role/whynot-design-workload-kv-read with oidc_scopes openid/profile/email/groups.
- Added the 127.0.0.1 local CLI callback URI alongside localhost and browser callbacks.
- at: '2026-06-28T14:01:47+00:00'
actor: codex
kind: non_secret_identity_group_check
result: applied
details:
- Positive login advanced from missing groups claim to bound claim mismatch; this confirms the groups scope is now requested.
- Live LLDAP group inventory did not contain whynot-design before this check.
- Created and verified the whynot-design LLDAP group for the approved OpenBao bound claim.
- No user membership was changed; positive verification still requires the authenticating account to be explicitly added to whynot-design.
- at: '2026-06-28T15:22:29+00:00'
actor: bernd.worsch
kind: positive_fetch_verification
result: passed
details:
- Attended OIDC login for auth/netkingdom/role/whynot-design-workload-kv-read succeeded with workload-kv-read-whynot-design-npm-publish policy.
- NPM_AUTH_TOKEN field fetch from platform/workloads/coulomb/whynot-design/npm-publish exited successfully with output redirected to /dev/null.
- The secret value was not printed or recorded.
- A short-lived OpenBao client token was printed by the CLI login output and was revoked by accessor immediately after the report.
- Negative denial verification is still pending; keep the front door non-resolvable until it passes.
- at: '2026-06-28T22:06:43+00:00'
actor: bernd.worsch
kind: negative_denial_verification
result: passed
details:
- platform-root was temporarily removed from the whynot-design LLDAP group for the attended negative check.
- OIDC login for auth/netkingdom/role/whynot-design-workload-kv-read failed with a groups bound-claim mismatch.
- No OpenBao client token was issued for the negative identity, and no NPM_AUTH_TOKEN value was printed or recorded.
- at: '2026-06-28T22:08:50+00:00'
actor: codex
kind: identity_group_restore
result: passed
details:
- Restored platform-root membership in the whynot-design LLDAP group after negative verification.
- Verified whynot-design membership contains platform-root and no unexpected additional users.
- Positive and negative verification gates are now complete; access_frontdoor is ready/resolvable.
lifecycle:
deactivate: Disable ops-warden catalog entry and remove or detach auth role policy.
rotate: Replace NPM_AUTH_TOKEN value directly in OpenBao and record non-secret rotation
evidence.
compromised: Immediately deactivate access front door, rotate npm token, record
blast-radius notes, and open incident follow-up tasks.
state_hub:
workplan_id: RAILIANCE-WP-0007
related_workplan_id: RAILIANCE-WP-0006
ops_warden_reply_message_id: b175c561-7858-43f5-a309-949b0dede1b4
ops_warden_batch_message_id: fe5b1696-8956-4bd5-9d6f-dbde1901a076
superseded_decision_id: 250669d0-8475-4527-9624-cd072249f9a9
superseded_decision_resolved_at: '2026-06-27T22:04:32.956077Z'
superseded_decision_reason: tenant/workload scope corrected before secret provisioning
decision_id: e6381a56-6b04-4fd5-b2de-f3ef59cde888
decision_api_url: http://127.0.0.1:8000/decisions/e6381a56-6b04-4fd5-b2de-f3ef59cde888
decision_dashboard_url: http://127.0.0.1:3000/decisions
decision_resolved_at: '2026-06-27T23:16:21.905924Z'

View File

@@ -0,0 +1,105 @@
id: CCR-2026-0002
kind: credential-change-request
schema_version: 1
request_type: workload-kv-read
title: issue-core runtime ingestion key lane
status: proposed
created: '2026-06-27'
updated: '2026-06-30'
requester:
agent: ops-warden
message_id: fe5b1696-8956-4bd5-9d6f-dbde1901a076
reason: Confirm and provision the issue-core workload KV lane requested in the ops-warden
batch.
review:
required: true
required_approvers:
- platform-operator
- issue-core-owner
comments:
- at: '2026-06-29T22:53:03+00:00'
reviewer: codex
decision: metadata_review_binding_confirmed
comment: Live cluster metadata on 2026-06-30 confirms ExternalSecret issue-core/issue-core-runtime
is Ready=True (SecretSynced) and maps ISSUE_CORE_API_KEY plus GITEA_BACKEND_TOKEN
from platform/workloads/issue-core/issue-core/issue-core-runtime. The workload
Deployment uses the default service account; OpenBao auth for this delivery
path is the platform ClusterSecretStore/openbao role external-secrets-issue-core
bound to service account external-secrets/external-secrets. Keep CCR status
proposed until platform/operator and issue-core-owner approval.
target:
domain: financials
tenant: issue-core
workload: issue-core
environment: production
purpose: issue-core runtime ingestion through OpenBao workload KV and External Secrets
openbao:
mount: platform
kv_path: platform/workloads/issue-core/issue-core/issue-core-runtime
fields:
- ISSUE_CORE_API_KEY
- GITEA_BACKEND_TOKEN
policy_name: workload-kv-read-issue-core-runtime
policy_file: openbao/policies/workload-kv-read-issue-core-runtime.hcl
auth:
method: kubernetes
mount: kubernetes
role: external-secrets-issue-core
bound_claims:
service_account_names:
- external-secrets
service_account_namespaces:
- external-secrets
bound_claims_confirmed: true
policies:
- workload-kv-read-issue-core-runtime
ttl: 15m
access_frontdoor:
type: ops-warden
catalog_id: issue-core-ingestion-api-key
selector: issue-core ingestion API key
command: warden access issue-core-ingestion-api-key --fetch ISSUE_CORE_API_KEY
resolvable: false
readiness: template
activation: draft-until-ccr-verified
delivery:
surface: external-secrets
target: ExternalSecret issue-core/issue-core-runtime -> Secret issue-core-runtime
in the issue-core namespace
risk:
classification: high
notes:
- Grants read access to issue-core runtime ingestion credentials through the platform
External Secrets path.
- GITEA_BACKEND_TOKEN remains included because the live issue-core ExternalSecret
maps it alongside ISSUE_CORE_API_KEY; remove it before approval only if the issue-core
owner confirms it is no longer required.
- The Kubernetes auth subject is the External Secrets operator service account external-secrets/external-secrets,
with ClusterSecretStore usage limited to the issue-core namespace.
- ops-warden must proxy reads as the caller and must not retain token values.
verification:
positive:
- ExternalSecret issue-core/issue-core-runtime is Ready=True and syncs the configured
fields without printing values.
- Approved issue-core runtime can consume the resulting Kubernetes Secret without
exposing values.
negative:
- A namespace outside the approved ClusterSecretStore condition cannot use this
store to read the path.
- A service account outside external-secrets/external-secrets cannot authenticate
through the External Secrets OpenBao role.
activation_conditions:
- Policy applied with platform-admin/operator authority.
- Kubernetes auth role bound to external-secrets/external-secrets for the issue-core
External Secrets delivery path.
- Secret values provisioned directly in OpenBao through approved operator custody.
- Positive and negative verification recorded with non-secret audit ids or timestamps.
lifecycle:
deactivate: Disable ops-warden catalog entry and remove or detach auth role policy.
rotate: Replace issue-core runtime secret values directly in OpenBao and record
non-secret rotation evidence.
compromised: Immediately deactivate access front door, rotate affected values, record
blast-radius notes, and open incident follow-up tasks.
state_hub:
workplan_id: RAILIANCE-WP-0007
ops_warden_batch_message_id: fe5b1696-8956-4bd5-9d6f-dbde1901a076

View File

@@ -0,0 +1,102 @@
id: CCR-2026-0003
kind: credential-change-request
schema_version: 1
request_type: workload-kv-read
title: llm-connect OpenRouter provider key lane
status: proposed
created: '2026-06-27'
updated: '2026-06-30'
requester:
agent: ops-warden
message_id: fe5b1696-8956-4bd5-9d6f-dbde1901a076
reason: Confirm and provision the llm-connect OpenRouter workload KV lane requested
in the ops-warden batch.
review:
required: true
required_approvers:
- platform-operator
- activity-core-owner
comments:
- at: '2026-06-29T22:53:03+00:00'
reviewer: codex
decision: metadata_review_binding_confirmed_pending_owner_approval
comment: Live cluster metadata on 2026-06-30 confirms namespace activity-core
exists and the External Secrets operator service account external-secrets/external-secrets
exists. The proposed llm-connect service account does not exist; the llm-connect
Deployment currently uses the default service account. Updated the proposed
OpenBao auth binding to the platform ESO pattern with role external-secrets-activity-core.
No activity-core ExternalSecret exists yet; a namespace-limited ClusterSecretStore
source manifest was added for future rollout. Keep CCR status proposed until
platform/operator and activity-core-owner approval.
target:
domain: financials
tenant: activity-core
workload: llm-connect
environment: production
purpose: llm-connect provider access through OpenBao workload KV and External Secrets
openbao:
mount: platform
kv_path: platform/workloads/activity-core/llm-connect/llm-connect-provider-secrets
fields:
- OPENROUTER_API_KEY
policy_name: workload-kv-read-llm-connect-provider-secrets
policy_file: openbao/policies/workload-kv-read-llm-connect-provider-secrets.hcl
auth:
method: kubernetes
mount: kubernetes
role: external-secrets-activity-core
bound_claims:
service_account_names:
- external-secrets
service_account_namespaces:
- external-secrets
bound_claims_confirmed: true
policies:
- workload-kv-read-llm-connect-provider-secrets
ttl: 15m
access_frontdoor:
type: ops-warden
catalog_id: llm-connect-openrouter-api-key
selector: llm-connect OpenRouter API key
command: warden access llm-connect-openrouter-api-key --fetch OPENROUTER_API_KEY
resolvable: false
readiness: template
activation: draft-until-ccr-verified
delivery:
surface: external-secrets
target: ExternalSecret to Secret llm-connect-provider-secrets in the activity-core
namespace
risk:
classification: high
notes:
- Grants read access to the provider key used by llm-connect for OpenRouter requests
through the platform External Secrets path.
- The Kubernetes auth subject is the External Secrets operator service account external-secrets/external-secrets,
with ClusterSecretStore usage limited to the activity-core namespace.
- ops-warden must proxy reads as the caller and must not retain token values.
verification:
positive:
- An approved activity-core ExternalSecret can sync field OPENROUTER_API_KEY to
Secret llm-connect-provider-secrets without printing the value.
- The llm-connect runtime can consume the resulting Kubernetes Secret without exposing
values.
negative:
- A namespace outside the approved ClusterSecretStore condition cannot use this
store to read the path.
- A service account outside external-secrets/external-secrets cannot authenticate
through the External Secrets OpenBao role.
activation_conditions:
- Policy applied with platform-admin/operator authority.
- Kubernetes auth role bound to external-secrets/external-secrets for the activity-core
External Secrets delivery path.
- Secret value provisioned directly in OpenBao through approved operator custody.
- Positive and negative verification recorded with non-secret audit ids or timestamps.
lifecycle:
deactivate: Disable ops-warden catalog entry and remove or detach auth role policy.
rotate: Replace OPENROUTER_API_KEY directly in OpenBao and record non-secret rotation
evidence.
compromised: Immediately deactivate access front door, rotate the provider key,
record blast-radius notes, and open incident follow-up tasks.
state_hub:
workplan_id: RAILIANCE-WP-0007
ops_warden_batch_message_id: fe5b1696-8956-4bd5-9d6f-dbde1901a076

View File

@@ -0,0 +1,123 @@
version: 1
updated: "2026-06-27"
owner_repo: railiance-platform
owner_domain: financials
workplan_id: RAILIANCE-WP-0005
state_hub_workstream_id: 2731fece-6c49-45b8-ab8a-4ea6c04ac603
delivery_modes:
allowed_known:
- exec-env
- response-wrap
- local-token-file
- kubernetes-auth
denied_known:
- chat
- state-hub-body
- git
- command-line-token-argument
- llm-prompt
grant_classes:
- self-service
- approval-required
- break-glass
grants:
- id: ops-warden/warden-sign
title: Ops Warden OpenBao SSH signing smoke token
status: pilot
grant_class: self-service
credential_type: openbao-token
issuer: openbao
audience: ops-warden
description: >
Short-lived OpenBao child token for ops-warden SSH signing smoke tests.
The token may only use the warden-sign policy and must not be treated as
an ops-warden-owned secret.
openbao:
namespace: openbao
token_role: warden-sign
issuer_policy: credential-broker-warden-sign-issuer
policies:
- warden-sign
disallowed_policies:
- root
- platform-admin
mount_paths:
- ssh/sign/adm-role
- ssh/sign/agt-role
- ssh/sign/atm-role
- ssh/roles
ttl:
default: 15m
max: 1h
renewable: false
requires_human_above: 1h
actors:
allowed_types:
- human-operator
- approved-agent
- ci-runner
required_subject_binding: keycape-or-kubernetes-service-account
authorization:
flex_auth_required: false
flex_auth_mode: optional-preflight
approval_required: false
purpose_required: true
allowed_purpose_examples:
- flex-auth-openbao-smoke
- ops-warden-production-sign-smoke
delivery:
allowed:
- exec-env
- response-wrap
- local-token-file
- kubernetes-auth
preferred: exec-env
denied:
- chat
- state-hub-body
- git
- command-line-token-argument
- llm-prompt
exec_env:
variable: VAULT_TOKEN
child_only: true
redact_logs: true
response_wrap:
ttl: 5m
unwrap_once: true
local_token_file:
directory: .local/credential-leases
mode: "0600"
kubernetes_auth:
mount: auth/kubernetes
role: credential-broker-warden-sign
audience: openbao
service_account_names:
- credential-broker
- ops-warden-smoke
namespaces:
- openbao
- ops-warden
audit:
openbao_audit_required: true
state_hub_metadata_allowed: true
record_secret_values: false
metadata_fields:
- grant_id
- actor
- subject
- purpose
- requested_ttl
- issued_ttl
- delivery_mode
- lease_accessor
- decision_id
- status
revocation:
required: true
by_accessor: true
on_exec_exit: true
on_denied_request: false

224
docs/argocd-gitops.md Normal file
View File

@@ -0,0 +1,224 @@
# ArgoCD GitOps Contract
Railiance ArgoCD is the cluster sync engine. This repo owns only the shared
platform contract around GitOps trust, guardrails, and OpenBao-backed secret
delivery. Application workload manifests remain in the owning application
repos.
## Ownership Boundary
`railiance-platform` owns:
- ArgoCD AppProject guardrails for Railiance tenant workloads.
- The root app-of-apps entrypoint.
- Repository credential registration templates.
- Secret delivery conventions through OpenBao and External Secrets Operator.
Tenant repos own:
- Container images.
- Workload manifests under `k8s/railiance/`.
- The proposed ArgoCD `Application` manifest for platform review.
- Application config that contains no secret values.
Cluster/runtime ownership remains outside this repo: installing or upgrading
ArgoCD itself belongs with the cluster layer.
## Bootstrap Layout
```text
argocd/bootstrap/
00-railiance-bootstrap-project.yaml
01-railiance-tenants-project.yaml
10-railiance-apps-root.application.yaml
argocd/applications/
*.application.yaml
argocd/repositories/
*.repository.sops.yaml
```
The bootstrap is applied once by an operator. If the Git source is private,
apply the encrypted `railiance-platform` repository Secret first so the root
Application can sync this repo:
```bash
ARGOCD_REPOSITORY_SECRET=argocd/repositories/railiance-platform.repository.sops.yaml \
make argocd-repo-apply
make argocd-bootstrap-dry-run
make argocd-bootstrap-deploy
make argocd-status
```
After that, `railiance-apps-root` syncs tenant Application manifests from
`argocd/applications/`.
## Repository Registration
Every Git source repo used by ArgoCD must be registered in the `argocd`
namespace with an ArgoCD repository Secret. Use one read-only deploy token or
deploy key per repo unless an operator approves a narrower shared credential
model.
Repository credentials are operator credentials, not workload secrets. Store
their source material in OpenBao under:
```text
platform/operators/argocd/repositories/<repo-name>
```
Create an encrypted repository Secret from the matching template:
```bash
cp argocd/repositories/issue-core.repository.sops.yaml.template \
argocd/repositories/issue-core.repository.sops.yaml
sops -e -i argocd/repositories/issue-core.repository.sops.yaml
```
Apply only encrypted files:
```bash
ARGOCD_REPOSITORY_SECRET=argocd/repositories/issue-core.repository.sops.yaml \
make argocd-repo-apply
```
Do not commit plaintext deploy tokens, passwords, SSH private keys, OpenBao
tokens, or ArgoCD API tokens.
## Tenant Application Contract
Tenant Applications are thin routing manifests reviewed into
`argocd/applications/`. The workload source remains in the tenant repo,
normally:
```text
k8s/railiance/
```
Default Application shape:
```yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: example-service
namespace: argocd
spec:
project: railiance-tenants
source:
repoURL: https://gitea.coulomb.social/coulomb/example-service.git
targetRevision: main
path: k8s/railiance
destination:
server: https://kubernetes.default.svc
namespace: example-service
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
- ApplyOutOfSyncOnly=true
- PruneLast=true
```
Use sync waves only for real dependency ordering. Secret delivery resources
should sync before Deployments that consume them.
## Secret Delivery
OpenBao is the canonical runtime secret custody authority.
Default pattern:
1. Store workload secret material in OpenBao.
2. Use External Secrets Operator to materialize a Kubernetes Secret in the
workload namespace.
3. Reference that Kubernetes Secret from the Deployment, Job, or CronJob.
Path convention for workload credential custody:
```text
platform/workloads/<tenant-or-org>/<workload>/<secret-purpose>
```
Kubernetes namespace and service-account bounds belong in the OpenBao auth role
or External Secrets binding, not in the tenant segment unless the namespace is
itself the approved workload identity.
Use CSI-mounted files only for workloads that need file references, sharper
mount boundaries, or refresh behavior that should not rewrite application
manifests. Do not use the OpenBao injector in the current deployment.
For `issue-core`, the expected custody shape is:
```text
platform/workloads/issue-core/issue-core/issue-core-runtime
```
with properties such as:
```text
ISSUE_CORE_API_KEY
GITEA_BACKEND_TOKEN
```
The exact ExternalSecret manifest belongs with `issue-core` workload
manifests, because it is part of that service's runtime deployment.
## AppProject Guardrails
`railiance-bootstrap` allows the root app to manage ArgoCD `Application`
objects in the `argocd` namespace.
`railiance-tenants` allows ordinary namespaced workload resources and namespace
creation. It does not allow tenant Applications to create CRDs, ClusterRoles,
ClusterRoleBindings, or other cluster-admin resources.
If a tenant needs a cluster-scoped platform resource, create a new
platform-owned workplan instead of broadening the tenant project by default.
## Platform Add-ons
External Secrets Operator is a platform-owned add-on because it installs CRDs,
webhooks, and cluster RBAC. Tenant Applications must not install or upgrade it.
The GitOps contract uses:
- `railiance-platform-addons` AppProject for cluster add-ons.
- `external-secrets` ArgoCD Application for the public Helm chart.
- `openbao-secretstore` ArgoCD Application for the OpenBao
`ClusterSecretStore`.
- OpenBao Kubernetes auth role `external-secrets-issue-core` for the
issue-core pilot.
- OpenBao Kubernetes auth role `external-secrets-activity-core` for the
activity-core/llm-connect provider-secret lane once approved.
`ClusterSecretStore/openbao` is limited to the `issue-core` namespace.
`ClusterSecretStore/openbao-activity-core` is limited to the `activity-core`
namespace and is intended for the llm-connect provider-secret lane. Broaden or
add stores only with platform review.
Configure the OpenBao side without printing token values:
```bash
OPENBAO_TOKEN_FILE=~/.local/openbao/platform-admin.token \
make openbao-configure-external-secrets-issue-core
OPENBAO_TOKEN_FILE=~/.local/openbao/platform-admin.token \
make openbao-configure-external-secrets-activity-core
```
The helper keeps Kubernetes auth in local-reviewer mode: OpenBao rereads its
own mounted service-account token and CA file instead of storing an expiring
reviewer JWT.
Then sync ArgoCD and verify:
```bash
make argocd-bootstrap-deploy
make argocd-status
kubectl -n external-secrets get deploy,pod
kubectl get clustersecretstore.external-secrets.io openbao
```

308
docs/credential-broker.md Normal file
View File

@@ -0,0 +1,308 @@
# Credential Request And Lease Broker
**Workplan:** `RAILIANCE-WP-0005`
**Owner:** `railiance-platform`
**Status:** source implementation complete; live verification pending approved token path
This document records the Railiance credential broker ownership decision and
the first implementation contract for short-lived OpenBao credential leases.
## Decision
`railiance-platform` owns OpenBao credential request, generation, delivery,
audit, and revocation because this repo owns the platform secrets service and
the OpenBao policy surface. The broker may later split into a dedicated
service repo if the implementation grows, but the grant catalog and OpenBao
policy contracts remain platform-owned.
The broker is not a new secret store. It is a controlled request path for
bounded credentials that already belong to OpenBao or adjacent platform
authorities.
## Boundaries
| Concern | Owner | Boundary |
| --- | --- | --- |
| OpenBao mounts, policies, token roles, response wrapping, audit | `railiance-platform` | Generates and revokes bounded credentials. |
| Human login, OIDC, MFA, IAM profile claims | `key-cape` | Authenticates human and service identities. |
| Authorization decision | `flex-auth` | Decides whether an actor may request a grant for a purpose, TTL, audience, and delivery mode. |
| SSH certificate signing | `ops-warden` | Issues SSH certificates only. It does not vend OpenBao tokens, API keys, or provider secrets. |
| Request tracking | State Hub | Stores non-secret metadata only: request ids, actor, grant, purpose, TTL, decision id, lease accessor, status, timestamps, and audit pointers. |
| Agent/runtime consumption | `llm-connect` and callers | Never place secrets in prompts. Consume credentials through local exec injection, response wrapping, service-account auth, or approved local files. |
## Non-Secret Metadata Only
State Hub, workplans, docs, Git, chat, and prompts may contain:
- grant ids such as `ops-warden/warden-sign`;
- requested TTL and bounded max TTL;
- actor and subject ids;
- purpose strings;
- lease handles or accessors when they are not sufficient to use the secret;
- OpenBao audit request ids or timestamps;
- status values such as requested, issued, denied, revoked, or expired.
They must not contain:
- OpenBao root tokens, platform-admin tokens, or wrapped token values;
- unseal shares, recovery codes, private keys, OTP seeds, passwords, or API keys;
- raw bearer tokens in command lines, prompt text, State Hub bodies, or logs;
- screenshots or pasted command output containing secret values.
## Grant Catalog
The catalog lives at:
```text
credential-grants/catalog.yaml
```
Validate it with:
```bash
make credential-grants-validate
```
Every grant entry defines:
- a stable grant id;
- credential type and OpenBao policy set;
- grant class: `self-service`, `approval-required`, or `break-glass`;
- default and max TTL;
- allowed actor types and purpose examples;
- allowed and denied delivery modes;
- audit and revocation expectations.
The first pilot grant is `ops-warden/warden-sign`, which creates a short-lived
OpenBao token with only the `warden-sign` policy.
## OpenBao Token Roles
OpenBao-token grants are configured from source with:
- an issuer policy under `openbao/policies/`;
- an `auth/token/roles/<role>` token role with allowed policies, disallowed
admin policies, non-renewable TTL bounds, no default policy, and orphan token
issuance;
- verification that reads the issuer policy, token role, and target workload
policy before any smoke token is minted.
Dry-run the current grant configuration with:
```bash
make openbao-token-grants-dry-run
make openbao-verify-token-grants-dry-run
```
Live application uses an operator-approved OpenBao token from
`OPENBAO_TOKEN_FILE` or an interactive hidden prompt. The token is passed to the
OpenBao pod through stdin, never through argv:
```bash
OPENBAO_TOKEN_FILE=~/.local/openbao/platform-admin.token make openbao-configure-token-grants
OPENBAO_TOKEN_FILE=~/.local/openbao/platform-admin.token make openbao-verify-token-grants
```
The smoke verifier can mint a short-lived child token, confirm that it can list
`ssh/roles`, confirm that it cannot list unrelated secret engines, and revoke
the token by accessor:
```bash
OPENBAO_TOKEN_FILE=~/.local/openbao/platform-admin.token make openbao-verify-token-grants-smoke
```
## Delivery Modes
`exec-env` is the preferred local path. The helper obtains a lease, injects
the credential only into a child process environment, redacts output, and then
revokes or lets the credential expire.
`response-wrap` is for attended handoff. The broker returns a single-use
OpenBao wrapping token instead of the raw credential. The recipient unwraps it
once; a second unwrap must fail.
`local-token-file` is for tools that cannot consume environment variables
cleanly. Files must be mode `0600`, stored under `.local/credential-leases/`,
and removed when the lease is revoked or expires. That directory is ignored by
Git.
`kubernetes-auth` is for in-cluster workloads. Workloads should authenticate
with service-account-bound auth instead of receiving manually handed tokens.
For the pilot grant, `request --delivery kubernetes-auth` returns only
non-secret OpenBao auth metadata such as the auth mount, role, service account
names, and namespaces; it does not mint or print a bearer token.
The denied modes are absolute unless a later ADR updates the catalog:
- `chat`
- `state-hub-body`
- `git`
- `command-line-token-argument`
- `llm-prompt`
## Pilot Flow
The target ops-warden smoke path is:
```bash
credential exec --grant ops-warden/warden-sign --ttl 15m -- \
SMOKE_VAULT=1 /home/worsch/ops-warden/scripts/policy_gate_production_smoke.sh
```
The source helper MVP lives at `scripts/credential.py` until this flow graduates
into a packaged command. It supports the same core shape:
```bash
scripts/credential.py request --grant ops-warden/warden-sign --purpose flex-auth-openbao-smoke
scripts/credential.py exec --grant ops-warden/warden-sign --purpose flex-auth-openbao-smoke -- \
SMOKE_VAULT=1 /home/worsch/ops-warden/scripts/policy_gate_production_smoke.sh
scripts/credential.py status <lease-accessor>
scripts/credential.py revoke <lease-accessor>
```
`request` defaults to `local-token-file`: the raw child token is written only to
`.local/credential-leases/` with mode `0600`, and stdout contains the lease
handle/accessor plus metadata. `--delivery response-wrap` returns an OpenBao
wrapping token for attended handoff, not the raw child token.
`exec` mints a bounded child token, injects it as `VAULT_TOKEN` only into the
child process environment, redacts token-looking output, and revokes the token
by accessor when the child exits. The helper rejects caller-supplied
`VAULT_TOKEN`/`BAO_TOKEN` env assignments and unsafe OpenBao debug/trace log
settings.
Dry-run all helper paths with:
```bash
make credential-helper-dry-run
```
Pass helper global options before the subcommand. For example, if the OpenBao
pod has an approved token helper session:
```bash
make credential-exec-ops-warden-smoke CREDENTIAL_HELPER_GLOBAL_ARGS=--use-token-helper
```
The child process receives `VAULT_TOKEN` in its environment. The token is not
printed, written to shell history, sent to State Hub, or placed in an LLM
prompt.
## Identity And Authorization
The helper records the following non-secret request identity fields:
- `actor`: the requester identity, defaulting to `codex:<local-user>`;
- `actor_type`: one of the grant-approved actor classes such as
`human-operator`, `approved-agent`, or `ci-runner`;
- `subject`: the bound human, agent, CI, or Kubernetes service-account subject.
Human operators should use the KeyCape/OIDC path with MFA when the grant class
or purpose requires it. Agents and CI runners should use stable subject strings
that can be mapped to IAM profile claims, for example
`agent:codex/railiance-platform` or
`system:serviceaccount:<namespace>:<service-account>`. Headless automation must
use Kubernetes auth or an explicitly approved non-interactive identity; it must
not reuse a human OpenBao token.
The helper performs local catalog checks before any issuance:
- purpose is required;
- requested TTL must not exceed the grant max TTL;
- delivery mode must be allowed by the grant;
- actor type must be allowed by the grant.
Optional flex-auth preflight is enabled with `--flex-auth-url` or `FLEX_AUTH_URL`.
The helper posts non-secret request metadata to
`/credential-grants/authorize` by default and accepts allow/deny responses using
`allowed`, `decision`, or `status` fields plus optional `decision_id` and
`reason`. Use `--require-flex-auth` when local preauthorization is not
acceptable. Use `--decision-id` to carry an already-approved external decision
without calling flex-auth again.
## State Hub Metadata
State Hub recording is opt-in through `--record-state-hub` or
`CREDENTIAL_RECORD_STATE_HUB=1`. The helper writes request lifecycle notes to
`/progress/` with non-secret metadata only:
- grant id, actor, actor type, subject, purpose, requested TTL, delivery mode;
- authorization mode, decision id, and decision reason;
- lease accessor, wrapping accessor, or wrapped accessor when available;
- status values such as `requested`, `issued`, `wrapped`, `revoked`, or
`delegated`.
It never records raw child tokens, wrapping tokens, token files, passwords,
OpenBao root/platform-admin tokens, or command output.
## Verification And Revocation
Offline checks:
```bash
make credential-grants-validate
make credential-tests
make openbao-token-grants-dry-run
make openbao-verify-token-grants-dry-run
make credential-helper-dry-run
```
Live source-owned checks, once an approved OpenBao issuer token path exists:
```bash
OPENBAO_TOKEN_FILE=~/.local/openbao/platform-admin.token make openbao-configure-token-grants
OPENBAO_TOKEN_FILE=~/.local/openbao/platform-admin.token make openbao-verify-token-grants-smoke
OPENBAO_TOKEN_FILE=~/.local/openbao/platform-admin.token make credential-exec-ops-warden-smoke
```
Use CREDENTIAL_HELPER_CHILD_ENV for child-only environment assignments needed by the smoke command, for example a Linux-only PATH that exposes ops-warden tooling. These assignments are passed after the credential helper separator and are not used for token handoff.
Emergency revocation by accessor:
```bash
scripts/credential.py revoke <lease-accessor>
```
When using `local-token-file`, remove stale local lease material after revoke or
expiry:
```bash
find .local/credential-leases -type f -maxdepth 1 -print
```
Response wrapping live verification is manual until a richer integration test
exists: unwrap the returned wrapping token once with OpenBao, confirm the second
unwrap attempt fails, then revoke the wrapped child token by accessor.
## Routing And Rollout
Credential routing remains split by responsibility:
- `ops-warden` signs SSH certificates only;
- OpenBao token or dynamic-lease needs route to `railiance-platform`;
- login/MFA routes to KeyCape;
- authorization decisions route to flex-auth.
The rollout sequence is:
1. `ops-warden/warden-sign` pilot for the flex-auth/ops-warden smoke.
2. Platform-readonly token helper for diagnostics.
3. Workload-specific grants for application repositories.
4. Optional split to a dedicated credential-broker repo if the helper grows
beyond platform ownership.
The workplan can close only after the live warden-sign pilot runs through the
helper and the credential routing catalog returns this railiance-platform flow
for VAULT_TOKEN/OpenBao-token requests.
## Implementation Sequence
1. Validate and maintain the non-secret grant catalog.
2. Add bounded OpenBao token role configuration for each OpenBao-token grant.
3. Build a small helper that supports `request`, `exec`, `status`, and `revoke`.
4. Add optional flex-auth preflight and State Hub request lifecycle metadata.
5. Update ops-warden routing so OpenBao token needs point here, while SSH certificate issuance remains in ops-warden.
Live token issuance requires an approved operator path to create or use the
non-root issuer capability. Source-only validation and dry-run helper behavior
must remain useful without a live token.

View File

@@ -0,0 +1,284 @@
# Credential Change Approval Workflow
This document sketches the operator workflow we want for it-sec and credential
changes. The goal is to remove raw OpenBao command authoring from routine human
operation while preserving explicit human approval, auditability, and safe
handling of secret values.
## Problem
The current workflow still asks operators to translate a reviewed intent into
OpenBao commands by hand:
- create or update policies;
- create auth roles with the right bound claims;
- create or rotate secret paths and fields;
- verify positive and negative access;
- tell ops-warden or another access front door when a lane may become active.
That is inefficient and easy to get wrong. It is also hard to review because
the actual unit of work is spread across chat, workplans, OpenBao UI screens,
State Hub notes, and shell commands.
## Direction
Treat OpenBao as the enforcement and audit engine, not the primary review UI.
Add a small approval control plane in front of it:
1. an agent or CLI creates a structured, non-secret credential change request;
2. humans review the rendered proposal, risk notes, generated OpenBao plan, and
verification plan;
3. a human approves or denies with a comment;
4. only approved requests can be applied by an operator-controlled helper;
5. the helper records non-secret evidence and marks the request active,
rejected, deactivated, rotated, or compromised.
This can be implemented with repo files, State Hub, and CLI/chat integration
first. An OpenBao UI extension can come later if the workflow proves itself.
## Core Object
The canonical unit is a credential change request, abbreviated `CCR`.
The CCR must be non-secret. It may contain:
- stable request id and title;
- requester, reviewer, approver, and applier identities;
- target domain, tenant, workload, environment, and purpose;
- OpenBao mount, path, field names, policy names, and auth role names;
- exact non-secret policy HCL or generated policy references;
- proposed auth bindings and bound claims;
- delivery surface such as ops-warden, External Secrets, CSI, or direct caller
fetch;
- machine-readable front-door readiness, including `readiness` and
`resolvable`;
- risk classification and approval requirements;
- generated apply plan;
- verification plan;
- rollback, deactivate, rotate, and compromise response plan;
- comments, approvals, denials, and timestamps;
- non-secret OpenBao audit request ids or timestamps after execution.
It must not contain:
- secret values;
- wrapped token values;
- root, platform-admin, or issuer tokens;
- passwords, API keys, private keys, OTP seeds, unseal shares, or recovery
codes;
- command output that includes secret values.
## State Machine
Suggested states:
```text
draft
proposed
needs_changes
approved
denied
apply_pending
applied
verified
active
deactivated
rotated
compromised
superseded
cancelled
```
Only `approved` requests may be applied. Only `verified` requests may become
`active`.
Emergency break-glass work may create a request after the fact, but it must be
marked as break-glass, reviewed retrospectively, and linked to audit evidence.
## Review Surface
A reviewer should see a concise rendered proposal:
```text
Request: whynot-design npm publish token lane
Type: workload-kv-read
Mount/path/field:
platform/workloads/coulomb/whynot-design/npm-publish
NPM_AUTH_TOKEN
Policy:
workload-kv-read-whynot-design-npm-publish
Auth binding:
netkingdom OIDC role whynot-design-workload-kv-read
bound claim: groups includes whynot-design
Access front door:
ops-warden whynot-design-npm-publish
readiness: template
resolvable: false
Risk:
grants read access to npm publish credential
Checks:
positive whynot fetch, negative non-whynot denial, OpenBao audit evidence
Decision:
approve | deny | needs changes
Comment:
free text
```
The reviewer should not need to know the exact `bao write` syntax. They should
be able to discuss the proposal in chat, request changes, and then make a
formal decision.
## Minimal Implementation
Version 1 should be boring:
- store CCR files under `credential-change-requests/`;
- validate CCR schema offline;
- render a human-readable review summary;
- generate OpenBao apply plans from approved CCRs;
- require an approval record before apply;
- apply only non-secret policy/auth/path metadata;
- prompt or delegate separately for secret value entry;
- record non-secret evidence in State Hub.
The first implemented CLI slice is:
```bash
make credential-change-validate
make credential-change-render CREDENTIAL_CHANGE=CCR-2026-0001
make credential-change-plan CREDENTIAL_CHANGE=CCR-2026-0001
make credential-change-status CREDENTIAL_CHANGE=CCR-2026-0001
make credential-change-status-json CREDENTIAL_CHANGE=CCR-2026-0001
scripts/credential-change.py confirm-binding CCR-2026-0001 --reviewer <name> --comment "..."
scripts/credential-change.py approve CCR-2026-0001 --reviewer <name> --comment "..."
scripts/credential-change.py deny CCR-2026-0001 --reviewer <name> --comment "..."
scripts/credential-change.py needs-changes CCR-2026-0001 --reviewer <name> --comment "..."
make credential-change-sync-decision CREDENTIAL_CHANGE=CCR-2026-0001
make credential-change-apply-plan CREDENTIAL_CHANGE=CCR-2026-0001
make credential-change-operator-commands CREDENTIAL_CHANGE=CCR-2026-0001
make credential-change-runbook CREDENTIAL_CHANGE=CCR-2026-0001
scripts/credential-change.py runbook CCR-2026-0001 --execute-metadata --actor <operator> --confirm "APPLY CCR-2026-0001"
scripts/credential-change.py record-evidence CCR-2026-0001 --actor <operator> --kind positive_verification --result passed --detail "<non-secret audit reference>" --record-state-hub
make credential-change-lifecycle-plan CREDENTIAL_CHANGE=CCR-2026-0001 CREDENTIAL_CHANGE_LIFECYCLE_ACTION=deactivate
scripts/credential-change.py lifecycle-event CCR-2026-0001 --action compromise --actor <operator> --reason "<non-secret reason>" --detail "<non-secret evidence>" --blast-radius "<non-secret scope>" --follow-up "<task/ref>" --record-state-hub
scripts/credential-change.py import-inventory CCR-YYYY-NNNN --title "existing lane" --tenant <tenant> --workload <workload> --environment production --purpose "<purpose>" --kv-path platform/workloads/<tenant>/<workload>/<purpose> --field <FIELD_NAME> --auth-method oidc --auth-mount netkingdom --auth-role <role> --bound-claim groups=<group> --bound-claims-confirmed --frontdoor-type ops-warden --catalog-id <catalog-id> --reason "Imported existing lane without secret values"
```
`apply-plan` and `operator-commands` are intentionally guarded: they refuse
anything not approved and refuse unconfirmed auth bindings. `operator-commands`
renders the reviewed non-secret `bao policy write` and `bao write auth/.../role`
commands for a platform operator; the actual secret value is still provisioned
through approved OpenBao/operator custody only.
The same operations can be exposed through chat by having the agent create the
proposal, show the rendered summary, then call the CLI only after the human
gives an explicit approval phrase.
## State Hub Role
State Hub should hold:
- request lifecycle events;
- review comments;
- approval/denial decisions;
- non-secret apply and verification evidence;
- links to workplans and CCR files.
State Hub should not hold secret values. It can be the first review UI because
it already supports messages, progress, task status, and cross-repo
coordination.
For CCR review, create a pending State Hub decision that links to the CCR and
contains only non-secret coordinates. Operators can inspect it in the dashboard
at `http://127.0.0.1:3000/decisions` and resolve it with a rationale beginning
with `APPROVE:`, `DENY:`, or `NEEDS_CHANGES:`. Then run
`make credential-change-sync-decision CREDENTIAL_CHANGE=<CCR>` to copy the
resolved decision back into the CCR file-backed state.
## OpenBao Role
OpenBao remains authoritative for:
- policy enforcement;
- auth method configuration;
- token issuance and revocation;
- secret storage;
- audit logs.
Where OpenBao supports non-secret metadata on secret paths or auth roles, we can
mirror CCR ids and status labels. The workflow must not depend on OpenBao being
the only index, because operators need to see proposed, rejected, deactivated,
rotated, and compromised items across repos and access front doors.
## ops-warden Role
ops-warden should consume only approved and active access lanes.
For draft requests, ops-warden may create a draft catalog entry that points to
the CCR, but it should not activate the entry until the CCR is verified.
For `warden access --fetch` / `--exec`, the catalog should include the CCR id
and refuse active use when the CCR state is not `active`, `readiness` is not
`ready`, or `resolvable` is not `true`.
## Interactive Runbook Role
The interactive runbook is the operator bridge:
1. load a CCR;
2. show the rendered summary and exact generated plan;
3. confirm the request is approved;
4. acquire operator authority through an approved path;
5. apply the plan;
6. ask for attended secret entry when needed;
7. run positive and negative verification;
8. record non-secret evidence;
9. notify downstream front doors such as ops-warden.
`credential-change.py runbook <CCR>` renders the checklist and exact final
confirmation phrase. `--execute-metadata` is intentionally opt-in and requires
that phrase; it uses the local `bao` CLI with ambient approved operator
authority, writes only policy/auth metadata, and records a non-secret
`metadata_apply` evidence entry. Secret value provisioning stays outside the
script through approved OpenBao/operator custody. Verification, activation, and
manual custody events are recorded with `record-evidence`, whose comments are
scanned for known secret markers before the CCR file or State Hub is updated.
This lets operators safely drive privileged work without needing to remember
every OpenBao command.
## Compromise And Deactivation
Every active CCR needs a deactivate and rotate path:
- `deactivated`: access intentionally disabled but not necessarily compromised;
- `rotated`: secret value replaced and old value no longer valid;
- `compromised`: emergency state requiring immediate disablement, rotation,
blast-radius notes, and incident follow-up.
`lifecycle-plan` renders the attended checklist for each case, including the
front-door state change and OpenBao metadata disable commands for deactivation
or compromise. `lifecycle-event` records the non-secret lifecycle event in the
CCR, sets the CCR status, and marks the access front door disabled, pending
verification, or compromised as appropriate. For compromise events it accepts
non-secret blast-radius notes and follow-up task references. Existing lanes that
predate CCRs can be imported with `import-inventory`, which writes a CCR and
matching read-policy artifact from metadata only; it never asks for or stores
the secret value.
The workflow must support marking an existing credential or lane as compromised
even when the original request predates this system.
## Near-Term Target
Use the whynot-design npm token lane as the pilot:
1. encode the existing non-secret lane as a CCR;
2. render it for review;
3. approve or request changes from chat;
4. generate/apply the OpenBao policy and auth role only after approval;
5. provision the secret value by attended operator custody;
6. verify and activate the ops-warden catalog entry.
Once that path feels good, reuse it for the sibling workload-KV lanes and the
credential broker's OpenBao token-role gates.

View File

@@ -0,0 +1,127 @@
# OpenBao Approved Automation Delegation
This document specifies the narrow OpenBao metadata surface that approved
credential-change automation may mutate. It exists to avoid routine use of broad
`platform-admin` while keeping secret values under operator custody.
## Scope
The delegated applier is for reviewed metadata only:
- ACL policies generated from approved CCRs;
- auth roles bound to reviewed OIDC claims or Kubernetes service accounts;
- credential-broker issuer policies and token roles generated from reviewed
grant catalog entries;
- readback and capability checks needed to prove the mutation landed.
It must not read, write, print, wrap, unwrap, or proxy managed secret values.
Production secret provisioning remains an attended OpenBao/operator custody
step unless a later workplan approves a stronger dual-control flow.
## Environment Boundaries
Build and development may use sandbox metadata once a non-production OpenBao
mount or namespace is declared. Generated test secrets must stay in the sandbox
and must never be copied into State Hub, prompts, Git, or chat.
The non-production applier policy candidate is
`openbao/policies/credential-change-nonprod-applier.hcl`. It currently grants
only metadata writes, matching the no-secret-value rule used in production.
Any future generated test-secret path needs a separate CCR-backed approval so
it cannot silently expand this delegation.
Test and staging may apply reviewed metadata after owner review. Verification
must include positive and negative access checks, and evidence must be
non-secret.
Production may apply only reviewed non-secret metadata. The production applier
policy is `openbao/policies/credential-change-prod-applier.hcl`, and every live
run must be preceded by `scripts/credential-change.py applier-dry-run <CCR>`.
Unapproved CCRs fail closed before any OpenBao mutation is rendered. Live
metadata mutation uses `scripts/credential-change.py applier-apply <CCR>` with
an exact `DELEGATED APPLY <CCR-ID>` confirmation phrase and the local `bao` CLI
under ambient delegated applier authority; the command does not accept OpenBao
tokens in argv.
## Production Mutation Surface
| Change class | Allowed OpenBao path | Notes |
| --- | --- | --- |
| Workload KV read policies | `sys/policies/acl/workload-kv-read-*` | Generated from CCR mount/path/field metadata. |
| Credential broker issuer policies | `sys/policies/acl/credential-broker-*-issuer` | Generated from grant catalog metadata. |
| OIDC workload roles | `auth/netkingdom/role/*-workload-kv-read` | Bound claims must be confirmed before apply. |
| Kubernetes workload roles | `auth/kubernetes/role/*` | Bound service accounts/namespaces must be confirmed before apply. |
| Credential broker token roles | `auth/token/roles/credential-broker-*` | Child-token roles only; no root or platform-admin policies. |
| Self checks | `auth/token/lookup-self`, `sys/capabilities-self` | Read/update only as required by OpenBao. |
Denied by omission:
- `platform/data/*`, `platform/metadata/*`, or any other secret value path;
- `sys/*` outside the approved ACL policy prefixes;
- `auth/*` outside the approved role prefixes;
- `identity/*`, unseal/recovery material, audit devices, mounts, and root/admin
policy assignment;
- wildcard, parent-directory, or mismatched policy and role names.
## Local Dry-Run Guardrails
The CCR dry-run is deliberately stricter than the OpenBao ACL policy. It must:
1. validate the CCR schema and secret-marker scan;
2. require CCR status `approved`, `applied`, `verified`, or `active`;
3. require `openbao.auth.bound_claims_confirmed=true`;
4. require mount `platform` and path `platform/workloads/...` for workload KV
requests;
5. require policy names to start with `workload-kv-read-` and remain under
`openbao/policies/<policy-name>.hcl`;
6. require OIDC roles to stay under `auth/netkingdom/role/*-workload-kv-read`;
7. require Kubernetes roles to stay under `auth/kubernetes/role/*-workload-kv-read`
or `auth/kubernetes/role/*-secrets-read`;
8. render only exact policy and auth-role metadata mutations;
9. leave secret value writes and front-door activation out of scope.
`applier-apply` reuses the same guardrails, renders the dry-run payload before
mutation, requires exact confirmation, writes only policy/auth-role metadata,
and appends non-secret `delegated_metadata_apply` evidence. For approved CCRs it
can advance file-backed status to `applied`; for already applied/verified/active
CCRs it records idempotent evidence without moving the lifecycle backward.
## Required Evidence
Record only non-secret evidence:
- CCR id and approval/decision reference;
- applier identity and timestamp;
- policy name and auth role path;
- OpenBao request id or audit timestamp;
- positive and negative verification references;
- front-door activation confirmation after verification.
## Applier Identity Setup
`openbao-apply-credential-change-appliers.py` configures the source-owned
metadata applier policies and matching OpenBao token roles:
- `credential-change-nonprod-applier` uses
`openbao/policies/credential-change-nonprod-applier.hcl`;
- `credential-change-prod-applier` uses
`openbao/policies/credential-change-prod-applier.hcl`.
The token roles allow only their matching applier policy, explicitly disallow
`root` and `platform-admin`, disable the default policy, use service tokens,
and do not issue tokens by themselves. Token issuance remains an approved
custody path outside this setup script. Use
`make openbao-credential-change-appliers-dry-run` before any live apply.
## Current Production Policy Candidate
`openbao/policies/credential-change-prod-applier.hcl` is the source candidate
for a future production applier identity. It is not a substitute for CCR review;
it is the OpenBao-side capability envelope used after local dry-run validation.
## Current Non-Production Policy Candidate
`openbao/policies/credential-change-nonprod-applier.hcl` is the source
candidate for a build/test/staging applier identity. It is intentionally
metadata-only until this repo declares a non-production OpenBao mount or
namespace and records live positive/negative evidence for that lane.

View File

@@ -18,15 +18,26 @@ S5 workloads / operators
-> openbao-0
-> integrated Raft storage on local-path PVC
-> audit storage PVC mounted at /openbao/audit
Platform operators with approved admin identity
-> https://bao.coulomb.social
-> Traefik Ingress + TLS
-> openbao-ui service
-> OpenBao UI/API
-> KeyCape OIDC at https://kc.coulomb.social for login
```
- OpenBao is the canonical Railiance S3 secrets service.
- SOPS/age remains the Git-at-rest bootstrap mechanism.
- The first Railiance01 deployment is single-replica Raft, not true HA.
- Public ingress is disabled. Operators use `kubectl exec` or port-forwarding.
- Browser UI/API exposure is declared for `https://bao.coulomb.social`.
Operators authenticate through KeyCape/OIDC with MFA and the
`platform-admin` role. Do not use the root token through the browser UI.
- `kubectl exec` and port-forwarding remain valid break-glass/operator paths
for maintenance and non-browser verification.
- TLS is disabled inside the pod listener for this internal-only bootstrap. Add
cert-manager-backed internal TLS before exposing OpenBao beyond cluster-local
traffic.
cert-manager-backed internal TLS before relying on cluster-internal traffic
from untrusted namespaces.
## Deployment
@@ -41,6 +52,12 @@ make openbao-deploy
make openbao-status
```
`make openbao-deploy` applies `helm/openbao-middleware.yaml` (Traefik
rate-limit and HSTS), upgrades the OpenBao Helm release, then applies the
KeyCape login overlay gateway (`helm/openbao-ui-overlay-k8s.yaml`). Public
ingress for `bao.coulomb.social` targets `openbao-ui-gateway`, not the chart
ingress (which stays disabled in `helm/openbao-values.yaml`).
On Railiance01 directly:
```bash
@@ -51,10 +68,13 @@ sudo env KUBECONFIG=/etc/rancher/k3s/k3s.yaml make openbao-status
```
If the repo is not present on Railiance01 yet, copy only the non-secret values
file and run Helm directly:
and middleware files, then run Helm directly:
```bash
scp helm/openbao-values.yaml tegwick@92.205.62.239:/tmp/openbao-values.yaml
scp helm/openbao-middleware.yaml tegwick@92.205.62.239:/tmp/openbao-middleware.yaml
ssh tegwick@92.205.62.239 \
'sudo env KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl apply -f /tmp/openbao-middleware.yaml'
ssh tegwick@92.205.62.239 \
'sudo env KUBECONFIG=/etc/rancher/k3s/k3s.yaml helm upgrade --install openbao openbao/openbao \
--version 0.28.2 \
@@ -78,6 +98,8 @@ Expected immediately after install:
- `openbao-0` is Running.
- `openbao`, `openbao-active`, `openbao-internal`, and `openbao-ui` services
exist as cluster-internal services.
- After DNS points at the cluster ingress, `https://bao.coulomb.social` serves
the OpenBao UI over valid TLS.
- data and audit PVCs are Bound.
- `bao status` reports `Initialized: false` and `Sealed: true`.
@@ -213,6 +235,50 @@ Store that token through the approved operator secret path, then revoke or
tightly escrow the initial root token. The root token should not become the
normal operator credential.
## SSH Secrets Engine (ops-warden)
After `openbao-configure-initial`, enable the SSH user CA used by `ops-warden`
(`warden sign` via `backend: vault`). This is **NET-WP-0020 T5** / **WP-0008 T2**
prerequisite.
Declarative artifacts:
- `openbao/ssh/roles-spec.yaml` — `adm-role`, `agt-role`, `atm-role` TTLs
- `openbao/policies/warden-sign.hcl` — least-privilege signing policy
- `scripts/openbao-apply-ssh-engine.sh` — idempotent apply via `kubectl exec`
- `scripts/openbao-verify-ssh-engine.sh` — non-mutating verification
Apply (requires `platform-admin` or equivalent token with `ssh/*` admin):
```bash
mkdir -p ~/.local/openbao
# Store platform-admin token locally (mode 600, never commit):
# echo '<token>' > ~/.local/openbao/platform-admin.token && chmod 600 ~/.local/openbao/platform-admin.token
OPENBAO_TOKEN_FILE=~/.local/openbao/platform-admin.token OPENBAO_SSH_CA_PUBKEY_OUT=/tmp/openbao-ssh-ca.pub make openbao-configure-ssh
OPENBAO_TOKEN_FILE=~/.local/openbao/platform-admin.token make openbao-verify-ssh
```
The apply script exports the CA public key to `OPENBAO_SSH_CA_PUBKEY_OUT` and
updates K8s secret `openbao/openbao-ssh-ca-pub` (non-secret pubkey only).
Create a dedicated warden signing token (do not use platform-admin daily):
```bash
kubectl exec -n openbao openbao-0 -- bao token create -policy=warden-sign -period=8h -orphan
```
Host trust and principals are **railiance-infra** scope:
```bash
cd ~/railiance-infra
make bootstrap-ssh-ca SSH_CA_PUBKEY=/tmp/openbao-ssh-ca.pub
```
Then on the workstation: `bao login` (or export `VAULT_TOKEN` from the
`warden-sign` token) and run `warden sign` per `ops-warden/wiki/OpenBaoSshEngineChecklist.md`.
## Auth And Workload Integration
Initial auth model:
@@ -228,7 +294,93 @@ Initial auth model:
| Human identity | NetKingdom IAM Profile/OIDC | target model; OpenBao is not the identity provider |
| Automation | Kubernetes auth or short-lived operator token | no root tokens in automation |
Workload delivery choice:
### Browser UI Login
The browser operator surface is:
```text
https://bao.coulomb.social
```
Operators see a streamlined **Sign in with KeyCape** mask. The raw OpenBao
fields (namespace, method, mount path, role) are hidden presets applied by the
UI overlay in `helm/openbao-ui-overlay/`. Public ingress targets the
`openbao-ui-gateway` nginx proxy, which injects overlay assets and forwards to
the OpenBao service.
Hidden defaults (also in `helm/openbao-ui-overlay/presets.json`):
```text
method: OIDC
namespace: leave blank
mount path: netkingdom
role: platform-admin
```
Deploy or refresh the overlay:
```bash
make openbao-overlay-apply
make openbao-verify-login-overlay
make openbao-verify-login-overlay OPENBAO_VERIFY_LOGIN_OVERLAY_ARGS=--check-upstream-drift
```
After an OpenBao image or chart upgrade, follow
`helm/openbao-ui-overlay/README.md` to refresh overlay selectors and
`patches/<version>/manifest.sha256` fingerprints if upstream login markup
changed.
OIDC mounts must be visible to the unauthenticated UI listing or Ember falls
back to token auth (`?with=token`). Apply once per cluster:
```bash
OPENBAO_TOKEN_FILE=~/.local/openbao/platform-admin.token \
scripts/openbao-tune-auth-listing.sh
```
The gateway serves a standalone KeyCape login page at `/ui/vault/auth` so Ember
never handles the bare auth route (avoids `?with=token` / `?with=netkingdom/`
bounce when OIDC mounts are hidden from the unauthenticated listing). Clicking
**Sign in with KeyCape** calls `auth_url` and redirects to KeyCape directly.
OIDC callbacks under `/ui/vault/auth/<mount>/oidc/callback` are handled by a
standalone page that exchanges the authorization code, stores the UI session
token, and redirects into the Ember app (no popup/`window.opener` flow).
The OpenBao UI redirects the browser to KeyCape at `kc.coulomb.social`, then
returns to:
```text
https://bao.coulomb.social/ui/vault/auth/netkingdom/oidc/callback
```
The legacy `keycape` mount remains a compatibility alias for existing
operator notes and CLI experiments. The preferred browser mount is
`netkingdom`.
The browser callback URI must be present in both:
- KeyCape `openbao-admin` client redirect URIs; and
- OpenBao `auth/netkingdom/role/platform-admin` `allowed_redirect_uris`.
If the compatibility alias is kept enabled, also keep
`https://bao.coulomb.social/ui/vault/auth/keycape/oidc/callback` in the
KeyCape client and `auth/keycape/role/platform-admin`.
Use the browser UI for metadata inspection and attended operator workflows.
Do not use the OpenBao root token through the browser UI. Do not copy secret
values, Inter-Hub keys, unseal shares, root tokens, OIDC client secrets, or
screenshots of secret values into Git, State Hub, chat, or workplans.
For `HF-WP-0001`, prefer metadata-only inspection of candidate paths such as:
```text
platform/
platform/operators/
platform/operators/inter-hub/
```
Workload delivery choice (see also `docs/argocd-gitops.md` for the GitOps
tenant contract):
- Prefer External Secrets Operator for values that should become Kubernetes
Secrets consumed by ordinary Helm charts.
@@ -243,7 +395,7 @@ Workload delivery choice:
Path convention:
```text
platform/workloads/<namespace>/<service-account>/<secret-name>
platform/workloads/<tenant-or-org>/<workload>/<secret-purpose>
platform/object-storage/<consumer>
platform/databases/<consumer>
platform/operators/<purpose>
@@ -252,6 +404,10 @@ platform/operators/<purpose>
The template policy for workload KV reads is
`openbao/policies/workload-kv-read-template.hcl`.
Concrete workload access lanes used by ops-warden and similar front doors are
tracked in `docs/workload-kv-access-lanes.md`. These docs carry non-secret
path, field, policy, auth-role, and verification pointers only.
## Backup, Restore, Audit, And Monitoring
Before any live application secrets move into OpenBao:
@@ -299,8 +455,8 @@ make openbao-verify-authenticated
The target prompts for the token without echoing it, never puts the token on
the command line, and only runs non-mutating checks. It verifies that
`bao audit list` shows `file/`, `bao secrets list` shows `platform/`,
`bao auth list` shows both `kubernetes/` and `keycape/`, and that the file
audit log is non-empty.
`bao auth list` shows `kubernetes/`, `netkingdom/`, and `keycape/`, and that
the file audit log is non-empty.
If a previous attended OIDC login stored a still-valid token in the pod token
helper, use:

View File

@@ -0,0 +1,253 @@
# whynot-design npm Publish Token Handoff
This is the next-session handoff for `CCR-2026-0001` and the
`whynot-design-npm-publish` access lane.
## Current State
- CCR: `CCR-2026-0001`
- Decision: `e6381a56-6b04-4fd5-b2de-f3ef59cde888`
- Status: active; non-secret OpenBao apply and verification checks passed
- Front door: `ready`, `resolvable=true`
- Positive verification: passed 2026-06-28
- Negative verification: passed 2026-06-28
- Catalog id: `whynot-design-npm-publish`
- Tenant/org: `coulomb`
- Workload/project: `whynot-design`
- Bound IAM group: `whynot-design`
- Secret path: `platform/workloads/coulomb/whynot-design/npm-publish`
- Field: `NPM_AUTH_TOKEN`
- Token source: Gitea package token for
`https://gitea.coulomb.social/api/packages/coulomb/npm/`
The operator reported that the Gitea token was generated and stored in OpenBao.
Using the temporary operator token only for non-secret infrastructure work, Codex
confirmed that the policy exists, the OIDC role exists with the whynot-design
binding and redirect URIs, the secret metadata has the expected catalog id, and
the `NPM_AUTH_TOKEN` field is present. No secret value was printed, recorded,
or copied into Git, State Hub, chat, or workplans.
On 2026-06-28, the attended positive OIDC login advanced from a missing
`groups` claim to a bound-claim mismatch. That means the role now requests the
`groups` scope correctly, but the authenticating identity is not a member of
`whynot-design`. The `whynot-design` LLDAP group was created and verified.
The intended publisher/verifier identity was later added, positive
verification passed, then `platform-root` was temporarily removed for negative
verification. The negative check passed with a groups bound-claim mismatch, and
`platform-root` was restored to `whynot-design`.
## Safety Rules
- Do not paste `NPM_AUTH_TOKEN` into Git, State Hub, chat, shell history, logs,
workplans, or screenshots.
- Do not run verification with shell tracing enabled.
- Record only non-secret evidence: path, field name, metadata keys, policy name,
role name, actor, timestamp, and pass/fail result.
- Mark ops-warden catalog entries ready only after positive and negative
verification are complete. For this lane, both checks have passed.
## OpenBao Secret Check
In the OpenBao UI, confirm the secret exists under the `platform` KV engine:
```text
workloads/coulomb/whynot-design/npm-publish
```
Expected field:
```text
NPM_AUTH_TOKEN
```
Expected custom metadata:
```text
catalog-id = whynot-design-npm-publish
```
Do not reveal the value during review.
## Policy
Create or update ACL policy:
```text
workload-kv-read-whynot-design-npm-publish
```
Policy body:
```hcl
path "platform/data/workloads/coulomb/whynot-design/npm-publish" {
capabilities = ["read"]
}
path "platform/metadata/workloads/coulomb/whynot-design/npm-publish" {
capabilities = ["read"]
}
```
Equivalent CLI command from an approved OpenBao operator context:
```bash
bao policy write workload-kv-read-whynot-design-npm-publish \
openbao/policies/workload-kv-read-whynot-design-npm-publish.hcl
```
## OIDC Role
Create or update:
```text
auth/netkingdom/role/whynot-design-workload-kv-read
```
Role payload:
```json
{
"role_type": "oidc",
"allowed_redirect_uris": [
"https://bao.coulomb.social/ui/vault/auth/netkingdom/oidc/callback",
"http://localhost:8250/oidc/callback",
"http://127.0.0.1:8250/oidc/callback"
],
"oidc_scopes": [
"openid",
"profile",
"email",
"groups"
],
"user_claim": "sub",
"groups_claim": "groups",
"bound_claims": {
"groups": ["whynot-design"]
},
"policies": "workload-kv-read-whynot-design-npm-publish",
"ttl": "15m"
}
```
Equivalent CLI command from an approved OpenBao operator shell:
```bash
role_payload_file="$(mktemp)"
trap 'rm -f "$role_payload_file"' EXIT
cat >"$role_payload_file" <<'JSON'
{
"allowed_redirect_uris": [
"https://bao.coulomb.social/ui/vault/auth/netkingdom/oidc/callback",
"http://localhost:8250/oidc/callback",
"http://127.0.0.1:8250/oidc/callback"
],
"oidc_scopes": [
"openid",
"profile",
"email",
"groups"
],
"bound_claims": {
"groups": [
"whynot-design"
]
},
"groups_claim": "groups",
"policies": "workload-kv-read-whynot-design-npm-publish",
"role_type": "oidc",
"ttl": "15m",
"user_claim": "sub"
}
JSON
bao write auth/netkingdom/role/whynot-design-workload-kv-read @"$role_payload_file"
```
The OpenBao Browser CLI cannot run this shell block and may treat
`bound_claims={...}` as a string. When staying in the Web UI, open the API
Explorer and submit the role payload JSON above with:
```text
method: PUT
path: /v1/auth/netkingdom/role/whynot-design-workload-kv-read
```
If the API Explorer asks for a path without the API prefix, use
`auth/netkingdom/role/whynot-design-workload-kv-read`.
## Non-Secret Reads
These commands should succeed from an operator-capable identity and do not print
the token value:
```bash
bao kv metadata get platform/workloads/coulomb/whynot-design/npm-publish
bao policy read workload-kv-read-whynot-design-npm-publish
bao read auth/netkingdom/role/whynot-design-workload-kv-read
```
## Positive Verification
Positive verification proves the approved whynot-design identity can fetch the
field without exposing it in logs.
Before retrying, confirm the account used for OIDC login is a member of the
`whynot-design` LLDAP group. If OpenBao reports:
```text
claim "groups" does not match any associated bound claim values
```
then the groups claim is present, but the account is not in `whynot-design` or
KeyCape did not emit that membership in the fresh login.
The positive verification passed on 2026-06-28. During that run, the CLI printed
the short-lived OpenBao login token; it was revoked immediately by accessor.
Prefer `bao login -no-print` for future attended verification if the installed
CLI accepts that flag.
Use an attended shell, keep tracing disabled, and suppress command output:
```bash
set +x
bao login -no-print -method=oidc -path=netkingdom role=whynot-design-workload-kv-read
bao kv get -format=json platform/workloads/coulomb/whynot-design/npm-publish \
| jq -e '.data.data.NPM_AUTH_TOKEN | type == "string" and length > 0' \
>/dev/null
```
Record only that the check passed.
## Negative Verification
Negative verification proves a non-whynot identity cannot read the same field.
Use a non-whynot identity or role and confirm the read is denied. Do not print
or store any token value.
Record only the denial result and non-secret audit timestamp/request metadata.
The negative verification passed on 2026-06-28. `platform-root` was temporarily
removed from `whynot-design`; OpenBao rejected the OIDC login with a groups
bound-claim mismatch, so no OpenBao client token was issued and the secret was
not read. `platform-root` was then restored to `whynot-design`.
## Activation
Only after these are true:
- secret metadata confirmed;
- policy exists and is scoped to the corrected `coulomb/whynot-design` path;
- OIDC role exists and binds only `groups=["whynot-design"]` with approved
browser/local CLI callback URIs and `groups` OIDC scope;
- positive verification passed;
- negative verification passed;
`CCR-2026-0001` is now `active`, and ops-warden can mark
`whynot-design-npm-publish` `ready`/`resolvable=true`.
Current front door:
```text
readiness = ready
resolvable = true
```

View File

@@ -0,0 +1,230 @@
# Workload KV Access Lanes
This document records concrete OpenBao workload KV paths that external access
front doors can reference without storing or vending secret values themselves.
The first lane is for ops-warden `warden access --fetch` / `--exec`.
## Safety Rules
- Do not put secret values in Git, State Hub, chat, prompts, workplans, or logs.
- Store only non-secret pointers here: path, field name, policy name, auth role,
flex-auth reference, and verification status.
- ops-warden may proxy a read as the caller, but it must not hold the returned
value beyond the caller-requested fetch/exec process.
- Live writes require an approved OpenBao/operator path and attended handling
of the secret value.
## whynot-design npm Publish Token
Ops-warden original request:
`551031d1-335e-4db8-9535-820fea52d0a3`
Ops-warden batch follow-up:
`fe5b1696-8956-4bd5-9d6f-dbde1901a076`
| Item | Value |
| --- | --- |
| ops-warden catalog id | `whynot-design-npm-publish` |
| Tenant/org | `coulomb` |
| Workload/project | `whynot-design` |
| KV mount | `platform` |
| OpenBao CLI path | `platform/workloads/coulomb/whynot-design/npm-publish` |
| Secret field | `NPM_AUTH_TOKEN` |
| Front-door readiness | `active`, `resolvable=true` in ops-warden |
| Read policy | `workload-kv-read-whynot-design-npm-publish` |
| Policy file | `openbao/policies/workload-kv-read-whynot-design-npm-publish.hcl` |
| OIDC auth mount | `netkingdom` |
| OIDC role | `whynot-design-workload-kv-read` |
| OIDC callback URIs | `https://bao.coulomb.social/ui/vault/auth/netkingdom/oidc/callback`, `http://localhost:8250/oidc/callback`, `http://127.0.0.1:8250/oidc/callback` |
| Kubernetes auth role | `whynot-design-workload-kv-read` if an in-cluster service account consumes this lane |
| flex-auth ref | `secret.read:whynot-design` if tenant policy requires pre-approval |
Expected caller login shape:
```bash
bao login -method=oidc -path=netkingdom role=whynot-design-workload-kv-read
```
Expected OpenBao fetch shape:
```bash
bao kv get -field=NPM_AUTH_TOKEN platform/workloads/coulomb/whynot-design/npm-publish
```
Expected ops-warden exec shape after activation:
```bash
warden access whynot-design-npm-publish --exec -- npm publish
```
Ops-warden confirmed activation in State Hub message
`f76d3a9e-a98f-4081-885d-b79d94312699`: selector
`whynot-design-npm-publish` is active, resolvable, and wired to this
caller-scoped lane. The sibling lanes `issue-core-ingestion-api-key` and
`openrouter-llm-connect` remain draft and are tracked separately by
`RAILIANCE-WP-0009` and `RAILIANCE-WP-0010`.
The fetch command returns the secret value to the authenticated caller. Run it
only in an attended shell or through a process that consumes the value without
logging it.
## OpenBao Policy
The source policy grants only:
```text
read platform/data/workloads/coulomb/whynot-design/npm-publish
read platform/metadata/workloads/coulomb/whynot-design/npm-publish
```
It does not grant write, delete, patch, sudo, auth, sibling workload, or parent
list capabilities.
Dry-run the policy apply path:
```bash
make openbao-workload-kv-lanes-dry-run
```
Apply the policy with an approved platform-admin/operator token:
```bash
OPENBAO_TOKEN_FILE=~/.local/openbao/platform-admin.token \
make openbao-configure-workload-kv-lanes
```
If the OpenBao pod has an approved token-helper session, use:
```bash
make openbao-configure-workload-kv-lanes OPENBAO_WORKLOAD_KV_ARGS=--use-token-helper
```
Do not paste the token into shell history or logs. The helper reads a token
from `OPENBAO_TOKEN_FILE` or an interactive hidden prompt unless
`--use-token-helper` is set, and passes it to OpenBao through stdin.
## Auth Role
The intended OpenBao OIDC role is:
```text
auth/netkingdom/role/whynot-design-workload-kv-read
```
The role must attach only:
```text
workload-kv-read-whynot-design-npm-publish
```
The OIDC role must include the browser and local CLI callback URIs accepted by
OpenBao:
```text
https://bao.coulomb.social/ui/vault/auth/netkingdom/oidc/callback
http://localhost:8250/oidc/callback
http://127.0.0.1:8250/oidc/callback
```
The role must request these OIDC scopes so KeyCape emits the group claim OpenBao
checks:
```text
openid
profile
email
groups
```
The whynot-design pilot claim is confirmed as `groups=whynot-design`. Before
applying any changed role, re-confirm the KeyCape/NetKingdom claim that
identifies the whynot-design caller. The role must bind to that claim; do not
create an unbounded OIDC role that grants this policy to every OIDC user.
If the consumer is an in-cluster service account instead of an OIDC caller, use
Kubernetes auth with the same role name and bind only the approved namespace
and service account.
## Secret Provisioning
An approved operator must create or confirm the secret with:
```text
path: platform/workloads/coulomb/whynot-design/npm-publish
field: NPM_AUTH_TOKEN
```
In the OpenBao UI, open the `platform` KV engine and create or edit the secret
at:
```text
workloads/coulomb/whynot-design/npm-publish
```
For policies and API checks, the same KV-v2 secret is addressed as:
```text
platform/data/workloads/coulomb/whynot-design/npm-publish
platform/metadata/workloads/coulomb/whynot-design/npm-publish
```
The OpenBao UI path does not include the `data/` or `metadata/` segment. Those
segments are the KV-v2 API and ACL policy paths.
The value must be entered directly through OpenBao/operator custody. Record only
non-secret evidence: actor, timestamp, path, field name, policy name, and
verification result.
## Verification
Positive verification:
1. Authenticate as the whynot-design caller using the approved OIDC or
Kubernetes auth role.
2. Fetch the field in an attended session or through `warden access --exec`.
3. Record only that the fetch succeeded; do not record the value.
Safe attended command shape before the dedicated ops-warden catalog id is
activated:
```bash
set +x
bao login -method=oidc -path=netkingdom role=whynot-design-workload-kv-read
warden access "npm token" \
--path platform/workloads/coulomb/whynot-design/npm-publish \
--field NPM_AUTH_TOKEN \
--no-policy \
--exec -- sh -lc 'test -n "$NPM_AUTH_TOKEN"'
```
Use `--no-policy` only while the local ops-warden config reports
`policy.enabled=false`; remove it once the flex-auth gate is enforced. If login
fails with `groups claim not found`, the OpenBao role is missing the `groups`
OIDC scope and must be corrected before retrying.
Negative verification:
1. Authenticate as a non-whynot identity.
2. Confirm the same field read is denied.
3. Record the non-secret OpenBao audit request ids or timestamps for the
allowed and denied attempts.
## ops-warden Handoff
Send ops-warden only these pointers:
```text
catalog id: whynot-design-npm-publish
mount: platform
path: platform/workloads/coulomb/whynot-design/npm-publish
field: NPM_AUTH_TOKEN
oidc login: bao login -method=oidc -path=netkingdom role=whynot-design-workload-kv-read
policy: workload-kv-read-whynot-design-npm-publish
policy file: openbao/policies/workload-kv-read-whynot-design-npm-publish.hcl
flex-auth ref: secret.read:whynot-design, if tenant policy requires it
runbook: docs/workload-kv-access-lanes.md
```
Until positive and negative caller verification are complete, ops-warden should
keep the catalog entry in `applied-pending-verify`/non-active state with
`resolvable=false`.

View File

@@ -0,0 +1,38 @@
# Traefik middlewares for OpenBao browser UI/API exposure.
#
# These names are referenced by helm/openbao-values.yaml as:
# openbao-openbao-rate-limit@kubernetescrd
# openbao-openbao-hsts@kubernetescrd
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: openbao-rate-limit
namespace: openbao
labels:
app.kubernetes.io/name: openbao
app.kubernetes.io/part-of: railiance-platform
railiance-platform/component: secrets
spec:
rateLimit:
# The OpenBao browser UI performs a burst of API calls on load, including
# repeated /v1/sys/health checks. Keep this high enough for normal admin
# use while still bounding runaway clients.
average: 600
period: 1m
burst: 180
---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: openbao-hsts
namespace: openbao
labels:
app.kubernetes.io/name: openbao
app.kubernetes.io/part-of: railiance-platform
railiance-platform/component: secrets
spec:
headers:
stsSeconds: 31536000
stsIncludeSubdomains: true
stsPreload: true

View File

@@ -0,0 +1,121 @@
# OpenBao browser UI gateway — injects the KeyCape login overlay and proxies
# to the OpenBao service. Public ingress for bao.coulomb.social targets this
# gateway instead of the chart-managed OpenBao ingress.
#
# ConfigMap data is applied by scripts/openbao-ui-overlay-apply.sh from
# helm/openbao-ui-overlay/*.
apiVersion: apps/v1
kind: Deployment
metadata:
name: openbao-ui-gateway
namespace: openbao
labels:
app.kubernetes.io/name: openbao-ui-gateway
app.kubernetes.io/part-of: railiance-platform
railiance-platform/component: secrets
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: openbao-ui-gateway
template:
metadata:
labels:
app.kubernetes.io/name: openbao-ui-gateway
app.kubernetes.io/part-of: railiance-platform
railiance-platform/component: secrets
spec:
containers:
- name: nginx
image: nginx:1.27-alpine
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 8080
protocol: TCP
readinessProbe:
httpGet:
path: /ui/platform-overlay/presets.json
port: http
initialDelaySeconds: 3
periodSeconds: 10
livenessProbe:
httpGet:
path: /ui/platform-overlay/presets.json
port: http
initialDelaySeconds: 10
periodSeconds: 20
resources:
requests:
cpu: 25m
memory: 32Mi
limits:
cpu: 200m
memory: 128Mi
volumeMounts:
- name: nginx-config
mountPath: /etc/nginx/nginx.conf
subPath: nginx.conf
readOnly: true
- name: overlay-assets
mountPath: /etc/nginx/overlay
readOnly: true
volumes:
- name: nginx-config
configMap:
name: openbao-ui-gateway-nginx
- name: overlay-assets
configMap:
name: openbao-ui-overlay
---
apiVersion: v1
kind: Service
metadata:
name: openbao-ui-gateway
namespace: openbao
labels:
app.kubernetes.io/name: openbao-ui-gateway
app.kubernetes.io/part-of: railiance-platform
railiance-platform/component: secrets
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: openbao-ui-gateway
ports:
- name: http
port: 8080
targetPort: http
protocol: TCP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: openbao-ui-gateway
namespace: openbao
labels:
app.kubernetes.io/name: openbao-ui-gateway
app.kubernetes.io/part-of: railiance-platform
railiance-platform/component: secrets
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
traefik.ingress.kubernetes.io/router.middlewares: >-
openbao-openbao-rate-limit@kubernetescrd,
openbao-openbao-hsts@kubernetescrd
spec:
ingressClassName: traefik
tls:
- secretName: bao-tls
hosts:
- bao.coulomb.social
rules:
- host: bao.coulomb.social
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: openbao-ui-gateway
port:
number: 8080

View File

@@ -0,0 +1,69 @@
# OpenBao KeyCape login overlay
Streamlines the browser login mask at `https://bao.coulomb.social` to a single
**Sign in with KeyCape** action. Namespace, auth method, mount path, and role
are preset in `presets.json` and hidden by `overlay.css` / `overlay.js`.
## Mechanism (T01 decision)
OpenBao ships UI assets inside the container image. There is no supported API
to customize the login form ([`/sys/config/ui`](https://openbao.org/api-docs/system/config-ui/)
only configures response headers).
We use an **nginx UI gateway** (`openbao-ui-gateway`) that:
1. Proxies all traffic to `openbao.openbao.svc.cluster.local:8200`.
2. Serves overlay assets from a ConfigMap at `/ui/platform-overlay/`.
3. Injects `overlay.css` and `overlay.js` into HTML responses via `sub_filter`.
Overlay assets live entirely in this directory. Upgrading OpenBao does not
require hand-editing files inside the OpenBao pod.
Track upstream [openbao/openbao#2936](https://github.com/openbao/openbao/issues/2936)
for native custom CSS. When available, keep `presets.json` and branding assets
and retire nginx `sub_filter` injection if the upstream API covers the same
behaviour.
## Layout
| File | Purpose |
| --- | --- |
| `VERSION` | OpenBao image tag this overlay targets (`openbao-values.yaml`) |
| `presets.json` | Hidden login defaults (`netkingdom`, `platform-admin`, …) |
| `overlay.css` | Hide raw OpenBao login fields |
| `overlay.js` | Apply presets, branding on post-login Ember pages |
| `login.html` / `login.js` / `login.css` | Standalone KeyCape login at `/ui/vault/auth` |
| `callback.html` / `callback.js` | OIDC code exchange at `/ui/vault/auth/*/oidc/callback` |
| `nginx.conf` | Gateway proxy + standalone auth page + HTML injection |
| `patches/<version>/manifest.sha256` | Upstream UI fingerprints for drift detection |
## Deploy
From `railiance-platform`:
```bash
make openbao-overlay-apply # overlay only
make openbao-deploy # middleware + overlay + Helm upgrade
make openbao-verify-login-overlay
```
## Reapply after an OpenBao upgrade
1. Bump `server.image.tag` in `helm/openbao-values.yaml`.
2. Deploy: `make openbao-deploy`.
3. Fetch live UI assets and compare hashes:
```bash
curl -sS https://bao.coulomb.social/ui/ -o /tmp/index.html
# locate vault-*.js path in /tmp/index.html, then:
curl -sS "https://bao.coulomb.social/ui/assets/vault-....js" -o /tmp/vault.js
sha256sum /tmp/index.html /tmp/vault.js
```
4. If hashes differ from `patches/<old-version>/manifest.sha256`, update
`overlay.css` / `overlay.js` selectors against the new Ember templates.
5. Write `patches/<new-version>/manifest.sha256`, update `VERSION`.
6. Run `make openbao-verify-login-overlay CHECK_UPSTREAM_DRIFT=1`.
7. Attended browser login through KeyCape MFA.
Workplan: `helix-forge/workplans/HF-WP-0003-openbao-keycape-login-overlay.md`

View File

@@ -0,0 +1 @@
2.5.4

View File

@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Signing in with KeyCape</title>
<link rel="stylesheet" href="/ui/platform-overlay/login.css" />
<script src="/ui/platform-overlay/callback.js" defer></script>
</head>
<body id="callback-root">
<main class="login-card">
<h1>Signing in with KeyCape</h1>
<p>Completing sign-in and opening OpenBao…</p>
</main>
</body>
</html>

View File

@@ -0,0 +1,142 @@
(function () {
"use strict";
const PRESETS_URL = "/ui/platform-overlay/presets.json";
const TOKEN_PREFIX = "vault-";
const TOKEN_SEPARATOR = "☃";
const CLUSTER_ID = "1";
const DEFAULT_PRESETS = { mount: "netkingdom", role: "platform-admin" };
const OIDC_BACKEND = {
type: "oidc",
typeDisplay: "OIDC",
description: "Authenticate using JWT or OIDC provider.",
tokenPath: "client_token",
displayNamePath: "display_name",
formAttributes: ["role", "jwt"],
};
const POST_LOGIN_PATH = "/ui/vault/vault/secrets";
function parseCallbackContext() {
const match = window.location.pathname.match(
/\/ui\/vault\/auth\/(.+)\/oidc\/callback\/?$/
);
if (!match) {
throw new Error("Unsupported OIDC callback path");
}
const mount = decodeURIComponent(match[1]).replace(/\/$/, "");
const params = new URLSearchParams(window.location.search);
let state = params.get("state") || "";
let namespace = "";
if (state.includes(",ns=")) {
const parts = state.split(",ns=");
state = parts[0];
namespace = parts[1] || "";
}
const code = params.get("code");
if (!mount || !state || !code) {
throw new Error("OIDC callback missing required parameters");
}
return { mount, state, code, namespace };
}
async function loadPresets() {
try {
const response = await fetch(PRESETS_URL, { cache: "no-store" });
if (!response.ok) return { ...DEFAULT_PRESETS };
return { ...DEFAULT_PRESETS, ...(await response.json()) };
} catch (_error) {
return { ...DEFAULT_PRESETS };
}
}
async function exchangeOidc({ mount, state, code }) {
const query = new URLSearchParams({ state, code });
const response = await fetch(
`/v1/auth/${encodeURIComponent(mount)}/oidc/callback?${query}`,
{ method: "GET", headers: { Accept: "application/json" } }
);
const payload = await response.json().catch(() => ({}));
if (!response.ok) {
const detail =
payload?.errors?.[0] ||
`OIDC callback exchange failed (${response.status})`;
throw new Error(detail);
}
const auth = payload.auth || payload.data?.auth || payload.data;
if (!auth?.client_token) {
throw new Error("OIDC callback did not return a client token");
}
return auth;
}
function persistAuthToken(auth, presets) {
const mount = presets.mount || "netkingdom";
const selectedAuth = mount.endsWith("/") ? mount : `${mount}/`;
const tokenName = `${TOKEN_PREFIX}oidc${TOKEN_SEPARATOR}${CLUSTER_ID}`;
const namespacePath = auth.namespace_path?.replace(/\/$/, "") || "";
const tokenData = {
userRootNamespace: namespacePath,
displayName:
auth.display_name ||
auth.metadata?.name ||
auth.metadata?.username ||
"KeyCape",
backend: OIDC_BACKEND,
token: auth.client_token,
policies: auth.policies || [],
renewable: Boolean(auth.renewable),
entity_id: auth.entity_id,
};
if (tokenData.renewable && auth.lease_duration) {
tokenData.ttl = auth.lease_duration;
tokenData.tokenExpirationEpoch =
Date.now() + auth.lease_duration * 1000;
}
window.localStorage.setItem("selectedAuth", selectedAuth);
window.localStorage.setItem(tokenName, JSON.stringify(tokenData));
}
function showError(message) {
const root = document.getElementById("callback-root");
if (!root) return;
root.innerHTML = `
<main class="login-card">
<h1>Sign-in failed</h1>
<p class="login-error is-visible">${message}</p>
<button class="login-button" type="button" id="callback-retry">
Back to sign in
</button>
</main>
`;
document.getElementById("callback-retry")?.addEventListener("click", () => {
window.location.assign("/ui/vault/auth");
});
}
async function init() {
try {
const presets = await loadPresets();
const context = parseCallbackContext();
const auth = await exchangeOidc(context);
persistAuthToken(auth, presets);
window.location.replace(POST_LOGIN_PATH);
} catch (error) {
showError(
error instanceof Error
? error.message
: "OIDC sign-in failed. Contact your administrator."
);
}
}
init();
})();

View File

@@ -0,0 +1,85 @@
:root {
color-scheme: light;
--bg: #f0f2f5;
--card: #ffffff;
--text: #1f2a37;
--muted: #5b6b7c;
--border: #d9dee3;
--accent: #1565c0;
--accent-hover: #0d47a1;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
background: var(--bg);
color: var(--text);
display: flex;
align-items: center;
justify-content: center;
padding: 1.5rem;
}
.login-card {
width: min(100%, 26rem);
background: var(--card);
border: 1px solid var(--border);
border-radius: 8px;
box-shadow: 0 8px 24px rgba(16, 24, 40, 0.08);
padding: 2rem 1.75rem 1.75rem;
}
.login-card h1 {
margin: 0 0 0.75rem;
font-size: 1.45rem;
font-weight: 600;
line-height: 1.3;
}
.login-card p {
margin: 0 0 1.5rem;
color: var(--muted);
font-size: 0.9rem;
line-height: 1.45;
}
.login-button {
width: 100%;
border: 0;
border-radius: 6px;
padding: 0.8rem 1rem;
font-size: 0.95rem;
font-weight: 600;
color: #fff;
background: var(--accent);
cursor: pointer;
}
.login-button:hover:not(:disabled) {
background: var(--accent-hover);
}
.login-button:disabled {
opacity: 0.7;
cursor: wait;
}
.login-error {
display: none;
margin-top: 1rem;
padding: 0.75rem 0.9rem;
border-radius: 6px;
background: #fdecea;
color: #8a1c13;
font-size: 0.85rem;
line-height: 1.4;
}
.login-error.is-visible {
display: block;
}

View File

@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Sign in with KeyCape</title>
<link rel="stylesheet" href="/ui/platform-overlay/login.css" />
<script src="/ui/platform-overlay/login.js" defer></script>
</head>
<body>
<main class="login-card">
<h1 id="login-title">Sign in with KeyCape</h1>
<p id="login-banner">
Platform operators authenticate through KeyCape at kc.coulomb.social.
</p>
<button id="login-submit" class="login-button" type="button">
Sign in with KeyCape
</button>
<div id="login-error" class="login-error" role="alert"></div>
</main>
</body>
</html>

View File

@@ -0,0 +1,86 @@
(function () {
"use strict";
const PRESETS_URL = "/ui/platform-overlay/presets.json";
const DEFAULT_PRESETS = {
mount: "netkingdom",
role: "platform-admin",
title: "Sign in with KeyCape",
signInLabel: "Sign in with KeyCape",
banner:
"Platform operators authenticate through KeyCape at kc.coulomb.social.",
};
async function loadPresets() {
try {
const response = await fetch(PRESETS_URL, { cache: "no-store" });
if (!response.ok) return { ...DEFAULT_PRESETS };
return { ...DEFAULT_PRESETS, ...(await response.json()) };
} catch (_error) {
return { ...DEFAULT_PRESETS };
}
}
async function redirectToKeyCape(presets) {
const mount = presets.mount || "netkingdom";
const role = presets.role || "platform-admin";
const redirectUri = `${window.location.origin}/ui/vault/auth/${mount}/oidc/callback`;
const response = await fetch(`/v1/auth/${mount}/oidc/auth_url`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
role,
redirect_uri: redirectUri,
}),
});
if (!response.ok) {
throw new Error(`OIDC auth_url request failed (${response.status})`);
}
const payload = await response.json();
const authUrl = payload?.data?.auth_url;
if (!authUrl) {
throw new Error("OIDC auth_url missing from OpenBao response");
}
window.location.assign(authUrl);
}
function showError(message) {
const error = document.getElementById("login-error");
if (!error) return;
error.textContent = message;
error.classList.add("is-visible");
}
async function init() {
const presets = await loadPresets();
const title = document.getElementById("login-title");
const banner = document.getElementById("login-banner");
const button = document.getElementById("login-submit");
if (title) title.textContent = presets.title;
if (banner) banner.textContent = presets.banner;
if (button) button.textContent = presets.signInLabel;
if (!button) return;
button.addEventListener("click", async () => {
button.disabled = true;
try {
await redirectToKeyCape(presets);
} catch (error) {
button.disabled = false;
showError(
error instanceof Error
? error.message
: "Sign-in failed. Contact your administrator."
);
}
});
}
init();
})();

View File

@@ -0,0 +1,69 @@
worker_processes auto;
error_log /dev/stderr notice;
pid /tmp/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
access_log /dev/stdout;
sendfile on;
keepalive_timeout 65;
server_tokens off;
upstream openbao_upstream {
server openbao.openbao.svc.cluster.local:8200;
}
server {
listen 8080;
location /ui/platform-overlay/ {
alias /etc/nginx/overlay/;
add_header Cache-Control "public, max-age=300";
}
# Standalone KeyCape login page — bypasses Ember auth route and ?with= bounce.
location = /ui/vault/auth {
alias /etc/nginx/overlay/login.html;
default_type text/html;
add_header Cache-Control "no-store";
}
# OIDC callback handler — exchanges code without Ember popup/postMessage flow.
location ~ ^/ui/vault/auth/.+/oidc/callback/?$ {
alias /etc/nginx/overlay/callback.html;
default_type text/html;
add_header Cache-Control "no-store";
}
# Static UI bundles and API calls bypass HTML injection and stay compressed.
location ~ ^/(v1|ui/assets|ui/engines-dist|ui/favicon\.svg) {
proxy_pass http://openbao_upstream;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location / {
proxy_pass http://openbao_upstream;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Disable upstream compression only for HTML shell injection.
proxy_set_header Accept-Encoding "";
proxy_buffering on;
sub_filter_types text/html;
sub_filter_once on;
sub_filter '</head>' '<link rel="stylesheet" href="/ui/platform-overlay/overlay.css"><script src="/ui/platform-overlay/overlay.js" defer></script></head>';
}
}
}

View File

@@ -0,0 +1,40 @@
/* KeyCape login overlay for OpenBao UI — see presets.json and overlay.js */
html.keycape-overlay-active .toolbar-namespace-picker,
html.keycape-overlay-active nav.tabs,
html.keycape-overlay-active label[for="namespace"],
html.keycape-overlay-active label[for="role"],
html.keycape-overlay-active label[for="custom-path"],
html.keycape-overlay-active #namespace,
html.keycape-overlay-active #role,
html.keycape-overlay-active #custom-path,
html.keycape-overlay-active #token,
html.keycape-overlay-active #username,
html.keycape-overlay-active #password,
html.keycape-overlay-active select[name="auth-method"],
html.keycape-overlay-active .auth-form .box.has-slim-padding.is-shadowless,
html.keycape-overlay-active .auth-form .has-bottom-margin-s {
display: none !important;
}
html.keycape-overlay-active .splash-page-header .brand-icon-large {
display: none !important;
}
html.keycape-overlay-active h1.title.is-3 {
font-size: 1.45rem;
font-weight: 600;
}
.keycape-overlay-banner {
padding: 0.75rem 1rem;
background: #f4f6f8;
border-bottom: 1px solid #d9dee3;
font-size: 0.875rem;
color: #3d4f5f;
line-height: 1.4;
}
html.keycape-overlay-active .login-form .auth-form {
padding-top: 0.25rem;
}

View File

@@ -0,0 +1,279 @@
(function () {
"use strict";
const PRESETS_URL = "/ui/platform-overlay/presets.json";
const MAX_APPLY_ATTEMPTS = 40;
const APPLY_INTERVAL_MS = 250;
const DEFAULT_PRESETS = {
namespace: "",
method: "oidc",
mount: "netkingdom",
role: "platform-admin",
title: "Sign in with KeyCape",
signInLabel: "Sign in with KeyCape",
banner:
"Platform operators authenticate through KeyCape at kc.coulomb.social.",
};
let presets = { ...DEFAULT_PRESETS };
let applyAttempts = 0;
let applyTimer = null;
let overlayApplied = false;
let signInHandlerInstalled = false;
function hasStoredSession() {
try {
return Object.keys(window.localStorage).some((key) =>
key.startsWith("vault-")
);
} catch (_error) {
return false;
}
}
function isUiEntryPath() {
const path = window.location.pathname;
return path === "/ui" || path === "/ui/";
}
function redirectUnauthenticatedUiEntry() {
if (!isUiEntryPath() || hasStoredSession()) return;
window.location.replace("/ui/vault/auth");
}
function isAuthPage() {
const path = window.location.pathname;
return (
/\/ui\/vault\/auth(?:\/|$)/.test(path) ||
/\/ui\/?$/.test(path)
);
}
function isOidcCallbackPage() {
return window.location.pathname.includes("/oidc/");
}
function hideNode(node) {
if (!node || node.dataset.keycapeOverlayHidden === "true") return;
const field =
node.closest(".field.is-horizontal") ||
node.closest(".field") ||
node.closest(".box") ||
node;
if (field.dataset.keycapeOverlayHidden === "true") return;
field.style.display = "none";
field.setAttribute("aria-hidden", "true");
field.dataset.keycapeOverlayHidden = "true";
}
function setInputValue(input, value) {
if (!input || input.dataset.keycapeOverlayPreset === value) return;
input.value = value;
input.dataset.keycapeOverlayPreset = value;
input.dispatchEvent(new Event("input", { bubbles: true }));
input.dispatchEvent(new Event("change", { bubbles: true }));
}
async function redirectToKeyCape() {
const mount = presets.mount || "netkingdom";
const role = presets.role || "platform-admin";
const redirectUri = `${window.location.origin}/ui/vault/auth/${mount}/oidc/callback`;
const response = await fetch(`/v1/auth/${mount}/oidc/auth_url`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
role,
redirect_uri: redirectUri,
}),
});
if (!response.ok) {
throw new Error(`OIDC auth_url request failed (${response.status})`);
}
const payload = await response.json();
const authUrl = payload?.data?.auth_url;
if (!authUrl) {
throw new Error("OIDC auth_url missing from OpenBao response");
}
window.location.assign(authUrl);
}
function installKeyCapeSignInHandler() {
if (signInHandlerInstalled) return;
signInHandlerInstalled = true;
document.addEventListener(
"click",
(event) => {
if (!isAuthPage() || isOidcCallbackPage()) return;
const button = event.target.closest(
'#auth-submit, button[data-test="auth-submit"], form#auth-form button[type="submit"]'
);
if (!button) return;
event.preventDefault();
event.stopPropagation();
button.disabled = true;
button.classList.add("is-loading");
redirectToKeyCape().catch(() => {
button.disabled = false;
button.classList.remove("is-loading");
});
},
true
);
}
function loginShellReady() {
return Boolean(
document.querySelector(".login-form") ||
document.querySelector(".auth-form") ||
document.querySelector(".toolbar-namespace-picker")
);
}
function applyDom() {
if (!isAuthPage() || isOidcCallbackPage() || overlayApplied) return false;
hideNode(document.querySelector(".toolbar-namespace-picker"));
document
.querySelectorAll(
'#namespace, input[name="namespace"], label[for="namespace"]'
)
.forEach(hideNode);
document
.querySelectorAll('select[name="auth-method"], #auth-method')
.forEach((el) => hideNode(el.closest(".field") || el));
document
.querySelectorAll('#custom-path, input[name="custom-path"]')
.forEach(hideNode);
document
.querySelectorAll('#role, input[name="role"], label[for="role"]')
.forEach(hideNode);
document
.querySelectorAll(
'#token, input[name="token"], label[for="token"], #username, input[name="username"], #password, input[name="password"]'
)
.forEach(hideNode);
document.querySelectorAll("nav.tabs").forEach((el) => {
if (el.dataset.keycapeOverlayHidden === "true") return;
el.style.display = "none";
el.setAttribute("aria-hidden", "true");
el.dataset.keycapeOverlayHidden = "true";
});
document
.querySelectorAll(".auth-form .has-bottom-margin-s")
.forEach(hideNode);
document.querySelectorAll("h1.title.is-3").forEach((heading) => {
if (
/Sign in to OpenBao|Authenticate/.test(heading.textContent) &&
heading.textContent !== presets.title
) {
heading.textContent = presets.title;
}
});
document
.querySelectorAll('#auth-submit, button[data-test="auth-submit"]')
.forEach((button) => {
if (button.textContent !== presets.signInLabel) {
button.textContent = presets.signInLabel;
}
});
document
.querySelectorAll('#namespace, input[name="namespace"]')
.forEach((input) => setInputValue(input, presets.namespace || ""));
document
.querySelectorAll('#role, input[name="role"]')
.forEach((input) =>
setInputValue(input, presets.role || "platform-admin")
);
if (!document.getElementById("keycape-overlay-banner")) {
const banner = document.createElement("div");
banner.id = "keycape-overlay-banner";
banner.className = "keycape-overlay-banner";
banner.textContent = presets.banner;
const loginForm = document.querySelector(".login-form");
if (loginForm) {
loginForm.prepend(banner);
}
}
document.documentElement.classList.add("keycape-overlay-active");
installKeyCapeSignInHandler();
if (loginShellReady()) {
overlayApplied = true;
return true;
}
return false;
}
function stopApplyLoop() {
if (applyTimer !== null) {
window.clearInterval(applyTimer);
applyTimer = null;
}
}
function scheduleApply() {
stopApplyLoop();
applyAttempts = 0;
const tick = () => {
applyAttempts += 1;
if (applyDom() || applyAttempts >= MAX_APPLY_ATTEMPTS) {
stopApplyLoop();
return;
}
};
tick();
applyTimer = window.setInterval(tick, APPLY_INTERVAL_MS);
}
async function loadPresets() {
try {
const response = await fetch(PRESETS_URL, { cache: "no-store" });
if (!response.ok) return;
const data = await response.json();
presets = { ...DEFAULT_PRESETS, ...data };
} catch (_error) {
presets = { ...DEFAULT_PRESETS };
}
}
async function init() {
redirectUnauthenticatedUiEntry();
if (!isAuthPage() || isOidcCallbackPage()) return;
await loadPresets();
installKeyCapeSignInHandler();
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", scheduleApply, {
once: true,
});
} else {
scheduleApply();
}
}
init();
})();

View File

@@ -0,0 +1,8 @@
# OpenBao UI asset fingerprints for image tag 2.5.4.
# Regenerate after an OpenBao image bump when login markup drifts.
# Compare vault.js only — index.html is intentionally modified by the gateway.
# curl -sS https://bao.coulomb.social/ui/ -o /tmp/index.html
# vault_path=$(rg -o '/ui/assets/vault-[a-f0-9]+\\.js' /tmp/index.html | head -1)
# curl -sS "https://bao.coulomb.social${vault_path}" -o /tmp/vault.js
# sha256sum /tmp/vault.js
f0214b5be89377395f8d6521c34139877529bd95ba703901c78b527ab0f1c231 ui/assets/vault-bae6b876038fbf475728f993b5a62002.js

View File

@@ -0,0 +1,9 @@
{
"namespace": "",
"method": "oidc",
"mount": "netkingdom",
"role": "platform-admin",
"title": "Sign in with KeyCape",
"signInLabel": "Sign in with KeyCape",
"banner": "Platform operators authenticate through KeyCape at kc.coulomb.social."
}

View File

@@ -30,6 +30,8 @@ server:
cpu: 500m
memory: 512Mi
# Public browser ingress is owned by helm/openbao-ui-overlay-k8s.yaml so the
# KeyCape login overlay gateway can inject overlay assets.
ingress:
enabled: false

View File

@@ -0,0 +1,23 @@
# Narrow issuer policy for the credential broker warden-sign pilot.
# This policy can create child tokens only through the warden-sign token role.
# Bind it to a broker/operator issuer identity, not to tenant workloads.
path "auth/token/create/warden-sign" {
capabilities = ["create", "update"]
}
path "auth/token/lookup-accessor" {
capabilities = ["update"]
}
path "auth/token/revoke-accessor" {
capabilities = ["update"]
}
path "auth/token/lookup-self" {
capabilities = ["read"]
}
path "sys/capabilities-self" {
capabilities = ["update"]
}

View File

@@ -0,0 +1,40 @@
# Non-production metadata applier for reviewed credential changes.
#
# This policy is intended for build/test/staging OpenBao lanes after a
# non-production mount or namespace is declared. It intentionally keeps the same
# no-secret-value rule as production; generated test secrets still require a
# separately approved non-production value path.
# Workload KV read-lane policies generated from approved CCRs.
path "sys/policies/acl/workload-kv-read-*" {
capabilities = ["create", "update", "read"]
}
# Credential broker issuer policies generated from approved grant metadata.
path "sys/policies/acl/credential-broker-*-issuer" {
capabilities = ["create", "update", "read"]
}
# OIDC roles for caller-scoped workload KV lanes.
path "auth/netkingdom/role/*-workload-kv-read" {
capabilities = ["create", "update", "read"]
}
# Kubernetes roles for in-cluster workload and provider-secret lanes.
path "auth/kubernetes/role/*" {
capabilities = ["create", "update", "read"]
}
# Token roles for approved credential-broker child-token issuers.
path "auth/token/roles/credential-broker-*" {
capabilities = ["create", "update", "read"]
}
# Self-checks and capability introspection only.
path "auth/token/lookup-self" {
capabilities = ["read"]
}
path "sys/capabilities-self" {
capabilities = ["update"]
}

View File

@@ -0,0 +1,41 @@
# Production metadata applier for reviewed credential changes.
#
# This policy intentionally permits only non-secret OpenBao metadata writes for
# approved CCRs. Secret value paths under platform/data are not granted here.
# The local credential-change applier-dry-run command must validate the CCR
# before this policy is used for any live mutation.
# Workload KV read-lane policies generated from approved CCRs.
path "sys/policies/acl/workload-kv-read-*" {
capabilities = ["create", "update", "read"]
}
# Credential broker issuer policies generated from approved grant metadata.
path "sys/policies/acl/credential-broker-*-issuer" {
capabilities = ["create", "update", "read"]
}
# OIDC roles for caller-scoped workload KV lanes.
path "auth/netkingdom/role/*-workload-kv-read" {
capabilities = ["create", "update", "read"]
}
# Kubernetes roles for in-cluster workload and provider-secret lanes. The local
# applier dry-run constrains role names and bound service accounts per CCR.
path "auth/kubernetes/role/*" {
capabilities = ["create", "update", "read"]
}
# Token roles for approved credential-broker child-token issuers.
path "auth/token/roles/credential-broker-*" {
capabilities = ["create", "update", "read"]
}
# Self-checks and capability introspection only.
path "auth/token/lookup-self" {
capabilities = ["read"]
}
path "sys/capabilities-self" {
capabilities = ["update"]
}

View File

@@ -0,0 +1,13 @@
# Least-privilege policy for the External Secrets Operator issue-core pilot.
#
# The matching Kubernetes auth role binds only the ESO service account in the
# external-secrets namespace. ClusterSecretStore usage is separately limited to
# the issue-core namespace.
path "platform/data/workloads/issue-core/issue-core/*" {
capabilities = ["read"]
}
path "platform/metadata/workloads/issue-core/issue-core/*" {
capabilities = ["read", "list"]
}

View File

@@ -0,0 +1,18 @@
# Narrow policy for ops-warden SSH signing (Vault/OpenBao SSH secrets engine).
# Bind to dedicated tokens or Kubernetes auth roles — not platform-admin.
path "ssh/sign/adm-role" {
capabilities = ["create", "update"]
}
path "ssh/sign/agt-role" {
capabilities = ["create", "update"]
}
path "ssh/sign/atm-role" {
capabilities = ["create", "update"]
}
path "ssh/roles" {
capabilities = ["list", "read"]
}

View File

@@ -0,0 +1,7 @@
path "platform/data/workloads/issue-core/issue-core/issue-core-runtime" {
capabilities = ["read"]
}
path "platform/metadata/workloads/issue-core/issue-core/issue-core-runtime" {
capabilities = ["read"]
}

View File

@@ -0,0 +1,7 @@
path "platform/data/workloads/activity-core/llm-connect/llm-connect-provider-secrets" {
capabilities = ["read"]
}
path "platform/metadata/workloads/activity-core/llm-connect/llm-connect-provider-secrets" {
capabilities = ["read"]
}

View File

@@ -0,0 +1,13 @@
# Least-privilege read policy for the whynot-design npm publish token.
#
# This policy intentionally grants only read access to the single KV-v2 secret
# path used by ops-warden's caller-scoped access lane. It does not grant list
# access to sibling workloads or mutation capabilities.
path "platform/data/workloads/coulomb/whynot-design/npm-publish" {
capabilities = ["read"]
}
path "platform/metadata/workloads/coulomb/whynot-design/npm-publish" {
capabilities = ["read"]
}

View File

@@ -0,0 +1,30 @@
# Declarative SSH CA roles for ops-warden ActorType policy.
# TTL max: adm 48h, agt 24h, atm 8h — wiki/OpsWardenConfig.md (ops-warden)
mount: ssh
roles:
adm-role:
key_type: ca
allowed_users: "*"
allow_user_certificates: true
allow_user_key_ids: true
default_user: adm
ttl: 48h
max_ttl: 48h
agt-role:
key_type: ca
allowed_users: "*"
allow_user_certificates: true
allow_user_key_ids: true
default_user: agt
ttl: 24h
max_ttl: 24h
atm-role:
key_type: ca
allowed_users: "*"
allow_user_certificates: true
allow_user_key_ids: true
default_user: atm
ttl: 8h
max_ttl: 8h

View File

@@ -0,0 +1,115 @@
schema_version: 1
kind: credential-change-request-schema
description: Non-secret schema contract for credential/security change requests.
required_top_level:
- id
- kind
- schema_version
- request_type
- title
- status
- created
- updated
- requester
- target
- openbao
- access_frontdoor
- risk
- verification
- lifecycle
allowed_statuses:
- draft
- proposed
- needs_changes
- approved
- denied
- apply_pending
- applied
- verified
- active
- deactivated
- rotated
- compromised
- superseded
- cancelled
allowed_request_types:
- workload-kv-read
secret_markers_rejected:
- AGE-SECRET-KEY-1
- "-----BEGIN PRIVATE KEY-----"
- "-----BEGIN OPENSSH PRIVATE KEY-----"
- OPENBAO_ROOT_TOKEN=
- VAULT_TOKEN=
- BAO_TOKEN=
- hvb.
- hvc.
- hvs.
- npm_
- ghp_
- sk-
workload_kv_read:
required:
openbao:
- mount
- kv_path
- fields
- policy_name
- policy_file
- auth
openbao.auth:
- method
- mount
- role
- bound_claims
- bound_claims_confirmed
- policies
access_frontdoor:
- type
- catalog_id
- readiness
- resolvable
verification:
- positive
- negative
- activation_conditions
lifecycle:
- deactivate
- rotate
- compromised
conditional:
openbao.auth.method=oidc:
required:
- allowed_redirect_uris
allowed_redirect_uris: non-empty list of OpenBao callback URIs accepted by the role
groups_claim: requires openbao.auth.oidc_scopes to include groups
access_frontdoor_readiness:
allowed:
- template
- pending-review
- approved-pending-apply
- applied-pending-verify
- ready
- disabled
- compromised
resolvable_true_requires_status: active
ops_warden_should_consume_only:
readiness: ready
resolvable: true
guardrails:
apply_plan_requires_status:
- approved
active_requires_status:
- verified
disallowed_policy_names:
- root
- platform-admin
disallowed_path_fragments:
- "*"
- ".."

2189
scripts/credential-change.py Executable file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,389 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import re
import sys
from pathlib import Path
from typing import Any
import yaml
SECRET_MARKERS = [
"AGE-SECRET-KEY-1",
"-----BEGIN PRIVATE KEY-----",
"-----BEGIN OPENSSH PRIVATE KEY-----",
"OPENBAO_ROOT_TOKEN=",
"VAULT_TOKEN=",
"BAO_TOKEN=",
"hvs.",
"hvb.",
"hvc.",
]
REQUIRED_DENIED_MODES = {
"chat",
"state-hub-body",
"git",
"command-line-token-argument",
"llm-prompt",
}
ALLOWED_CREDENTIAL_TYPES = {"openbao-token"}
ALLOWED_GRANT_CLASSES = {"self-service", "approval-required", "break-glass"}
ALLOWED_GRANT_STATUSES = {"pilot", "active", "deprecated", "disabled"}
DISALLOWED_POLICIES = {"root", "platform-admin"}
TTL_RE = re.compile(r"^([1-9][0-9]*)([smhd])$")
def fail(message: str) -> None:
print(f"[FAIL] {message}", file=sys.stderr)
def ttl_seconds(value: Any, field: str, errors: list[str]) -> int | None:
if not isinstance(value, str):
errors.append(f"{field} must be a string TTL such as 15m")
return None
match = TTL_RE.match(value)
if not match:
errors.append(f"{field} must match <positive integer><s|m|h|d>: {value!r}")
return None
amount = int(match.group(1))
unit = match.group(2)
multiplier = {"s": 1, "m": 60, "h": 3600, "d": 86400}[unit]
return amount * multiplier
def require_dict(value: Any, field: str, errors: list[str]) -> dict[str, Any]:
if not isinstance(value, dict):
errors.append(f"{field} must be an object")
return {}
return value
def require_list(value: Any, field: str, errors: list[str]) -> list[Any]:
if not isinstance(value, list):
errors.append(f"{field} must be a list")
return []
return value
def require_nonempty_string(value: Any, field: str, errors: list[str]) -> str:
if not isinstance(value, str) or not value.strip():
errors.append(f"{field} must be a non-empty string")
return ""
return value.strip()
def validate_grant(
grant: Any, index: int, catalog: dict[str, Any], errors: list[str]
) -> str:
prefix = f"grants[{index}]"
grant_obj = require_dict(grant, prefix, errors)
if not grant_obj:
return ""
grant_id = require_nonempty_string(grant_obj.get("id"), f"{prefix}.id", errors)
require_nonempty_string(grant_obj.get("title"), f"{prefix}.title", errors)
require_nonempty_string(
grant_obj.get("description"), f"{prefix}.description", errors
)
status = require_nonempty_string(
grant_obj.get("status"), f"{prefix}.status", errors
)
if status and status not in ALLOWED_GRANT_STATUSES:
errors.append(
f"{prefix}.status must be one of {sorted(ALLOWED_GRANT_STATUSES)}"
)
grant_class = require_nonempty_string(
grant_obj.get("grant_class"), f"{prefix}.grant_class", errors
)
if grant_class and grant_class not in ALLOWED_GRANT_CLASSES:
errors.append(
f"{prefix}.grant_class must be one of {sorted(ALLOWED_GRANT_CLASSES)}"
)
credential_type = require_nonempty_string(
grant_obj.get("credential_type"), f"{prefix}.credential_type", errors
)
if credential_type and credential_type not in ALLOWED_CREDENTIAL_TYPES:
errors.append(
f"{prefix}.credential_type must be one of {sorted(ALLOWED_CREDENTIAL_TYPES)}"
)
require_nonempty_string(grant_obj.get("issuer"), f"{prefix}.issuer", errors)
require_nonempty_string(grant_obj.get("audience"), f"{prefix}.audience", errors)
openbao = require_dict(grant_obj.get("openbao"), f"{prefix}.openbao", errors)
policies = [
str(policy)
for policy in require_list(
openbao.get("policies"), f"{prefix}.openbao.policies", errors
)
]
if not policies:
errors.append(f"{prefix}.openbao.policies must contain at least one policy")
for policy in policies:
if not policy or policy in DISALLOWED_POLICIES:
errors.append(
f"{prefix}.openbao.policies contains disallowed policy: {policy!r}"
)
configured_disallowed = set(
str(policy)
for policy in require_list(
openbao.get("disallowed_policies"),
f"{prefix}.openbao.disallowed_policies",
errors,
)
)
missing_disallowed = DISALLOWED_POLICIES - configured_disallowed
if missing_disallowed:
errors.append(
f"{prefix}.openbao.disallowed_policies missing {sorted(missing_disallowed)}"
)
require_nonempty_string(
openbao.get("token_role"), f"{prefix}.openbao.token_role", errors
)
require_nonempty_string(
openbao.get("issuer_policy"), f"{prefix}.openbao.issuer_policy", errors
)
require_list(openbao.get("mount_paths"), f"{prefix}.openbao.mount_paths", errors)
ttl = require_dict(grant_obj.get("ttl"), f"{prefix}.ttl", errors)
default_ttl = ttl_seconds(ttl.get("default"), f"{prefix}.ttl.default", errors)
max_ttl = ttl_seconds(ttl.get("max"), f"{prefix}.ttl.max", errors)
if default_ttl is not None and max_ttl is not None and default_ttl > max_ttl:
errors.append(f"{prefix}.ttl.default must not exceed ttl.max")
if ttl.get("renewable") is not False:
errors.append(f"{prefix}.ttl.renewable must be false for the pilot")
actors = require_dict(grant_obj.get("actors"), f"{prefix}.actors", errors)
allowed_actor_types = require_list(
actors.get("allowed_types"), f"{prefix}.actors.allowed_types", errors
)
if not allowed_actor_types:
errors.append(f"{prefix}.actors.allowed_types must not be empty")
require_nonempty_string(
actors.get("required_subject_binding"),
f"{prefix}.actors.required_subject_binding",
errors,
)
authorization = require_dict(
grant_obj.get("authorization"), f"{prefix}.authorization", errors
)
if authorization.get("purpose_required") is not True:
errors.append(f"{prefix}.authorization.purpose_required must be true")
require_list(
authorization.get("allowed_purpose_examples"),
f"{prefix}.authorization.allowed_purpose_examples",
errors,
)
delivery = require_dict(grant_obj.get("delivery"), f"{prefix}.delivery", errors)
modes = catalog.get("delivery_modes", {})
allowed_known = set(
str(mode)
for mode in require_list(
modes.get("allowed_known"), "delivery_modes.allowed_known", errors
)
)
denied_known = set(
str(mode)
for mode in require_list(
modes.get("denied_known"), "delivery_modes.denied_known", errors
)
)
allowed = set(
str(mode)
for mode in require_list(
delivery.get("allowed"), f"{prefix}.delivery.allowed", errors
)
)
denied = set(
str(mode)
for mode in require_list(
delivery.get("denied"), f"{prefix}.delivery.denied", errors
)
)
if allowed - allowed_known:
errors.append(
f"{prefix}.delivery.allowed has unknown modes: {sorted(allowed - allowed_known)}"
)
if denied - denied_known:
errors.append(
f"{prefix}.delivery.denied has unknown modes: {sorted(denied - denied_known)}"
)
if allowed & denied:
errors.append(
f"{prefix}.delivery modes both allowed and denied: {sorted(allowed & denied)}"
)
missing_denied = REQUIRED_DENIED_MODES - denied
if missing_denied:
errors.append(f"{prefix}.delivery.denied missing {sorted(missing_denied)}")
preferred = require_nonempty_string(
delivery.get("preferred"), f"{prefix}.delivery.preferred", errors
)
if preferred and preferred not in allowed:
errors.append(f"{prefix}.delivery.preferred must be in delivery.allowed")
if "local-token-file" in allowed:
local_file = require_dict(
delivery.get("local_token_file"),
f"{prefix}.delivery.local_token_file",
errors,
)
if local_file.get("directory") != ".local/credential-leases":
errors.append(
f"{prefix}.delivery.local_token_file.directory must be .local/credential-leases"
)
if str(local_file.get("mode")) != "0600":
errors.append(f"{prefix}.delivery.local_token_file.mode must be 0600")
if "kubernetes-auth" in allowed:
kubernetes_auth = require_dict(
delivery.get("kubernetes_auth"),
f"{prefix}.delivery.kubernetes_auth",
errors,
)
require_nonempty_string(
kubernetes_auth.get("mount"),
f"{prefix}.delivery.kubernetes_auth.mount",
errors,
)
require_nonempty_string(
kubernetes_auth.get("role"),
f"{prefix}.delivery.kubernetes_auth.role",
errors,
)
if not require_list(
kubernetes_auth.get("service_account_names"),
f"{prefix}.delivery.kubernetes_auth.service_account_names",
errors,
):
errors.append(
f"{prefix}.delivery.kubernetes_auth.service_account_names must not be empty"
)
if not require_list(
kubernetes_auth.get("namespaces"),
f"{prefix}.delivery.kubernetes_auth.namespaces",
errors,
):
errors.append(
f"{prefix}.delivery.kubernetes_auth.namespaces must not be empty"
)
audit = require_dict(grant_obj.get("audit"), f"{prefix}.audit", errors)
if audit.get("openbao_audit_required") is not True:
errors.append(f"{prefix}.audit.openbao_audit_required must be true")
if audit.get("record_secret_values") is not False:
errors.append(f"{prefix}.audit.record_secret_values must be false")
revocation = require_dict(
grant_obj.get("revocation"), f"{prefix}.revocation", errors
)
if revocation.get("required") is not True:
errors.append(f"{prefix}.revocation.required must be true")
if revocation.get("by_accessor") is not True:
errors.append(f"{prefix}.revocation.by_accessor must be true")
return grant_id
def main() -> int:
parser = argparse.ArgumentParser(
description="Validate non-secret credential grant catalog."
)
parser.add_argument(
"catalog",
nargs="?",
default="credential-grants/catalog.yaml",
help="Path to catalog YAML",
)
args = parser.parse_args()
path = Path(args.catalog)
if not path.exists():
fail(f"catalog file is missing: {path}")
return 1
raw = path.read_text(encoding="utf-8")
errors: list[str] = []
for marker in SECRET_MARKERS:
if marker in raw:
errors.append(f"secret-looking marker present: {marker}")
try:
catalog = yaml.safe_load(raw)
except yaml.YAMLError as exc:
fail(f"catalog YAML is invalid: {exc}")
return 1
catalog_obj = require_dict(catalog, "catalog", errors)
if catalog_obj.get("version") != 1:
errors.append("version must be 1")
require_nonempty_string(catalog_obj.get("updated"), "updated", errors)
require_nonempty_string(catalog_obj.get("owner_repo"), "owner_repo", errors)
require_nonempty_string(catalog_obj.get("workplan_id"), "workplan_id", errors)
delivery_modes = require_dict(
catalog_obj.get("delivery_modes"), "delivery_modes", errors
)
allowed_known = set(
str(mode)
for mode in require_list(
delivery_modes.get("allowed_known"), "delivery_modes.allowed_known", errors
)
)
denied_known = set(
str(mode)
for mode in require_list(
delivery_modes.get("denied_known"), "delivery_modes.denied_known", errors
)
)
if not REQUIRED_DENIED_MODES.issubset(denied_known):
errors.append(
f"delivery_modes.denied_known missing {sorted(REQUIRED_DENIED_MODES - denied_known)}"
)
if allowed_known & denied_known:
errors.append(
f"delivery_modes overlap between allowed and denied: {sorted(allowed_known & denied_known)}"
)
grant_classes = set(
str(item)
for item in require_list(
catalog_obj.get("grant_classes"), "grant_classes", errors
)
)
if grant_classes != ALLOWED_GRANT_CLASSES:
errors.append(f"grant_classes must be exactly {sorted(ALLOWED_GRANT_CLASSES)}")
grants = require_list(catalog_obj.get("grants"), "grants", errors)
if not grants:
errors.append("grants must not be empty")
seen: set[str] = set()
for index, grant in enumerate(grants):
grant_id = validate_grant(grant, index, catalog_obj, errors)
if grant_id:
if grant_id in seen:
errors.append(f"duplicate grant id: {grant_id}")
seen.add(grant_id)
if "ops-warden/warden-sign" not in seen:
errors.append("initial grant ops-warden/warden-sign is required")
if errors:
for error in errors:
fail(error)
return 1
print(f"[OK] credential grant catalog is valid: {path}")
print(f"[OK] grants: {len(grants)}")
for grant in grants:
print(f"[OK] {grant['id']}: {grant['grant_class']} {grant['credential_type']}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

1075
scripts/credential.py Executable file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,217 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import getpass
import os
import shlex
import subprocess
import sys
from pathlib import Path
from typing import Any
REPO_DIR = Path(__file__).resolve().parents[1]
APPLIERS: dict[str, dict[str, Any]] = {
"nonprod": {
"title": "Credential change non-production metadata applier",
"policy_name": "credential-change-nonprod-applier",
"policy_file": "openbao/policies/credential-change-nonprod-applier.hcl",
"token_role": "credential-change-nonprod-applier",
"max_ttl": "1h",
},
"prod": {
"title": "Credential change production metadata applier",
"policy_name": "credential-change-prod-applier",
"policy_file": "openbao/policies/credential-change-prod-applier.hcl",
"token_role": "credential-change-prod-applier",
"max_ttl": "30m",
},
}
DISALLOWED_POLICIES = ("root", "platform-admin")
class BaoRunner:
def __init__(
self,
*,
kubectl: str,
namespace: str,
release: str,
dry_run: bool,
use_token_helper: bool,
token: str | None,
) -> None:
self.kubectl_parts = shlex.split(kubectl)
self.namespace = namespace
self.pod = f"{release}-0"
self.dry_run = dry_run
self.use_token_helper = use_token_helper
self.token = token
def run(
self, args: list[str], input_text: str | None = None
) -> subprocess.CompletedProcess[str]:
rendered = "bao " + shlex.join(args)
if self.dry_run:
print(f"DRY-RUN: {rendered}")
return subprocess.CompletedProcess(args, 0, "", "")
if self.use_token_helper:
cmd = (
self.kubectl_parts
+ ["exec", "-i", "-n", self.namespace, self.pod, "--", "bao"]
+ args
)
proc_input = input_text
else:
if not self.token:
raise RuntimeError(
"OpenBao token is required unless --use-token-helper is set"
)
cmd = (
self.kubectl_parts
+ [
"exec",
"-i",
"-n",
self.namespace,
self.pod,
"--",
"sh",
"-c",
'read -r BAO_TOKEN; export BAO_TOKEN; exec bao "$@"',
"sh",
]
+ args
)
proc_input = self.token + "\n" + (input_text or "")
result = subprocess.run(cmd, input=proc_input, capture_output=True, text=True)
if result.stdout:
print(result.stdout, end="")
if result.returncode != 0:
if result.stderr:
print(result.stderr, file=sys.stderr, end="")
raise SystemExit(result.returncode)
if result.stderr:
print(result.stderr, file=sys.stderr, end="")
return result
def read_token(
token_file: str | None, dry_run: bool, use_token_helper: bool
) -> str | None:
if dry_run or use_token_helper:
return None
if token_file:
path = Path(token_file)
if not path.exists():
raise SystemExit(f"ERROR: OPENBAO_TOKEN_FILE does not exist: {path}")
lines = path.read_text(encoding="utf-8").splitlines()
token = lines[0].strip() if lines else ""
if not token:
raise SystemExit(f"ERROR: OPENBAO_TOKEN_FILE is empty: {path}")
return token
token = getpass.getpass("OpenBao token: ")
if not token:
raise SystemExit("ERROR: empty OpenBao token")
return token
def selected_appliers(selector: str) -> list[dict[str, Any]]:
if selector == "all":
return [APPLIERS["nonprod"], APPLIERS["prod"]]
try:
return [APPLIERS[selector]]
except KeyError:
raise SystemExit(f"ERROR: applier must be one of {sorted(APPLIERS) + ['all']}")
def role_args(applier: dict[str, Any]) -> list[str]:
return [
"write",
f"auth/token/roles/{applier['token_role']}",
f"allowed_policies={applier['policy_name']}",
f"disallowed_policies={','.join(DISALLOWED_POLICIES)}",
"orphan=true",
"renewable=false",
f"token_explicit_max_ttl={applier['max_ttl']}",
"token_no_default_policy=true",
"token_type=service",
]
def write_policy(runner: BaoRunner, applier: dict[str, Any], policy_dir: Path) -> None:
policy_file = policy_dir / Path(applier["policy_file"]).name
if not policy_file.exists():
raise SystemExit(f"ERROR: missing policy file: {policy_file}")
if runner.dry_run:
print(f"DRY-RUN: bao policy write {applier['policy_name']} {policy_file}")
return
runner.run(
["policy", "write", applier["policy_name"], "-"],
input_text=policy_file.read_text(encoding="utf-8"),
)
print(f"OK: policy {applier['policy_name']} applied")
def apply_applier(runner: BaoRunner, applier: dict[str, Any], policy_dir: Path) -> None:
write_policy(runner, applier, policy_dir)
runner.run(role_args(applier))
runner.run(["read", f"auth/token/roles/{applier['token_role']}"])
print(
"OK: applier role "
f"{applier['token_role']} configured for policy {applier['policy_name']}"
)
def main() -> int:
parser = argparse.ArgumentParser(
description="Apply OpenBao credential-change delegated applier policies and token roles."
)
parser.add_argument(
"--applier", choices=["nonprod", "prod", "all"], default="all"
)
parser.add_argument("--policy-dir", default="openbao/policies")
parser.add_argument("--dry-run", action="store_true")
parser.add_argument(
"--use-token-helper",
action="store_true",
help="Use the OpenBao CLI token helper inside the pod",
)
parser.add_argument("--namespace", default=None)
parser.add_argument("--release", default=None)
parser.add_argument("--kubectl", default=None)
args = parser.parse_args()
namespace = args.namespace or os.environ.get("OPENBAO_NAMESPACE", "openbao")
release = args.release or os.environ.get("OPENBAO_RELEASE", "openbao")
kubectl = args.kubectl or os.environ.get("KUBECTL", "kubectl")
token_file = os.environ.get("OPENBAO_TOKEN_FILE")
token = read_token(token_file, args.dry_run, args.use_token_helper)
runner = BaoRunner(
kubectl=kubectl,
namespace=namespace,
release=release,
dry_run=args.dry_run,
use_token_helper=args.use_token_helper,
token=token,
)
if not args.dry_run:
runner.run(["status"])
policy_dir = REPO_DIR / args.policy_dir
for applier in selected_appliers(args.applier):
apply_applier(runner, applier, policy_dir)
print("NEXT: issue short-lived child tokens through an approved custody path only.")
print("NEXT: run scripts/credential-change.py applier-apply <CCR> with that ambient delegated authority.")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,139 @@
#!/usr/bin/env bash
set -euo pipefail
OPENBAO_NAMESPACE="${OPENBAO_NAMESPACE:-openbao}"
OPENBAO_RELEASE="${OPENBAO_RELEASE:-openbao}"
KUBECTL="${KUBECTL:-kubectl}"
TOKEN_FILE="${OPENBAO_TOKEN_FILE:-}"
ROLE_NAME="${OPENBAO_ESO_ROLE:-external-secrets-issue-core}"
POLICY_NAME="${OPENBAO_ESO_POLICY:-external-secrets-issue-core}"
ESO_NAMESPACE="${ESO_NAMESPACE:-external-secrets}"
ESO_SERVICE_ACCOUNT="${ESO_SERVICE_ACCOUNT:-external-secrets}"
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
POLICY_FILE="${POLICY_FILE:-$REPO_DIR/openbao/policies/external-secrets-issue-core.hcl}"
NEXT_KV_PATH="${OPENBAO_ESO_NEXT_PATH:-platform/workloads/issue-core/issue-core/issue-core-runtime}"
NEXT_FIELDS="${OPENBAO_ESO_NEXT_FIELDS:-ISSUE_CORE_API_KEY and GITEA_BACKEND_TOKEN}"
NEXT_TARGET="${OPENBAO_ESO_NEXT_TARGET:-ExternalSecret/issue-core-runtime}"
DRY_RUN=0
usage() {
cat <<'USAGE'
Usage: scripts/openbao-apply-external-secrets-issue-core.sh [--dry-run]
Configures OpenBao for the issue-core External Secrets Operator pilot:
- refreshes Kubernetes auth config for in-cluster short-lived tokens
- writes the external-secrets-issue-core read policy
- writes the Kubernetes auth role bound to external-secrets/external-secrets
The script reads an OpenBao operator token from OPENBAO_TOKEN_FILE or an
interactive hidden prompt. It never prints or stores the token.
USAGE
}
while [ "$#" -gt 0 ]; do
case "$1" in
--dry-run)
DRY_RUN=1
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "ERROR: unknown argument: $1" >&2
usage >&2
exit 2
;;
esac
done
pod="${OPENBAO_RELEASE}-0"
read_token() {
if [ "$DRY_RUN" -eq 1 ]; then
printf 'dry-run-token\n'
return
fi
if [ -n "$TOKEN_FILE" ]; then
if [ ! -f "$TOKEN_FILE" ]; then
echo "ERROR: OPENBAO_TOKEN_FILE does not exist: $TOKEN_FILE" >&2
exit 1
fi
head -n 1 "$TOKEN_FILE"
return
fi
local token
read -r -s -p "OpenBao token: " token
printf '\n' >&2
printf '%s\n' "$token"
}
kubectl_exec() {
# shellcheck disable=SC2086
$KUBECTL "$@"
}
remote_bao() {
local token="$1"
shift
if [ "$DRY_RUN" -eq 1 ]; then
printf 'DRY-RUN: bao %s\n' "$*"
return 0
fi
printf '%s\n' "$token" | kubectl_exec exec -i -n "$OPENBAO_NAMESPACE" "$pod" -- \
sh -c 'read -r BAO_TOKEN; export BAO_TOKEN; exec bao "$@"' sh "$@"
}
remote_sh() {
local token="$1"
local script="$2"
if [ "$DRY_RUN" -eq 1 ]; then
printf 'DRY-RUN: remote shell: %s\n' "$script"
return 0
fi
printf '%s\n%s\n' "$token" "$script" | kubectl_exec exec -i -n "$OPENBAO_NAMESPACE" "$pod" -- \
sh -c 'read -r BAO_TOKEN; export BAO_TOKEN; sh'
}
write_policy() {
local token="$1"
if [ ! -f "$POLICY_FILE" ]; then
echo "ERROR: missing policy file: $POLICY_FILE" >&2
exit 1
fi
if [ "$DRY_RUN" -eq 1 ]; then
printf 'DRY-RUN: bao policy write %s %s\n' "$POLICY_NAME" "$POLICY_FILE"
return 0
fi
{ printf '%s\n' "$token"; cat "$POLICY_FILE"; } | kubectl_exec exec -i -n "$OPENBAO_NAMESPACE" "$pod" -- \
sh -c 'read -r BAO_TOKEN; export BAO_TOKEN; bao policy write "$1" -' sh "$POLICY_NAME"
}
token="$(read_token)"
if [ -z "$token" ]; then
echo "ERROR: empty token" >&2
exit 1
fi
remote_bao "$token" status
remote_sh "$token" 'bao write auth/kubernetes/config \
kubernetes_host="https://${KUBERNETES_SERVICE_HOST}:${KUBERNETES_SERVICE_PORT}" \
disable_iss_validation=true'
write_policy "$token"
remote_bao "$token" write "auth/kubernetes/role/${ROLE_NAME}" \
"bound_service_account_names=${ESO_SERVICE_ACCOUNT}" \
"bound_service_account_namespaces=${ESO_NAMESPACE}" \
"policies=${POLICY_NAME}" \
ttl=15m
remote_bao "$token" read "auth/kubernetes/role/${ROLE_NAME}"
cat <<NEXT
External Secrets OpenBao role configured.
Next steps:
1. Sync the external-secrets and openbao-secretstore ArgoCD Applications.
2. Provision ${NEXT_KV_PATH} with ${NEXT_FIELDS} without printing values.
3. Confirm ${NEXT_TARGET} becomes Ready.
NEXT

View File

@@ -188,8 +188,7 @@ enable_optional "$token" "kubernetes/ auth method is already enabled." auth enab
remote_sh "$token" 'bao write auth/kubernetes/config \
kubernetes_host="https://${KUBERNETES_SERVICE_HOST}:${KUBERNETES_SERVICE_PORT}" \
token_reviewer_jwt=@/var/run/secrets/kubernetes.io/serviceaccount/token \
kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt'
disable_iss_validation=true'
write_policy "$token" platform-admin "$POLICY_DIR/platform-admin.hcl"
write_policy "$token" platform-readonly "$POLICY_DIR/platform-readonly.hcl"

View File

@@ -0,0 +1,271 @@
#!/usr/bin/env bash
set -euo pipefail
OPENBAO_NAMESPACE="${OPENBAO_NAMESPACE:-openbao}"
OPENBAO_RELEASE="${OPENBAO_RELEASE:-openbao}"
KUBECTL="${KUBECTL:-kubectl}"
TOKEN_FILE="${OPENBAO_TOKEN_FILE:-}"
SSH_MOUNT="${OPENBAO_SSH_MOUNT:-ssh}"
ROLES_SPEC="${ROLES_SPEC:-}"
POLICY_DIR="${POLICY_DIR:-}"
CA_PUBKEY_OUT="${OPENBAO_SSH_CA_PUBKEY_OUT:-}"
K8S_CA_SECRET="${OPENBAO_SSH_CA_K8S_SECRET:-openbao-ssh-ca-pub}"
DRY_RUN=0
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
ROLES_SPEC="${ROLES_SPEC:-$REPO_DIR/openbao/ssh/roles-spec.yaml}"
POLICY_DIR="${POLICY_DIR:-$REPO_DIR/openbao/policies}"
usage() {
cat <<'USAGE'
Usage: scripts/openbao-apply-ssh-engine.sh [--dry-run]
Applies the OpenBao SSH secrets engine for ops-warden signing:
- enable ssh/ mount (idempotent)
- write adm/agt/atm roles from openbao/ssh/roles-spec.yaml
- load warden-sign policy
- export CA public key (optional K8s secret + local file)
Requires initialized, unsealed OpenBao and a token with ssh engine admin
(typically platform-admin). Token from OPENBAO_TOKEN_FILE or prompt.
Env:
OPENBAO_SSH_CA_PUBKEY_OUT Write CA pubkey to this path (non-secret)
OPENBAO_SSH_CA_K8S_SECRET K8s secret name in openbao namespace (default: openbao-ssh-ca-pub)
USAGE
}
while [ "$#" -gt 0 ]; do
case "$1" in
--dry-run)
DRY_RUN=1
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "ERROR: unknown argument: $1" >&2
usage >&2
exit 2
;;
esac
done
pod="${OPENBAO_RELEASE}-0"
WARNINGS=0
warn() {
WARNINGS=$((WARNINGS + 1))
printf 'WARN: %s\n' "$*" >&2
}
read_token() {
if [ "$DRY_RUN" -eq 1 ]; then
printf 'dry-run-token\n'
return
fi
if [ -n "$TOKEN_FILE" ]; then
if [ ! -f "$TOKEN_FILE" ]; then
echo "ERROR: OPENBAO_TOKEN_FILE does not exist: $TOKEN_FILE" >&2
exit 1
fi
head -n 1 "$TOKEN_FILE"
return
fi
local token
read -r -s -p "OpenBao token: " token
printf '\n' >&2
printf '%s\n' "$token"
}
kubectl_exec() {
# shellcheck disable=SC2086
$KUBECTL "$@"
}
remote_bao() {
local token="$1"
shift
if [ "$DRY_RUN" -eq 1 ]; then
printf 'DRY-RUN: bao %s\n' "$*"
return 0
fi
printf '%s\n' "$token" | kubectl_exec exec -i -n "$OPENBAO_NAMESPACE" "$pod" -- \
sh -c 'read -r BAO_TOKEN; export BAO_TOKEN; exec bao "$@"' sh "$@"
}
write_policy() {
local token="$1"
local name="$2"
local file="$3"
if [ ! -f "$file" ]; then
echo "ERROR: missing policy file: $file" >&2
exit 1
fi
if [ "$DRY_RUN" -eq 1 ]; then
printf 'DRY-RUN: bao policy write %s %s\n' "$name" "$file"
return 0
fi
{ printf '%s\n' "$token"; cat "$file"; } | kubectl_exec exec -i -n "$OPENBAO_NAMESPACE" "$pod" -- \
sh -c 'read -r BAO_TOKEN; export BAO_TOKEN; bao policy write "$1" -' sh "$name"
}
ensure_default_issuer() {
local token="$1"
local issuers_out
if [ "$DRY_RUN" -eq 1 ]; then
printf 'DRY-RUN: bao read %s/config/issuers\n' "$SSH_MOUNT"
printf 'DRY-RUN: bao write %s/config/ca generate_signing_key=true key_type=ed25519\n' "$SSH_MOUNT"
return 0
fi
if issuers_out="$(remote_bao "$token" read "${SSH_MOUNT}/config/issuers" 2>&1)"; then
printf 'OK: default SSH issuer already configured.\n'
return 0
fi
case "$issuers_out" in
*"no default issuer"*)
remote_bao "$token" write "${SSH_MOUNT}/config/ca" \
generate_signing_key=true key_type=ed25519
printf 'OK: generated default SSH CA issuer.\n'
;;
*)
printf '%s\n' "$issuers_out" >&2
echo "ERROR: failed to read SSH issuer configuration" >&2
exit 1
;;
esac
}
enable_ssh_engine() {
local token="$1"
local output status
if output="$(remote_bao "$token" secrets enable -path="$SSH_MOUNT" ssh 2>&1)"; then
printf '%s\n' "$output"
return 0
fi
status=$?
case "$output" in
*"path is already in use"*)
printf 'OK: %s/ SSH secrets engine is already enabled.\n' "$SSH_MOUNT"
return 0
;;
*)
printf '%s\n' "$output" >&2
echo "ERROR: failed to enable SSH engine (exit $status)" >&2
exit 1
;;
esac
}
apply_roles() {
local token="$1"
if [ ! -f "$ROLES_SPEC" ]; then
echo "ERROR: roles spec not found: $ROLES_SPEC" >&2
exit 1
fi
if [ "$DRY_RUN" -eq 1 ]; then
python3 - "$ROLES_SPEC" "$SSH_MOUNT" <<'PY'
import sys, yaml
spec = yaml.safe_load(open(sys.argv[1]))
mount = sys.argv[2]
for name, params in (spec.get("roles") or {}).items():
args = " ".join(f"{k}={v}" for k, v in params.items())
print(f"DRY-RUN: bao write {mount}/roles/{name} {args}")
PY
return 0
fi
python3 - "$token" "$ROLES_SPEC" "$SSH_MOUNT" "$OPENBAO_NAMESPACE" "$pod" "$KUBECTL" <<'PY'
import shlex
import subprocess
import sys
token, spec_path, mount, namespace, pod, kubectl = sys.argv[1:7]
import yaml
kubectl_parts = shlex.split(kubectl) if kubectl else ["kubectl"]
spec = yaml.safe_load(open(spec_path))
roles = spec.get("roles") or {}
for role_name, params in roles.items():
args = [f"{k}={v}" for k, v in params.items()]
cmd = ["bao", "write", f"{mount}/roles/{role_name}"] + args
proc = subprocess.run(
kubectl_parts + ["exec", "-i", "-n", namespace, pod, "--", "sh", "-c",
"read -r BAO_TOKEN; export BAO_TOKEN; exec bao \"$@\"", "sh"] + cmd,
input=(token + "\n").encode(),
capture_output=True,
)
if proc.returncode != 0:
sys.stderr.write(proc.stderr.decode())
raise SystemExit(f"failed to write role {role_name}")
print(f"OK: {mount}/roles/{role_name}")
PY
}
export_ca_pubkey() {
local token="$1"
if [ "$DRY_RUN" -eq 1 ]; then
printf 'DRY-RUN: bao read -field=public_key %s/public_key\n' "$SSH_MOUNT"
return 0
fi
local pubkey
pubkey="$(remote_bao "$token" read -field=public_key "${SSH_MOUNT}/config/ca" 2>/dev/null || true)"
if [ -z "$pubkey" ]; then
pubkey="$(remote_bao "$token" read -field=public_key "${SSH_MOUNT}/public_key" 2>/dev/null || true)"
fi
if [ -z "$pubkey" ]; then
warn "Could not read SSH CA public key from ${SSH_MOUNT}/config/ca or ${SSH_MOUNT}/public_key"
return 0
fi
local fingerprint
fingerprint="$(printf '%s' "$pubkey" | sha256sum | awk '{print $1}')"
printf 'OK: SSH CA public key fingerprint sha256:%s\n' "$fingerprint"
if [ -n "$CA_PUBKEY_OUT" ]; then
mkdir -p "$(dirname "$CA_PUBKEY_OUT")"
printf '%s\n' "$pubkey" >"$CA_PUBKEY_OUT"
chmod 644 "$CA_PUBKEY_OUT"
printf 'OK: wrote CA pubkey to %s\n' "$CA_PUBKEY_OUT"
fi
if [ -n "$K8S_CA_SECRET" ]; then
local tmp
tmp="$(mktemp)"
printf '%s\n' "$pubkey" >"$tmp"
kubectl_exec create secret generic "$K8S_CA_SECRET" \
--namespace "$OPENBAO_NAMESPACE" \
--from-file=ca_user.pub="$tmp" \
--dry-run=client -o yaml | kubectl_exec apply -f -
rm -f "$tmp"
printf 'OK: K8s secret %s/%s updated\n' "$OPENBAO_NAMESPACE" "$K8S_CA_SECRET"
fi
}
token="$(read_token)"
if [ -z "$token" ]; then
echo "ERROR: empty token" >&2
exit 1
fi
remote_bao "$token" status
enable_ssh_engine "$token"
ensure_default_issuer "$token"
apply_roles "$token"
write_policy "$token" warden-sign "$POLICY_DIR/warden-sign.hcl"
remote_bao "$token" list "${SSH_MOUNT}/roles"
export_ca_pubkey "$token"
cat <<NEXT
OpenBao SSH engine configuration applied.
Next steps:
1. make openbao-verify-ssh
2. railiance-infra: make bootstrap-ssh-ca SSH_CA_PUBKEY=<path>
3. ops-warden: warden sign smoke (WP-0008 T2)
NEXT
if [ "$WARNINGS" -gt 0 ]; then
printf '\nCompleted with %s warning(s).\n' "$WARNINGS"
fi

View File

@@ -0,0 +1,219 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import getpass
import shlex
import subprocess
import sys
from pathlib import Path
from typing import Any
import yaml
REPO_DIR = Path(__file__).resolve().parents[1]
class BaoRunner:
def __init__(
self,
*,
kubectl: str,
namespace: str,
release: str,
dry_run: bool,
use_token_helper: bool,
token: str | None,
) -> None:
self.kubectl_parts = shlex.split(kubectl)
self.namespace = namespace
self.pod = f"{release}-0"
self.dry_run = dry_run
self.use_token_helper = use_token_helper
self.token = token
def run(
self, args: list[str], input_text: str | None = None
) -> subprocess.CompletedProcess[str]:
rendered = "bao " + shlex.join(args)
if self.dry_run:
print(f"DRY-RUN: {rendered}")
return subprocess.CompletedProcess(args, 0, "", "")
if self.use_token_helper:
cmd = (
self.kubectl_parts
+ ["exec", "-i", "-n", self.namespace, self.pod, "--", "bao"]
+ args
)
proc_input = input_text
else:
if not self.token:
raise RuntimeError(
"OpenBao token is required unless --use-token-helper is set"
)
cmd = (
self.kubectl_parts
+ [
"exec",
"-i",
"-n",
self.namespace,
self.pod,
"--",
"sh",
"-c",
'read -r BAO_TOKEN; export BAO_TOKEN; export VAULT_TOKEN="$BAO_TOKEN"; exec bao "$@"',
"sh",
]
+ args
)
proc_input = self.token + "\n" + (input_text or "")
result = subprocess.run(cmd, input=proc_input, capture_output=True, text=True)
if result.stdout:
print(result.stdout, end="")
if result.returncode != 0:
if result.stderr:
print(result.stderr, file=sys.stderr, end="")
raise SystemExit(result.returncode)
if result.stderr:
print(result.stderr, file=sys.stderr, end="")
return result
def read_token(
token_file: str | None, dry_run: bool, use_token_helper: bool
) -> str | None:
if dry_run or use_token_helper:
return None
if token_file:
path = Path(token_file)
if not path.exists():
raise SystemExit(f"ERROR: OPENBAO_TOKEN_FILE does not exist: {path}")
lines = path.read_text(encoding="utf-8").splitlines()
token = lines[0].strip() if lines else ""
if not token:
raise SystemExit(f"ERROR: OPENBAO_TOKEN_FILE is empty: {path}")
return token
token = getpass.getpass("OpenBao token: ")
if not token:
raise SystemExit("ERROR: empty OpenBao token")
return token
def load_catalog(path: Path) -> dict[str, Any]:
with path.open("r", encoding="utf-8") as handle:
data = yaml.safe_load(handle)
if not isinstance(data, dict):
raise SystemExit(f"ERROR: catalog root must be an object: {path}")
return data
def selected_grants(
catalog: dict[str, Any], grant_id: str | None
) -> list[dict[str, Any]]:
grants = catalog.get("grants") or []
if not isinstance(grants, list):
raise SystemExit("ERROR: catalog grants must be a list")
selected = [
grant for grant in grants if not grant_id or grant.get("id") == grant_id
]
if grant_id and not selected:
raise SystemExit(f"ERROR: grant not found in catalog: {grant_id}")
return selected
def role_args(grant: dict[str, Any]) -> list[str]:
openbao = grant["openbao"]
ttl = grant["ttl"]
policies = ",".join(openbao["policies"])
disallowed = ",".join(openbao.get("disallowed_policies") or [])
args = [
"write",
f"auth/token/roles/{openbao['token_role']}",
f"allowed_policies={policies}",
f"disallowed_policies={disallowed}",
"orphan=true",
"renewable=false",
f"token_explicit_max_ttl={ttl['max']}",
"token_no_default_policy=true",
"token_type=service",
]
return args
def write_policy(runner: BaoRunner, name: str, policy_file: Path) -> None:
if not policy_file.exists():
raise SystemExit(f"ERROR: missing policy file: {policy_file}")
if runner.dry_run:
print(f"DRY-RUN: bao policy write {name} {policy_file}")
return
runner.run(
["policy", "write", name, "-"],
input_text=policy_file.read_text(encoding="utf-8"),
)
print(f"OK: policy {name} applied")
def apply_grant(runner: BaoRunner, grant: dict[str, Any], policy_dir: Path) -> None:
if grant.get("credential_type") != "openbao-token":
print(f"SKIP: {grant.get('id')} is not an openbao-token grant")
return
openbao = grant["openbao"]
issuer_policy = openbao["issuer_policy"]
policy_file = policy_dir / f"{issuer_policy}.hcl"
write_policy(runner, issuer_policy, policy_file)
runner.run(role_args(grant))
runner.run(["read", f"auth/token/roles/{openbao['token_role']}"])
print(f"OK: token role {openbao['token_role']} configured for grant {grant['id']}")
def main() -> int:
parser = argparse.ArgumentParser(
description="Apply OpenBao token roles for credential grants."
)
parser.add_argument("--catalog", default="credential-grants/catalog.yaml")
parser.add_argument("--policy-dir", default="openbao/policies")
parser.add_argument("--grant", help="Limit to one grant id")
parser.add_argument("--dry-run", action="store_true")
parser.add_argument(
"--use-token-helper",
action="store_true",
help="Use the OpenBao CLI token helper inside the pod",
)
parser.add_argument("--namespace", default=None)
parser.add_argument("--release", default=None)
parser.add_argument("--kubectl", default=None)
args = parser.parse_args()
import os
namespace = args.namespace or os.environ.get("OPENBAO_NAMESPACE", "openbao")
release = args.release or os.environ.get("OPENBAO_RELEASE", "openbao")
kubectl = args.kubectl or os.environ.get("KUBECTL", "kubectl")
token_file = os.environ.get("OPENBAO_TOKEN_FILE")
token = read_token(token_file, args.dry_run, args.use_token_helper)
catalog = load_catalog(REPO_DIR / args.catalog)
runner = BaoRunner(
kubectl=kubectl,
namespace=namespace,
release=release,
dry_run=args.dry_run,
use_token_helper=args.use_token_helper,
token=token,
)
if not args.dry_run:
runner.run(["status"])
policy_dir = REPO_DIR / args.policy_dir
for grant in selected_grants(catalog, args.grant):
apply_grant(runner, grant, policy_dir)
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,137 @@
#!/usr/bin/env bash
set -euo pipefail
OPENBAO_NAMESPACE="${OPENBAO_NAMESPACE:-openbao}"
OPENBAO_RELEASE="${OPENBAO_RELEASE:-openbao}"
KUBECTL="${KUBECTL:-kubectl}"
TOKEN_FILE="${OPENBAO_TOKEN_FILE:-}"
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
POLICY_NAME="${WORKLOAD_KV_POLICY_NAME:-workload-kv-read-whynot-design-npm-publish}"
POLICY_FILE="${WORKLOAD_KV_POLICY_FILE:-$REPO_DIR/openbao/policies/workload-kv-read-whynot-design-npm-publish.hcl}"
DRY_RUN=0
USE_TOKEN_HELPER=0
usage() {
cat <<'USAGE'
Usage: scripts/openbao-apply-workload-kv-lanes.sh [--dry-run] [--use-token-helper]
Applies source-owned OpenBao workload KV read-lane policies.
Current lane:
- policy: workload-kv-read-whynot-design-npm-publish
- path: platform/workloads/coulomb/whynot-design/npm-publish
- field: NPM_AUTH_TOKEN
The script reads an OpenBao operator token from OPENBAO_TOKEN_FILE or an
interactive hidden prompt unless --dry-run or --use-token-helper is set. It
never prints or stores the token.
This script intentionally does not create an OIDC role until the whynot-design
KeyCape/NetKingdom bound claim is confirmed.
USAGE
}
while [ "$#" -gt 0 ]; do
case "$1" in
--dry-run)
DRY_RUN=1
shift
;;
--use-token-helper)
USE_TOKEN_HELPER=1
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "ERROR: unknown argument: $1" >&2
usage >&2
exit 2
;;
esac
done
pod="${OPENBAO_RELEASE}-0"
read_token() {
if [ "$DRY_RUN" -eq 1 ] || [ "$USE_TOKEN_HELPER" -eq 1 ]; then
return
fi
if [ -n "$TOKEN_FILE" ]; then
if [ ! -f "$TOKEN_FILE" ]; then
echo "ERROR: OPENBAO_TOKEN_FILE does not exist: $TOKEN_FILE" >&2
exit 1
fi
head -n 1 "$TOKEN_FILE"
return
fi
local token
read -r -s -p "OpenBao token: " token
printf '\n' >&2
printf '%s\n' "$token"
}
remote_bao() {
local token="$1"
shift
if [ "$DRY_RUN" -eq 1 ]; then
printf 'DRY-RUN: bao %s\n' "$*"
return 0
fi
if [ "$USE_TOKEN_HELPER" -eq 1 ]; then
# shellcheck disable=SC2086
$KUBECTL exec -i -n "$OPENBAO_NAMESPACE" "$pod" -- bao "$@"
return
fi
# shellcheck disable=SC2086
printf '%s\n' "$token" | $KUBECTL exec -i -n "$OPENBAO_NAMESPACE" "$pod" -- \
sh -c 'read -r BAO_TOKEN; export BAO_TOKEN; exec bao "$@"' sh "$@"
}
write_policy() {
local token="$1"
if [ ! -f "$POLICY_FILE" ]; then
echo "ERROR: missing policy file: $POLICY_FILE" >&2
exit 1
fi
if [ "$DRY_RUN" -eq 1 ]; then
printf 'DRY-RUN: bao policy write %s %s\n' "$POLICY_NAME" "$POLICY_FILE"
return 0
fi
if [ "$USE_TOKEN_HELPER" -eq 1 ]; then
# shellcheck disable=SC2086
cat "$POLICY_FILE" | $KUBECTL exec -i -n "$OPENBAO_NAMESPACE" "$pod" -- \
bao policy write "$POLICY_NAME" -
return
fi
# shellcheck disable=SC2086
{ printf '%s\n' "$token"; cat "$POLICY_FILE"; } | \
$KUBECTL exec -i -n "$OPENBAO_NAMESPACE" "$pod" -- \
sh -c 'read -r BAO_TOKEN; export BAO_TOKEN; bao policy write "$1" -' sh "$POLICY_NAME"
}
token="$(read_token)"
if [ "$DRY_RUN" -eq 0 ] && [ "$USE_TOKEN_HELPER" -eq 0 ] && [ -z "$token" ]; then
echo "ERROR: empty OpenBao token" >&2
exit 1
fi
remote_bao "$token" status
write_policy "$token"
remote_bao "$token" policy read "$POLICY_NAME"
cat <<'NEXT'
Workload KV read-lane policy apply path completed.
Remaining live steps:
1. Confirm the whynot-design KeyCape/NetKingdom bound claim or service account.
2. Create auth/netkingdom/role/whynot-design-workload-kv-read with only the
workload-kv-read-whynot-design-npm-publish policy.
3. Provision platform/workloads/coulomb/whynot-design/npm-publish with
field NPM_AUTH_TOKEN through approved OpenBao/operator custody.
4. Run positive and negative fetch verification without printing the token.
NEXT

View File

@@ -0,0 +1,49 @@
#!/usr/bin/env bash
set -euo pipefail
OPENBAO_NAMESPACE="${OPENBAO_NAMESPACE:-openbao}"
OPENBAO_RELEASE="${OPENBAO_RELEASE:-openbao}"
KUBECTL="${KUBECTL:-kubectl}"
TOKEN_FILE="${OPENBAO_TOKEN_FILE:-}"
MOUNTS="${OPENBAO_AUTH_LISTING_MOUNTS:-netkingdom keycape}"
usage() {
cat <<'USAGE'
Usage: scripts/openbao-tune-auth-listing.sh
Sets listing_visibility=unauth on configured OIDC auth mounts so the OpenBao
browser UI can discover netkingdom without falling back to token auth.
Environment:
OPENBAO_TOKEN_FILE Token file with platform-admin or root token
OPENBAO_AUTH_LISTING_MOUNTS Space-separated mount paths. Default: netkingdom keycape
USAGE
}
read_token() {
if [ -n "$TOKEN_FILE" ]; then
head -n 1 "$TOKEN_FILE"
return
fi
local token
read -r -s -p "OpenBao token: " token
printf '\n' >&2
printf '%s\n' "$token"
}
if [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ]; then
usage
exit 0
fi
pod="${OPENBAO_RELEASE}-0"
token="$(read_token)"
for mount in $MOUNTS; do
printf '%s\n' "$token" | $KUBECTL exec -i -n "$OPENBAO_NAMESPACE" "$pod" -- \
bao write "sys/auth/${mount}/tune" listing_visibility=unauth
printf '[OK] auth/%s listing_visibility=unauth\n' "$mount"
done
printf '\nVerify unauthenticated UI mount listing:\n'
curl -fsS "https://bao.coulomb.social/v1/sys/internal/ui/mounts" | python3 -m json.tool

View File

@@ -0,0 +1,74 @@
#!/usr/bin/env bash
set -euo pipefail
OPENBAO_NAMESPACE="${OPENBAO_NAMESPACE:-openbao}"
KUBECTL="${KUBECTL:-kubectl}"
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
OVERLAY_DIR="${OPENBAO_UI_OVERLAY_DIR:-$ROOT_DIR/helm/openbao-ui-overlay}"
K8S_MANIFEST="${OPENBAO_UI_OVERLAY_K8S:-$ROOT_DIR/helm/openbao-ui-overlay-k8s.yaml}"
usage() {
cat <<'USAGE'
Usage: scripts/openbao-ui-overlay-apply.sh
Builds and applies the OpenBao KeyCape login overlay ConfigMaps and gateway
Deployment/Service/Ingress. Idempotent — safe to run on every openbao-deploy.
Environment:
OPENBAO_NAMESPACE Kubernetes namespace. Default: openbao
KUBECTL kubectl command, including --kubeconfig if needed
OPENBAO_UI_OVERLAY_DIR Overlay asset directory
OPENBAO_UI_OVERLAY_K8S Gateway manifest path
USAGE
}
if [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ]; then
usage
exit 0
fi
for required in overlay.css overlay.js callback.html callback.js login.css login.html login.js presets.json nginx.conf VERSION; do
if [ ! -f "$OVERLAY_DIR/$required" ]; then
echo "missing overlay asset: $OVERLAY_DIR/$required" >&2
exit 1
fi
done
if [ ! -f "$K8S_MANIFEST" ]; then
echo "missing gateway manifest: $K8S_MANIFEST" >&2
exit 1
fi
# shellcheck disable=SC2086
$KUBECTL create namespace "$OPENBAO_NAMESPACE" --dry-run=client -o yaml | $KUBECTL apply -f -
# shellcheck disable=SC2086
$KUBECTL create configmap openbao-ui-overlay \
--namespace "$OPENBAO_NAMESPACE" \
--from-file="$OVERLAY_DIR/overlay.css" \
--from-file="$OVERLAY_DIR/overlay.js" \
--from-file="$OVERLAY_DIR/callback.html" \
--from-file="$OVERLAY_DIR/callback.js" \
--from-file="$OVERLAY_DIR/login.css" \
--from-file="$OVERLAY_DIR/login.html" \
--from-file="$OVERLAY_DIR/login.js" \
--from-file="$OVERLAY_DIR/presets.json" \
--from-file="$OVERLAY_DIR/VERSION" \
--dry-run=client -o yaml | $KUBECTL apply -f -
# shellcheck disable=SC2086
$KUBECTL create configmap openbao-ui-gateway-nginx \
--namespace "$OPENBAO_NAMESPACE" \
--from-file=nginx.conf="$OVERLAY_DIR/nginx.conf" \
--dry-run=client -o yaml | $KUBECTL apply -f -
# shellcheck disable=SC2086
$KUBECTL apply -f "$K8S_MANIFEST"
# shellcheck disable=SC2086
$KUBECTL rollout restart deployment/openbao-ui-gateway -n "$OPENBAO_NAMESPACE"
# shellcheck disable=SC2086
$KUBECTL rollout status deployment/openbao-ui-gateway -n "$OPENBAO_NAMESPACE" --timeout=120s
printf '[OK] OpenBao UI overlay applied from %s\n' "$OVERLAY_DIR"

View File

@@ -15,7 +15,7 @@ Usage: scripts/openbao-verify-authenticated.sh [--dry-run] [--use-token-helper]
Runs authenticated, non-mutating OpenBao readiness checks:
- audit list includes file/
- secrets list includes platform/
- auth list includes kubernetes/ and keycape/
- auth list includes kubernetes/, netkingdom/, and keycape/
- audit log exists and is non-empty
The token is read from OPENBAO_TOKEN_FILE or an interactive hidden prompt. The
@@ -130,6 +130,7 @@ Path Type
---- ----
keycape/ oidc
kubernetes/ kubernetes
netkingdom/ oidc
token/ token
AUTH
;;
@@ -210,6 +211,7 @@ step "Auth methods"
if auth_output="$(remote_bao "$token" auth list 2>&1)"; then
printf '%s\n' "$auth_output"
require_pattern "kubernetes/ auth method is visible" "$auth_output" '(^|[[:space:]])kubernetes/'
require_pattern "netkingdom/ auth method is visible" "$auth_output" '(^|[[:space:]])netkingdom/'
require_pattern "keycape/ auth method is visible" "$auth_output" '(^|[[:space:]])keycape/'
else
printf '%s\n' "$auth_output" >&2

View File

@@ -0,0 +1,173 @@
#!/usr/bin/env bash
set -euo pipefail
BASE_URL="${OPENBAO_UI_BASE_URL:-https://bao.coulomb.social}"
OVERLAY_DIR="${OPENBAO_UI_OVERLAY_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/helm/openbao-ui-overlay}"
CHECK_DRIFT="${CHECK_UPSTREAM_DRIFT:-0}"
ok() { printf '[OK] %s\n' "$*"; }
err() { printf '[ERR] %s\n' "$*" >&2; }
step() { printf '\n==> %s\n' "$*"; }
usage() {
cat <<'USAGE'
Usage: scripts/openbao-verify-login-overlay.sh [--check-upstream-drift]
Verifies the public OpenBao UI serves the KeyCape login overlay assets and
that index.html injection is present.
Environment:
OPENBAO_UI_BASE_URL Public UI base URL. Default: https://bao.coulomb.social
OPENBAO_UI_OVERLAY_DIR Local overlay directory for drift fingerprints
CHECK_UPSTREAM_DRIFT Set to 1 to compare live UI hashes with patches/
USAGE
}
while [ "$#" -gt 0 ]; do
case "$1" in
--check-upstream-drift)
CHECK_DRIFT=1
shift
;;
-h|--help)
usage
exit 0
;;
*)
err "unknown argument: $1"
usage >&2
exit 2
;;
esac
done
require_pattern() {
local label="$1"
local haystack="$2"
local pattern="$3"
if ! grep -Eq "$pattern" <<<"$haystack"; then
err "$label"
return 1
fi
ok "$label"
}
step "Standalone login page"
auth_html="$(curl -fsS "$BASE_URL/ui/vault/auth")"
require_pattern \
"auth page serves standalone KeyCape login" \
"$auth_html" \
'id="login-submit"|Sign in with KeyCape'
if grep -Eq 'vault-|engines-dist' <<<"$auth_html"; then
err "auth page still serves Ember shell (expected standalone login.html)"
exit 1
fi
ok "auth page is standalone login.html (no Ember shell)"
callback_html="$(curl -fsS "$BASE_URL/ui/vault/auth/netkingdom/oidc/callback")"
require_pattern \
"OIDC callback serves standalone handler" \
"$callback_html" \
'Signing in with KeyCape|callback.js'
if grep -Eq 'window\.opener\.postMessage|vault-' <<<"$callback_html"; then
err "OIDC callback still serves Ember shell (expected standalone callback.html)"
exit 1
fi
ok "OIDC callback is standalone callback.html (no Ember postMessage flow)"
step "Overlay asset endpoints"
index_html="$(curl -fsS "$BASE_URL/ui/")"
overlay_js="$(curl -fsS "$BASE_URL/ui/platform-overlay/overlay.js")"
overlay_css="$(curl -fsS "$BASE_URL/ui/platform-overlay/overlay.css")"
presets_json="$(curl -fsS "$BASE_URL/ui/platform-overlay/presets.json")"
require_pattern \
"index.html injects overlay.js" \
"$index_html" \
'/ui/platform-overlay/overlay\.js'
require_pattern \
"index.html injects overlay.css" \
"$index_html" \
'/ui/platform-overlay/overlay\.css'
require_pattern \
"overlay.js activates KeyCape overlay" \
"$overlay_js" \
'keycape-overlay-active'
require_pattern \
"overlay.js starts direct KeyCape OIDC redirect" \
"$overlay_js" \
'oidc/auth_url'
require_pattern \
"presets.json targets netkingdom mount" \
"$presets_json" \
'"mount"[[:space:]]*:[[:space:]]*"netkingdom"'
require_pattern \
"presets.json targets platform-admin role" \
"$presets_json" \
'"role"[[:space:]]*:[[:space:]]*"platform-admin"'
require_pattern \
"overlay.css hides namespace picker" \
"$overlay_css" \
'toolbar-namespace-picker'
require_pattern \
"overlay branding title present in presets" \
"$presets_json" \
'Sign in with KeyCape'
step "Hidden-field selectors still present in overlay.js"
require_pattern \
"overlay.js hides namespace input" \
"$overlay_js" \
'#namespace|input\[name="namespace"\]'
require_pattern \
"overlay.js hides role input" \
"$overlay_js" \
'#role|input\[name="role"\]'
require_pattern \
"overlay.js hides mount path input" \
"$overlay_js" \
'#custom-path|input\[name="custom-path"\]'
if [ "$CHECK_DRIFT" = "1" ]; then
step "Upstream UI drift check"
version_file="$OVERLAY_DIR/VERSION"
if [ ! -f "$version_file" ]; then
err "missing overlay VERSION file: $version_file"
exit 1
fi
version="$(tr -d '[:space:]' < "$version_file")"
manifest="$OVERLAY_DIR/patches/$version/manifest.sha256"
if [ ! -f "$manifest" ]; then
err "missing fingerprint manifest: $manifest"
exit 1
fi
vault_asset="$(grep -Eo '/ui/assets/vault-[a-f0-9]+\.js' <<<"$index_html" | head -1 || true)"
if [ -z "$vault_asset" ]; then
err "could not locate vault.js asset path in index.html"
exit 1
fi
live_vault_hash="$(curl -fsS "$BASE_URL$vault_asset" | sha256sum | awk '{print $1}')"
expected_vault_hash="$(awk '!/^#/ && /ui\/assets\/vault-/ {print $1; exit}' "$manifest")"
expected_vault_path="$(awk '!/^#/ && /ui\/assets\/vault-/ {print $2; exit}' "$manifest")"
if [ -n "$expected_vault_hash" ] && [ "$live_vault_hash" != "$expected_vault_hash" ]; then
err "vault bundle hash drift for ${vault_asset:-unknown}: expected $expected_vault_hash got $live_vault_hash"
exit 1
fi
ok "vault bundle hash matches patches/$version/manifest.sha256 (${expected_vault_path:-$vault_asset})"
fi
printf '\nOpenBao login overlay verification passed for %s\n' "$BASE_URL"

View File

@@ -0,0 +1,111 @@
#!/usr/bin/env bash
set -euo pipefail
OPENBAO_NAMESPACE="${OPENBAO_NAMESPACE:-openbao}"
OPENBAO_RELEASE="${OPENBAO_RELEASE:-openbao}"
KUBECTL="${KUBECTL:-kubectl}"
TOKEN_FILE="${OPENBAO_TOKEN_FILE:-}"
SSH_MOUNT="${OPENBAO_SSH_MOUNT:-ssh}"
EXPECTED_ROLES="${OPENBAO_SSH_EXPECTED_ROLES:-adm-role agt-role atm-role}"
USE_TOKEN_HELPER=0
DRY_RUN=0
usage() {
cat <<'USAGE'
Usage: scripts/openbao-verify-ssh-engine.sh [--dry-run] [--use-token-helper]
Non-mutating checks: ssh/ mount present, expected roles listed, warden-sign policy.
USAGE
}
while [ "$#" -gt 0 ]; do
case "$1" in
--dry-run) DRY_RUN=1; shift ;;
--use-token-helper) USE_TOKEN_HELPER=1; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "ERROR: unknown argument: $1" >&2; usage >&2; exit 2 ;;
esac
done
pod="${OPENBAO_RELEASE}-0"
FAILURES=0
fail() { FAILURES=$((FAILURES + 1)); printf '[FAIL] %s\n' "$*" >&2; }
ok() { printf '[OK] %s\n' "$*"; }
read_token() {
if [ "$USE_TOKEN_HELPER" -eq 1 ]; then
printf '__USE_TOKEN_HELPER__\n'
return
fi
if [ "$DRY_RUN" -eq 1 ]; then
printf 'dry-run-token\n'
return
fi
if [ -n "$TOKEN_FILE" ] && [ -f "$TOKEN_FILE" ]; then
head -n 1 "$TOKEN_FILE"
return
fi
local token
read -r -s -p "OpenBao token: " token
printf '\n' >&2
printf '%s\n' "$token"
}
remote_bao() {
local token="$1"
shift
if [ "$token" = "__USE_TOKEN_HELPER__" ]; then
$KUBECTL exec -n "$OPENBAO_NAMESPACE" "$pod" -- bao "$@"
return
fi
if [ "$DRY_RUN" -eq 1 ]; then
printf 'DRY-RUN: bao %s\n' "$*"
return 0
fi
printf '%s\n' "$token" | $KUBECTL exec -i -n "$OPENBAO_NAMESPACE" "$pod" -- \
sh -c 'read -r BAO_TOKEN; export BAO_TOKEN; exec bao "$@"' sh "$@"
}
token="$(read_token)"
secrets_out="$(remote_bao "$token" secrets list 2>&1)" || {
fail "secrets list failed: $secrets_out"
exit 1
}
if printf '%s\n' "$secrets_out" | grep -Eq "(^|[[:space:]])${SSH_MOUNT}/"; then
ok "SSH mount ${SSH_MOUNT}/ is enabled"
else
fail "SSH mount ${SSH_MOUNT}/ not found in secrets list"
fi
roles_out="$(remote_bao "$token" list "${SSH_MOUNT}/roles" 2>&1)" || {
fail "list ${SSH_MOUNT}/roles failed: $roles_out"
exit 1
}
for role in $EXPECTED_ROLES; do
if printf '%s\n' "$roles_out" | grep -q "$role"; then
ok "role ${role} exists"
else
fail "role ${role} missing"
fi
done
policy_out="$(remote_bao "$token" policy list 2>&1)" || {
fail "policy list failed: $policy_out"
exit 1
}
if printf '%s\n' "$policy_out" | grep -q 'warden-sign'; then
ok "policy warden-sign present"
else
fail "policy warden-sign missing"
fi
if [ "$FAILURES" -gt 0 ]; then
printf '\nSSH engine verification failed (%s failure(s)).\n' "$FAILURES" >&2
exit 1
fi
printf '\nSSH engine verification passed.\n'

View File

@@ -0,0 +1,313 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import getpass
import json
import shlex
import subprocess
import sys
import tempfile
from pathlib import Path
from typing import Any
import yaml
REPO_DIR = Path(__file__).resolve().parents[1]
class BaoRunner:
def __init__(
self,
*,
kubectl: str,
namespace: str,
release: str,
dry_run: bool,
use_token_helper: bool,
token: str | None,
) -> None:
self.kubectl_parts = shlex.split(kubectl)
self.namespace = namespace
self.pod = f"{release}-0"
self.dry_run = dry_run
self.use_token_helper = use_token_helper
self.token = token
def run(
self,
args: list[str],
*,
input_text: str | None = None,
check: bool = True,
quiet: bool = False,
) -> subprocess.CompletedProcess[str]:
if self.dry_run:
print("DRY-RUN: bao " + shlex.join(args))
return subprocess.CompletedProcess(args, 0, "", "")
if self.use_token_helper:
cmd = (
self.kubectl_parts
+ ["exec", "-i", "-n", self.namespace, self.pod, "--", "bao"]
+ args
)
proc_input = input_text
else:
if not self.token:
raise RuntimeError(
"OpenBao token is required unless --use-token-helper is set"
)
cmd = (
self.kubectl_parts
+ [
"exec",
"-i",
"-n",
self.namespace,
self.pod,
"--",
"sh",
"-c",
'read -r BAO_TOKEN; export BAO_TOKEN; export VAULT_TOKEN="$BAO_TOKEN"; exec bao "$@"',
"sh",
]
+ args
)
proc_input = self.token + "\n" + (input_text or "")
result = subprocess.run(cmd, input=proc_input, capture_output=True, text=True)
if check and result.returncode != 0:
if result.stdout and not quiet:
print(result.stdout, end="")
if result.stderr:
print(result.stderr, file=sys.stderr, end="")
raise SystemExit(result.returncode)
if not quiet and result.stdout:
print(result.stdout, end="")
if not quiet and result.stderr:
print(result.stderr, file=sys.stderr, end="")
return result
def run_with_token(
*,
kubectl: str,
namespace: str,
release: str,
token: str,
args: list[str],
check: bool,
) -> subprocess.CompletedProcess[str]:
kubectl_parts = shlex.split(kubectl)
cmd = (
kubectl_parts
+ [
"exec",
"-i",
"-n",
namespace,
f"{release}-0",
"--",
"sh",
"-c",
'read -r BAO_TOKEN; export BAO_TOKEN; export VAULT_TOKEN="$BAO_TOKEN"; exec bao "$@"',
"sh",
]
+ args
)
return subprocess.run(
cmd, input=token + "\n", capture_output=True, text=True, check=False
)
def read_token(
token_file: str | None, dry_run: bool, use_token_helper: bool
) -> str | None:
if dry_run or use_token_helper:
return None
if token_file:
path = Path(token_file).expanduser()
if not path.exists():
raise SystemExit(f"ERROR: OPENBAO_TOKEN_FILE does not exist: {path}")
lines = path.read_text(encoding="utf-8").splitlines()
token = lines[0].strip() if lines else ""
if not token:
raise SystemExit(f"ERROR: OPENBAO_TOKEN_FILE is empty: {path}")
return token
token = getpass.getpass("OpenBao token: ")
if not token:
raise SystemExit("ERROR: empty OpenBao token")
return token
def load_catalog(path: Path) -> dict[str, Any]:
with path.open("r", encoding="utf-8") as handle:
data = yaml.safe_load(handle)
if not isinstance(data, dict):
raise SystemExit(f"ERROR: catalog root must be an object: {path}")
return data
def selected_grants(
catalog: dict[str, Any], grant_id: str | None
) -> list[dict[str, Any]]:
grants = catalog.get("grants") or []
if not isinstance(grants, list):
raise SystemExit("ERROR: catalog grants must be a list")
selected = [
grant for grant in grants if not grant_id or grant.get("id") == grant_id
]
if grant_id and not selected:
raise SystemExit(f"ERROR: grant not found in catalog: {grant_id}")
return selected
def verify_static(runner: BaoRunner, grant: dict[str, Any]) -> None:
openbao = grant["openbao"]
runner.run(["read", f"auth/token/roles/{openbao['token_role']}"])
runner.run(["policy", "read", openbao["issuer_policy"]])
runner.run(["policy", "read", openbao["policies"][0]])
print(f"OK: static token-grant config readable for {grant['id']}")
def issue_smoke_token(
runner: BaoRunner,
*,
kubectl: str,
namespace: str,
release: str,
grant: dict[str, Any],
) -> None:
openbao = grant["openbao"]
ttl = grant["ttl"]["default"]
args = [
"token",
"create",
f"-role={openbao['token_role']}",
f"-ttl={ttl}",
"-format=json",
]
for policy in openbao["policies"]:
args.append(f"-policy={policy}")
result = runner.run(args, quiet=True, check=False)
if result.returncode != 0:
raise SystemExit(
f"ERROR: token create failed (rc={result.returncode}): "
f"{(result.stderr or result.stdout or '').strip()}"
)
try:
payload = json.loads(result.stdout)
auth = payload.get("auth") or payload.get("data") or {}
child_token = auth["client_token"]
accessor = auth["accessor"]
except Exception as exc: # noqa: BLE001
raise SystemExit(
f"ERROR: could not parse token create response: {exc}"
) from exc
try:
with tempfile.TemporaryDirectory() as tmpdir:
key_path = Path(tmpdir) / "warden-sign-smoke_ed25519"
keygen = subprocess.run(
["ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-f", str(key_path)],
capture_output=True,
text=True,
check=False,
)
if keygen.returncode != 0:
raise SystemExit(
"ERROR: could not generate smoke SSH key: "
f"{(keygen.stderr or keygen.stdout).strip()}"
)
public_key = key_path.with_suffix(key_path.suffix + ".pub").read_text(encoding="utf-8").strip()
positive = run_with_token(
kubectl=kubectl,
namespace=namespace,
release=release,
token=child_token,
args=["write", "-field=signed_key", "ssh/sign/agt-role", f"public_key={public_key}"],
check=False,
)
if positive.returncode != 0 or not positive.stdout.strip():
raise SystemExit(
"ERROR: child token could not sign with ssh/sign/agt-role: "
f"{(positive.stderr or positive.stdout).strip()}"
)
print("OK: child token can sign with ssh/sign/agt-role")
negative = run_with_token(
kubectl=kubectl,
namespace=namespace,
release=release,
token=child_token,
args=["policy", "read", "warden-sign"],
check=False,
)
if negative.returncode == 0:
raise SystemExit("ERROR: child token unexpectedly read policy metadata")
print("OK: child token cannot read policy metadata")
finally:
runner.run(["write", "auth/token/revoke-accessor", f"accessor={accessor}"], quiet=True)
print("OK: smoke child token revoked by accessor")
def main() -> int:
parser = argparse.ArgumentParser(description="Verify OpenBao token grants.")
parser.add_argument("--catalog", default="credential-grants/catalog.yaml")
parser.add_argument("--grant", help="Limit to one grant id")
parser.add_argument("--dry-run", action="store_true")
parser.add_argument(
"--use-token-helper",
action="store_true",
help="Use the OpenBao CLI token helper inside the pod",
)
parser.add_argument(
"--issue-smoke-token",
action="store_true",
help="Mint and revoke a short-lived child token for live verification",
)
parser.add_argument("--namespace", default=None)
parser.add_argument("--release", default=None)
parser.add_argument("--kubectl", default=None)
args = parser.parse_args()
import os
namespace = args.namespace or os.environ.get("OPENBAO_NAMESPACE", "openbao")
release = args.release or os.environ.get("OPENBAO_RELEASE", "openbao")
kubectl = args.kubectl or os.environ.get("KUBECTL", "kubectl")
token_file = os.environ.get("OPENBAO_TOKEN_FILE")
token = read_token(token_file, args.dry_run, args.use_token_helper)
catalog = load_catalog(REPO_DIR / args.catalog)
runner = BaoRunner(
kubectl=kubectl,
namespace=namespace,
release=release,
dry_run=args.dry_run,
use_token_helper=args.use_token_helper,
token=token,
)
for grant in selected_grants(catalog, args.grant):
verify_static(runner, grant)
if args.issue_smoke_token:
if args.dry_run:
print(
f"DRY-RUN: would mint and revoke smoke child token for {grant['id']}"
)
else:
issue_smoke_token(
runner,
kubectl=kubectl,
namespace=namespace,
release=release,
grant=grant,
)
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,672 @@
from __future__ import annotations
import importlib.util
import os
import shutil
import sys
import tempfile
import unittest
from pathlib import Path
REPO_DIR = Path(__file__).resolve().parents[1]
SPEC = importlib.util.spec_from_file_location(
"credential_change", REPO_DIR / "scripts/credential-change.py"
)
credential_change = importlib.util.module_from_spec(SPEC)
assert SPEC.loader is not None
sys.modules[SPEC.name] = credential_change
SPEC.loader.exec_module(credential_change)
class CredentialChangeTests(unittest.TestCase):
def setUp(self) -> None:
self.sample = (
REPO_DIR
/ "credential-change-requests/CCR-2026-0001-whynot-design-npm-publish.yaml"
)
self.issue_core = (
REPO_DIR
/ "credential-change-requests/CCR-2026-0002-issue-core-ingestion-api-key.yaml"
)
def test_sample_ccr_validates_without_bound_claim_warning(self) -> None:
ccr, errors, warnings = credential_change.validate_ccr(self.sample)
self.assertEqual(errors, [])
self.assertEqual(warnings, [])
self.assertTrue(ccr["openbao"]["auth"]["bound_claims_confirmed"])
def test_issue_core_ccr_has_confirmed_eso_binding(self) -> None:
ccr, errors, warnings = credential_change.validate_ccr(self.issue_core)
self.assertEqual(errors, [])
self.assertEqual(warnings, [])
self.assertEqual(ccr["openbao"]["auth"]["role"], "external-secrets-issue-core")
def test_all_repo_ccrs_validate(self) -> None:
for path in sorted((REPO_DIR / "credential-change-requests").glob("*.yaml")):
with self.subTest(path=path.name):
_ccr, errors, _warnings = credential_change.validate_ccr(path)
self.assertEqual(errors, [])
def test_render_summary_contains_review_fields(self) -> None:
ccr, _errors, warnings = credential_change.validate_ccr(self.sample)
rendered = credential_change.render_summary(ccr, warnings)
self.assertIn("whynot-design npm publish token lane", rendered)
self.assertIn("platform/workloads/coulomb/whynot-design/npm-publish", rendered)
self.assertIn("whynot-design-npm-publish", rendered)
self.assertIn("readiness: ready resolvable=True", rendered)
self.assertIn("approve | deny | needs_changes", rendered)
def test_status_payload_marks_active_ready_resolvable(self) -> None:
ccr, _errors, warnings = credential_change.validate_ccr(self.sample)
payload = credential_change.status_payload(ccr, warnings)
self.assertFalse(payload["apply_allowed"])
self.assertTrue(payload["apply_complete"])
self.assertTrue(payload["frontdoor_resolvable"])
self.assertEqual(payload["status"], "active")
self.assertEqual(payload["access_frontdoor"]["readiness"], "ready")
self.assertEqual(payload["access_frontdoor"]["catalog_id"], "whynot-design-npm-publish")
self.assertEqual(payload["apply_blockers"], [])
self.assertEqual(payload["frontdoor_blockers"], [])
self.assertEqual(payload["warnings"], [])
self.assertEqual(
payload["state_hub"]["decision_id"],
"e6381a56-6b04-4fd5-b2de-f3ef59cde888",
)
def test_state_hub_rationale_prefix_maps_to_ccr_status(self) -> None:
cases = {
"APPROVE: scoped path and binding are correct": "approved",
"DENY: wrong tenant": "denied",
"NEEDS_CHANGES: use a read-only token": "needs_changes",
"request changes: clarify service account": "needs_changes",
}
for rationale, expected in cases.items():
with self.subTest(rationale=rationale):
self.assertEqual(
credential_change.ccr_status_from_state_hub_rationale(rationale),
expected,
)
with self.assertRaises(SystemExit):
credential_change.ccr_status_from_state_hub_rationale("looks good")
def test_sync_state_hub_decision_updates_ccr_status(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
copied = Path(tmp) / self.sample.name
shutil.copy2(self.sample, copied)
copied_ccr = credential_change.load_yaml(copied)
copied_ccr["status"] = "proposed"
copied_ccr["access_frontdoor"]["readiness"] = "template"
copied_ccr["access_frontdoor"]["resolvable"] = False
copied_ccr["access_frontdoor"]["activation"] = "pending-review"
copied_ccr.setdefault("state_hub", {})[
"decision_id"
] = "250669d0-8475-4527-9624-cd072249f9a9"
credential_change.dump_yaml(copied, copied_ccr)
original = credential_change.state_hub_decision_status
try:
credential_change.state_hub_decision_status = lambda _ccr, _url: {
"id": "250669d0-8475-4527-9624-cd072249f9a9",
"status": "resolved",
"rationale": "APPROVE: scoped path and confirmed binding are acceptable",
"decided_by": "unit-test",
"decided_at": "2026-06-27T22:00:00Z",
}
credential_change.sync_state_hub_decision(copied, "http://state-hub.test")
finally:
credential_change.state_hub_decision_status = original
ccr, errors, warnings = credential_change.validate_ccr(copied)
self.assertEqual(errors, [])
self.assertEqual(warnings, [])
self.assertEqual(ccr["status"], "approved")
self.assertEqual(ccr["review"]["comments"][-1]["reviewer"], "unit-test")
self.assertIn("State Hub decision", ccr["review"]["comments"][-1]["comment"])
self.assertEqual(ccr["state_hub"]["decision_resolved_at"], "2026-06-27T22:00:00Z")
def test_kubernetes_auth_payload_uses_service_account_bounds(self) -> None:
ccr, errors, _warnings = credential_change.validate_ccr(self.issue_core)
self.assertEqual(errors, [])
payload = credential_change.auth_payload(ccr)
self.assertEqual(payload["bound_service_account_names"], ["external-secrets"])
self.assertEqual(payload["bound_service_account_namespaces"], ["external-secrets"])
self.assertNotIn("bound_claims", payload)
def test_oidc_auth_payload_includes_redirect_uris(self) -> None:
ccr, errors, _warnings = credential_change.validate_ccr(self.sample)
self.assertEqual(errors, [])
payload = credential_change.auth_payload(ccr)
self.assertEqual(
payload["allowed_redirect_uris"],
[
"https://bao.coulomb.social/ui/vault/auth/netkingdom/oidc/callback",
"http://localhost:8250/oidc/callback",
"http://127.0.0.1:8250/oidc/callback",
],
)
self.assertEqual(
payload["oidc_scopes"],
["openid", "profile", "email", "groups"],
)
def test_apply_plan_refuses_unapproved_ccr(self) -> None:
with self.assertRaises(SystemExit):
credential_change.command_apply_plan(type("Args", (), {"ref": str(self.issue_core)})())
def test_plan_includes_source_artifact_diff_status(self) -> None:
ccr, errors, _warnings = credential_change.validate_ccr(self.sample)
self.assertEqual(errors, [])
rendered = credential_change.render_plan(ccr)
self.assertIn("Source artifact diff:", rendered)
self.assertIn("artifact status: matches", rendered)
def test_decision_templates_prefill_review_context(self) -> None:
ccr, errors, _warnings = credential_change.validate_ccr(self.sample)
self.assertEqual(errors, [])
rendered = credential_change.render_decision_templates(ccr)
self.assertIn("APPROVE: CCR-2026-0001", rendered)
self.assertIn("DENY: CCR-2026-0001", rendered)
self.assertIn("NEEDS_CHANGES: CCR-2026-0001", rendered)
self.assertIn("platform/workloads/coulomb/whynot-design/npm-publish", rendered)
self.assertIn("workload-kv-read-whynot-design-npm-publish", rendered)
self.assertIn("auth/netkingdom/role/whynot-design-workload-kv-read", rendered)
def test_invalid_state_hub_rationale_shows_templates(self) -> None:
ccr, errors, _warnings = credential_change.validate_ccr(self.sample)
self.assertEqual(errors, [])
with self.assertRaises(SystemExit) as raised:
credential_change.ccr_status_from_state_hub_rationale("looks good", ccr)
self.assertIn("APPROVE: CCR-2026-0001", str(raised.exception))
self.assertIn("NEEDS_CHANGES: CCR-2026-0001", str(raised.exception))
def test_decision_command_can_record_state_hub_event(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
copied = Path(tmp) / self.issue_core.name
shutil.copy2(self.issue_core, copied)
events = []
original = credential_change.state_hub_post_json
try:
credential_change.state_hub_post_json = (
lambda _base_url, _path, payload: events.append(payload) or {"id": "event-1"}
)
exit_code = credential_change.command_decision(
type(
"Args",
(),
{
"ref": str(copied),
"reviewer": "unit-test",
"comment": "scoped metadata looks correct",
"record_state_hub": True,
"state_hub_url": "http://state-hub.test",
},
)(),
"approved",
)
finally:
credential_change.state_hub_post_json = original
self.assertEqual(exit_code, 0)
self.assertEqual(events[0]["event_type"], "credential_change_decision")
self.assertIn("CCR-2026-0002", events[0]["summary"])
self.assertIn("ISSUE_CORE_API_KEY", events[0]["summary"])
def test_operator_commands_render_non_secret_policy_and_role_handoff(self) -> None:
ccr, errors, warnings = credential_change.validate_ccr(self.sample)
self.assertEqual(errors, [])
self.assertEqual(warnings, [])
rendered = credential_change.render_operator_commands(ccr)
self.assertIn(
"bao policy write workload-kv-read-whynot-design-npm-publish",
rendered,
)
self.assertIn(
"bao write auth/netkingdom/role/whynot-design-workload-kv-read",
rendered,
)
self.assertIn("# Do not paste this shell block into the OpenBao Browser CLI.", rendered)
self.assertIn(
"# Web UI API Explorer path for the role JSON body: /v1/auth/netkingdom/role/whynot-design-workload-kv-read",
rendered,
)
self.assertIn('role_payload_file="$(mktemp)"', rendered)
self.assertIn('"bound_claims": {', rendered)
self.assertIn('"allowed_redirect_uris": [', rendered)
self.assertIn('"oidc_scopes": [', rendered)
self.assertIn('"groups"', rendered)
self.assertIn(
'"https://bao.coulomb.social/ui/vault/auth/netkingdom/oidc/callback"',
rendered,
)
self.assertIn(
'bao write auth/netkingdom/role/whynot-design-workload-kv-read @"$role_payload_file"',
rendered,
)
self.assertIn(
"# bao kv put platform/workloads/coulomb/whynot-design/npm-publish",
rendered,
)
self.assertIn("NPM_AUTH_TOKEN=<enter-through-approved-custody>", rendered)
self.assertNotIn("npm_", rendered)
def test_operator_commands_refuse_unapproved_ccr(self) -> None:
with self.assertRaises(SystemExit):
credential_change.command_operator_commands(
type("Args", (), {"ref": str(self.issue_core)})()
)
def test_approve_records_comment_but_unconfirmed_claim_still_blocks_apply(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
tmp_path = Path(tmp)
ccr_dir = tmp_path / "ccrs"
ccr_dir.mkdir()
copied = ccr_dir / self.issue_core.name
shutil.copy2(self.issue_core, copied)
old_ccr_dir = os.environ.get("CCR_DIR")
os.environ["CCR_DIR"] = str(ccr_dir)
try:
credential_change.append_decision(
copied, "approved", "unit-test", "looks right"
)
copied_data = credential_change.load_yaml(copied)
copied_data["openbao"]["auth"]["bound_claims_confirmed"] = False
credential_change.dump_yaml(copied, copied_data)
ccr, errors, _warnings = credential_change.validate_ccr(copied)
self.assertEqual(errors, [])
self.assertEqual(ccr["status"], "approved")
self.assertEqual(ccr["review"]["comments"][-1]["comment"], "looks right")
with self.assertRaises(SystemExit):
credential_change.command_apply_plan(
type("Args", (), {"ref": "CCR-2026-0002"})()
)
finally:
if old_ccr_dir is None:
os.environ.pop("CCR_DIR", None)
else:
os.environ["CCR_DIR"] = old_ccr_dir
def test_confirm_binding_records_comment_and_clears_warning(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
copied = Path(tmp) / self.issue_core.name
shutil.copy2(self.issue_core, copied)
credential_change.confirm_binding(
copied, "unit-test", "service account binding confirmed"
)
ccr, errors, warnings = credential_change.validate_ccr(copied)
self.assertEqual(errors, [])
self.assertEqual(warnings, [])
self.assertTrue(ccr["openbao"]["auth"]["bound_claims_confirmed"])
self.assertEqual(ccr["review"]["comments"][-1]["decision"], "binding_confirmed")
def test_generated_policy_is_narrow(self) -> None:
ccr, _errors, _warnings = credential_change.validate_ccr(self.sample)
policy = credential_change.generated_policy_hcl(ccr)
self.assertIn('path "platform/data/workloads/coulomb/whynot-design/npm-publish"', policy)
self.assertNotIn("*", policy)
self.assertNotIn("delete", policy)
def test_applier_dry_run_succeeds_for_active_ccr(self) -> None:
ccr, errors, warnings = credential_change.validate_ccr(self.sample)
self.assertEqual(errors, [])
self.assertEqual(warnings, [])
self.assertEqual(credential_change.applier_readiness_blockers(ccr), [])
payload = credential_change.applier_dry_run_payload(ccr, warnings)
self.assertEqual(payload["source_artifacts"]["policy"]["status"], "matches")
self.assertEqual(
payload["mutations"][0]["openbao_path"],
"sys/policies/acl/workload-kv-read-whynot-design-npm-publish",
)
self.assertEqual(
payload["mutations"][1]["openbao_path"],
"auth/netkingdom/role/whynot-design-workload-kv-read",
)
rendered = credential_change.render_applier_dry_run(payload)
self.assertIn("Allowed metadata mutations", rendered)
self.assertIn("secret value writes", rendered)
self.assertNotIn("<enter-through-approved-custody>", rendered)
def test_applier_dry_run_refuses_unapproved_ccr(self) -> None:
exit_code = credential_change.command_applier_dry_run(
type("Args", (), {"ref": str(self.issue_core), "json": False})()
)
self.assertEqual(exit_code, 1)
def test_applier_dry_run_rejects_out_of_policy_policy_name(self) -> None:
ccr, errors, _warnings = credential_change.validate_ccr(self.sample)
self.assertEqual(errors, [])
ccr["status"] = "approved"
ccr["openbao"]["policy_name"] = "platform-admin"
ccr["openbao"]["auth"]["policies"] = ["platform-admin"]
blockers = credential_change.applier_readiness_blockers(ccr)
self.assertTrue(
any("disallowed" in blocker for blocker in blockers),
blockers,
)
def test_applier_dry_run_rejects_out_of_policy_auth_role(self) -> None:
ccr, errors, _warnings = credential_change.validate_ccr(self.sample)
self.assertEqual(errors, [])
ccr["status"] = "approved"
ccr["openbao"]["auth"]["role"] = "platform-admin"
blockers = credential_change.applier_readiness_blockers(ccr)
self.assertTrue(
any("auth.role is disallowed" in blocker for blocker in blockers),
blockers,
)
def test_applier_dry_run_rejects_out_of_scope_mount_and_path(self) -> None:
ccr, errors, _warnings = credential_change.validate_ccr(self.sample)
self.assertEqual(errors, [])
ccr["status"] = "approved"
ccr["openbao"]["mount"] = "secret"
ccr["openbao"]["kv_path"] = "secret/platform-admin"
blockers = credential_change.applier_readiness_blockers(ccr)
self.assertIn("openbao.mount must be platform, got secret", blockers)
self.assertIn("openbao.kv_path must stay under platform/workloads/", blockers)
def test_nonprod_applier_policy_remains_metadata_only(self) -> None:
policy = (
REPO_DIR / "openbao/policies/credential-change-nonprod-applier.hcl"
).read_text(encoding="utf-8")
self.assertIn('path "sys/policies/acl/workload-kv-read-*"', policy)
self.assertIn('path "auth/kubernetes/role/*"', policy)
self.assertNotIn('path "platform/data/', policy)
self.assertNotIn('path "platform/metadata/', policy)
def test_applier_apply_plan_renders_confirmation(self) -> None:
ccr, errors, warnings = credential_change.validate_ccr(self.sample)
self.assertEqual(errors, [])
rendered = credential_change.render_applier_apply_plan(ccr, warnings)
self.assertIn("DELEGATED APPLY CCR-2026-0001", rendered)
self.assertIn("applier-apply CCR-2026-0001", rendered)
self.assertIn("secret value writes", rendered)
def test_applier_apply_refuses_unapproved_ccr(self) -> None:
exit_code = credential_change.command_applier_apply(
type(
"Args",
(),
{
"ref": str(self.issue_core),
"actor": "unit-test",
"confirm": None,
"bao_bin": "bao",
"plan_only": False,
"json": False,
"quiet": True,
"record_state_hub": False,
"state_hub_url": "http://state-hub.test",
},
)()
)
self.assertEqual(exit_code, 1)
def test_applier_apply_records_metadata_evidence(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
copied = Path(tmp) / self.sample.name
shutil.copy2(self.sample, copied)
ccr = credential_change.load_yaml(copied)
ccr["status"] = "approved"
ccr["access_frontdoor"]["readiness"] = "approved-pending-apply"
ccr["access_frontdoor"]["resolvable"] = False
credential_change.dump_yaml(copied, ccr)
calls = []
events = []
original_apply = credential_change.run_bao_metadata_apply
original_post = credential_change.state_hub_post_json
try:
credential_change.run_bao_metadata_apply = lambda ccr, bao_bin: calls.append((ccr["id"], bao_bin))
credential_change.state_hub_post_json = (
lambda _base_url, _path, payload: events.append(payload) or {"id": "event-1"}
)
exit_code = credential_change.command_applier_apply(
type(
"Args",
(),
{
"ref": str(copied),
"actor": "unit-test",
"confirm": "DELEGATED APPLY CCR-2026-0001",
"bao_bin": "bao-test",
"plan_only": False,
"json": False,
"quiet": True,
"record_state_hub": True,
"state_hub_url": "http://state-hub.test",
},
)()
)
finally:
credential_change.run_bao_metadata_apply = original_apply
credential_change.state_hub_post_json = original_post
self.assertEqual(exit_code, 0)
self.assertEqual(calls, [("CCR-2026-0001", "bao-test")])
updated = credential_change.load_yaml(copied)
self.assertEqual(updated["status"], "applied")
self.assertEqual(updated["verification"]["evidence"][-1]["kind"], "delegated_metadata_apply")
self.assertEqual(events[0]["event_type"], "credential_change_evidence")
self.assertIn("delegated_metadata_apply", events[0]["summary"])
def test_applier_apply_requires_exact_confirmation(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
copied = Path(tmp) / self.sample.name
shutil.copy2(self.sample, copied)
ccr = credential_change.load_yaml(copied)
ccr["status"] = "approved"
credential_change.dump_yaml(copied, ccr)
with self.assertRaises(SystemExit):
credential_change.command_applier_apply(
type(
"Args",
(),
{
"ref": str(copied),
"actor": "unit-test",
"confirm": "apply it",
"bao_bin": "bao",
"plan_only": False,
"json": False,
"quiet": True,
"record_state_hub": False,
"state_hub_url": "http://state-hub.test",
},
)()
)
def test_runbook_renders_apply_verify_guidance(self) -> None:
ccr, errors, warnings = credential_change.validate_ccr(self.sample)
self.assertEqual(errors, [])
payload = credential_change.runbook_payload(ccr, warnings)
rendered = credential_change.render_runbook(payload)
self.assertIn("APPLY CCR-2026-0001", rendered)
self.assertIn("runbook <CCR> --execute-metadata", rendered)
self.assertIn("record-evidence <CCR>", rendered)
self.assertIn("Field presence checked without printing values", rendered)
self.assertNotIn("npm_", rendered)
def test_runbook_refuses_unapproved_ccr(self) -> None:
exit_code = credential_change.command_runbook(
type(
"Args",
(),
{
"ref": str(self.issue_core),
"json": False,
"execute_metadata": False,
"actor": "unit-test",
"confirm": None,
"bao_bin": "bao",
"record_state_hub": False,
"state_hub_url": "http://state-hub.test",
},
)()
)
self.assertEqual(exit_code, 1)
def test_record_evidence_appends_non_secret_entry_and_status(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
copied = Path(tmp) / self.sample.name
shutil.copy2(self.sample, copied)
ccr = credential_change.load_yaml(copied)
ccr["status"] = "approved"
ccr["access_frontdoor"]["readiness"] = "approved-pending-apply"
ccr["access_frontdoor"]["resolvable"] = False
credential_change.dump_yaml(copied, ccr)
updated = credential_change.append_evidence(
copied,
"unit-test",
"metadata_apply",
"passed",
["OpenBao audit timestamp recorded without secret values"],
set_status="applied",
)
self.assertEqual(updated["status"], "applied")
self.assertEqual(updated["verification"]["evidence"][-1]["kind"], "metadata_apply")
self.assertEqual(
updated["verification"]["evidence"][-1]["details"],
["OpenBao audit timestamp recorded without secret values"],
)
def test_record_evidence_can_mark_frontdoor_ready(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
copied = Path(tmp) / self.sample.name
shutil.copy2(self.sample, copied)
updated = credential_change.append_evidence(
copied,
"unit-test",
"frontdoor_activation",
"passed",
["Catalog readiness checked without secret values"],
set_status="active",
frontdoor_ready=True,
)
self.assertEqual(updated["status"], "active")
self.assertEqual(updated["access_frontdoor"]["readiness"], "ready")
self.assertTrue(updated["access_frontdoor"]["resolvable"])
def test_record_evidence_rejects_secret_markers(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
copied = Path(tmp) / self.sample.name
shutil.copy2(self.sample, copied)
with self.assertRaises(SystemExit):
credential_change.append_evidence(
copied,
"unit-test",
"positive_verification",
"passed",
["accidentally pasted sk-test"],
)
def test_lifecycle_plan_renders_deactivation_steps(self) -> None:
ccr, errors, _warnings = credential_change.validate_ccr(self.sample)
self.assertEqual(errors, [])
payload = credential_change.lifecycle_payload(ccr, "deactivate")
rendered = credential_change.render_lifecycle_plan(payload)
self.assertIn("lifecycle plan: deactivate", rendered)
self.assertIn("readiness=disabled resolvable=False", rendered)
self.assertIn("bao delete auth/netkingdom/role/whynot-design-workload-kv-read", rendered)
self.assertIn("bao policy delete workload-kv-read-whynot-design-npm-publish", rendered)
self.assertNotIn("NPM_AUTH_TOKEN=", rendered)
def test_lifecycle_event_marks_deactivated_and_disables_frontdoor(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
copied = Path(tmp) / self.sample.name
shutil.copy2(self.sample, copied)
updated = credential_change.append_lifecycle_event(
copied,
"unit-test",
"deactivate",
"No longer needed",
["Front door disabled in catalog"],
)
self.assertEqual(updated["status"], "deactivated")
self.assertEqual(updated["access_frontdoor"]["readiness"], "disabled")
self.assertFalse(updated["access_frontdoor"]["resolvable"])
self.assertEqual(updated["lifecycle"]["events"][-1]["action"], "deactivate")
def test_lifecycle_event_records_compromise_blast_radius_and_follow_up(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
copied = Path(tmp) / self.sample.name
shutil.copy2(self.sample, copied)
updated = credential_change.append_lifecycle_event(
copied,
"unit-test",
"compromise",
"Unexpected exposure signal",
["Access disabled before rotation"],
blast_radius=["npm publishing lane only"],
follow_up=["incident-task-1"],
)
event = updated["lifecycle"]["events"][-1]
self.assertEqual(updated["status"], "compromised")
self.assertEqual(updated["access_frontdoor"]["readiness"], "compromised")
self.assertEqual(event["blast_radius"], ["npm publishing lane only"])
self.assertEqual(event["follow_up"], ["incident-task-1"])
def test_lifecycle_event_rejects_secret_markers(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
copied = Path(tmp) / self.sample.name
shutil.copy2(self.sample, copied)
with self.assertRaises(SystemExit):
credential_change.append_lifecycle_event(
copied,
"unit-test",
"rotate",
"accidentally pasted ghp_bad",
["rotation needed"],
)
def test_import_inventory_writes_non_secret_ccr_and_policy(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
tmp_path = Path(tmp)
output_dir = tmp_path / "ccrs"
policy_file = tmp_path / "policies" / "workload-kv-read-imported-lane.hcl"
args = type(
"Args",
(),
{
"id": "CCR-2099-0001",
"title": "imported lane",
"tenant": "coulomb",
"workload": "imported",
"environment": "production",
"purpose": "runtime token",
"mount": "platform",
"kv_path": "platform/workloads/coulomb/imported/runtime-token",
"field": ["RUNTIME_TOKEN"],
"policy_name": "workload-kv-read-imported-lane",
"policy_file": str(policy_file),
"auth_method": "oidc",
"auth_mount": "netkingdom",
"auth_role": "imported-workload-kv-read",
"bound_claim": ["groups=imported"],
"service_account": None,
"service_account_namespace": None,
"bound_claims_confirmed": True,
"ttl": "15m",
"frontdoor_type": "ops-warden",
"catalog_id": "imported-runtime-token",
"selector": None,
"command": None,
"status": "active",
"readiness": "ready",
"resolvable": True,
"risk": "high",
"positive_check": "Authorized caller can fetch RUNTIME_TOKEN with output suppressed.",
"negative_check": "Unauthorized caller cannot read the imported path.",
"requester_agent": "unit-test",
"actor": "unit-test",
"reason": "Imported existing lane without secret values",
"output_dir": str(output_dir),
"write_policy": True,
},
)()
path = credential_change.write_inventory_ccr(args)
self.assertTrue(path.exists())
self.assertTrue(policy_file.exists())
ccr, errors, warnings = credential_change.validate_ccr(path)
self.assertEqual(errors, [])
self.assertEqual(warnings, [])
self.assertEqual(ccr["openbao"]["fields"], ["RUNTIME_TOKEN"])
self.assertNotIn("ghp_", path.read_text(encoding="utf-8"))
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,69 @@
from __future__ import annotations
import contextlib
import importlib.util
import io
import sys
import unittest
from pathlib import Path
REPO_DIR = Path(__file__).resolve().parents[1]
SPEC = importlib.util.spec_from_file_location(
"openbao_credential_change_appliers",
REPO_DIR / "scripts/openbao-apply-credential-change-appliers.py",
)
appliers = importlib.util.module_from_spec(SPEC)
assert SPEC.loader is not None
sys.modules[SPEC.name] = appliers
SPEC.loader.exec_module(appliers)
class CredentialChangeApplierSetupTests(unittest.TestCase):
def test_selected_appliers_all_is_stable(self) -> None:
selected = appliers.selected_appliers("all")
self.assertEqual(
[item["token_role"] for item in selected],
["credential-change-nonprod-applier", "credential-change-prod-applier"],
)
def test_role_args_are_bounded(self) -> None:
args = appliers.role_args(appliers.APPLIERS["prod"])
self.assertIn("auth/token/roles/credential-change-prod-applier", args)
self.assertIn("allowed_policies=credential-change-prod-applier", args)
self.assertIn("disallowed_policies=root,platform-admin", args)
self.assertIn("token_no_default_policy=true", args)
self.assertIn("token_type=service", args)
def test_dry_run_applies_policy_role_and_readback(self) -> None:
runner = appliers.BaoRunner(
kubectl="kubectl",
namespace="openbao",
release="openbao",
dry_run=True,
use_token_helper=False,
token=None,
)
output = io.StringIO()
with contextlib.redirect_stdout(output):
appliers.apply_applier(
runner,
appliers.APPLIERS["nonprod"],
REPO_DIR / "openbao/policies",
)
rendered = output.getvalue()
self.assertIn(
"DRY-RUN: bao policy write credential-change-nonprod-applier",
rendered,
)
self.assertIn(
"DRY-RUN: bao write auth/token/roles/credential-change-nonprod-applier",
rendered,
)
self.assertIn(
"DRY-RUN: bao read auth/token/roles/credential-change-nonprod-applier",
rendered,
)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,130 @@
from __future__ import annotations
import importlib.util
import os
import stat
import subprocess
import sys
import tempfile
import unittest
from argparse import Namespace
from pathlib import Path
REPO_DIR = Path(__file__).resolve().parents[1]
SPEC = importlib.util.spec_from_file_location(
"credential_helper", REPO_DIR / "scripts/credential.py"
)
credential = importlib.util.module_from_spec(SPEC)
assert SPEC.loader is not None
sys.modules[SPEC.name] = credential
SPEC.loader.exec_module(credential)
def sample_grant() -> dict:
return {
"id": "ops-warden/warden-sign",
"issuer": "openbao",
"audience": "ops-warden",
"credential_type": "openbao-token",
"openbao": {
"token_role": "warden-sign",
"policies": ["warden-sign"],
},
"ttl": {"default": "15m", "max": "1h"},
"actors": {"allowed_types": ["human-operator", "approved-agent"]},
"delivery": {
"allowed": [
"exec-env",
"response-wrap",
"local-token-file",
"kubernetes-auth",
],
"kubernetes_auth": {
"mount": "auth/kubernetes",
"role": "credential-broker-warden-sign",
"service_account_names": ["credential-broker"],
"namespaces": ["openbao"],
},
},
}
class CredentialHelperTests(unittest.TestCase):
def test_ttl_over_max_is_rejected(self) -> None:
with self.assertRaises(SystemExit):
credential.validate_issue_request(
sample_grant(), "2h", "purpose", "exec-env", "approved-agent"
)
def test_actor_type_is_checked(self) -> None:
with self.assertRaises(SystemExit):
credential.validate_issue_request(
sample_grant(), "15m", "purpose", "exec-env", "unknown-actor"
)
def test_split_env_prefix_rejects_token_injection(self) -> None:
with self.assertRaises(SystemExit):
credential.split_env_prefix(["--", "VAULT_TOKEN=hvs.bad", "/bin/true"])
def test_split_env_prefix_accepts_safe_assignments(self) -> None:
extra_env, command = credential.split_env_prefix(
["--", "SMOKE_VAULT=1", "/bin/true"]
)
self.assertEqual(extra_env, {"SMOKE_VAULT": "1"})
self.assertEqual(command, ["/bin/true"])
def test_redaction_catches_bao_tokens_and_env_assignments(self) -> None:
text = "token=hvb.abc123 VAULT_TOKEN=hvs.secret BAO_TOKEN=hvb.secret"
redacted = credential.redact(text)
self.assertNotIn("hvb.abc123", redacted)
self.assertNotIn("hvs.secret", redacted)
self.assertIn("[REDACTED]", redacted)
def test_local_lease_is_mode_0600_and_cleanup_stays_in_lease_dir(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
lease_dir = Path(tmp) / "leases"
authz = credential.AuthorizationResult(True, "unit-test", "decision-1")
payload = credential.write_local_lease(
lease_dir=lease_dir,
grant=sample_grant(),
purpose="unit-test",
ttl="15m",
token="hvb.unit-test-secret",
accessor="accessor-unit-test",
authz=authz,
)
token_file = Path(payload["token_file"])
metadata_file = Path(payload["metadata_file"])
self.assertEqual(stat.S_IMODE(token_file.stat().st_mode), 0o600)
self.assertEqual(stat.S_IMODE(metadata_file.stat().st_mode), 0o600)
removed = credential.remove_local_lease_files(
lease_dir, "accessor-unit-test"
)
self.assertIn(str(token_file), removed)
self.assertIn(str(metadata_file), removed)
self.assertFalse(token_file.exists())
self.assertFalse(metadata_file.exists())
def test_kubernetes_auth_payload_issues_no_token(self) -> None:
authz = credential.AuthorizationResult(True, "dry-run-local", None)
payload = credential.kubernetes_auth_payload(
sample_grant(), "15m", "unit-test", authz
)
self.assertEqual(payload["delivery_mode"], "kubernetes-auth")
self.assertEqual(payload["openbao_auth_role"], "credential-broker-warden-sign")
self.assertNotIn("token", payload)
self.assertIn("service_account_names", payload)
def test_lease_paths_are_gitignored(self) -> None:
result = subprocess.run(
["git", "check-ignore", ".local/credential-leases/example.openbao-token"],
cwd=REPO_DIR,
capture_output=True,
text=True,
check=False,
)
self.assertEqual(result.returncode, 0, result.stderr)
if __name__ == "__main__":
unittest.main()

View File

@@ -2,7 +2,7 @@
id: RAIL-PL-WP-0002
type: workplan
title: "OpenBao Platform Secrets Service"
domain: railiance
domain: financials
repo: railiance-platform
status: finished
owner: codex

View File

@@ -2,7 +2,7 @@
id: RAILIANCE-WP-0003
type: workplan
title: "Provision shared CNPG cluster apps-pg"
domain: railiance
domain: financials
repo: railiance-platform
status: finished
owner: codex

View File

@@ -0,0 +1,281 @@
---
id: RAILIANCE-WP-0004
type: workplan
title: "Establish ArgoCD GitOps bootstrap contract"
domain: financials
repo: railiance-platform
status: finished
owner: codex
topic_slug: railiance
planning_priority: high
planning_order: 4
created: "2026-06-19"
updated: "2026-06-25"
state_hub_workstream_id: "e57e487b-8557-439d-8093-0457c73ede93"
---
# RAILIANCE-WP-0004 - Establish ArgoCD GitOps Bootstrap Contract
## Goal
Establish the minimal platform-owned ArgoCD GitOps contract needed for
Railiance application teams to deploy through the already-installed ArgoCD
instance on `railiance01`.
This work responds to the `issue-core` dependency message from 2026-06-18:
ArgoCD is installed and healthy on `railiance01`, but unused. `issue-core`
will be the first tenant Application and needs platform decisions before it
can author its workload deployment.
## Intent Alignment
`INTENT.md` defines this repo as the shared platform-services layer:
stateful services, secret custody, stable interfaces, and recoverable
operational contracts.
ArgoCD itself is not an application, database, or secret store. The work in
this repo is therefore intentionally limited to the platform contract around
GitOps:
- repository trust and credential registration for ArgoCD;
- AppProject guardrails that keep tenant syncs inside expected boundaries;
- a root app-of-apps entrypoint that provides a stable onboarding surface;
- the OpenBao-backed runtime secret delivery convention tenants must use.
Application workloads, container images, per-service manifests, and business
logic remain owned by the tenant repos.
## Scope
In scope:
- Define the bootstrap manifests for ArgoCD AppProjects and the root
app-of-apps Application.
- Define how Git source repositories are registered without committing
credentials.
- Define where tenant Application manifests are placed and how they point
back to tenant-owned workload manifests.
- Confirm the runtime secret delivery pattern: OpenBao custody delivered to
Kubernetes via External Secrets Operator by default; CSI-mounted files only
when a workload requires file references; OpenBao injector remains disabled.
- Provide an `issue-core` pilot Application example so that repo can author
its final manifest against a concrete contract.
Out of scope:
- Installing or upgrading ArgoCD itself; that is cluster/runtime ownership.
- Moving S5 application workload manifests into this repo.
- Storing ArgoCD repository credentials, API tokens, or application secrets in
Git, workplans, State Hub, or chat.
- Applying live manifests that require operator-owned credentials.
## Decisions
### D-01 - Bootstrap Layout
Use this repo only for the platform-owned GitOps bootstrap:
```text
argocd/bootstrap/ AppProjects and root app-of-apps Application
argocd/applications/ thin tenant Application manifests reviewed by platform
argocd/repositories/ SOPS templates for ArgoCD repository Secret objects
docs/argocd-gitops.md GitOps contract and onboarding guidance
```
The root Application syncs `argocd/applications/` from this repo. Tenant
Application manifests in that directory point to workload manifests in each
tenant repo, normally `k8s/railiance/`.
### D-02 - AppProject Model
Create two AppProjects:
- `railiance-bootstrap` only allows the root app to manage ArgoCD
`Application` objects in the `argocd` namespace.
- `railiance-tenants` allows tenant Applications to sync ordinary namespaced
workload resources into their own namespaces, plus namespace creation. It
does not grant CRD, ClusterRole, ClusterRoleBinding, or arbitrary
cluster-admin authority.
### D-03 - Sync Policy
Default tenant Applications use automated sync with prune and self-heal
enabled after platform review. Recommended sync options are:
```yaml
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
- ApplyOutOfSyncOnly=true
- PruneLast=true
```
Sync waves are reserved for dependency ordering. Platform services and secret
delivery resources should sync before workloads that consume them.
### D-04 - Secret Delivery
OpenBao remains the canonical runtime secret custody service. For ordinary
Kubernetes workloads, use External Secrets Operator to materialize OpenBao
values as Kubernetes Secrets. Do not use the OpenBao injector in the current
deployment.
Runtime path convention for workload credential custody:
```text
platform/workloads/<tenant-or-org>/<workload>/<secret-purpose>
```
Kubernetes namespace and service-account bounds belong in the auth role or
External Secrets binding unless the namespace is itself the approved workload
identity.
ArgoCD repository credentials are operator credentials, not workload secrets,
and should live under:
```text
platform/operators/argocd/repositories/<repo-name>
```
## Current Evidence
- State Hub inbox message `d7a18ff9-e6c6-4e44-a39e-78369e530dfc` reports
ArgoCD is installed and healthy on `railiance01`, with zero Applications,
zero ApplicationSets, zero registered repositories, and only the stock
`default` AppProject.
- `INTENT.md` and `SCOPE.md` keep this repo focused on shared platform
services and secret custody. This work therefore creates a bootstrap
contract and secret-delivery convention, not app workload ownership.
- `docs/openbao.md` already states the preferred delivery pattern:
External Secrets Operator for values that become Kubernetes Secrets, CSI for
file-reference workloads, and no OpenBao injector in the current deployment.
## Follow-up Progress (2026-06-25)
- Added a platform-owned `railiance-platform-addons` AppProject for
cluster-scoped add-ons.
- Added the `external-secrets` ArgoCD Application for External Secrets
Operator and the `openbao-secretstore` Application for
`ClusterSecretStore/openbao`.
- Added the least-privilege OpenBao policy and Kubernetes auth role helper for
the issue-core ESO pilot. The role binds only the
`external-secrets/external-secrets` service account and reads only
`platform/workloads/issue-core/issue-core/*`.
- Limited the initial `ClusterSecretStore/openbao` to the `issue-core`
namespace; broaden only through a later platform review.
## Target State
- `argocd/bootstrap/` contains the two AppProjects and root app-of-apps
Application.
- `argocd/applications/` documents the tenant Application contract and includes
an `issue-core` example manifest.
- `argocd/repositories/` contains non-secret SOPS templates for ArgoCD
repository registration.
- `docs/argocd-gitops.md` answers the four questions raised by `issue-core`:
repository registration, source layout, sync policy, and secret delivery.
- Make targets exist for dry-run, deploy, status, and SOPS-backed repository
secret application.
- `issue-core` can author its final Application and workload manifests against
this contract without waiting for more platform design.
## Tasks
### T01 - Review intent and scope boundary
```task
id: RAILIANCE-WP-0004-T01
status: done
priority: high
state_hub_task_id: "7cb56ad6-5435-41af-b416-e68fe661b7a0"
```
Review `INTENT.md`, `SCOPE.md`, existing OpenBao delivery docs, and the
`issue-core` inbox request. Capture the boundary that ArgoCD bootstrap belongs
here only as a platform trust and secret-delivery contract.
### T02 - Add ArgoCD bootstrap manifests
```task
id: RAILIANCE-WP-0004-T02
status: done
priority: high
state_hub_task_id: "68f7ef19-686d-4d16-bf75-ffcbba158023"
```
Add AppProject manifests and the root app-of-apps Application under
`argocd/bootstrap/`.
Done when the manifests can be rendered by `kubectl apply -k` and avoid secret
material.
### T03 - Define tenant onboarding and repository registration
```task
id: RAILIANCE-WP-0004-T03
status: done
priority: high
state_hub_task_id: "e6dc9176-af33-4216-9871-a61ad7e69943"
```
Add documentation and templates for tenant Applications, per-repo ArgoCD
repository Secret registration, and the `issue-core` pilot example.
### T04 - Confirm OpenBao-backed secret delivery
```task
id: RAILIANCE-WP-0004-T04
status: done
priority: high
state_hub_task_id: "d859e4ef-d8d1-4403-8225-839925f8bedf"
```
Document that OpenBao remains the runtime custody authority, External Secrets
Operator is the default Kubernetes delivery mechanism, CSI is reserved for
file-reference workloads, and the OpenBao injector remains disabled.
### T05 - Operator live bootstrap
```task
id: RAILIANCE-WP-0004-T05
status: done
priority: high
state_hub_task_id: "981f46c0-8dd7-4111-9a4f-2ca58ddb0664"
```
Apply the bootstrap and repository credentials to live ArgoCD after these repo
changes are merged to the Git source ArgoCD reads and, if the source repo is
private, after an operator provides or materializes read-only repository
credentials through the approved OpenBao/operator path.
Applied 2026-06-19 on the live ArgoCD cluster (`92.205.130.254`, default
`~/.kube/config`). `make argocd-bootstrap-dry-run` and
`make argocd-bootstrap-deploy` succeeded. Repository registration was skipped
because `railiance-platform` and `issue-core` Gitea repos are currently public.
Post-bootstrap status:
- `railiance-apps-root`: Synced / Healthy
- `issue-core`: OutOfSync / Missing — sync fails because
`external-secrets.io/ExternalSecret` CRD is not installed on the cluster
Do not paste credentials into the workplan, State Hub, or chat.
### T06 - Notify first tenant
```task
id: RAILIANCE-WP-0004-T06
status: done
priority: medium
state_hub_task_id: "73bdda1d-8e25-48d2-ab92-b203c5050d45"
```
Reply to `issue-core` with the GitOps contract pointer and confirm that it owns
the final `issue-core` Application proposal and workload manifests. Include the
OpenBao path convention for `ISSUE_CORE_API_KEY` and the Gitea backend token.
State Hub reply: `56df276d-77d0-427f-92a5-a99cacc1290f`.

View File

@@ -0,0 +1,421 @@
---
id: RAILIANCE-WP-0005
type: workplan
title: "Credential Request and Lease Broker"
domain: financials
repo: railiance-platform
status: active
owner: codex
topic_slug: railiance
planning_priority: high
planning_order: 5
created: "2026-06-24"
updated: "2026-07-01"
depends_on_workplans:
- RAIL-PL-WP-0002
state_hub_workstream_id: "2731fece-6c49-45b8-ab8a-4ea6c04ac603"
---
# RAILIANCE-WP-0005 - Credential Request and Lease Broker
## Goal
Provide a clean, secure, low-friction way for operators, agents, and approved
automations to request, generate, receive, use, renew, and revoke short-lived
credentials such as the OpenBao token needed for ops-warden vault-backed SSH
signing smoke.
The target experience is self-service for routine, policy-approved leases and
explicit human approval for high-risk grants, without ever pasting secret values
into Git, State Hub, chat, prompts, workplans, or shell history.
## Repository Decision
The primary owner is railiance-platform because OpenBao is the canonical
runtime secret custody service and this repo owns platform secrets, identity
integration, and shared credential delivery contracts.
Cross-repo responsibilities:
| Concern | Owner | Boundary |
| --- | --- | --- |
| OpenBao policies, token roles, lease broker, audit | railiance-platform | Owns secret custody and credential generation. |
| Login, OIDC, MFA, IAM profile claims | key-cape | Authenticates humans and service identities. |
| Authorization decision for requested grants | flex-auth | May decide whether actor X may request grant Y for purpose Z. |
| SSH certificate signing | ops-warden | Issues SSH certs only; does not vend OpenBao tokens. |
| Request tracking and progress | state-hub | Stores non-secret request metadata, status, decision ids, and audit pointers only. |
| Agent inference/runtime | llm-connect and callers | Never place secrets in prompts; consume via local env injection or wrapped lease handles. |
This work should update the ops-warden routing catalog when complete, but the
implementation belongs here. If the broker later becomes a general NetKingdom
service, code can split to a dedicated credential-broker repo while OpenBao
policies and grants remain owned by railiance-platform.
## Design Principles
- Prefer dynamic or short-lived leases over static secrets.
- Use response wrapping or local exec-time injection; do not print raw tokens by default.
- Store non-secret lease metadata only: actor, grant, TTL, purpose, lease id or accessor, decision id, timestamps, and revocation state.
- Keep OpenBao audit logs as the source of truth for secret access.
- Make the common path easy: one command to run a task with the right credential.
- Keep high-risk paths explicit: human approval and MFA for elevated grants.
- Every grant has a catalog entry, max TTL, allowed actors/subjects, delivery mode, audit expectations, and revocation behavior.
## Proposed User Experience
Initial pilot command shapes:
credential request vault-token --grant ops-warden/warden-sign --purpose flex-auth-openbao-smoke --ttl 15m
credential exec --grant ops-warden/warden-sign --ttl 15m -- SMOKE_VAULT=1 /home/worsch/ops-warden/scripts/policy_gate_production_smoke.sh
credential status <lease-handle>
credential revoke <lease-handle>
For the ops-warden smoke, the preferred path is credential exec. It obtains a
bounded OpenBao token with the warden-sign policy, injects it as VAULT_TOKEN
only into the child process environment, redacts logs, and revokes or lets the
lease expire after the command finishes.
## Threat Model Summary
Primary risks:
- token leakage through shell history, logs, prompts, chat, State Hub, or Git;
- confused-deputy issuance where an agent requests a broader token than needed;
- stale leases surviving after a task completes;
- bypassing KeyCape identity or flex-auth authorization checks;
- replacing one manual secret-handling ritual with another brittle one.
Mitigations required by this workplan:
- no raw token in command-line arguments, State Hub payloads, workplans, or logs;
- bounded OpenBao token roles and policies;
- response wrapping for copy/paste or remote handoff flows;
- exec-time environment injection for local command execution;
- default TTLs measured in minutes, with explicit max TTLs per grant;
- revocation by lease handle/accessor;
- OpenBao audit verification and non-secret State Hub progress events.
## Tasks
## T01 - Record ownership and architecture decision
```task
id: RAILIANCE-WP-0005-T01
status: done
priority: high
state_hub_task_id: "cd680de8-a483-40d6-84fa-369bad60e7c7"
```
Write an ADR or docs section confirming railiance-platform as the owner for
OpenBao credential request/generation/delivery, with key-cape, flex-auth,
ops-warden, state-hub, and llm-connect boundaries.
Acceptance:
- Docs state that ops-warden routes SSH certs only and must not vend OpenBao tokens.
- Docs state that State Hub stores request metadata only, never secret values.
- Ops-warden credential routing can point OpenBao token requests here.
**2026-06-25:** Added `docs/credential-broker.md` as the ownership and architecture decision. It records that railiance-platform owns OpenBao credential request/generation/delivery, ops-warden owns SSH certificate signing only, State Hub stores non-secret request metadata only, and llm-connect/callers must not place secrets in prompts.
## T02 - Define credential grant catalog
```task
id: RAILIANCE-WP-0005-T02
status: done
priority: high
state_hub_task_id: "6b64ad4b-90cd-475b-aaa9-73997c6b011b"
```
Add a non-secret grant catalog schema and initial grant entries.
Initial grant:
- id: ops-warden/warden-sign
- credential type: openbao-token
- policies: warden-sign
- default TTL: 15 minutes
- max TTL: 1 hour unless a human approves more
- purpose examples: flex-auth OpenBao smoke, ops-warden production sign smoke
- allowed delivery: exec-env, response-wrap, local-token-file mode 0600
- denied delivery: chat, State Hub body, Git, command-line token argument
Acceptance:
- Catalog can be validated in CI.
- The catalog distinguishes self-service, approval-required, and break-glass grants.
- No grant entry contains a secret.
**2026-06-25:** Added the non-secret grant catalog at `credential-grants/catalog.yaml` with the initial `ops-warden/warden-sign` pilot grant, plus `scripts/credential-grants-validate.py` and `make credential-grants-validate`. The validator enforces required fields, TTL bounds, denied delivery modes, disallowed OpenBao policies, audit/revocation expectations, and secret-looking marker rejection.
## T03 - Configure bounded OpenBao token roles and policies
```task
id: RAILIANCE-WP-0005-T03
status: done
priority: high
state_hub_task_id: "d8498e3b-b2fb-47b7-ab88-cd6592c1807e"
```
Create idempotent scripts/manifests for OpenBao token roles or equivalent lease
issuance paths that can generate child tokens only for approved policies and
TTLs. Start with warden-sign.
Acceptance:
- A non-root issuer path can create a warden-sign token with bounded TTL.
- The resulting token cannot administer OpenBao and can only call the SSH sign paths allowed by openbao/policies/warden-sign.hcl.
- Verification proves the token can run ops-warden vault signing and cannot list unrelated secrets.
**2026-06-26:** Added the source-side OpenBao token-grant implementation for
the `ops-warden/warden-sign` pilot: issuer policy
`openbao/policies/credential-broker-warden-sign-issuer.hcl`, idempotent apply
and verify scripts, Make targets for dry-run/live apply/live verification, and
catalog validation for `openbao.issuer_policy`. Dry-run validation is expected
to work offline. Live closure still requires an approved OpenBao operator token
path and successful runs of `make openbao-configure-token-grants` and
`make openbao-verify-token-grants-smoke`, so T03 remains `progress`.
**2026-06-27:** Attempted the live idempotent apply with
`make openbao-configure-token-grants OPENBAO_TOKEN_GRANT_ARGS=--use-token-helper`.
OpenBao was reachable and unsealed, but the pod token helper received
`403 permission denied` while writing
`sys/policies/acl/credential-broker-warden-sign-issuer`. T03 is now `wait`
until an approved OpenBao issuer/platform-admin path applies the policy and
role, or the pod token helper is granted that narrow capability.
**2026-07-01:** Operator unsealed OpenBao. Live apply succeeded with
`OPENBAO_TOKEN_FILE=~/.local/openbao/platform-admin.token make openbao-configure-token-grants`:
`credential-broker-warden-sign-issuer` policy and `warden-sign` token role are
configured. T03 is `done`.
**2026-07-01 follow-up:** Live smoke succeeded with openbao-verify-token-grants-smoke: a child token minted from role warden-sign signed a throwaway SSH public key through ssh/sign/agt-role, was denied policy metadata read, and was revoked by accessor.
## T04 - Build credential helper MVP
```task
id: RAILIANCE-WP-0005-T04
status: done
priority: high
state_hub_task_id: "0c543cb3-36cb-4b25-9a58-de8efc1216c9"
```
Build a small CLI/helper in this repo first, for example credential or
openbao-lease, with request, exec, status, and revoke commands.
Acceptance:
- credential exec can run the ops-warden production smoke with VAULT_TOKEN only in the child process environment.
- request returns a wrapped token or lease handle by default, not the raw token.
- status and revoke work by non-secret lease handle/accessor.
- The helper redacts token-looking values from logs and refuses to run in verbose modes that would print secrets.
**2026-06-26:** Added `scripts/credential.py` as the source helper MVP with
`request`, `exec`, `status`, and `revoke` subcommands. The helper validates the
grant catalog, enforces purpose and TTL bounds, defaults `request` to a local
mode-0600 token file plus non-secret accessor metadata, supports response-wrap
handoff, injects `VAULT_TOKEN` only into the child process for `exec`, redacts
token-looking child output, rejects caller-supplied token env assignments, and
revokes exec tokens by accessor in a `finally` block. Added Make dry-run and
ops-warden smoke targets. T04 remains `progress` until a live OpenBao issuer
token is available to prove `credential-exec-ops-warden-smoke` end to end.
**2026-06-27:** Extended the helper with optional flex-auth preflight,
non-secret State Hub lifecycle metadata, actor/subject binding fields,
`--decision-id` support, and Kubernetes-auth delegation output. Fixed the Make
surface so global helper flags such as `--use-token-helper` are passed before
the subcommand. T04 is now `wait` on the same OpenBao live gate as T03 before
ops-warden smoke can be proven end to end.
**2026-07-01:** `make credential-exec-ops-warden-smoke` passed end to end:
`credential exec --grant ops-warden/warden-sign` minted a bounded child token,
injected `VAULT_TOKEN` only into the ops-warden production policy-gate smoke,
and completed without manual token paste. T04 is `done`.
**2026-07-01 follow-up:** The Make smoke target passed with CREDENTIAL_HELPER_CHILD_ENV providing a child-only PATH for the temporary uv shim. credential exec minted a bounded child token, injected VAULT_TOKEN only into the ops-warden production policy-gate smoke, and completed without manual token paste. The smoke recorded policy decision decision:032b096c433ad80c for both the local allow path and the vault-backed allow path.
## T05 - Implement secure delivery modes
```task
id: RAILIANCE-WP-0005-T05
status: wait
priority: high
state_hub_task_id: "66f3cd6d-7520-4584-90b8-672866ef3490"
```
Support safe delivery modes for different runtime contexts.
Required modes:
- exec-env: inject credential into one child process, then forget it;
- response-wrap: produce a single-use OpenBao wrapping token for attended handoff;
- local-token-file: write mode 0600 under an ignored local state directory, with TTL metadata and cleanup;
- kubernetes-auth: use service-account-bound auth for in-cluster workloads instead of handing them tokens manually.
Acceptance:
- No delivery mode requires pasting the secret into chat or State Hub.
- local-token-file paths are gitignored and rejected by secret scans if accidentally staged.
- response-wrap unwraps once and fails on second use.
**2026-06-27:** Source support now covers all four delivery modes: `exec-env`,
`response-wrap`, `local-token-file`, and `kubernetes-auth`. The helper refuses
caller-supplied token env assignments, writes local leases under the ignored
`.local/credential-leases/` path with mode `0600`, and emits only service
account auth metadata for Kubernetes-auth. T05 is `wait` until live response-wrap
single-use behavior and the OpenBao-backed exec path are verified with an
approved issuer token.
**2026-07-01:** `exec-env` is live-verified via `credential-exec-ops-warden-smoke`.
`response-wrap`, `local-token-file`, and `kubernetes-auth` still need live
evidence. T05 is `progress`.
## T06 - Integrate KeyCape identity and agent subject binding
```task
id: RAILIANCE-WP-0005-T06
status: done
priority: medium
state_hub_task_id: "e1dd5973-bf2b-4aa9-842e-9f530afa1ab6"
```
Define how humans and agents authenticate to request grants.
Acceptance:
- Human operator path uses KeyCape/OIDC with MFA where required.
- Agent/service path has a documented subject id shape compatible with IAM profile claims and existing actor naming.
- Headless automation uses Kubernetes auth or an explicitly approved non-interactive identity; it does not reuse a human token.
**2026-06-27:** Documented the identity contract in `docs/credential-broker.md`:
KeyCape/OIDC with MFA for human operators, stable IAM-compatible subjects for
agents and CI, and Kubernetes service-account subjects for headless workloads.
The helper now exposes `--actor`, `--actor-type`, and `--subject`, and validates
actor type against the grant catalog. T06 is done source-side.
## T07 - Add flex-auth preflight authorization and State Hub request metadata
```task
id: RAILIANCE-WP-0005-T07
status: wait
priority: medium
state_hub_task_id: "1269bb58-0699-43ef-aa4f-43bc49c61a49"
```
Before issuing a lease, optionally call flex-auth with actor, subject, grant,
purpose, TTL, audience, and requested delivery mode. Record non-secret request
metadata and decision ids in State Hub when available.
Acceptance:
- flex-auth can deny overbroad TTL, wrong actor type, wrong purpose, or disallowed delivery mode.
- State Hub records request lifecycle without token values.
- The helper works in offline/degraded mode only for pre-authorized local flows; it never caches new secret material in State Hub.
**2026-06-27:** Added optional flex-auth preflight via `--flex-auth-url` /
`FLEX_AUTH_URL`, strict `--require-flex-auth`, provided decision ids via
`--decision-id`, and opt-in State Hub lifecycle notes via `--record-state-hub`.
The helper records only non-secret metadata. T07 is `wait` until a live flex-auth
credential authorization endpoint is available and the OpenBao live gate is
cleared.
## T08 - Integrate ops-warden smoke and routing catalog
```task
id: RAILIANCE-WP-0005-T08
status: done
priority: high
state_hub_task_id: "4571d4c9-d4de-4ee9-97e0-ff03e49e65ec"
```
Replace the manual VAULT_TOKEN step in ops-warden smoke docs with the credential
helper flow and update the credential routing catalog.
Acceptance:
- FLEX-WP-0007 T4 can be run with one command once the grant is configured:
credential exec --grant ops-warden/warden-sign --ttl 15m -- SMOKE_VAULT=1 /home/worsch/ops-warden/scripts/policy_gate_production_smoke.sh
- ops-warden docs still make clear it owns SSH cert signing, not OpenBao token vending.
- warden route find VAULT_TOKEN points to this railiance-platform flow.
**2026-06-27:** Added `make credential-exec-ops-warden-smoke` for the intended
one-command smoke and confirmed credential routing locally with
`uv run warden route show openbao-api-key --json`: OpenBao/API/dynamic lease
needs belong to `railiance-platform`; ops-warden executes SSH cert issuance
only. T08 is `wait` because this workspace cannot update the external
ops-warden routing catalog and the live OpenBao grant apply is still denied.
**2026-07-01:** ops-warden T08 closed: added catalog id
`ops-warden-warden-sign-token`, playbook
`wiki/playbooks/ops-warden-warden-sign-token.md`, and updated
`operator-openbao-token-hygiene.md`, `PolicyGatedSigning.md`, and
`CredentialRouting.md`. `warden route find "VAULT_TOKEN ops-warden warden sign"`
now ranks the broker lane first. Live smoke already proven via
`make credential-exec-ops-warden-smoke`. T08 is `done`.
## T09 - Verification, audit, and red-team checks
```task
id: RAILIANCE-WP-0005-T09
status: wait
priority: high
state_hub_task_id: "78d1db83-12fb-4ac2-95eb-54c91ac125b5"
```
Add tests and operator verification for the complete flow.
Acceptance:
- Unit tests cover grant validation, TTL bounds, redaction, and delivery-mode restrictions.
- Dry-run tests require no secrets.
- Live smoke proves OpenBao audit logs record issuance and use.
- Negative tests prove denied grants do not mint tokens.
- Documentation includes emergency revocation and cleanup commands.
**2026-06-27:** Added `tests/test_credential_helper.py` and `make credential-tests`
covering TTL bounds, actor-type restrictions, token redaction, unsafe env
rejection, local lease mode/cleanup, Kubernetes-auth delegation, and gitignore
coverage for local lease files. Offline validation is passing. T09 is `wait`
until live OpenBao audit evidence, response-wrap unwrap-once evidence, and
negative live mint checks can be collected.
**2026-07-01:** Live verification moved forward. make credential-tests passed 50 tests. make openbao-verify-token-grants-smoke minted a child token with policy warden-sign, proved it can sign via ssh/sign/agt-role, proved it cannot read policy metadata, and revoked it by accessor. make credential-exec-ops-warden-smoke passed with the child-only PATH hook, proving the flex-auth allow/deny smoke and vault-backed ops-warden signing path without manual VAULT_TOKEN paste. T09 is progress; remaining evidence is OpenBao audit-log reference collection plus response-wrap unwrap-once verification.
## T10 - Rollout and migration
```task
id: RAILIANCE-WP-0005-T10
status: wait
priority: medium
state_hub_task_id: "44ce4082-fa8f-44d0-8f86-172d14ecfb0e"
```
Roll out in phases.
Phases:
1. warden-sign VAULT_TOKEN pilot for flex-auth/ops-warden smoke.
2. Platform-readonly token helper for diagnostics.
3. Workload-specific grants for app repositories.
4. Optional split to a dedicated credential-broker repo if code grows beyond railiance-platform ownership.
Acceptance:
- The VAULT_TOKEN blocker from FLEX-WP-0007 is cleared without manual token paste.
- Operators have a documented fast path and a break-glass path.
- State Hub, ops-warden, key-cape, and flex-auth docs link to the same routing truth.
**2026-06-27:** Documented rollout phases, emergency revocation, delivery modes,
identity binding, flex-auth preflight, State Hub metadata, and routing ownership
in `docs/credential-broker.md`. T10 is `wait` on the live warden-sign pilot and
external routing-doc/catalog updates.
**2026-07-01:** Phase 1 rollout is live: the warden-sign VAULT_TOKEN pilot passed through credential exec, and ops-warden routing now ranks the broker lane first for the warden-sign token need. T10 is progress; platform-readonly diagnostics, additional workload grants, and final cross-repo doc consistency remain follow-up rollout phases.
## Exit Criteria
- A policy-approved actor can request or exec with a short-lived OpenBao token without seeing or pasting the raw token.
- The ops-warden vault-backed smoke can run without manual VAULT_TOKEN handling.
- All issued credentials are bounded, auditable, and revocable.
- State Hub and workplans contain only non-secret metadata.
- The credential routing catalog points token/dynamic-lease requests to railiance-platform.

View File

@@ -0,0 +1,359 @@
---
id: RAILIANCE-WP-0006
type: workplan
title: "Workload KV Access Lanes for ops-warden Fetch"
domain: financials
repo: railiance-platform
status: finished
owner: codex
topic_slug: railiance
planning_priority: high
planning_order: 6
created: "2026-06-27"
updated: "2026-06-29"
depends_on_workplans:
- RAIL-PL-WP-0002
- RAILIANCE-WP-0004
related_state_hub_messages:
- "551031d1-335e-4db8-9535-820fea52d0a3"
- "f76d3a9e-a98f-4081-885d-b79d94312699"
state_hub_workstream_id: "96c8a93d-7a5a-4fa9-8f7b-865119551da3"
---
# RAILIANCE-WP-0006 - Workload KV Access Lanes for ops-warden Fetch
## Goal
Provision concrete, least-privilege OpenBao workload KV read lanes that
`ops-warden` can expose through `warden access --fetch` / `--exec` without
holding secret values itself.
The immediate request is for `whynot-design` to retrieve its npm publish token.
The path must be concrete, policy-scoped, and documented so the ops-warden
catalog can replace the current unresolved template path with a live
`whynot-design-npm-publish` entry.
No task in this workplan may paste, commit, log, or send secret values through
Git, State Hub, chat, prompts, or workplan text.
## Requirements Reviewed
Ops-warden message `551031d1-335e-4db8-9535-820fea52d0a3` asks
`railiance-platform` to provide non-secret pointers for:
- a concrete OpenBao KV path and field for `NPM_AUTH_TOKEN`;
- the KV mount used by the path;
- the OIDC login role for whynot-design or its operator identity;
- a read policy scoped to whynot-design's identity/service account;
- the flex-auth policy reference, if pre-approval is required.
Once these pointers are live, ops-warden will add a dedicated
`whynot-design-npm-publish` access catalog entry and a playbook, then notify
whynot-design.
## Proposed Contract
Use the workload credential convention documented in `docs/openbao.md`:
```text
platform/workloads/<tenant-or-org>/<workload>/<secret-purpose>
```
For this lane, the proposed non-secret contract is:
| Item | Proposed value |
| --- | --- |
| KV mount | `platform` |
| Tenant/org | `coulomb` |
| Workload/project | `whynot-design` |
| CLI path | `platform/workloads/coulomb/whynot-design/npm-publish` |
| KV-v2 policy data path | `platform/data/workloads/coulomb/whynot-design/npm-publish` |
| KV-v2 policy metadata path | `platform/metadata/workloads/coulomb/whynot-design/npm-publish` |
| Secret field | `NPM_AUTH_TOKEN` |
| OpenBao read policy | `workload-kv-read-whynot-design-npm-publish` |
| OIDC auth mount | `netkingdom` unless KeyCape compatibility requires `keycape` |
| OIDC role | `whynot-design-workload-kv-read` |
| Kubernetes auth role | `whynot-design-workload-kv-read` if an in-cluster service account consumes it |
| flex-auth ref | `secret.read:whynot-design` if tenant policy requires pre-approval |
The expected caller-facing read shape is:
```bash
bao login -method=oidc -path=netkingdom role=whynot-design-workload-kv-read
bao kv get -field=NPM_AUTH_TOKEN platform/workloads/coulomb/whynot-design/npm-publish
```
The command shape is illustrative only. Verification must avoid printing the
secret value; use attended operator checks or commands that prove read access
without persisting the token in logs.
## Tasks
## T01 - Capture ops-warden request and path contract
```task
id: RAILIANCE-WP-0006-T01
status: done
priority: high
state_hub_task_id: "0c93496a-48bf-44e7-a75b-52e51e2639bc"
```
Record the ops-warden request, existing workload path convention, and proposed
whynot-design contract in this workplan.
Acceptance:
- The workplan names the concrete path, field, mount, policy, auth role, and
optional flex-auth ref needed by ops-warden.
- The plan distinguishes non-secret pointers from secret values.
- The plan keeps this workload KV read lane separate from
`RAILIANCE-WP-0005`, which tracks short-lived OpenBao token issuance for the
ops-warden signing smoke.
**2026-06-27:** Reviewed the unread ops-warden request and existing
`platform/workloads/<tenant-or-org>/<workload>/<secret-purpose>` convention.
Captured the proposed `whynot-design` npm publish lane above with no secret
values.
## T02 - Add least-privilege OpenBao read policy
```task
id: RAILIANCE-WP-0006-T02
status: done
priority: high
state_hub_task_id: "9c06d531-2566-4767-aa2f-8339605f23d5"
```
Create a concrete policy artifact for the whynot-design npm publish lane,
derived from `openbao/policies/workload-kv-read-template.hcl` but narrowed to
the selected `npm-publish` path.
Acceptance:
- A policy file under `openbao/policies/` defines read access to the exact
`platform/data/workloads/coulomb/whynot-design/npm-publish` path.
- Metadata/list capabilities are only as broad as needed for the caller and
ops-warden fetch UX.
- The policy grants no write, delete, patch, sudo, auth, or unrelated workload
capabilities.
- The policy name matches the pointer intended for ops-warden:
`workload-kv-read-whynot-design-npm-publish`.
**2026-06-27:** Added the concrete policy artifact at
`openbao/policies/workload-kv-read-whynot-design-npm-publish.hcl`. It grants
only `read` on the exact KV-v2 data and metadata paths for
`platform/workloads/coulomb/whynot-design/npm-publish`; it does not grant
write/delete/list/sudo/auth or sibling workload access. Added
`scripts/openbao-apply-workload-kv-lanes.sh`,
`make openbao-workload-kv-lanes-dry-run`, and
`make openbao-configure-workload-kv-lanes` for the source-owned policy apply
step. Dry-run passed. A live apply attempt with
`OPENBAO_WORKLOAD_KV_ARGS=--use-token-helper` reached unsealed OpenBao but was
denied with `403 permission denied` while writing the policy, so live policy
application waits on an approved platform-admin/operator token or a narrow
token-helper capability.
**2026-06-28:** Using the temporary operator token provided outside the repo,
Codex applied/confirmed the live policy in OpenBao. The verification read of the
policy succeeded and no secret values were printed or recorded.
## T03 - Define and apply auth bindings
```task
id: RAILIANCE-WP-0006-T03
status: done
priority: high
state_hub_task_id: "a217371a-0f85-40c6-b691-ac67834c86b5"
```
Define the auth role that lets whynot-design or an approved operator identity
read the lane as itself.
Acceptance:
- The OIDC login role is documented as
`bao login -method=oidc -path=netkingdom role=whynot-design-workload-kv-read`,
or a different approved role is recorded with the reason.
- The role attaches only the whynot-design npm publish read policy.
- If an in-cluster whynot-design service account consumes the token, the
Kubernetes auth role binds only the approved namespace and service account.
- Compatibility with the legacy `keycape` auth mount is either configured or
explicitly declined.
**2026-06-27:** Documented the intended OIDC role pointer as
`auth/netkingdom/role/whynot-design-workload-kv-read` in
`docs/workload-kv-access-lanes.md`. Live application is waiting on confirmation
of the KeyCape/NetKingdom whynot-design bound claim or approved service-account
subject; do not create an unbounded OIDC role.
**2026-06-28:** Created/confirmed
`auth/netkingdom/role/whynot-design-workload-kv-read` with
`groups=["whynot-design"]`, only the
`workload-kv-read-whynot-design-npm-publish` policy, `ttl=15m`, and the approved
browser/local CLI callback URIs.
**2026-06-28:** Positive verification found the OIDC role was missing
`oidc_scopes`, causing OpenBao login to fail with `groups claim not found`.
Updated the live role and source CCR to request `openid`, `profile`, `email`,
and `groups`, matching the platform-admin OIDC scope shape.
## T04 - Provision the KV path without exposing the token
```task
id: RAILIANCE-WP-0006-T04
status: done
priority: high
state_hub_task_id: "c43724a3-c83e-4ab6-b7d1-e427fd93a9a9"
```
Have an approved operator create or confirm the OpenBao KV entry for the npm
publish token.
Acceptance:
- The path exists at
`platform/workloads/coulomb/whynot-design/npm-publish`.
- The field is named exactly `NPM_AUTH_TOKEN`.
- The token value is entered through an approved operator/OpenBao path and is
never written to Git, State Hub, chat, prompts, shell history, or workplan
text.
- Non-secret evidence records only the path, field name, actor, timestamp,
policy name, and verification result.
**2026-06-27:** The concrete path and field are now documented. Live secret
provisioning is waiting on an approved operator/OpenBao custody path for the
actual `NPM_AUTH_TOKEN` value.
**2026-06-28:** Confirmed the OpenBao metadata at
`platform/workloads/coulomb/whynot-design/npm-publish` includes
`catalog-id=whynot-design-npm-publish` and that the `NPM_AUTH_TOKEN` field is
present. The value was not printed, recorded, or copied into Git, State Hub,
chat, or workplans.
## T05 - Verify caller-scoped fetch behavior
```task
id: RAILIANCE-WP-0006-T05
status: done
priority: high
state_hub_task_id: "dc1f470b-e78a-48a9-9957-965aed47861f"
```
Prove that the authorized identity can read the token through the intended
OpenBao path and that unauthorized identities cannot.
Acceptance:
- An approved whynot-design identity or operator role can authenticate and
perform the fetch without unresolved `<...>` placeholders.
- Negative verification shows a non-whynot identity cannot read the path.
- Verification output contains no token value.
- OpenBao audit evidence exists for the authorized read and denied read, with
only non-secret request ids/timestamps recorded in the workplan or State Hub.
**2026-06-27:** Verification is waiting on live policy/role application and
secret provisioning. The runbook requires positive and negative fetch evidence
without printing the token value.
**2026-06-28:** Non-secret operator checks now pass for policy, auth role,
metadata, and field presence. Remaining verification is the attended
whynot-design OIDC positive check and a non-whynot denial check, both without
printing the token.
**2026-06-29:** Positive and negative caller verification passed without
printing the token value. The negative check failed OIDC login with the expected
groups bound-claim mismatch. `platform-root` was restored to the
`whynot-design` group after the temporary negative-test removal.
## T06 - Coordinate ops-warden catalog activation
```task
id: RAILIANCE-WP-0006-T06
status: done
priority: high
state_hub_task_id: "8e84ec19-01db-4baf-a532-de87e51d4994"
```
Send ops-warden the non-secret pointers needed to create and activate its
dedicated access catalog entry.
Acceptance:
- The State Hub reply to ops-warden includes only path, field, KV mount,
OIDC role, policy name/path, optional flex-auth ref, and runbook location.
- Ops-warden confirms the `whynot-design-npm-publish` catalog entry no longer
contains unresolved placeholders.
- `warden access "npm auth token" --fetch` or the agreed exact selector resolves
to the whynot-design lane and proxies the read as the caller.
- ops-warden confirms it holds no token value and only proxies OpenBao access.
**2026-06-27:** Added `docs/workload-kv-access-lanes.md` with the non-secret
handoff payload for ops-warden and sent the pointers by State Hub message. The
entry should remain draft/non-active until live OpenBao provisioning and
verification complete.
**2026-06-28:** The generic `openbao-api-key` ops-warden access lane can proxy
the check with explicit `--path` and `--field`, but the dedicated
`whynot-design-npm-publish` route is not yet present in the ops-warden routing
catalog. Keep activation pending until caller verification and catalog update.
**2026-06-29:** `CCR-2026-0001` is now active with
`access_frontdoor.readiness=ready` and `resolvable=true`. ops-warden still needs
to confirm that its dedicated `whynot-design-npm-publish` catalog selector
resolves through the caller-scoped lane.
**2026-06-29:** ops-warden confirmed in State Hub message
`f76d3a9e-a98f-4081-885d-b79d94312699` that catalog selector
`whynot-design-npm-publish` is `status: active`, `resolvable: true`, and wired
to the owner-confirmed lane:
`platform/workloads/coulomb/whynot-design/npm-publish`, field
`NPM_AUTH_TOKEN`, OIDC role `whynot-design-workload-kv-read`, and policy
`workload-kv-read-whynot-design-npm-publish`. ops-warden also confirmed it
notified whynot-design with `warden access whynot-design-npm-publish --exec -- npm publish`,
and that the sibling lanes remain draft for separate planning.
## T07 - Decide whether to batch sibling workload-KV requests
```task
id: RAILIANCE-WP-0006-T07
status: done
priority: medium
state_hub_task_id: "0b3ab5f5-e933-41f2-b29a-ab4ac50593aa"
```
Ops-warden noted similar still-open access lanes for
`issue-core-ingestion-api-key` and `openrouter-llm-connect`. Decide whether to
batch those paths in the same provisioning pass or keep this workplan scoped to
whynot-design.
Acceptance:
- The decision is recorded without secret values.
- If batching is approved, add concrete sub-tasks or a follow-up workplan for
each additional lane.
- If batching is deferred, notify ops-warden that this workplan will deliver
whynot-design first and leave the sibling entries for separate planning.
**2026-06-27:** Initially deferred sibling lanes (`issue-core-ingestion-api-key`
and `openrouter-llm-connect`) so the whynot-design npm token request could be
serviced first. The later ops-warden batch follow-up is now represented as
proposed CCRs in `RAILIANCE-WP-0007`, still unapproved and unresolvable until
human review and verification.
**2026-06-29:** Reviewed the sibling lane suggestions against `INTENT.md`.
Created follow-up workplans `RAILIANCE-WP-0009` for the issue-core runtime
ingestion credential lane and `RAILIANCE-WP-0010` for the llm-connect
OpenRouter provider key lane. Both plans keep this repo's scope limited to
shared platform secret custody, least-privilege OpenBao/External Secrets
delivery, verification, and ops-warden front-door handoff.
## Exit Criteria
- The whynot-design npm publish token has a concrete OpenBao KV path, field,
read policy, and auth role.
- The authorized caller can fetch the token as itself through OpenBao and
ops-warden without ops-warden storing the value.
- Unauthorized reads are denied.
- ops-warden has enough non-secret pointers to activate
`whynot-design-npm-publish`.
- No secret values appear in Git, State Hub, chat, prompts, logs, or workplans.

View File

@@ -0,0 +1,404 @@
---
id: RAILIANCE-WP-0007
type: workplan
title: "Credential Change Proposal Review Workflow"
domain: financials
repo: railiance-platform
status: finished
owner: codex
topic_slug: railiance
planning_priority: high
planning_order: 7
created: "2026-06-27"
updated: "2026-06-30"
depends_on_workplans:
- RAIL-PL-WP-0002
- RAILIANCE-WP-0005
- RAILIANCE-WP-0006
state_hub_workstream_id: "4d7ce243-f40a-4249-a46a-a24f75d6fe4c"
---
# RAILIANCE-WP-0007 - Credential Change Proposal Review Workflow
## Goal
Create a proposal -> review -> approve/deny with comment -> apply -> verify
workflow for credential and it-sec changes, so operators do not need to author
or mentally validate raw OpenBao commands.
The first target is the whynot-design npm token lane from `RAILIANCE-WP-0006`.
The workflow should then generalize to workload KV paths, OpenBao token roles,
ops-warden access catalog entries, External Secrets lanes, credential rotation,
deactivation, and compromise handling.
## Direction
Do not start by extending OpenBao. Instead, build a small approval control
plane around OpenBao:
- OpenBao remains the enforcement, secret storage, token, and audit engine.
- State Hub stores non-secret request lifecycle, comments, decisions, and
evidence.
- Repo files store reviewable non-secret request specs and generated policy
artifacts.
- Agents and CLIs create proposals and render them for human review.
- Humans approve or deny with comments.
- Only approved requests can be applied by an operator-controlled runner or
interactive runbook.
If the workflow proves valuable, a later UI or OpenBao extension can surface the
same request index and statuses.
## Proposed Object
Introduce a non-secret Credential Change Request, or `CCR`.
Each CCR captures:
- request id, title, requester, reviewer, approver, and applier;
- target tenant/workload/environment/purpose;
- OpenBao mount, path, fields, policies, auth roles, and bound claims;
- access front door such as ops-warden, External Secrets, CSI, or direct caller
fetch;
- risk classification and approval requirements;
- generated apply plan and verification plan;
- rollback, deactivate, rotate, and compromise response plan;
- comments, decision, timestamps, and non-secret audit evidence.
Each CCR explicitly excludes secret values, token values, private keys,
passwords, unseal/recovery material, and secret-bearing command output.
## Tasks
## T01 - Record the approval workflow design
```task
id: RAILIANCE-WP-0007-T01
status: done
priority: high
state_hub_task_id: "c82ee783-80f1-48da-a9ed-4565eac699fc"
```
Document the desired operator workflow and why it should sit around OpenBao
rather than inside the OpenBao UI initially.
Acceptance:
- The design describes the proposal, review, approval/denial, apply, verify,
activate, deactivate, rotate, and compromised states.
- The design names where State Hub, OpenBao, ops-warden, repo files, agents,
and interactive runbooks fit.
- The design keeps secret values out of State Hub, Git, chat, and prompts.
**2026-06-27:** Added `docs/credential-change-approval.md` with the control
plane direction, CCR object, state machine, State Hub/OpenBao/ops-warden roles,
interactive runbook role, and compromise/deactivation path.
## T02 - Define the CCR schema and storage layout
```task
id: RAILIANCE-WP-0007-T02
status: done
priority: high
state_hub_task_id: "d50fb9e2-68c2-4a2b-8476-ce646d13e60a"
```
Create a versioned non-secret schema for credential change requests.
Acceptance:
- A schema exists for `workload-kv-read` requests covering mount, path, fields,
policy name, auth role, bound claims, access front door, verification plan,
and activation conditions.
- The schema supports decision metadata: requested, proposed, approved,
denied, needs_changes, applied, verified, active, deactivated, rotated,
compromised, superseded, and cancelled.
- The schema supports comments and references State Hub ids without storing
secrets.
- Example CCR fixtures include the whynot-design npm token lane.
**2026-06-27:** Added `schemas/credential-change-request.schema.yaml`, the
`credential-change-requests/` storage directory, and
`credential-change-requests/CCR-2026-0001-whynot-design-npm-publish.yaml` as the
first non-secret CCR fixture. The whynot CCR is intentionally `proposed` and
marks the bound claim as unconfirmed, so apply is blocked until review.
## T03 - Add offline validation and rendering
```task
id: RAILIANCE-WP-0007-T03
status: done
priority: high
state_hub_task_id: "012f05cd-30ce-43dd-802b-4acc938db133"
```
Add a helper that validates CCR files and renders human review summaries.
Acceptance:
- Invalid CCRs fail before any OpenBao apply is attempted.
- The renderer produces a compact review block that a human can understand in
chat or State Hub.
- The renderer highlights risky fields: broad claims, wildcard paths,
privileged policies, missing negative verification, and missing deactivation
plan.
- A secret-pattern scan rejects likely token values in CCR files.
**2026-06-27:** Added `scripts/credential-change.py validate` and `render`,
plus Make targets `credential-change-validate` and `credential-change-render`.
Validation rejects secret-looking markers and broad/unsafe request shapes; render
produces the chat/State Hub review summary and highlights unconfirmed bound
claims. CCRs now also carry machine-readable front-door readiness fields:
`access_frontdoor.readiness` and `access_frontdoor.resolvable`. Unit coverage
lives in `tests/test_credential_change.py`.
## T04 - Generate OpenBao apply plans from approved CCRs
```task
id: RAILIANCE-WP-0007-T04
status: done
priority: high
state_hub_task_id: "1b2e7752-815c-46f8-a2e2-212e8d04da80"
```
Generate deterministic, reviewable OpenBao apply plans from CCRs.
Acceptance:
- A workload KV CCR can generate policy HCL and auth-role commands or API
payloads.
- The plan includes a dry-run mode and a diff against existing source
artifacts when available.
- Applying a plan is refused unless the CCR is approved.
- The applier uses an approved operator authority path and does not accept raw
tokens in argv or logs.
**2026-06-27:** Added `plan` and guarded `apply-plan` rendering for workload KV
CCRs, with Make targets `credential-change-plan` and
`credential-change-apply-plan`. `apply-plan` currently refuses any CCR that is
not `approved` and also refuses unconfirmed bound claims. Remaining T04 work is
to add a richer diff against existing source artifacts and eventually bridge
from reviewed plan to the interactive live applier.
**2026-06-28:** Added OIDC `allowed_redirect_uris` to the CCR contract and
generated role payloads after live OpenBao rejected an OIDC role without
callbacks. Unit coverage now checks the generated whynot-design role payload.
**2026-06-30:** Added source-artifact diff rendering to `plan` and delegated
`applier-dry-run` output. The generated plan now reports whether the checked-in
policy artifact matches the CCR-generated HCL and shows a unified diff when it
does not. Approved-only `apply-plan`/`operator-commands` remain gated by CCR
status and confirmed auth binding.
## T05 - Add chat/CLI approval commands
```task
id: RAILIANCE-WP-0007-T05
status: done
priority: high
state_hub_task_id: "e6d4d2d1-1881-4db7-92f8-05e3fdb846ae"
```
Make the workflow usable from chat and command line.
Acceptance:
- Operators can approve, deny, or request changes with a comment.
- Approvals/denials are recorded as non-secret State Hub events and in the CCR
file or linked decision record.
- The system refuses apply when the latest human decision is denied or
needs_changes.
- Agents can propose changes and respond to review comments without receiving
secret values.
**2026-06-27:** Added file-backed `approve`, `deny`, and `needs-changes`
commands that require reviewer and comment text and append non-secret review
comments to the CCR. Added `confirm-binding` for explicit non-secret auth
binding confirmation. Added `status` plus Make targets
`credential-change-status` and `credential-change-status-json` so ops-warden can
consume `readiness`/`resolvable` without scraping prose. Remaining T05 work is
State Hub decision-event emission and tighter chat integration. Created a
State Hub decision for `CCR-2026-0001` and added `sync-decision` so resolved
State Hub decisions can update the file-backed CCR status.
**2026-06-30:** Added optional `--record-state-hub` emission for approve, deny,
and needs-changes commands. Review comments are checked for known secret markers
before being written, and the State Hub progress event records only non-secret
CCR id/path/policy/field/auth-role metadata plus the reviewer comment.
## T06 - Build an interactive runbook for apply and verify
```task
id: RAILIANCE-WP-0007-T06
status: done
priority: high
state_hub_task_id: "3c3fc38c-afa4-4367-b3e6-ba4b286ced30"
```
Wrap privileged application in an operator-friendly guided runbook.
Acceptance:
- The runbook loads an approved CCR, shows the plan, asks for final attended
confirmation, then applies policy/auth metadata.
- Secret value entry is handled through an approved OpenBao/operator path and
is never echoed or logged.
- Positive and negative verification steps are guided.
- Non-secret evidence is recorded automatically.
**2026-06-30:** Added `scripts/credential-change.py runbook <CCR>` and Make
target `credential-change-runbook` to render the attended operator checklist,
final confirmation phrase, metadata apply guidance, secret custody instructions,
positive/negative verification steps, activation conditions, and evidence
commands. `runbook --execute-metadata` is opt-in, requires the exact `APPLY
<CCR-ID>` confirmation phrase, uses the local `bao` CLI with ambient approved
operator authority, writes only policy/auth metadata, and records a non-secret
`metadata_apply` evidence entry. Added `record-evidence` plus Make target
`credential-change-record-evidence` so operators can append apply, secret
provisioning, verification, and activation evidence to the CCR and optionally
State Hub without storing secret values.
## T07 - Pilot with whynot-design and ops-warden
```task
id: RAILIANCE-WP-0007-T07
status: done
priority: high
state_hub_task_id: "07a7d8bf-5528-41c8-a791-d6ccd0466a33"
```
Use the existing whynot-design npm token lane as the first end-to-end pilot.
Acceptance:
- The current whynot-design lane is represented as a CCR.
- The CCR is rendered and reviewed in chat or State Hub.
- A human approval or denial comment is recorded.
- If approved, the runbook applies the policy/auth metadata, guides secret
provisioning, verifies access, and notifies ops-warden.
- ops-warden activates its catalog entry only after CCR verification.
**2026-06-27:** The whynot-design lane is represented as `CCR-2026-0001` and
can be rendered for review. The whynot-design bound claim was confirmed from
operator chat context and recorded in the CCR, but it remains proposed/unapproved,
so live apply and ops-warden activation are correctly blocked.
**2026-06-27:** Converted the ops-warden batch follow-up
`fe5b1696-8956-4bd5-9d6f-dbde1901a076` into three proposed CCRs:
`CCR-2026-0001` for `whynot-design-npm-publish`, `CCR-2026-0002` for
`issue-core-ingestion-api-key`, and `CCR-2026-0003` for
`llm-connect-openrouter-api-key`. All three are explicitly `readiness: template`
and `resolvable: false` until owner confirmation, approval, OpenBao apply,
secret provisioning, and verification are complete.
**2026-06-28:** Synced State Hub decision
`250669d0-8475-4527-9624-cd072249f9a9` into `CCR-2026-0001`; the lane is now
`approved` with confirmed binding and `apply_allowed: true`. A live OpenBao
policy apply using the available token helper reached the active OpenBao pod but
still failed with `403 permission denied` on
`sys/policies/acl/workload-kv-read-whynot-design-npm-publish`, so the front door
remains `readiness: template` and `resolvable: false`. Added guarded
`credential-change-operator-commands` output so a platform operator can run the
reviewed non-secret policy and auth-role commands without hand-writing them;
secret value provisioning and verification remain under approved custody.
**2026-06-28:** After correcting the tenant/org to `coulomb`, the corrected
approval was synced from State Hub decision
`e6381a56-6b04-4fd5-b2de-f3ef59cde888`; `CCR-2026-0001` is approved and
`apply_allowed: true` for
`platform/workloads/coulomb/whynot-design/npm-publish`. The operator reported
secret provisioning likely completed, but Codex metadata-only verification still
received `403 permission denied`. Prepared
`docs/whynot-design-npm-publish-handoff.md` as the next-session checklist for
policy, auth-role, metadata verification, positive verification, negative
verification, and activation without printing the token.
**2026-06-28:** With the temporary operator token, Codex applied/confirmed the
OpenBao read policy and OIDC role, confirmed metadata `catalog-id`, and confirmed
`NPM_AUTH_TOKEN` field presence without printing or recording the value. The CCR
now records non-secret evidence for that apply check. Positive whynot-design and
negative non-whynot caller verification still gate `active`/`ready`.
**2026-06-29:** The whynot-design pilot completed OpenBao verification. Positive
fetch succeeded with output suppressed, negative login failed with the expected
groups bound-claim mismatch, `platform-root` membership was restored afterward,
and `CCR-2026-0001` is now active/ready/resolvable. ops-warden catalog
confirmation remains the external closeout step.
**2026-06-30:** Closed the pilot task based on the active/ready/resolvable CCR
state and prior ops-warden catalog confirmation that the selector is active and
resolvable. The remaining lifecycle work is now tracked separately in T08.
## T08 - Add deactivation, rotation, and compromise flows
```task
id: RAILIANCE-WP-0007-T08
status: done
priority: medium
state_hub_task_id: "23d6ef9d-8dbc-4468-b486-5ec8ada71130"
```
Support lifecycle states beyond initial creation.
Acceptance:
- Existing credentials can be imported as CCR-backed inventory without secret
values.
- Operators can mark a lane deactivated, rotated, or compromised with reason
and evidence.
- Deactivation disables the relevant access front door and auth/policy path.
- Compromise flow records blast-radius notes and required follow-up tasks.
**2026-06-30:** Added `lifecycle-plan`, `lifecycle-event`, and
`import-inventory` commands plus Make targets. Lifecycle plans render
deactivation, rotation, and compromise guidance, including access-front-door
state changes and OpenBao metadata disable commands for deactivation or
compromise. Lifecycle events update CCR status/front-door readiness, append
non-secret lifecycle evidence, and optionally post State Hub progress.
Compromise events accept non-secret blast-radius and follow-up references.
`import-inventory` can create a CCR-backed inventory file and matching read
policy artifact for an existing lane without asking for or storing secret
values.
## T09 - Add decision templates and guided review actions
```task
id: RAILIANCE-WP-0007-T09
status: done
priority: high
state_hub_task_id: "c436fd8b-cd82-4600-81b0-87ec069d7ae6"
```
Remove the current friction where reviewers must know magic rationale prefixes
for State Hub decisions to sync back into CCR status.
Acceptance:
- Each CCR review page or chat handoff shows explicit approve, deny, and needs
changes templates.
- Generated templates include the accepted prefixes (`APPROVE:`, `DENY:`, and
`NEEDS_CHANGES:`) and pre-fill the CCR id, corrected path, policy, auth role,
and non-secret rationale prompt.
- The dashboard or agent response links directly to the decision and states what
phrase or button will be recognized.
- The sync tooling refuses ambiguous free-text approvals with a friendly message
that shows the valid templates.
- Future UI work can replace prefix parsing with structured decision outcomes
without changing the CCR audit trail.
**2026-06-30:** Added `scripts/credential-change.py decision-templates <CCR>`
and Make target `credential-change-decision-templates`. The generated templates
include accepted prefixes, CCR id, KV path, policy, auth-role path, and the
linked State Hub decision. Ambiguous State Hub rationale text now fails with the
valid templates in the error message.
## Exit Criteria
- A human can review and approve or deny a credential/security change without
writing raw OpenBao commands.
- An approved request can be applied by an operator-controlled helper or
interactive runbook.
- State Hub and repo artifacts contain non-secret lifecycle, decision, and
evidence records.
- OpenBao remains the enforcement and audit source for actual secret access.
- The whynot-design npm token lane can complete through this workflow.

View File

@@ -0,0 +1,281 @@
---
id: RAILIANCE-WP-0008
type: workplan
title: "OpenBao Approved Automation Delegation"
domain: financials
repo: railiance-platform
status: active
owner: codex
topic_slug: railiance
planning_priority: high
planning_order: 8
created: "2026-06-28"
updated: "2026-06-30"
depends_on_workplans:
- RAIL-PL-WP-0002
- RAILIANCE-WP-0005
- RAILIANCE-WP-0006
- RAILIANCE-WP-0007
state_hub_workstream_id: "671898ef-2378-4814-b8f6-066148cdad46"
---
# RAILIANCE-WP-0008 - OpenBao Approved Automation Delegation
## Goal
Create a narrow OpenBao automation delegation path so approved credential and
it-sec changes can be applied without handing broad `platform-admin` power to an
agent, while still preserving different build, test, and production security
requirements.
This closes the current gap where reviewed CCRs can be approved and rendered,
but live application still fails with `403 permission denied` on
`sys/policies/acl/...` unless a human operator uses broad OpenBao admin
privileges.
## Problem
The current model has two bad modes:
- fully manual OpenBao UI work, which can unblock one credential but is slow,
hard to review, and easy to drift from the CCR;
- broad `platform-admin`, which is too powerful for routine automation and too
risky to hand to agents.
The issue appears in multiple workstreams:
- `RAILIANCE-WP-0005` token-grant apply failed on
`sys/policies/acl/credential-broker-warden-sign-issuer`;
- `RAILIANCE-WP-0007` whynot-design CCR apply failed on
`sys/policies/acl/workload-kv-read-whynot-design-npm-publish`.
Both are approved non-secret metadata changes. They should not require a human
to hand-type OpenBao policy and auth-role writes once review is complete.
## Direction
Add one or more constrained OpenBao applier identities that can apply only
reviewed, generated metadata for approved change classes.
The applier must not be a general `platform-admin` replacement. It should:
- verify a resolved State Hub decision or CCR status before live mutation;
- write only allowed policy names and auth role prefixes;
- never read or print secret values;
- record non-secret apply evidence;
- keep production secret value provisioning under approved operator custody
until a separate wrapped or dual-control flow is approved.
## Environment Model
Build and development:
- automation may apply approved sandbox metadata;
- generated test secrets may be allowed only in non-production mounts;
- approvals can be lightweight but still recorded.
Test and staging:
- automation may apply approved metadata after validation and owner review;
- verification must include positive and negative access checks;
- secret values should use wrapped delivery or operator custody depending on
risk classification.
Production:
- automation may apply only approved non-secret metadata such as narrow ACL
policies and auth roles;
- production secret values remain out-of-band operator custody unless a later
workplan creates a stronger wrapped or dual-control provisioning path;
- activation requires verification evidence before ops-warden or another front
door becomes resolvable.
## Proposed Policy Shape
Start with one production candidate policy, for example
`credential-change-prod-applier`, and keep it intentionally narrow:
- allow `create`, `update`, and `read` on `sys/policies/acl/workload-kv-read-*`;
- allow `create`, `update`, and `read` on approved credential-broker issuer
policy names such as `credential-broker-*-issuer`;
- allow `create`, `update`, and `read` on selected auth role prefixes such as
`auth/netkingdom/role/*-workload-kv-read`,
`auth/kubernetes/role/*`, and `auth/token/roles/credential-broker-*`;
- allow read/list only where needed for idempotent verification;
- deny broad `sys/*`, `auth/*`, `platform/*`, `identity/*`, `root`, and
`platform-admin` semantics.
Policy-name and role-name restrictions should be enforced in both OpenBao ACLs
and the local applier script.
## Tasks
## T01 - Specify delegated applier policy boundaries
```task
id: RAILIANCE-WP-0008-T01
status: done
priority: high
state_hub_task_id: "d19fdfc5-addb-4813-8086-3aca2e948cea"
```
Define build, test, and production applier capabilities, including exact
OpenBao paths, allowed name prefixes, denied paths, and required audit evidence.
Acceptance:
- A human reviewer can see which OpenBao paths each environment can mutate.
- Production applier policy excludes secret value reads and broad admin paths.
- The proposal covers both workload KV read lanes and credential broker issuer
policies.
**2026-06-29:** Added `docs/openbao-approved-automation-delegation.md` and
`openbao/policies/credential-change-prod-applier.hcl`. The document defines
build/development, test/staging, and production boundaries, the allowed
production metadata mutation surface, denied secret/admin paths, and required
non-secret evidence. The production policy candidate allows only reviewed
metadata writes for workload KV read policies, credential-broker issuer
policies, approved auth-role prefixes, and self capability checks; it does not
grant secret value reads or writes.
## T02 - Implement a CCR-aware applier dry-run
```task
id: RAILIANCE-WP-0008-T02
status: done
priority: high
state_hub_task_id: "2613f40d-fbd9-44f3-a864-85ec1d54e8f7"
```
Extend the credential-change tooling so a proposed applier can validate a CCR,
check approval state, render the exact mutations, and refuse any out-of-policy
policy name, auth role, mount, path, or environment.
Acceptance:
- Dry-run succeeds for `CCR-2026-0001`.
- Dry-run refuses unapproved CCRs.
- Dry-run refuses attempts to create `root`, `platform-admin`, wildcard, or
unrelated policy names.
**2026-06-29:** Added `scripts/credential-change.py applier-dry-run <CCR>` and
Make target `credential-change-applier-dry-run`. The dry-run validates the CCR,
requires approved/applied/verified/active status, requires confirmed auth
bindings, verifies the OpenBao mount/path/policy/role stay inside the delegated
metadata surface, compares the policy artifact to the generated CCR policy body,
and renders only policy/auth-role mutations. It explicitly leaves secret value
writes, secret reads, and front-door activation out of scope. Unit tests cover
the active whynot-design CCR success path, unapproved CCR refusal, and rejection
of `platform-admin`/out-of-scope mount and path attempts. `make
credential-tests` passed with 28 tests.
## T03 - Add non-production applier role first
```task
id: RAILIANCE-WP-0008-T03
status: progress
priority: medium
state_hub_task_id: "ff927a19-50fb-4351-8db1-c60a0cce0995"
```
Create a build/test applier identity and prove it can apply approved metadata in
a non-production lane without gaining unrelated OpenBao permissions.
Acceptance:
- Apply succeeds in a non-production mount or namespace.
- Negative checks prove unrelated policy/auth/secret paths are denied.
- Evidence is recorded without secret values.
**2026-06-30:** Added the non-production metadata-only policy candidate
`openbao/policies/credential-change-nonprod-applier.hcl` and documented that
generated test-secret paths require separate CCR-backed approval. Live non-prod
identity creation and positive/negative OpenBao evidence remain to close this
task.
**2026-06-30:** Added the guarded `applier-apply` execution path that reuses the
CCR dry-run guardrails, requires exact `DELEGATED APPLY <CCR-ID>` confirmation,
uses the local `bao` CLI with ambient delegated applier authority, writes only
policy/auth-role metadata, and records non-secret `delegated_metadata_apply`
evidence. Non-production task closure still needs a live build/test applier
identity plus positive and negative capability evidence.
**2026-06-30:** Added `scripts/openbao-apply-credential-change-appliers.py` and
Make target `openbao-credential-change-appliers-dry-run` to install/dry-run the
non-production applier policy plus bounded `auth/token/roles/credential-change-
nonprod-applier` role. The token role allows only the matching applier policy,
disallows `root` and `platform-admin`, disables the default policy, and does not
issue tokens by itself. Live non-production apply and denial evidence remains
the closeout gate.
## T04 - Add production metadata applier with human approval gate
```task
id: RAILIANCE-WP-0008-T04
status: progress
priority: high
state_hub_task_id: "414abd65-22d3-420f-994d-f7fdd1302db5"
```
Create the production metadata applier path and require a resolved CCR/State Hub
approval before mutation.
Acceptance:
- Approved `CCR-2026-0001` metadata can be applied without `platform-admin`.
- Unapproved CCRs fail closed.
- Secret value provisioning is still not automated in production.
**2026-06-30:** Strengthened the production gate by adding source-artifact
checks to the CCR applier dry-run and documenting that unapproved CCRs fail
closed before OpenBao mutation rendering. The production policy candidate exists
and remains metadata-only; live delegated identity creation/application evidence
still needs an operator-held OpenBao step.
**2026-06-30:** Added `applier-apply` and Make targets
`credential-change-applier-apply-plan` / `credential-change-applier-apply`. The
command fails closed for unapproved CCRs, renders the dry-run payload before
mutation, requires exact confirmation, does not accept tokens in argv, leaves
secret values out of scope, and appends State Hub/file-backed non-secret apply
evidence when requested. Production closure still requires live execution using
the constrained applier identity rather than broad `platform-admin`.
**2026-06-30:** Added `scripts/openbao-apply-credential-change-appliers.py` and
Make targets `openbao-credential-change-appliers-dry-run` /
`openbao-configure-credential-change-appliers` to configure the production
`credential-change-prod-applier` policy and bounded token role. The role allows
only `credential-change-prod-applier`, disallows `root` and `platform-admin`,
uses service tokens, disables default policy attachment, and keeps token issuance
outside the setup script. Production closure still needs a live run and
capability evidence using this constrained identity.
## T05 - Close the whynot-design pilot
```task
id: RAILIANCE-WP-0008-T05
status: wait
priority: high
state_hub_task_id: "18f34c95-4d2b-4a08-a5ad-5ab700ff9dfe"
```
Use the delegated production metadata applier to finish the whynot-design npm
publish token lane after the actual token is provisioned through approved
custody.
Acceptance:
- Policy and auth role are applied by the delegated applier.
- `NPM_AUTH_TOKEN` is provisioned through approved custody.
- Positive and negative verification pass without printing the token.
- `CCR-2026-0001` can move to `active`.
- ops-warden can mark `whynot-design-npm-publish` ready/resolvable.
## Exit Criteria
- Routine approved OpenBao metadata changes no longer require broad
`platform-admin`.
- Production automation cannot read or exfiltrate managed secret values.
- Build, test, and production each have distinct, documented security
requirements.
- CCR approval, apply, verification, and front-door activation form one
reviewable chain.

View File

@@ -0,0 +1,276 @@
---
id: RAILIANCE-WP-0009
type: workplan
title: "Issue-Core Runtime Ingestion Credential Lane"
domain: financials
repo: railiance-platform
status: active
owner: codex
topic_slug: railiance
planning_priority: high
planning_order: 9
created: "2026-06-29"
updated: "2026-06-30"
depends_on_workplans:
- RAIL-PL-WP-0002
- RAILIANCE-WP-0004
- RAILIANCE-WP-0007
- RAILIANCE-WP-0008
related_state_hub_messages:
- "f76d3a9e-a98f-4081-885d-b79d94312699"
related_ccrs:
- CCR-2026-0002
state_hub_workstream_id: "b059c81d-96f1-451f-896f-a05cd73744a1"
---
# RAILIANCE-WP-0009 - Issue-Core Runtime Ingestion Credential Lane
## Goal
Promote the draft `issue-core-ingestion-api-key` access lane from a proposed
CCR to a reviewed, least-privilege, verified OpenBao workload KV lane that
`issue-core` can consume through External Secrets and that `ops-warden` can
reference without holding secret values.
This follows the same platform pattern proven by `RAILIANCE-WP-0006`, but keeps
the issue-core lane independent so the field set, service-account binding,
verification evidence, and activation decision can be reviewed on their own.
No task in this workplan may paste, commit, log, or send secret values through
Git, State Hub, chat, prompts, shell history, or workplan text.
## Suggestion Reviewed
Ops-warden confirmed the whynot-design lane in State Hub message
`f76d3a9e-a98f-4081-885d-b79d94312699` and noted that
`issue-core-ingestion-api-key` remains draft on its side. The repo already has
the proposed non-secret CCR:
- `credential-change-requests/CCR-2026-0002-issue-core-ingestion-api-key.yaml`
## INTENT Fit
This work belongs in railiance-platform because it provides shared, secure,
operable secret custody and delivery behind stable interfaces. It does not add
issue-core application behavior. The platform-owned output is the OpenBao path,
read policy, Kubernetes auth role, External Secrets contract, verification
evidence, and ops-warden handoff.
The plan supports these `INTENT.md` principles:
- secure custody: secret values stay in OpenBao/operator custody;
- stable interfaces: issue-core consumes a documented path, fields, role, and
External Secrets target rather than internal OpenBao topology;
- operable and observable: activation requires positive and negative checks
plus non-secret audit evidence;
- independently evolvable: the credential store, auth role, and front door can
change underneath the consumer contract.
## Proposed Contract
| Item | Proposed value |
| --- | --- |
| CCR | `CCR-2026-0002` |
| ops-warden catalog id | `issue-core-ingestion-api-key` |
| Tenant/org | `issue-core` |
| Workload/project | `issue-core` |
| KV mount | `platform` |
| OpenBao CLI path | `platform/workloads/issue-core/issue-core/issue-core-runtime` |
| Secret fields | `ISSUE_CORE_API_KEY`, pending decision on `GITEA_BACKEND_TOKEN` |
| Read policy | `workload-kv-read-issue-core-runtime` |
| Policy file | `openbao/policies/workload-kv-read-issue-core-runtime.hcl` |
| Auth method | Kubernetes auth |
| Auth role | `external-secrets-issue-core` |
| OpenBao auth service account | `external-secrets` |
| OpenBao auth namespace | `external-secrets` |
| Delivery surface | `ExternalSecret issue-core/issue-core-runtime` to Secret `issue-core-runtime` |
| ops-warden command | `warden access issue-core-ingestion-api-key --fetch ISSUE_CORE_API_KEY` |
The `GITEA_BACKEND_TOKEN` field remains an explicit review point. Remove it
from the CCR before approval if issue-core no longer needs it in this lane.
## Tasks
## T01 - Review CCR scope and field set
```task
id: RAILIANCE-WP-0009-T01
status: done
priority: high
state_hub_task_id: "64d85288-38fb-4374-b889-fd0d136d3bdf"
```
Review `CCR-2026-0002` with the platform operator and issue-core owner before
any live OpenBao apply.
Acceptance:
- The issue-core owner confirms whether `GITEA_BACKEND_TOKEN` belongs in this
lane or should be removed before approval.
- The approved field set is limited to runtime ingestion credentials needed by
issue-core.
- Review comments and approval state are recorded in the CCR without secret
values.
- The lane remains clearly platform-owned secret custody, not issue-core
application logic.
**2026-06-30:** Live cluster metadata confirms
`ExternalSecret issue-core/issue-core-runtime` is `Ready=True` with reason
`SecretSynced` and maps both `ISSUE_CORE_API_KEY` and `GITEA_BACKEND_TOKEN` from
`platform/workloads/issue-core/issue-core/issue-core-runtime`. Retain both
fields in `CCR-2026-0002` unless the issue-core owner later removes one through
review. The CCR remains `proposed`; this task records non-secret scope review,
not approval to apply.
## T02 - Confirm Kubernetes auth and External Secrets binding
```task
id: RAILIANCE-WP-0009-T02
status: done
priority: high
state_hub_task_id: "7f4a8317-13f0-4be3-948c-a2e2f90447cf"
```
Confirm the exact Kubernetes service account, namespace, and External Secrets
target that should consume this lane.
Acceptance:
- The service account and namespace are confirmed as `issue-core`/`issue-core`
or the CCR is updated with the approved alternative.
- The auth role binds only the approved service account and namespace.
- The External Secrets target and expected field names are documented.
- No direct human or agent read path is activated unless separately approved.
**2026-06-30:** Confirmed the current delivery path uses the platform External
Secrets operator, not a workload pod service account. The `issue-core`
Deployment uses the `default` service account, and no `issue-core` service
account exists. `ClusterSecretStore/openbao` authenticates to OpenBao as
`external-secrets/external-secrets` with role `external-secrets-issue-core` and
is limited to the `issue-core` namespace. Updated `CCR-2026-0002` to this
confirmed auth subject while keeping the exact
`workload-kv-read-issue-core-runtime` policy. `credential-change.py
applier-dry-run CCR-2026-0002` now blocks only because the CCR is still
`proposed`.
## T03 - Apply or confirm least-privilege OpenBao metadata
```task
id: RAILIANCE-WP-0009-T03
status: wait
priority: high
state_hub_task_id: "e8566cf4-bb74-4515-b434-7cbf60f9f684"
```
Apply the read policy and Kubernetes auth role only after review and binding
confirmation.
Acceptance:
- `openbao/policies/workload-kv-read-issue-core-runtime.hcl` grants only read
access to the exact KV-v2 data and metadata paths.
- The Kubernetes auth role attaches only
`workload-kv-read-issue-core-runtime`.
- Live apply uses an approved operator path or the delegated applier from
`RAILIANCE-WP-0008`; broad `platform-admin` handoffs are avoided where
possible.
- Apply evidence records only policy name, role name, actor, timestamp, and
non-secret OpenBao request ids.
## T04 - Provision values through approved custody
```task
id: RAILIANCE-WP-0009-T04
status: wait
priority: high
state_hub_task_id: "4990fe6a-ae84-4720-bc8d-e026d73a304b"
```
Have an approved operator create or confirm the OpenBao KV entry and fields.
Acceptance:
- The path exists at
`platform/workloads/issue-core/issue-core/issue-core-runtime`.
- Approved fields are present with the exact reviewed names.
- Values are entered directly through OpenBao/operator custody, never through
Git, State Hub, chat, prompts, workplans, or shell history.
- Non-secret evidence records only path, field names, actor, timestamp, and
verification result.
## T05 - Verify positive and negative access
```task
id: RAILIANCE-WP-0009-T05
status: wait
priority: high
state_hub_task_id: "65e83572-2e46-4196-8f4d-4ab35ba8d1a6"
```
Prove that the approved issue-core identity can consume the lane and that other
identities cannot.
Acceptance:
- Positive verification shows the approved issue-core service account can read
the configured fields through OpenBao or External Secrets without printing
values.
- Negative verification shows an unapproved service account cannot read the
path.
- OpenBao audit evidence exists for allowed and denied attempts, recorded only
as non-secret request ids or timestamps.
- Verification includes the External Secrets delivery path if that is the
production consumer interface.
## T06 - Activate ops-warden catalog front door
```task
id: RAILIANCE-WP-0009-T06
status: wait
priority: medium
state_hub_task_id: "0d9a02da-c032-43d5-8019-61ab4d87b40b"
```
Send ops-warden the non-secret pointers needed to promote
`issue-core-ingestion-api-key` from draft to active.
Acceptance:
- The handoff includes only catalog id, mount, path, field names, auth role,
policy name/path, External Secrets target, optional flex-auth ref, and
runbook/workplan links.
- ops-warden confirms the catalog entry has no unresolved placeholders.
- ops-warden confirms it proxies access as the caller and holds no secret
value.
- The CCR front-door readiness becomes active/resolvable only after positive
and negative verification.
## T07 - Record lifecycle operations
```task
id: RAILIANCE-WP-0009-T07
status: wait
priority: medium
state_hub_task_id: "c85d1139-1f7d-4ed4-a2fc-5ea4ecbdf0c6"
```
Document how to deactivate, rotate, and respond to compromise for this lane.
Acceptance:
- Deactivation disables the ops-warden front door and removes or detaches the
auth role policy without deleting required audit evidence.
- Rotation keeps values inside OpenBao/operator custody and records only
non-secret evidence.
- Compromise response names the immediate front-door disable, affected field
rotation, and follow-up incident workplan path.
## Exit Criteria
- `CCR-2026-0002` is reviewed, approved, applied, verified, and active.
- issue-core can consume the required runtime ingestion credential through the
approved platform interface.
- Unauthorized access is denied and recorded.
- ops-warden can resolve `issue-core-ingestion-api-key` without storing the
value.
- No secret values appear in Git, State Hub, chat, prompts, logs, or workplans.

View File

@@ -0,0 +1,283 @@
---
id: RAILIANCE-WP-0010
type: workplan
title: "llm-connect OpenRouter Provider Key Lane"
domain: financials
repo: railiance-platform
status: active
owner: codex
topic_slug: railiance
planning_priority: high
planning_order: 10
created: "2026-06-29"
updated: "2026-06-30"
depends_on_workplans:
- RAIL-PL-WP-0002
- RAILIANCE-WP-0004
- RAILIANCE-WP-0007
- RAILIANCE-WP-0008
related_state_hub_messages:
- "f76d3a9e-a98f-4081-885d-b79d94312699"
related_ccrs:
- CCR-2026-0003
state_hub_workstream_id: "f364d405-a85d-4b89-b600-1964ab436cad"
---
# RAILIANCE-WP-0010 - llm-connect OpenRouter Provider Key Lane
## Goal
Promote the draft llm-connect OpenRouter provider-key access lane from a
proposed CCR to a reviewed, least-privilege, verified OpenBao workload KV lane
that the live llm-connect runtime can consume through External Secrets and that
`ops-warden` can reference without holding the provider key.
This keeps provider credential custody in the shared platform layer while
leaving llm-connect behavior, model routing, and provider-specific runtime
logic with the owning application/service.
No task in this workplan may paste, commit, log, or send secret values through
Git, State Hub, chat, prompts, shell history, or workplan text.
## Suggestion Reviewed
Ops-warden confirmed the whynot-design lane in State Hub message
`f76d3a9e-a98f-4081-885d-b79d94312699` and noted that the OpenRouter/llm-connect
sibling lane remains draft on its side. The repo already has the proposed
non-secret CCR:
- `credential-change-requests/CCR-2026-0003-llm-connect-openrouter-api-key.yaml`
The message called the sibling lane `openrouter-llm-connect`; the CCR uses
catalog id `llm-connect-openrouter-api-key`. Resolve that naming with
ops-warden before activation so automated callers have one stable selector.
## INTENT Fit
This work belongs in railiance-platform because it provides the dependable
secret custody and delivery substrate for a shared runtime service. It does not
decide which model or provider llm-connect should use. The platform-owned
output is the OpenBao path, read policy, Kubernetes auth role, External Secrets
target, verification evidence, and ops-warden handoff.
The plan supports these `INTENT.md` principles:
- secure custody: the provider key stays in OpenBao/operator custody;
- stable interfaces: llm-connect consumes a documented path, field, role, and
External Secrets target;
- operable and observable: activation requires positive and negative checks
plus non-secret audit evidence;
- independently evolvable: the provider key storage and front-door routing can
change without forcing runtime consumers to know internal topology.
## Proposed Contract
| Item | Proposed value |
| --- | --- |
| CCR | `CCR-2026-0003` |
| ops-warden catalog id | `llm-connect-openrouter-api-key` pending naming confirmation |
| Tenant/org | `activity-core` |
| Workload/project | `llm-connect` |
| KV mount | `platform` |
| OpenBao CLI path | `platform/workloads/activity-core/llm-connect/llm-connect-provider-secrets` |
| Secret field | `OPENROUTER_API_KEY` |
| Read policy | `workload-kv-read-llm-connect-provider-secrets` |
| Policy file | `openbao/policies/workload-kv-read-llm-connect-provider-secrets.hcl` |
| Auth method | Kubernetes auth |
| Auth role | `external-secrets-activity-core` |
| OpenBao auth service account | `external-secrets` |
| OpenBao auth namespace | `external-secrets` |
| Delivery surface | Future activity-core ExternalSecret to Secret `llm-connect-provider-secrets` |
| ops-warden command | `warden access llm-connect-openrouter-api-key --fetch OPENROUTER_API_KEY` |
## Tasks
## T01 - Review CCR scope and selector naming
```task
id: RAILIANCE-WP-0010-T01
status: progress
priority: high
state_hub_task_id: "307b75a6-a3a8-473b-b171-7379d2848698"
```
Review `CCR-2026-0003` with the platform operator and activity-core owner
before any live OpenBao apply.
Acceptance:
- The activity-core owner confirms that llm-connect should receive
`OPENROUTER_API_KEY` through this platform lane.
- ops-warden and railiance-platform agree on one stable catalog id/selector,
reconciling `openrouter-llm-connect` with
`llm-connect-openrouter-api-key`.
- Review comments and approval state are recorded in the CCR without secret
values.
- The lane remains clearly platform-owned secret custody, not llm-connect model
routing or provider selection logic.
**2026-06-30:** Confirmed `activity-core` namespace exists and Kubernetes Secret
`activity-core/llm-connect-provider-secrets` exists, but no activity-core
`ExternalSecret` exists yet. Kept canonical CCR catalog id
`llm-connect-openrouter-api-key`; ops-warden previously mentioned
`openrouter-llm-connect`, so selector agreement remains open and this task stays
`progress`. OpenBao public seal status now reports `sealed=false`; the prior
sealed message is no longer the active blocker.
## T02 - Confirm Kubernetes auth and External Secrets binding
```task
id: RAILIANCE-WP-0010-T02
status: done
priority: high
state_hub_task_id: "829192f5-4502-44e0-8020-656d74d5282a"
```
Confirm the exact Kubernetes service account, namespace, and External Secrets
target that should consume this lane.
Acceptance:
- The service account and namespace are confirmed as
`llm-connect`/`activity-core` or the CCR is updated with the approved
alternative.
- The auth role binds only the approved service account and namespace.
- The External Secrets target is confirmed as
`llm-connect-provider-secrets` or updated with the approved alternative.
- No direct human or agent read path is activated unless separately approved.
**2026-06-30:** Confirmed the proposed `llm-connect` service account does not
exist and the current `llm-connect` Deployment uses the namespace `default`
service account. Updated `CCR-2026-0003` to the approved platform ESO pattern:
OpenBao Kubernetes auth role `external-secrets-activity-core` bound to
`external-secrets/external-secrets`. Added
`argocd/platform-addons/openbao-secretstore/openbao-activity-core.clustersecretstore.yaml`,
limited to the `activity-core` namespace, and Make target
`openbao-configure-external-secrets-activity-core` for the matching OpenBao
role/policy apply. `kubectl kustomize argocd/platform-addons/openbao-secretstore`
renders both the existing issue-core store and the new activity-core store.
`credential-change.py applier-dry-run CCR-2026-0003` now blocks only because the
CCR is still `proposed`.
## T03 - Apply or confirm least-privilege OpenBao metadata
```task
id: RAILIANCE-WP-0010-T03
status: wait
priority: high
state_hub_task_id: "42796ef5-c4a0-45a7-ae41-0ebdeccdb01d"
```
Apply the read policy and Kubernetes auth role only after review and binding
confirmation.
Acceptance:
- `openbao/policies/workload-kv-read-llm-connect-provider-secrets.hcl` grants
only read access to the exact KV-v2 data and metadata paths.
- The Kubernetes auth role attaches only
`workload-kv-read-llm-connect-provider-secrets`.
- Live apply uses an approved operator path or the delegated applier from
`RAILIANCE-WP-0008`; broad `platform-admin` handoffs are avoided where
possible.
- Apply evidence records only policy name, role name, actor, timestamp, and
non-secret OpenBao request ids.
## T04 - Provision the provider key through approved custody
```task
id: RAILIANCE-WP-0010-T04
status: wait
priority: high
state_hub_task_id: "651f6ec8-b7d6-45e6-9fef-08646ff737c2"
```
Have an approved operator create or confirm the OpenBao KV entry and
`OPENROUTER_API_KEY` field.
Acceptance:
- The path exists at
`platform/workloads/activity-core/llm-connect/llm-connect-provider-secrets`.
- Field `OPENROUTER_API_KEY` is present.
- The value is entered directly through OpenBao/operator custody, never through
Git, State Hub, chat, prompts, workplans, or shell history.
- Non-secret evidence records only path, field name, actor, timestamp, and
verification result.
## T05 - Verify positive and negative access
```task
id: RAILIANCE-WP-0010-T05
status: wait
priority: high
state_hub_task_id: "d538cfc0-bf68-4889-a5b3-ed94c1679856"
```
Prove that the approved llm-connect identity can consume the lane and that
other identities cannot.
Acceptance:
- Positive verification shows the approved llm-connect service account can read
`OPENROUTER_API_KEY` through OpenBao or External Secrets without printing the
value.
- Negative verification shows an unapproved service account cannot read the
path.
- OpenBao audit evidence exists for allowed and denied attempts, recorded only
as non-secret request ids or timestamps.
- Verification includes the External Secrets delivery path because that is the
intended production consumer interface.
## T06 - Activate ops-warden catalog front door
```task
id: RAILIANCE-WP-0010-T06
status: wait
priority: medium
state_hub_task_id: "376de3fe-ef9c-4b57-b238-1ba21ac8bb1c"
```
Send ops-warden the non-secret pointers needed to promote the agreed
OpenRouter/llm-connect selector from draft to active.
Acceptance:
- The handoff includes only catalog id, mount, path, field name, auth role,
policy name/path, External Secrets target, optional flex-auth ref, and
runbook/workplan links.
- ops-warden confirms the catalog entry has no unresolved placeholders.
- ops-warden confirms it proxies access as the caller and holds no provider key
value.
- The CCR front-door readiness becomes active/resolvable only after positive
and negative verification.
## T07 - Record lifecycle operations
```task
id: RAILIANCE-WP-0010-T07
status: wait
priority: medium
state_hub_task_id: "130155a5-e0f9-49f8-ba27-b48098746f02"
```
Document how to deactivate, rotate, and respond to compromise for this lane.
Acceptance:
- Deactivation disables the ops-warden front door and removes or detaches the
auth role policy without deleting required audit evidence.
- Rotation keeps the provider key inside OpenBao/operator custody and records
only non-secret evidence.
- Compromise response names the immediate front-door disable, provider-key
rotation, and follow-up incident workplan path.
## Exit Criteria
- `CCR-2026-0003` is reviewed, approved, applied, verified, and active.
- llm-connect can consume `OPENROUTER_API_KEY` through the approved platform
interface.
- Unauthorized access is denied and recorded.
- ops-warden can resolve the agreed OpenRouter/llm-connect selector without
storing the value.
- No secret values appear in Git, State Hub, chat, prompts, logs, or workplans.