Compare commits
92 Commits
423eccc8e9
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 06844c2669 | |||
| 8ac7314c43 | |||
| 5b5e8616bf | |||
| 5a5383eadb | |||
| 4936b8970b | |||
| d284fea400 | |||
| 38c6b11103 | |||
| f803bf167b | |||
| 1a075cafd1 | |||
| b86001fe2b | |||
| b1b9bc5474 | |||
| b26a04cab3 | |||
| 7fc334f67d | |||
| 8327ce9fe3 | |||
| ddd916d71c | |||
| 8395862760 | |||
| 60814fc76a | |||
| 5dc71fb6ff | |||
| 16020daf58 | |||
| ab08a21337 | |||
| 797a957e42 | |||
| 38936d8fd6 | |||
| 2db4d1afe1 | |||
| 268437a36d | |||
| 0a24ab8475 | |||
| 6ed18ca709 | |||
| 8321e14b46 | |||
| a95236d2e5 | |||
| c626bfcf15 | |||
| 3e28e9ae79 | |||
| aa4c9ac492 | |||
| b59718fc83 | |||
| 51b85f802d | |||
| efb5432eb8 | |||
| 5c6a3ce95e | |||
| a07f2d3d7f | |||
| 481e64c3f4 | |||
| 8f617fcbf4 | |||
| e88c7829f3 | |||
| 1e769c75a0 | |||
| 2c1e76efca | |||
| 3527bc1cae | |||
| adf865611c | |||
| 271aa94642 | |||
| 3ef25cb787 | |||
| 53f3f4ca10 | |||
| f630d5135e | |||
| e3147b7fd5 | |||
| 06f2f4e315 | |||
| 00fb93544c | |||
| 5e0ed95127 | |||
| 6effdb80ca | |||
| eb24e04b71 | |||
| ad47a136f7 | |||
| 82d15cfea2 | |||
| 0e3ea30c75 | |||
| f92d07d5a1 | |||
| 248bc58b6a | |||
| a27a114491 | |||
| 3706ff703e | |||
| 52687d8b3e | |||
| aee0dcefad | |||
| 815b124ab1 | |||
| 8c1e64d5e0 | |||
| 85a4278a55 | |||
| 9d42c73833 | |||
| 704ee99218 | |||
| 76c9661db3 | |||
| 673ec46e25 | |||
| 2268a9375e | |||
| 752cfd6f00 | |||
| 6e663dfd20 | |||
| c7393d94ab | |||
| 693dc71833 | |||
| 0f0b14001e | |||
| c022cb2f83 | |||
| 86eb6ea269 | |||
| d59704deef | |||
| f39180583a | |||
| 0b384f8485 | |||
| 8e6892f4bf | |||
| 6712eed995 | |||
| a1dbb26842 | |||
| 50799938db | |||
| 520c7ea2c0 | |||
| ae4d967481 | |||
| 80648a78b7 | |||
| 64d7c18c3f | |||
| cb45f29fb2 | |||
| a6a87ae282 | |||
| 6ddf4e56b4 | |||
| 665d43386f |
50
.claude/rules/credential-routing.md
Normal file
50
.claude/rules/credential-routing.md
Normal 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 workplans; **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`
|
||||||
@@ -1,37 +1,41 @@
|
|||||||
## First Session Protocol
|
## First Session Protocol
|
||||||
|
|
||||||
Triggered when `get_domain_summary("railiance")` shows **no workstreams**.
|
Triggered when `get_domain_summary("financials")` shows **no workplans**.
|
||||||
The project is registered but work has not yet been structured.
|
The project is registered but work has not yet been structured.
|
||||||
|
|
||||||
**Step 1 — Read, don't write**
|
**Step 1 — Read, don't write**
|
||||||
- `~/the-custodian/canon/projects/railiance/project_charter_v0.1.md` — purpose, scope
|
- `~/the-custodian/canon/projects/financials/project_charter_v0.1.md` — purpose, scope
|
||||||
- `~/the-custodian/canon/projects/railiance/roadmap_v0.1.md` — planned phases
|
- `~/the-custodian/canon/projects/financials/roadmap_v0.1.md` — planned phases
|
||||||
- Scan repo root: README, directory structure, existing code or docs
|
- Scan repo root: README, directory structure, existing code or docs
|
||||||
|
|
||||||
**Step 2 — Survey in-progress work**
|
**Step 2 — Survey in-progress work**
|
||||||
Look for TODOs, open branches, half-finished files. Note done vs. started but incomplete.
|
Look for TODOs, open branches, half-finished files. Note done vs. started but incomplete.
|
||||||
|
|
||||||
**Step 3 — Propose workstreams to Bernd**
|
**Step 3 — Propose workplans to Bernd**
|
||||||
Propose 1–3 workstreams — each a coherent strand, weeks to months, anchored to a
|
Propose 1–3 workplans — each a coherent strand, weeks to months, anchored to a
|
||||||
roadmap phase. **Wait for approval before creating.**
|
roadmap phase. **Wait for approval before creating.**
|
||||||
|
|
||||||
**Step 4 — Create workplan file first, then DB record (ADR-001)**
|
**Step 4 — Write the workplan file; fix-consistency registers it (ADR-001)**
|
||||||
```
|
```
|
||||||
workplans/railiance-platform-WP-NNNN-<slug>.md ← write this first
|
workplans/RAILIANCE-WP-NNNN-<slug>.md ← write this, commit it
|
||||||
```
|
```
|
||||||
Then register in the hub:
|
Then register by running the consistency check — do **not** call
|
||||||
```
|
`create_workplan`/`create_task` (or legacy `create_workstream`) yourself;
|
||||||
create_workstream(topic_id="ca369340-a64e-442e-98f1-a4fa7dc74a38", title="...", owner="...", description="...")
|
manual registration duplicates what C-06 creates from the file:
|
||||||
create_task(workstream_id="<id>", title="...", priority="high|medium|low")
|
```bash
|
||||||
|
statehub fix-consistency --repo railiance-platform
|
||||||
```
|
```
|
||||||
|
C-06 creates the hub workplan + tasks and writes `state_hub_workstream_id` /
|
||||||
|
`state_hub_task_id` back into the file (legacy field names, kept for
|
||||||
|
compatibility — they hold workplan/task IDs).
|
||||||
|
|
||||||
**Step 5 — Record the setup**
|
**Step 5 — Record the setup**
|
||||||
```
|
```
|
||||||
add_progress_event(
|
add_progress_event(
|
||||||
summary="First session: structured railiance into N workstreams, M tasks",
|
summary="First session: structured financials into N workplans, M tasks",
|
||||||
event_type="milestone",
|
event_type="milestone",
|
||||||
topic_id="ca369340-a64e-442e-98f1-a4fa7dc74a38",
|
topic_id="ca369340-a64e-442e-98f1-a4fa7dc74a38",
|
||||||
detail={"workstreams": [...], "tasks_created": M}
|
detail={"workplans": [...], "tasks_created": M}
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
**Purpose:** OAS S3 Platform Services — PostgreSQL HA, object storage, secret management, identity
|
**Purpose:** OAS S3 Platform Services — PostgreSQL HA, object storage, secret management, identity
|
||||||
|
|
||||||
**Domain:** railiance
|
**Domain:** financials
|
||||||
**Repo slug:** railiance-platform
|
**Repo slug:** railiance-platform
|
||||||
**Topic ID:** ca369340-a64e-442e-98f1-a4fa7dc74a38
|
**Topic ID:** ca369340-a64e-442e-98f1-a4fa7dc74a38
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
## Session Protocol
|
## 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**
|
**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:
|
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:
|
If MCP tools are unavailable in the current agent session, use the REST API:
|
||||||
```bash
|
```bash
|
||||||
@@ -39,11 +40,11 @@ curl -s -X PATCH "http://127.0.0.1:8000/messages/<id>/read" \
|
|||||||
ls workplans/
|
ls workplans/
|
||||||
```
|
```
|
||||||
For each file with `status: ready`, `active`, or `blocked`, note pending
|
For each file with `status: ready`, `active`, or `blocked`, note pending
|
||||||
`todo`/`in_progress` tasks.
|
`wait`/`todo`/`progress` tasks.
|
||||||
|
|
||||||
**Step 4 — Present brief**
|
**Step 4 — Present brief**
|
||||||
|
|
||||||
1. **Active workstreams** for `railiance` — title, task counts, blocking decisions
|
1. **Active workplans** for `financials` — title, task counts, blocking decisions
|
||||||
2. **Pending tasks** from `workplans/` + any `[repo:railiance-platform]` hub tasks
|
2. **Pending tasks** from `workplans/` + any `[repo:railiance-platform]` hub tasks
|
||||||
3. **Goal guidance** — if `goal_guidance` in summary:
|
3. **Goal guidance** — if `goal_guidance` in summary:
|
||||||
- `needs_workplan`: surface as top action — *"Repo goal '{title}' has no workplan yet"*
|
- `needs_workplan`: surface as top action — *"Repo goal '{title}' has no workplan yet"*
|
||||||
@@ -51,33 +52,42 @@ For each file with `status: ready`, `active`, or `blocked`, note pending
|
|||||||
4. **Suggested next action** — highest-priority open item
|
4. **Suggested next action** — highest-priority open item
|
||||||
5. **SBOM status** — flag if `last_sbom_at` is unset for this repo
|
5. **SBOM status** — flag if `last_sbom_at` is unset for this repo
|
||||||
|
|
||||||
If no workstreams: follow First Session Protocol (`first-session.md`).
|
If no workplans: follow First Session Protocol (`first-session.md`).
|
||||||
|
|
||||||
**During work:** `record_decision()` · `add_progress_event()` · `resolve_decision()`
|
**During work:** `record_decision()` · `add_progress_event()` · `resolve_decision()`
|
||||||
|
|
||||||
> State Hub is a *read model*. Bootstrap tools (`create_workstream`, `create_task`)
|
> State Hub is a *read model*. **Never register workplans or tasks by hand**
|
||||||
> are First Session Protocol only. Work structure belongs in repo files (ADR-001).
|
> (`create_workplan`, `create_task`, or the legacy `create_workstream`) — write
|
||||||
|
> the workplan file in `workplans/` and run `fix-consistency`; its C-06 check
|
||||||
|
> registers the workplan and its tasks in the hub and writes the IDs back into
|
||||||
|
> the file. Manual registration creates duplicates the moment fix-consistency
|
||||||
|
> runs. Work structure belongs in repo files (ADR-001).
|
||||||
|
>
|
||||||
|
> Terminology: "workstream" is the legacy name for workplan. Some API/frontmatter
|
||||||
|
> field names keep it for compatibility (`state_hub_workstream_id`,
|
||||||
|
> `workstream_id` params) — treat them as workplan IDs.
|
||||||
|
|
||||||
**Session close:**
|
**Session close:**
|
||||||
With MCP tools:
|
With MCP tools:
|
||||||
```
|
```
|
||||||
add_progress_event(summary="...", topic_id="ca369340-a64e-442e-98f1-a4fa7dc74a38", workstream_id="<uuid>")
|
add_progress_event(summary="...", topic_id="ca369340-a64e-442e-98f1-a4fa7dc74a38", workplan_id="<uuid>")
|
||||||
```
|
```
|
||||||
Without MCP tools:
|
Without MCP tools:
|
||||||
```bash
|
```bash
|
||||||
curl -s -X POST http://127.0.0.1:8000/progress/ \
|
curl -s -X POST http://127.0.0.1:8000/progress/ \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{"topic_id":"ca369340-a64e-442e-98f1-a4fa7dc74a38","workstream_id":"<uuid>","event_type":"note","summary":"what changed","author":"codex"}'
|
-d '{"topic_id":"ca369340-a64e-442e-98f1-a4fa7dc74a38","workplan_id":"<uuid>","event_type":"note","summary":"what changed","author":"codex"}'
|
||||||
```
|
```
|
||||||
If workplan files were modified, ensure the local copy is up to date first:
|
If workplan files were modified, ensure the local copy is up to date first,
|
||||||
|
then sync from the repo checkout:
|
||||||
```bash
|
```bash
|
||||||
git -C <repo_path> pull --ff-only
|
git pull --ff-only
|
||||||
cd ~/state-hub && make fix-consistency REPO=railiance-platform
|
statehub fix-consistency
|
||||||
```
|
```
|
||||||
For repos where implementation runs on a remote machine (e.g. CoulombCore),
|
For repos where implementation runs on a remote machine (e.g. CoulombCore),
|
||||||
use the combined target which pulls before fixing:
|
use the pull-before-fix mode from any shell with the State Hub CLI:
|
||||||
```bash
|
```bash
|
||||||
cd ~/state-hub && make fix-consistency-remote REPO=railiance-platform
|
statehub fix-consistency --repo railiance-platform --remote
|
||||||
```
|
```
|
||||||
**C-15** (DB task ahead of file) is normal in multi-machine workflows — writeback
|
**C-15** (DB task ahead of file) is normal in multi-machine workflows — writeback
|
||||||
will sync the file to match DB. **C-16** (repo behind remote) blocks all writes
|
will sync the file to match DB. **C-16** (repo behind remote) blocks all writes
|
||||||
|
|||||||
@@ -1,19 +1,18 @@
|
|||||||
## Stack
|
## Stack
|
||||||
|
|
||||||
<!-- TODO: Fill in language, frameworks, and key dependencies -->
|
- **Language:** Kubernetes manifests + Helm values, Bash Make targets
|
||||||
- **Language:**
|
- **Key deps:** CloudNative-PG (cnpg) clusters, Valkey, OpenBao, SOPS/age; runs against the S2 cluster on railiance01
|
||||||
- **Key deps:**
|
|
||||||
|
|
||||||
## Dev Commands
|
## Dev Commands
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# TODO: Fill in the standard commands for this repo
|
make help # list all targets (db-*, apps-pg-*, pg-*, valkey-*, openbao-*)
|
||||||
|
make db-status # gitea-db cnpg cluster health (read-only)
|
||||||
# Install dependencies
|
make apps-pg-status # shared apps-pg cluster health (read-only)
|
||||||
|
make pg-status # standalone PostgreSQL HA pod status (read-only)
|
||||||
# Run tests
|
make valkey-status # Valkey pod status (read-only)
|
||||||
|
make db-deploy | apps-pg-deploy | pg-deploy | valkey-deploy # production-touching
|
||||||
# Lint / type check
|
|
||||||
|
|
||||||
# Build / package (if applicable)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`*-deploy` targets change production platform services — operator approval
|
||||||
|
required. `*-status` / `*-logs` targets are read-only and safe.
|
||||||
|
|||||||
@@ -1,28 +1,45 @@
|
|||||||
## Workplan Convention (ADR-001)
|
## Workplan Convention (ADR-001)
|
||||||
|
|
||||||
File location: `workplans/railiance-platform-WP-NNNN-<slug>.md`
|
File location: `workplans/RAILIANCE-WP-NNNN-<slug>.md`
|
||||||
ID prefix: `RAILIANCE-WP`
|
ID prefix: `RAILIANCE-WP-`
|
||||||
|
|
||||||
Work items originate as files in this repo **before** being registered in the hub.
|
Work items originate as files in this repo **before** being registered in the hub.
|
||||||
|
|
||||||
Canonical workplan/workstream frontmatter statuses are:
|
Canonical workplan frontmatter statuses are:
|
||||||
`proposed`, `ready`, `active`, `blocked`, `backlog`, `finished`, `archived`.
|
`proposed`, `ready`, `active`, `blocked`, `backlog`, `finished`, `archived`.
|
||||||
Use `proposed` for a newly drafted plan, `ready` after review against current
|
Use `proposed` for a newly drafted plan, `ready` after review against current
|
||||||
repo state, and `finished` when implementation is complete. `stalled` and
|
repo state, and `finished` when implementation is complete. `stalled` and
|
||||||
`needs_review` are derived health labels, not stored statuses.
|
`needs_review` are derived health labels, not stored statuses.
|
||||||
|
|
||||||
Closed workplans may be moved to `workplans/archived/` with a completion-date
|
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.
|
unchanged; the prefix is only for quick visual reference.
|
||||||
|
|
||||||
Small opportunistic tasks discovered during another session use **Ad Hoc Tasks**:
|
Small opportunistic tasks discovered during another session use **Ad Hoc Tasks**:
|
||||||
`workplans/ADHOC-YYYY-MM-DD.md`, workstream slug `adhoc-YYYY-MM-DD`, and task ids
|
`workplans/ADHOC-YYYY-MM-DD.md`, workplan slug `adhoc-YYYY-MM-DD`, and task ids
|
||||||
`ADHOC-YYYY-MM-DD-T01`, `T02`, etc. Use adhocs only for low-risk work completed
|
`ADHOC-YYYY-MM-DD-T01`, `T02`, etc. Use adhocs only for low-risk work completed
|
||||||
directly. Promote anything requiring analysis, design, approval, dependencies, or
|
directly. Promote anything requiring analysis, design, approval, dependencies, or
|
||||||
multiple planned phases into a normal workplan.
|
multiple planned phases into a normal workplan.
|
||||||
|
|
||||||
Ecosystem todos from other agents arrive as `[repo:railiance-platform]` hub tasks —
|
Ecosystem todos from other agents arrive as `[repo:railiance-platform]` hub tasks —
|
||||||
visible at session start. Pick one up by creating the workplan file, then registering
|
visible at session start. Pick one up by creating the workplan file, committing,
|
||||||
the workstream.
|
and running `statehub fix-consistency` — C-06 registers the workplan in the hub.
|
||||||
|
Never register by hand with `create_workplan`/`create_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.
|
||||||
|
|
||||||
|
Workplan frontmatter carries `state_hub_workstream_id` — a legacy field name
|
||||||
|
kept for compatibility ("workstream" is the old term for workplan); it holds
|
||||||
|
the hub workplan id and is written by fix-consistency. Do not edit or rename it.
|
||||||
|
|
||||||
<!-- Ralph Loop rules and HEUREKA sequence: ~/.claude/CLAUDE.md — do not duplicate here -->
|
<!-- Ralph Loop rules and HEUREKA sequence: ~/.claude/CLAUDE.md — do not duplicate here -->
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<!-- custodian-brief: generated by fix-consistency — do not edit manually -->
|
<!-- custodian-brief: generated by fix-consistency — do not edit manually -->
|
||||||
# Custodian Brief — railiance-platform
|
# Custodian Brief — railiance-platform
|
||||||
|
|
||||||
**Domain:** railiance
|
**Domain:** financials
|
||||||
**Last synced:** 2026-05-29 00:09 UTC
|
**Last synced:** 2026-07-02 18:57 UTC
|
||||||
**State Hub:** http://127.0.0.1:8000 *(adjust if running on a remote machine)*
|
**State Hub:** http://127.0.0.1:8000 *(adjust if running on a remote machine)*
|
||||||
|
|
||||||
## Active Workstreams
|
## Active Workstreams
|
||||||
@@ -13,6 +13,6 @@
|
|||||||
## MCP Orientation (when available)
|
## MCP Orientation (when available)
|
||||||
|
|
||||||
If the state-hub MCP server is reachable, call:
|
If the state-hub MCP server is reachable, call:
|
||||||
`get_domain_summary("railiance")`
|
`get_domain_summary("financials")`
|
||||||
This provides richer cross-domain context.
|
This provides richer cross-domain context.
|
||||||
If the MCP call fails, use this file as your orientation source.
|
If the MCP call fails, use this file as your orientation source.
|
||||||
|
|||||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -4,10 +4,19 @@ helm/*.yaml
|
|||||||
!helm/*.yaml.template
|
!helm/*.yaml.template
|
||||||
!helm/openbao-values.yaml
|
!helm/openbao-values.yaml
|
||||||
!helm/openbao-middleware.yaml
|
!helm/openbao-middleware.yaml
|
||||||
|
!helm/openbao-ui-overlay-k8s.yaml
|
||||||
# Kubernetes manifests (no secrets) are safe to commit
|
# Kubernetes manifests (no secrets) are safe to commit
|
||||||
!helm/*-cluster.yaml
|
!helm/*-cluster.yaml
|
||||||
!helm/*-networkpolicies.yaml
|
!helm/*-networkpolicies.yaml
|
||||||
!helm/*-databases.yaml
|
!helm/*-databases.yaml
|
||||||
|
|
||||||
|
# ArgoCD repository credentials — encrypt locally, never commit
|
||||||
|
argocd/repositories/*.repository.sops.yaml
|
||||||
|
!argocd/repositories/*.repository.sops.yaml.template
|
||||||
|
|
||||||
# Kubeconfig
|
# Kubeconfig
|
||||||
*.kubeconfig
|
*.kubeconfig
|
||||||
|
|
||||||
|
# Credential broker local lease/token material
|
||||||
|
.local/credential-leases/
|
||||||
|
*.openbao-token
|
||||||
|
|||||||
23
.repo-classification.yaml
Normal file
23
.repo-classification.yaml
Normal 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.
|
||||||
95
AGENTS.md
95
AGENTS.md
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
**Purpose:** OAS S3 Platform Services — PostgreSQL HA, object storage, secret management, identity
|
**Purpose:** OAS S3 Platform Services — PostgreSQL HA, object storage, secret management, identity
|
||||||
|
|
||||||
**Domain:** railiance
|
**Domain:** financials
|
||||||
**Repo slug:** railiance-platform
|
**Repo slug:** railiance-platform
|
||||||
**Topic ID:** `ca369340-a64e-442e-98f1-a4fa7dc74a38`
|
**Topic ID:** `ca369340-a64e-442e-98f1-a4fa7dc74a38`
|
||||||
**Workplan prefix:** `RAILIANCE-WP-`
|
**Workplan prefix:** `RAILIANCE-WP-`
|
||||||
@@ -20,6 +20,12 @@ there is no MCP server for Codex agents.
|
|||||||
|---------|-----|
|
|---------|-----|
|
||||||
| Local workstation | `http://127.0.0.1:8000` |
|
| Local workstation | `http://127.0.0.1:8000` |
|
||||||
| Remote via tunnel | `http://127.0.0.1:18000` |
|
| Remote via tunnel | `http://127.0.0.1:18000` |
|
||||||
|
| Optional local edge relay | http://127.0.0.1:18080 |
|
||||||
|
|
||||||
|
When an operator has enabled the edge relay, set API_BASE to the relay URL.
|
||||||
|
Queueable writes return an explicit queued receipt if the central hub is
|
||||||
|
unreachable. Treat that as pending local evidence, then ask the operator to run
|
||||||
|
statehub outbox status/replay after connectivity returns.
|
||||||
|
|
||||||
### Orient at session start
|
### Orient at session start
|
||||||
|
|
||||||
@@ -27,8 +33,8 @@ there is no MCP server for Codex agents.
|
|||||||
# Offline brief — works without hub connection
|
# Offline brief — works without hub connection
|
||||||
cat .custodian-brief.md
|
cat .custodian-brief.md
|
||||||
|
|
||||||
# Active workstreams for this domain
|
# Active workplans for this domain
|
||||||
curl -s "http://127.0.0.1:8000/workstreams/?topic_id=ca369340-a64e-442e-98f1-a4fa7dc74a38&status=active" \
|
curl -s "http://127.0.0.1:8000/workplans/?topic_id=ca369340-a64e-442e-98f1-a4fa7dc74a38&status=active" \
|
||||||
| python3 -m json.tool
|
| python3 -m json.tool
|
||||||
|
|
||||||
# Check inbox
|
# Check inbox
|
||||||
@@ -51,20 +57,20 @@ curl -s -X POST http://127.0.0.1:8000/progress/ \
|
|||||||
"summary": "what was done",
|
"summary": "what was done",
|
||||||
"event_type": "note",
|
"event_type": "note",
|
||||||
"author": "codex",
|
"author": "codex",
|
||||||
"workstream_id": "<uuid>",
|
"workplan_id": "<uuid>",
|
||||||
"task_id": "<uuid>"
|
"task_id": "<uuid>"
|
||||||
}'
|
}'
|
||||||
```
|
```
|
||||||
|
|
||||||
Omit `workstream_id` / `task_id` when not applicable.
|
Omit `workplan_id` / `task_id` when not applicable.
|
||||||
|
|
||||||
### Update task status
|
### Update task status
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -s -X PATCH "http://127.0.0.1:8000/tasks/<task_id>" \
|
curl -s -X PATCH "http://127.0.0.1:8000/tasks/<task_id>" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{"status": "in_progress"}'
|
-d '{"status": "progress"}'
|
||||||
# values: todo | in_progress | done | blocked
|
# values: wait | todo | progress | done | cancel
|
||||||
```
|
```
|
||||||
|
|
||||||
### Flag a task for human review
|
### Flag a task for human review
|
||||||
@@ -80,10 +86,10 @@ curl -s -X PATCH "http://127.0.0.1:8000/tasks/<task_id>" \
|
|||||||
## Session Protocol
|
## Session Protocol
|
||||||
|
|
||||||
**Start:**
|
**Start:**
|
||||||
1. `cat .custodian-brief.md` — domain goal and open workstreams (offline-safe)
|
1. `cat .custodian-brief.md` — domain goal and open workplans (offline-safe)
|
||||||
2. Check inbox: `GET /messages/?to_agent=railiance-platform&unread_only=true`; mark read
|
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
|
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:**
|
**During work:**
|
||||||
- Update task statuses in workplan files as tasks progress
|
- Update task statuses in workplan files as tasks progress
|
||||||
@@ -92,12 +98,69 @@ curl -s -X PATCH "http://127.0.0.1:8000/tasks/<task_id>" \
|
|||||||
**Close:**
|
**Close:**
|
||||||
1. Update workplan file task statuses to reflect progress
|
1. Update workplan file task statuses to reflect progress
|
||||||
2. Log: `POST /progress/` with a summary of what changed
|
2. Log: `POST /progress/` with a summary of what changed
|
||||||
3. Note for the custodian operator: after workplan file changes, run from
|
3. After workplan file changes, run:
|
||||||
`~/state-hub`:
|
|
||||||
```bash
|
```bash
|
||||||
make fix-consistency REPO=railiance-platform
|
statehub fix-consistency
|
||||||
```
|
```
|
||||||
This syncs task status from files into the hub DB.
|
Coding agents should run this directly; ask the operator only if the CLI or
|
||||||
|
State Hub API is unavailable. This syncs task status from files into the hub DB.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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 workplans; **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. -->
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -124,7 +187,7 @@ anything needing analysis, design, approval, dependencies, or multiple phases.
|
|||||||
id: RAILIANCE-WP-NNNN
|
id: RAILIANCE-WP-NNNN
|
||||||
type: workplan
|
type: workplan
|
||||||
title: "..."
|
title: "..."
|
||||||
domain: railiance
|
domain: financials
|
||||||
repo: railiance-platform
|
repo: railiance-platform
|
||||||
status: proposed | ready | active | blocked | backlog | finished | archived
|
status: proposed | ready | active | blocked | backlog | finished | archived
|
||||||
owner: codex
|
owner: codex
|
||||||
@@ -146,7 +209,7 @@ derived health labels, not frontmatter statuses.
|
|||||||
|
|
||||||
` ` `task
|
` ` `task
|
||||||
id: RAILIANCE-WP-NNNN-T01
|
id: RAILIANCE-WP-NNNN-T01
|
||||||
status: todo | in_progress | done | blocked
|
status: wait | todo | progress | done | cancel
|
||||||
priority: high | medium | low
|
priority: high | medium | low
|
||||||
state_hub_task_id: "<uuid>" # written by fix-consistency — do not edit
|
state_hub_task_id: "<uuid>" # written by fix-consistency — do not edit
|
||||||
` ` `
|
` ` `
|
||||||
@@ -154,7 +217,7 @@ state_hub_task_id: "<uuid>" # written by fix-consistency — do not edit
|
|||||||
Task description text.
|
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:
|
To create a new workplan:
|
||||||
1. Write the file following the format above
|
1. Write the file following the format above
|
||||||
|
|||||||
@@ -8,4 +8,5 @@
|
|||||||
@.claude/rules/stack-and-commands.md
|
@.claude/rules/stack-and-commands.md
|
||||||
@.claude/rules/architecture.md
|
@.claude/rules/architecture.md
|
||||||
@.claude/rules/repo-boundary.md
|
@.claude/rules/repo-boundary.md
|
||||||
|
@.claude/rules/credential-routing.md
|
||||||
@.claude/rules/agents.md
|
@.claude/rules/agents.md
|
||||||
|
|||||||
183
Makefile
183
Makefile
@@ -14,9 +14,29 @@ OPENBAO_NAMESPACE ?= openbao
|
|||||||
OPENBAO_RELEASE ?= openbao
|
OPENBAO_RELEASE ?= openbao
|
||||||
OPENBAO_VALUES ?= helm/openbao-values.yaml
|
OPENBAO_VALUES ?= helm/openbao-values.yaml
|
||||||
OPENBAO_MIDDLEWARE ?= helm/openbao-middleware.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_VERIFY_AUTH_ARGS ?=
|
||||||
OPENBAO_RESTORE_EVIDENCE ?= /tmp/netkingdom-openbao-restore-drill/evidence.json
|
OPENBAO_RESTORE_EVIDENCE ?= /tmp/netkingdom-openbao-restore-drill/evidence.json
|
||||||
OPENBAO_EMERGENCY_EVIDENCE ?= /tmp/netkingdom-openbao-emergency-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
|
##@ CloudNative PG (cnpg) — primary database operator
|
||||||
|
|
||||||
@@ -103,6 +123,16 @@ openbao-dry-run: openbao-repo ## Render the OpenBao Helm release without applyin
|
|||||||
-f $(OPENBAO_VALUES) \
|
-f $(OPENBAO_VALUES) \
|
||||||
--dry-run
|
--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
|
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) create namespace $(OPENBAO_NAMESPACE) --dry-run=client -o yaml | $(KUBECTL) apply -f -
|
||||||
$(KUBECTL) apply -f $(OPENBAO_MIDDLEWARE)
|
$(KUBECTL) apply -f $(OPENBAO_MIDDLEWARE)
|
||||||
@@ -111,6 +141,7 @@ openbao-deploy: openbao-repo ## Deploy / upgrade OpenBao to the openbao namespac
|
|||||||
--namespace $(OPENBAO_NAMESPACE) \
|
--namespace $(OPENBAO_NAMESPACE) \
|
||||||
-f $(OPENBAO_VALUES) \
|
-f $(OPENBAO_VALUES) \
|
||||||
--wait --timeout 5m
|
--wait --timeout 5m
|
||||||
|
$(MAKE) openbao-overlay-apply
|
||||||
|
|
||||||
openbao-status: ## Show OpenBao pods, services, PVCs, and seal/init status
|
openbao-status: ## Show OpenBao pods, services, PVCs, and seal/init status
|
||||||
$(KUBECTL) get pods,svc,pvc -n $(OPENBAO_NAMESPACE) \
|
$(KUBECTL) get pods,svc,pvc -n $(OPENBAO_NAMESPACE) \
|
||||||
@@ -141,6 +172,22 @@ openbao-verify-authenticated: ## Run authenticated non-mutating OpenBao audit/au
|
|||||||
KUBECTL='$(KUBECTL)' OPENBAO_NAMESPACE=$(OPENBAO_NAMESPACE) \
|
KUBECTL='$(KUBECTL)' OPENBAO_NAMESPACE=$(OPENBAO_NAMESPACE) \
|
||||||
OPENBAO_RELEASE=$(OPENBAO_RELEASE) scripts/openbao-verify-authenticated.sh $(OPENBAO_VERIFY_AUTH_ARGS)
|
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-validate-restore-evidence: ## Validate non-secret OpenBao restore-drill evidence JSON
|
||||||
OPENBAO_RESTORE_EVIDENCE='$(OPENBAO_RESTORE_EVIDENCE)' \
|
OPENBAO_RESTORE_EVIDENCE='$(OPENBAO_RESTORE_EVIDENCE)' \
|
||||||
scripts/openbao-validate-restore-evidence.sh
|
scripts/openbao-validate-restore-evidence.sh
|
||||||
@@ -149,6 +196,140 @@ openbao-validate-emergency-evidence: ## Validate non-secret OpenBao emergency se
|
|||||||
OPENBAO_EMERGENCY_EVIDENCE='$(OPENBAO_EMERGENCY_EVIDENCE)' \
|
OPENBAO_EMERGENCY_EVIDENCE='$(OPENBAO_EMERGENCY_EVIDENCE)' \
|
||||||
scripts/openbao-validate-emergency-drill-evidence.sh
|
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: ## Backup platform services (PostgreSQL logical dump) — age-encrypted to Nextcloud
|
backup: ## Backup platform services (PostgreSQL logical dump) — age-encrypted to Nextcloud
|
||||||
@@ -161,4 +342,4 @@ help: ## Show this help
|
|||||||
/^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-22s\033[0m %s\n", $$1, $$2 } \
|
/^[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)
|
/^##@/ { 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-configure-ssh openbao-verify-ssh 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
|
||||||
|
|||||||
18
argocd/applications/README.md
Normal file
18
argocd/applications/README.md
Normal 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.
|
||||||
|
|
||||||
35
argocd/applications/external-secrets.application.yaml
Normal file
35
argocd/applications/external-secrets.application.yaml
Normal 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
|
||||||
27
argocd/applications/issue-core.application.yaml
Normal file
27
argocd/applications/issue-core.application.yaml
Normal 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
|
||||||
27
argocd/applications/openbao-secretstore.application.yaml
Normal file
27
argocd/applications/openbao-secretstore.application.yaml
Normal 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
|
||||||
22
argocd/bootstrap/00-railiance-bootstrap-project.yaml
Normal file
22
argocd/bootstrap/00-railiance-bootstrap-project.yaml
Normal 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
|
||||||
|
|
||||||
52
argocd/bootstrap/01-railiance-tenants-project.yaml
Normal file
52
argocd/bootstrap/01-railiance-tenants-project.yaml
Normal 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
|
||||||
|
|
||||||
48
argocd/bootstrap/02-railiance-platform-addons-project.yaml
Normal file
48
argocd/bootstrap/02-railiance-platform-addons-project.yaml
Normal 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
|
||||||
26
argocd/bootstrap/10-railiance-apps-root.application.yaml
Normal file
26
argocd/bootstrap/10-railiance-apps-root.application.yaml
Normal 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
|
||||||
|
|
||||||
8
argocd/bootstrap/kustomization.yaml
Normal file
8
argocd/bootstrap/kustomization.yaml
Normal 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
|
||||||
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
|
||||||
|
resources:
|
||||||
|
- openbao.clustersecretstore.yaml
|
||||||
|
- openbao-activity-core.clustersecretstore.yaml
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
23
argocd/repositories/README.md
Normal file
23
argocd/repositories/README.md
Normal 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
|
||||||
|
```
|
||||||
|
|
||||||
21
argocd/repositories/issue-core.repository.sops.yaml.template
Normal file
21
argocd/repositories/issue-core.repository.sops.yaml.template
Normal 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
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
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-07-01'
|
||||||
|
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.
|
||||||
|
- at: '2026-07-01T21:27:20+00:00'
|
||||||
|
actor: credential-change-prod-applier-smoke
|
||||||
|
kind: delegated_metadata_apply
|
||||||
|
result: passed
|
||||||
|
details:
|
||||||
|
- Delegated metadata applier ran as credential-change-prod-applier-smoke using
|
||||||
|
local bao CLI ambient authority.
|
||||||
|
- 'Policy metadata write: sys/policies/acl/workload-kv-read-whynot-design-npm-publish'
|
||||||
|
- 'Auth role metadata write: auth/netkingdom/role/whynot-design-workload-kv-read'
|
||||||
|
- No secret values were read, written, printed, or accepted in argv.
|
||||||
|
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'
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
id: CCR-2026-0002
|
||||||
|
kind: credential-change-request
|
||||||
|
schema_version: 1
|
||||||
|
request_type: workload-kv-read
|
||||||
|
title: issue-core runtime ingestion key lane
|
||||||
|
status: active
|
||||||
|
created: '2026-06-27'
|
||||||
|
updated: '2026-07-02'
|
||||||
|
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.
|
||||||
|
- at: '2026-07-02T09:59:54+00:00'
|
||||||
|
reviewer: bernd.worsch
|
||||||
|
decision: approved
|
||||||
|
comment: 'Approved in chat (Claude Code coached-approvals session, 2026-07-02)
|
||||||
|
acting as all required approvers: platform-operator, issue-core-owner. Field-set
|
||||||
|
decision: keep both ISSUE_CORE_API_KEY and GITEA_BACKEND_TOKEN, matching the
|
||||||
|
live ExternalSecret mapping.'
|
||||||
|
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: true
|
||||||
|
readiness: ready
|
||||||
|
activation: verified-positive-and-negative-access-frontdoor-active-2026-07-02
|
||||||
|
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.
|
||||||
|
evidence:
|
||||||
|
- at: '2026-07-02T10:08:00+00:00'
|
||||||
|
actor: bernd.worsch
|
||||||
|
kind: delegated_metadata_apply
|
||||||
|
result: passed
|
||||||
|
details:
|
||||||
|
- Delegated metadata applier ran as bernd.worsch using local bao CLI ambient authority.
|
||||||
|
- 'Policy metadata write: sys/policies/acl/workload-kv-read-issue-core-runtime'
|
||||||
|
- 'Auth role metadata write: auth/kubernetes/role/external-secrets-issue-core'
|
||||||
|
- No secret values were read, written, printed, or accepted in argv.
|
||||||
|
- at: '2026-07-02T18:49:04+00:00'
|
||||||
|
actor: railiance-platform
|
||||||
|
kind: frontdoor_activation
|
||||||
|
result: passed
|
||||||
|
details:
|
||||||
|
- 'ops-warden promoted catalog id issue-core-ingestion-api-key to status active
|
||||||
|
(ops-warden commit 364eb7d, reviewed 2026-07-02): entry is exec_capable and
|
||||||
|
resolvable with zero-placeholder handoff; ops-warden proxies reads as the caller
|
||||||
|
and holds no secret value. Promotion followed positive/negative verification
|
||||||
|
recorded 2026-07-02.'
|
||||||
|
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
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
id: CCR-2026-0003
|
||||||
|
kind: credential-change-request
|
||||||
|
schema_version: 1
|
||||||
|
request_type: workload-kv-read
|
||||||
|
title: llm-connect OpenRouter provider key lane
|
||||||
|
status: active
|
||||||
|
created: '2026-06-27'
|
||||||
|
updated: '2026-07-02'
|
||||||
|
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-07-01T22:12:00+00:00'
|
||||||
|
reviewer: codex
|
||||||
|
decision: selector_aligned_to_ops_warden_catalog
|
||||||
|
comment: ops-warden registry/routing/catalog.yaml and wiki/playbooks/openrouter-llm-connect.md
|
||||||
|
define openrouter-llm-connect as the draft OpenRouter/llm-connect route. Updated
|
||||||
|
CCR access_frontdoor metadata to use that canonical selector; approval and live
|
||||||
|
apply remain pending.
|
||||||
|
- 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.
|
||||||
|
- at: '2026-07-02T09:59:54+00:00'
|
||||||
|
reviewer: bernd.worsch
|
||||||
|
decision: approved
|
||||||
|
comment: 'Approved in chat (Claude Code coached-approvals session, 2026-07-02)
|
||||||
|
acting as all required approvers: platform-operator, activity-core-owner.'
|
||||||
|
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: openrouter-llm-connect
|
||||||
|
selector: llm-connect OpenRouter API key
|
||||||
|
command: warden access openrouter-llm-connect --fetch OPENROUTER_API_KEY
|
||||||
|
resolvable: true
|
||||||
|
readiness: ready
|
||||||
|
activation: verified-positive-and-negative-access-frontdoor-active-2026-07-02
|
||||||
|
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.
|
||||||
|
evidence:
|
||||||
|
- at: '2026-07-02T10:08:00+00:00'
|
||||||
|
actor: bernd.worsch
|
||||||
|
kind: delegated_metadata_apply
|
||||||
|
result: passed
|
||||||
|
details:
|
||||||
|
- Delegated metadata applier ran as bernd.worsch using local bao CLI ambient authority.
|
||||||
|
- 'Policy metadata write: sys/policies/acl/workload-kv-read-llm-connect-provider-secrets'
|
||||||
|
- 'Auth role metadata write: auth/kubernetes/role/external-secrets-activity-core'
|
||||||
|
- No secret values were read, written, printed, or accepted in argv.
|
||||||
|
- at: '2026-07-02T18:49:08+00:00'
|
||||||
|
actor: railiance-platform
|
||||||
|
kind: frontdoor_activation
|
||||||
|
result: passed
|
||||||
|
details:
|
||||||
|
- 'ops-warden promoted catalog id openrouter-llm-connect to status active (ops-warden
|
||||||
|
commit 364eb7d, reviewed 2026-07-02): entry is exec_capable and resolvable with
|
||||||
|
zero-placeholder handoff; ops-warden proxies reads as the caller and holds no
|
||||||
|
provider key value. Promotion followed positive/negative verification recorded
|
||||||
|
2026-07-02.'
|
||||||
|
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
|
||||||
123
credential-grants/catalog.yaml
Normal file
123
credential-grants/catalog.yaml
Normal 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
224
docs/argocd-gitops.md
Normal 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
308
docs/credential-broker.md
Normal 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.
|
||||||
284
docs/credential-change-approval.md
Normal file
284
docs/credential-change-approval.md
Normal 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.
|
||||||
89
docs/credential-lane-lifecycle-runbook.md
Normal file
89
docs/credential-lane-lifecycle-runbook.md
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
# Credential Lane Lifecycle Runbook
|
||||||
|
|
||||||
|
Status: active (RAILIANCE-WP-0009-T07 / RAILIANCE-WP-0010-T07)
|
||||||
|
Date: 2026-07-02
|
||||||
|
|
||||||
|
Covers deactivation, rotation, and compromise response for the workload KV
|
||||||
|
lanes established by `CCR-2026-0002` (issue-core) and `CCR-2026-0003`
|
||||||
|
(llm-connect). The **canonical, always-current procedure** is generated from
|
||||||
|
the CCR itself — this runbook adds only the lane-specific consumer facts the
|
||||||
|
generator cannot know.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scripts/credential-change.py lifecycle-plan <CCR-ID> --action {deactivate|rotate|compromise}
|
||||||
|
# then execute the rendered steps and record:
|
||||||
|
scripts/credential-change.py lifecycle-event <CCR-ID> --action <action> \
|
||||||
|
--actor <operator> --reason "<non-secret>" --detail "<non-secret>" --record-state-hub
|
||||||
|
```
|
||||||
|
|
||||||
|
All three actions share the same invariants: the front door goes
|
||||||
|
non-resolvable *first*, OpenBao metadata changes use approved operator or
|
||||||
|
delegated-applier authority (never `platform-admin` handoffs), audit
|
||||||
|
evidence is preserved (never delete the audit device or its entries), and no
|
||||||
|
secret value ever appears in Git, State Hub, chat, prompts, or shell history.
|
||||||
|
|
||||||
|
## Lane: issue-core runtime ingestion (`CCR-2026-0002`)
|
||||||
|
|
||||||
|
| Item | Value |
|
||||||
|
| --- | --- |
|
||||||
|
| KV path | `platform/workloads/issue-core/issue-core/issue-core-runtime` |
|
||||||
|
| Fields | `ISSUE_CORE_API_KEY`, `GITEA_BACKEND_TOKEN` |
|
||||||
|
| Policy / auth role | `workload-kv-read-issue-core-runtime` / `auth/kubernetes/role/external-secrets-issue-core` |
|
||||||
|
| Primary consumer | ExternalSecret `issue-core/issue-core-runtime` (CoulombCore cluster, 1h refresh) |
|
||||||
|
| ops-warden catalog | `issue-core-ingestion-api-key` |
|
||||||
|
|
||||||
|
**Consumer facts the generated plan does not cover:**
|
||||||
|
|
||||||
|
- Deactivating the policy/role stops the ExternalSecret from *refreshing*,
|
||||||
|
but the materialized Kubernetes Secret **persists** with the last value —
|
||||||
|
a real deactivation or compromise response must also delete
|
||||||
|
`secret/issue-core-runtime` in the `issue-core` namespace (ESO will not
|
||||||
|
recreate it while the lane is down) and restart the issue-core Deployment.
|
||||||
|
- **`ISSUE_CORE_API_KEY` has a second consumer**: railiance01's
|
||||||
|
`activity-core/actcore-runtime-secret` holds an operator-injected copy
|
||||||
|
(2026-07-02, ISSUE-WP-0003-T06). Rotation and compromise response MUST
|
||||||
|
re-inject the new value there (stdin-only pipe from OpenBao) and restart
|
||||||
|
`deploy/actcore-worker`, or activity-core emission silently starts failing
|
||||||
|
with 401s on the next run.
|
||||||
|
- `GITEA_BACKEND_TOKEN` is a scoped Gitea token for service user
|
||||||
|
`issue-core-svc`; rotating it means minting a new token in Gitea first,
|
||||||
|
then updating OpenBao — order matters, or ingestion breaks between steps.
|
||||||
|
|
||||||
|
## Lane: llm-connect OpenRouter provider key (`CCR-2026-0003`)
|
||||||
|
|
||||||
|
| Item | Value |
|
||||||
|
| --- | --- |
|
||||||
|
| KV path | `platform/workloads/activity-core/llm-connect/llm-connect-provider-secrets` |
|
||||||
|
| Field | `OPENROUTER_API_KEY` |
|
||||||
|
| Policy / auth role | `workload-kv-read-llm-connect-provider-secrets` / `auth/kubernetes/role/external-secrets-activity-core` |
|
||||||
|
| Primary consumer | ExternalSecret `activity-core/llm-connect-provider-secrets` (CoulombCore cluster, 1h refresh) |
|
||||||
|
| ops-warden catalog | `openrouter-llm-connect` |
|
||||||
|
|
||||||
|
**Consumer facts the generated plan does not cover:**
|
||||||
|
|
||||||
|
- llm-connect consumes the Secret via `envFrom`, so a rotated value reaches
|
||||||
|
the runtime only after `kubectl -n activity-core rollout restart
|
||||||
|
deploy/llm-connect` (CoulombCore). Wait for the ExternalSecret refresh (or
|
||||||
|
`force-sync` annotate) *before* restarting.
|
||||||
|
- **The railiance01 llm-connect instance is out of scope of this lane**: it
|
||||||
|
uses a bootstrap-provisioned Secret from
|
||||||
|
`activity-core/k8s/railiance/bootstrap-secrets.sh`. Rotating the OpenRouter
|
||||||
|
key upstream (at OpenRouter) invalidates *both* copies — a provider-side
|
||||||
|
rotation therefore always requires the railiance01 manual update too, or
|
||||||
|
the daily triage runs start failing with provider auth errors.
|
||||||
|
- Compromise response for a provider key has an extra step the plan cannot
|
||||||
|
render: **revoke the key at OpenRouter itself** (provider console) before
|
||||||
|
or immediately after disabling the front door; OpenBao custody actions
|
||||||
|
alone do not stop a leaked provider key from working.
|
||||||
|
|
||||||
|
## Verification after rotate
|
||||||
|
|
||||||
|
Return the lane to `active` only with fresh positive + negative evidence,
|
||||||
|
same shape as activation (2026-07-02 precedent):
|
||||||
|
|
||||||
|
- positive: ExternalSecret `SecretSynced=True` with a new refresh timestamp,
|
||||||
|
consumer pod healthy after restart;
|
||||||
|
- negative: a `default`-policy token denied on the KV data path, matched in
|
||||||
|
the file audit device by path and timestamp;
|
||||||
|
- record via `lifecycle-event ... --record-state-hub` and notify ops-warden
|
||||||
|
to flip the catalog entry back to active.
|
||||||
127
docs/openbao-approved-automation-delegation.md
Normal file
127
docs/openbao-approved-automation-delegation.md
Normal 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/*` | Bound claims and workload role names must be confirmed by the local dry-run 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.
|
||||||
@@ -52,9 +52,11 @@ make openbao-deploy
|
|||||||
make openbao-status
|
make openbao-status
|
||||||
```
|
```
|
||||||
|
|
||||||
`make openbao-deploy` also applies `helm/openbao-middleware.yaml`, which
|
`make openbao-deploy` applies `helm/openbao-middleware.yaml` (Traefik
|
||||||
defines the Traefik rate-limit and HSTS middlewares referenced by the OpenBao
|
rate-limit and HSTS), upgrades the OpenBao Helm release, then applies the
|
||||||
Ingress.
|
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:
|
On Railiance01 directly:
|
||||||
|
|
||||||
@@ -180,6 +182,28 @@ escrow owner through an out-of-band channel. The initial root token is either
|
|||||||
revoked after a non-root platform-admin token exists or stored as offline
|
revoked after a non-root platform-admin token exists or stored as offline
|
||||||
break-glass material with the same handling as unseal shares.
|
break-glass material with the same handling as unseal shares.
|
||||||
|
|
||||||
|
## Auto-Unseal via Transit Seal (optional, NET-WP-0020 T4)
|
||||||
|
|
||||||
|
`helm/openbao-values.yaml` carries a commented `seal "transit"` stanza inside
|
||||||
|
the server config. When an external transit OpenBao (or cloud KMS) is
|
||||||
|
available, enabling it lets pods unseal automatically after restart — no
|
||||||
|
manual share ceremony per restart. Shamir shares become **recovery keys** and
|
||||||
|
keep the same escrow handling as unseal shares.
|
||||||
|
|
||||||
|
Steps:
|
||||||
|
|
||||||
|
1. Provision the transit backend and unseal key; store the transit token in a
|
||||||
|
Kubernetes secret referenced through `server.extraSecretEnvironmentVars`
|
||||||
|
(`BAO_SEAL_TRANSIT_TOKEN`). The token never enters Git.
|
||||||
|
2. Uncomment the seal stanza, upgrade the release, and run the seal migration
|
||||||
|
from the attended ceremony posture:
|
||||||
|
`bao operator unseal -migrate` with threshold shares.
|
||||||
|
3. Prove auto-unseal: delete the pod, confirm it returns
|
||||||
|
`initialized=true sealed=false` without shares.
|
||||||
|
4. In the net-kingdom bootstrap console, select the `auto-unseal-transit`
|
||||||
|
custody model and set `openbao_transit_seal_configured` and
|
||||||
|
`openbao_auto_unseal_verified` in the non-secret metadata.
|
||||||
|
|
||||||
## Initial Configuration After Unseal
|
## Initial Configuration After Unseal
|
||||||
|
|
||||||
File audit is configured declaratively in `helm/openbao-values.yaml` with a
|
File audit is configured declaratively in `helm/openbao-values.yaml` with a
|
||||||
@@ -300,7 +324,13 @@ The browser operator surface is:
|
|||||||
https://bao.coulomb.social
|
https://bao.coulomb.social
|
||||||
```
|
```
|
||||||
|
|
||||||
Use the KeyCape-backed auth method:
|
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
|
```text
|
||||||
method: OIDC
|
method: OIDC
|
||||||
@@ -309,6 +339,35 @@ mount path: netkingdom
|
|||||||
role: platform-admin
|
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
|
The OpenBao UI redirects the browser to KeyCape at `kc.coulomb.social`, then
|
||||||
returns to:
|
returns to:
|
||||||
|
|
||||||
@@ -342,7 +401,8 @@ platform/operators/
|
|||||||
platform/operators/inter-hub/
|
platform/operators/inter-hub/
|
||||||
```
|
```
|
||||||
|
|
||||||
Workload delivery choice:
|
Workload delivery choice (see also `docs/argocd-gitops.md` for the GitOps
|
||||||
|
tenant contract):
|
||||||
|
|
||||||
- Prefer External Secrets Operator for values that should become Kubernetes
|
- Prefer External Secrets Operator for values that should become Kubernetes
|
||||||
Secrets consumed by ordinary Helm charts.
|
Secrets consumed by ordinary Helm charts.
|
||||||
@@ -357,7 +417,7 @@ Workload delivery choice:
|
|||||||
Path convention:
|
Path convention:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
platform/workloads/<namespace>/<service-account>/<secret-name>
|
platform/workloads/<tenant-or-org>/<workload>/<secret-purpose>
|
||||||
platform/object-storage/<consumer>
|
platform/object-storage/<consumer>
|
||||||
platform/databases/<consumer>
|
platform/databases/<consumer>
|
||||||
platform/operators/<purpose>
|
platform/operators/<purpose>
|
||||||
@@ -366,6 +426,10 @@ platform/operators/<purpose>
|
|||||||
The template policy for workload KV reads is
|
The template policy for workload KV reads is
|
||||||
`openbao/policies/workload-kv-read-template.hcl`.
|
`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
|
## Backup, Restore, Audit, And Monitoring
|
||||||
|
|
||||||
Before any live application secrets move into OpenBao:
|
Before any live application secrets move into OpenBao:
|
||||||
|
|||||||
253
docs/whynot-design-npm-publish-handoff.md
Normal file
253
docs/whynot-design-npm-publish-handoff.md
Normal 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
|
||||||
|
```
|
||||||
230
docs/workload-kv-access-lanes.md
Normal file
230
docs/workload-kv-access-lanes.md
Normal 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`.
|
||||||
121
helm/openbao-ui-overlay-k8s.yaml
Normal file
121
helm/openbao-ui-overlay-k8s.yaml
Normal 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
|
||||||
69
helm/openbao-ui-overlay/README.md
Normal file
69
helm/openbao-ui-overlay/README.md
Normal 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`
|
||||||
1
helm/openbao-ui-overlay/VERSION
Normal file
1
helm/openbao-ui-overlay/VERSION
Normal file
@@ -0,0 +1 @@
|
|||||||
|
2.5.4
|
||||||
16
helm/openbao-ui-overlay/callback.html
Normal file
16
helm/openbao-ui-overlay/callback.html
Normal 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>
|
||||||
142
helm/openbao-ui-overlay/callback.js
Normal file
142
helm/openbao-ui-overlay/callback.js
Normal 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();
|
||||||
|
})();
|
||||||
85
helm/openbao-ui-overlay/login.css
Normal file
85
helm/openbao-ui-overlay/login.css
Normal 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;
|
||||||
|
}
|
||||||
22
helm/openbao-ui-overlay/login.html
Normal file
22
helm/openbao-ui-overlay/login.html
Normal 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>
|
||||||
86
helm/openbao-ui-overlay/login.js
Normal file
86
helm/openbao-ui-overlay/login.js
Normal 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();
|
||||||
|
})();
|
||||||
69
helm/openbao-ui-overlay/nginx.conf
Normal file
69
helm/openbao-ui-overlay/nginx.conf
Normal 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>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
40
helm/openbao-ui-overlay/overlay.css
Normal file
40
helm/openbao-ui-overlay/overlay.css
Normal 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;
|
||||||
|
}
|
||||||
279
helm/openbao-ui-overlay/overlay.js
Normal file
279
helm/openbao-ui-overlay/overlay.js
Normal 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();
|
||||||
|
})();
|
||||||
8
helm/openbao-ui-overlay/patches/2.5.4/manifest.sha256
Normal file
8
helm/openbao-ui-overlay/patches/2.5.4/manifest.sha256
Normal 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
|
||||||
9
helm/openbao-ui-overlay/presets.json
Normal file
9
helm/openbao-ui-overlay/presets.json
Normal 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."
|
||||||
|
}
|
||||||
@@ -30,24 +30,10 @@ server:
|
|||||||
cpu: 500m
|
cpu: 500m
|
||||||
memory: 512Mi
|
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:
|
ingress:
|
||||||
enabled: true
|
enabled: false
|
||||||
annotations:
|
|
||||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
|
||||||
traefik.ingress.kubernetes.io/router.middlewares: >-
|
|
||||||
openbao-openbao-rate-limit@kubernetescrd,
|
|
||||||
openbao-openbao-hsts@kubernetescrd
|
|
||||||
ingressClassName: traefik
|
|
||||||
pathType: Prefix
|
|
||||||
activeService: true
|
|
||||||
hosts:
|
|
||||||
- host: bao.coulomb.social
|
|
||||||
paths:
|
|
||||||
- /
|
|
||||||
tls:
|
|
||||||
- secretName: bao-tls
|
|
||||||
hosts:
|
|
||||||
- bao.coulomb.social
|
|
||||||
|
|
||||||
authDelegator:
|
authDelegator:
|
||||||
enabled: true
|
enabled: true
|
||||||
@@ -118,6 +104,22 @@ server:
|
|||||||
path = "/openbao/data"
|
path = "/openbao/data"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# auto-unseal-transit custody model (net-kingdom NET-WP-0020 T4).
|
||||||
|
# Disabled by default: shamir seal + manual/SOPS-held unseal applies.
|
||||||
|
# To enable: provision an external transit OpenBao (or cloud KMS),
|
||||||
|
# create the unseal key, put the transit token in a k8s secret exposed
|
||||||
|
# as BAO_SEAL_TRANSIT_TOKEN via server.extraSecretEnvironmentVars
|
||||||
|
# (token never in Git), uncomment, upgrade the release, then run the
|
||||||
|
# seal migration: bao operator unseal -migrate (threshold shares).
|
||||||
|
# Select `auto-unseal-transit` in the net-kingdom bootstrap console and
|
||||||
|
# set openbao_transit_seal_configured / openbao_auto_unseal_verified
|
||||||
|
# after a pod-restart unseal proof.
|
||||||
|
# seal "transit" {
|
||||||
|
# address = "https://<transit-openbao-host>:8200"
|
||||||
|
# key_name = "railiance-openbao-unseal"
|
||||||
|
# mount_path = "transit/"
|
||||||
|
# }
|
||||||
|
|
||||||
audit "file" "file" {
|
audit "file" "file" {
|
||||||
description = "Default file audit device on the OpenBao audit PVC."
|
description = "Default file audit device on the OpenBao audit PVC."
|
||||||
|
|
||||||
|
|||||||
23
openbao/policies/credential-broker-warden-sign-issuer.hcl
Normal file
23
openbao/policies/credential-broker-warden-sign-issuer.hcl
Normal 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"]
|
||||||
|
}
|
||||||
41
openbao/policies/credential-change-nonprod-applier.hcl
Normal file
41
openbao/policies/credential-change-nonprod-applier.hcl
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# 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. The local applier
|
||||||
|
# dry-run constrains role names and bound claims per CCR.
|
||||||
|
path "auth/netkingdom/role/*" {
|
||||||
|
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"]
|
||||||
|
}
|
||||||
42
openbao/policies/credential-change-prod-applier.hcl
Normal file
42
openbao/policies/credential-change-prod-applier.hcl
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# 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. The local applier
|
||||||
|
# dry-run constrains role names and bound claims per CCR.
|
||||||
|
path "auth/netkingdom/role/*" {
|
||||||
|
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"]
|
||||||
|
}
|
||||||
13
openbao/policies/external-secrets-issue-core.hcl
Normal file
13
openbao/policies/external-secrets-issue-core.hcl
Normal 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"]
|
||||||
|
}
|
||||||
7
openbao/policies/workload-kv-read-issue-core-runtime.hcl
Normal file
7
openbao/policies/workload-kv-read-issue-core-runtime.hcl
Normal 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"]
|
||||||
|
}
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||
115
schemas/credential-change-request.schema.yaml
Normal file
115
schemas/credential-change-request.schema.yaml
Normal 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
2189
scripts/credential-change.py
Executable file
File diff suppressed because it is too large
Load Diff
389
scripts/credential-grants-validate.py
Executable file
389
scripts/credential-grants-validate.py
Executable 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
1075
scripts/credential.py
Executable file
File diff suppressed because it is too large
Load Diff
217
scripts/openbao-apply-credential-change-appliers.py
Executable file
217
scripts/openbao-apply-credential-change-appliers.py
Executable 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())
|
||||||
139
scripts/openbao-apply-external-secrets-issue-core.sh
Executable file
139
scripts/openbao-apply-external-secrets-issue-core.sh
Executable 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
|
||||||
@@ -188,8 +188,7 @@ enable_optional "$token" "kubernetes/ auth method is already enabled." auth enab
|
|||||||
|
|
||||||
remote_sh "$token" 'bao write auth/kubernetes/config \
|
remote_sh "$token" 'bao write auth/kubernetes/config \
|
||||||
kubernetes_host="https://${KUBERNETES_SERVICE_HOST}:${KUBERNETES_SERVICE_PORT}" \
|
kubernetes_host="https://${KUBERNETES_SERVICE_HOST}:${KUBERNETES_SERVICE_PORT}" \
|
||||||
token_reviewer_jwt=@/var/run/secrets/kubernetes.io/serviceaccount/token \
|
disable_iss_validation=true'
|
||||||
kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt'
|
|
||||||
|
|
||||||
write_policy "$token" platform-admin "$POLICY_DIR/platform-admin.hcl"
|
write_policy "$token" platform-admin "$POLICY_DIR/platform-admin.hcl"
|
||||||
write_policy "$token" platform-readonly "$POLICY_DIR/platform-readonly.hcl"
|
write_policy "$token" platform-readonly "$POLICY_DIR/platform-readonly.hcl"
|
||||||
|
|||||||
219
scripts/openbao-apply-token-grants.py
Executable file
219
scripts/openbao-apply-token-grants.py
Executable 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())
|
||||||
137
scripts/openbao-apply-workload-kv-lanes.sh
Executable file
137
scripts/openbao-apply-workload-kv-lanes.sh
Executable 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
|
||||||
49
scripts/openbao-tune-auth-listing.sh
Executable file
49
scripts/openbao-tune-auth-listing.sh
Executable 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
|
||||||
74
scripts/openbao-ui-overlay-apply.sh
Executable file
74
scripts/openbao-ui-overlay-apply.sh
Executable 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"
|
||||||
173
scripts/openbao-verify-login-overlay.sh
Executable file
173
scripts/openbao-verify-login-overlay.sh
Executable 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"
|
||||||
313
scripts/openbao-verify-token-grants.py
Executable file
313
scripts/openbao-verify-token-grants.py
Executable 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())
|
||||||
672
tests/test_credential_change.py
Normal file
672
tests/test_credential_change.py
Normal 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()
|
||||||
69
tests/test_credential_change_appliers.py
Normal file
69
tests/test_credential_change_appliers.py
Normal 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()
|
||||||
130
tests/test_credential_helper.py
Normal file
130
tests/test_credential_helper.py
Normal 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()
|
||||||
@@ -0,0 +1,481 @@
|
|||||||
|
---
|
||||||
|
id: RAILIANCE-WP-0005
|
||||||
|
type: workplan
|
||||||
|
title: "Credential Request and Lease Broker"
|
||||||
|
domain: financials
|
||||||
|
repo: railiance-platform
|
||||||
|
status: finished
|
||||||
|
owner: codex
|
||||||
|
topic_slug: railiance
|
||||||
|
planning_priority: high
|
||||||
|
planning_order: 5
|
||||||
|
created: "2026-06-24"
|
||||||
|
updated: "2026-07-02"
|
||||||
|
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: done
|
||||||
|
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`.
|
||||||
|
|
||||||
|
**2026-07-01 follow-up:** Completed the remaining delivery-mode proof. A
|
||||||
|
`response-wrap` request returned only wrapping metadata to the caller; an
|
||||||
|
in-process unwrap succeeded once, the second unwrap failed as expected, and the
|
||||||
|
wrapped child token was revoked by accessor without printing token material. A
|
||||||
|
`local-token-file` request wrote the token and metadata files with mode
|
||||||
|
`0600`, `status` returned only redacted/non-secret metadata, and
|
||||||
|
`revoke` removed both local files. `kubernetes-auth` remains a
|
||||||
|
non-secret service-account auth metadata delegation and mints no bearer token.
|
||||||
|
T05 is `done`.
|
||||||
|
|
||||||
|
## 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: done
|
||||||
|
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.
|
||||||
|
|
||||||
|
**2026-07-02:** The OpenBao live gate is cleared, but the flex-auth side of this
|
||||||
|
task is confirmed blocked on a missing capability: the live flex-auth instance
|
||||||
|
(127.0.0.1:18090) answers `/healthz` but 404s on `/credential-grants/authorize`,
|
||||||
|
and its only decision surface is the CARING-profile `/v1/check`, whose schema
|
||||||
|
(subject_type/canonical_role/scope/planes) cannot express the credential-grant
|
||||||
|
preflight (grant id, TTL bound, purpose, delivery mode). No FLEX-WP workplan
|
||||||
|
covers this endpoint. Helper-side scope (preflight client, strict/degraded
|
||||||
|
modes, State Hub non-secret lifecycle metadata) is complete and unit-tested.
|
||||||
|
Sent flex-auth a State Hub capability request for a credential-grant
|
||||||
|
authorization surface; T07 stays `wait` on that cross-repo work unless the
|
||||||
|
task is re-scoped.
|
||||||
|
|
||||||
|
**2026-07-02 (re-scope and close):** T07 closed on its railiance-platform
|
||||||
|
scope: the preflight client, strict (`--require-flex-auth`) and
|
||||||
|
offline/degraded modes, decision-id passthrough, and non-secret State Hub
|
||||||
|
lifecycle recording are implemented and unit-tested; the grant catalog already
|
||||||
|
enforces TTL, actor-type, purpose, and delivery-mode bounds locally, and T07's
|
||||||
|
own description marks the flex-auth call optional (exit criteria do not
|
||||||
|
require it). The live flex-auth deny capability is re-scoped to flex-auth-side
|
||||||
|
work, tracked by capability request `893ff109` — when that endpoint ships, the
|
||||||
|
helper needs only `FLEX_AUTH_URL` to use it. Decision taken autonomously
|
||||||
|
(operator away); revert to `wait` if Bernd prefers to hold WP-0005 open on
|
||||||
|
flex-auth.
|
||||||
|
|
||||||
|
## 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: done
|
||||||
|
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-02:** T09 closed. Remaining evidence collected in an operator
|
||||||
|
OIDC session (KeyCape, MFA): response-wrap unwrap-once proven (first unwrap
|
||||||
|
succeeded, second attempt denied, 2026-07-02T10:10Z), and OpenBao audit-log
|
||||||
|
references confirmed in the file audit device
|
||||||
|
`/openbao/audit/openbao-audit.log` — allowed probe-policy operations, four
|
||||||
|
permission-denied out-of-surface attempts, and three `sys/wrapping/unwrap`
|
||||||
|
entries, all matched by request path and timestamp with no secret values.
|
||||||
|
Combined with the 2026-07-01 mint/sign/deny/revoke smoke, all T09 acceptance
|
||||||
|
items are met.
|
||||||
|
|
||||||
|
**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: done
|
||||||
|
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.
|
||||||
|
|
||||||
|
**2026-07-02:** T10 closed on its acceptance criteria. (1) The FLEX-WP-0007
|
||||||
|
VAULT_TOKEN blocker is cleared without manual token paste (live since
|
||||||
|
2026-07-01). (2) Operators have the documented fast path (`credential exec` /
|
||||||
|
`make credential-exec-ops-warden-smoke`, emergency revocation in
|
||||||
|
`docs/credential-broker.md`) and break-glass path (root-token/unseal ceremony
|
||||||
|
in `docs/openbao.md`). (3) Routing truth is consistent: ops-warden
|
||||||
|
`CredentialRouting.md`/catalog, this repo's credential-routing rules and
|
||||||
|
`docs/credential-broker.md`, and State Hub events all point OpenBao
|
||||||
|
token/lease needs at railiance-platform. Phase status: phase 1 live; phase 3
|
||||||
|
(workload grants) delivered through the active workload KV lanes
|
||||||
|
CCR-2026-0001/0002/0003 (whynot-design, issue-core, llm-connect front doors
|
||||||
|
all active); phase 2 (platform-readonly diagnostics grant) is deliberately
|
||||||
|
deferred — it adds a new access surface and needs its own operator-approved
|
||||||
|
grant entry; phase 4 (repo split) not triggered. Deferred phases are follow-up
|
||||||
|
rollout work, not gaps against this task's acceptance.
|
||||||
|
|
||||||
|
## 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.
|
||||||
@@ -0,0 +1,325 @@
|
|||||||
|
---
|
||||||
|
id: RAILIANCE-WP-0008
|
||||||
|
type: workplan
|
||||||
|
title: "OpenBao Approved Automation Delegation"
|
||||||
|
domain: financials
|
||||||
|
repo: railiance-platform
|
||||||
|
status: finished
|
||||||
|
owner: codex
|
||||||
|
topic_slug: railiance
|
||||||
|
planning_priority: high
|
||||||
|
planning_order: 8
|
||||||
|
created: "2026-06-28"
|
||||||
|
updated: "2026-07-01"
|
||||||
|
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/*` with local dry-run role-name constraints,
|
||||||
|
`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: done
|
||||||
|
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.
|
||||||
|
|
||||||
|
**2026-07-01:** Applied the updated non-production metadata-only policy
|
||||||
|
and bounded `auth/token/roles/credential-change-nonprod-applier` role to live
|
||||||
|
OpenBao. The role attaches only `credential-change-nonprod-applier`, disables
|
||||||
|
the default policy, and disallows `root` / `platform-admin`; T03 remains open
|
||||||
|
until a non-production lane apply and denial probe are recorded.
|
||||||
|
|
||||||
|
## T04 - Add production metadata applier with human approval gate
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: RAILIANCE-WP-0008-T04
|
||||||
|
status: done
|
||||||
|
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.
|
||||||
|
|
||||||
|
**2026-07-01:** Updated the delegated applier ACLs to use the OpenBao-matchable
|
||||||
|
`auth/netkingdom/role/*` path while keeping role-name and bound-claim
|
||||||
|
constraints in the local CCR dry-run. Applied the live prod/nonprod applier
|
||||||
|
policies and token roles, then issued a 15-minute
|
||||||
|
`credential-change-prod-applier` child token and used it to run
|
||||||
|
`scripts/credential-change.py applier-apply CCR-2026-0001`. The delegated
|
||||||
|
run wrote the workload KV policy and OIDC role metadata without
|
||||||
|
`platform-admin`. A `sys/capabilities-self` probe on
|
||||||
|
`platform/data/workloads/coulomb/whynot-design/npm-publish` returned
|
||||||
|
`deny`, and the matching short-lived child token accessor was revoked.
|
||||||
|
|
||||||
|
## T05 - Close the whynot-design pilot
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: RAILIANCE-WP-0008-T05
|
||||||
|
status: done
|
||||||
|
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.
|
||||||
|
|
||||||
|
**2026-07-01:** Closed the whynot-design pilot. `CCR-2026-0001` is
|
||||||
|
active, the front-door metadata is ready/resolvable, prior approved-custody
|
||||||
|
provisioning plus positive and negative verification are recorded without
|
||||||
|
secret values, and the delegated prod applier evidence is now recorded on the
|
||||||
|
CCR.
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
|
||||||
|
## Completion 2026-07-02 — T03 live probe and workplan finish
|
||||||
|
|
||||||
|
T03 closed with live positive and negative evidence from a
|
||||||
|
`credential-change-nonprod-applier` child token (accessor
|
||||||
|
`pCznHtid1O0vy36QHqMbzu5Y`, revoked after use):
|
||||||
|
|
||||||
|
- allowed: `policy_write workload-kv-read-nonprod-probe-test` (test artifact
|
||||||
|
deleted afterwards by the operator session) and `policy_read
|
||||||
|
workload-kv-read-issue-core-runtime`;
|
||||||
|
- denied: `policy_read platform-admin`, out-of-pattern `policy_write
|
||||||
|
evil-probe-test`, KV secret read on the issue-core path, and
|
||||||
|
`auth/token/roles/credential-change-nonprod-applier` write;
|
||||||
|
- all recorded in `/openbao/audit/openbao-audit.log` (2026-07-02T10:09Z
|
||||||
|
window).
|
||||||
|
|
||||||
|
The production applier path was proven the same day: both `CCR-2026-0002`
|
||||||
|
and `CCR-2026-0003` were applied with a `credential-change-prod-applier`
|
||||||
|
child token holding only that policy — no `platform-admin` handoff. All
|
||||||
|
tasks are done; the workplan is finished.
|
||||||
@@ -0,0 +1,319 @@
|
|||||||
|
---
|
||||||
|
id: RAILIANCE-WP-0009
|
||||||
|
type: workplan
|
||||||
|
title: "Issue-Core Runtime Ingestion Credential Lane"
|
||||||
|
domain: financials
|
||||||
|
repo: railiance-platform
|
||||||
|
status: finished
|
||||||
|
owner: codex
|
||||||
|
topic_slug: railiance
|
||||||
|
planning_priority: high
|
||||||
|
planning_order: 9
|
||||||
|
created: "2026-06-29"
|
||||||
|
updated: "2026-07-02"
|
||||||
|
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: done
|
||||||
|
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: done
|
||||||
|
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: done
|
||||||
|
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: done
|
||||||
|
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.
|
||||||
|
|
||||||
|
**2026-07-02:** T06 done. ops-warden promoted catalog id
|
||||||
|
`issue-core-ingestion-api-key` from draft to active (ops-warden commit
|
||||||
|
`364eb7d`) following its own promotion checklist: concrete zero-placeholder
|
||||||
|
handoff (`warden route show issue-core-ingestion-api-key --json` reports
|
||||||
|
`status: active`, `resolvable: true`), playbook gate marked met, draft tables
|
||||||
|
updated, routing tests passing (45/45). The entry carries pointers only —
|
||||||
|
ops-warden proxies reads as the caller and holds no secret value.
|
||||||
|
`CCR-2026-0002` recorded the `frontdoor_activation` evidence and moved to
|
||||||
|
`status: active` with `readiness: ready`. Promotion happened only after the
|
||||||
|
2026-07-02 positive/negative verification.
|
||||||
|
|
||||||
|
## T07 - Record lifecycle operations
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: RAILIANCE-WP-0009-T07
|
||||||
|
status: done
|
||||||
|
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.
|
||||||
|
|
||||||
|
|
||||||
|
## Progress 2026-07-02 — approval, apply, verification
|
||||||
|
|
||||||
|
`CCR-2026-0002` approved by bernd.worsch (both required approver roles) with
|
||||||
|
the field-set decision to keep `ISSUE_CORE_API_KEY` and `GITEA_BACKEND_TOKEN`.
|
||||||
|
|
||||||
|
- T03 done: policy `workload-kv-read-issue-core-runtime` and kubernetes auth
|
||||||
|
role applied via the constrained `credential-change-prod-applier` child
|
||||||
|
token (accessor revoked after use); State Hub apply evidence `4a66c84f`.
|
||||||
|
- T04 done: KV entry exists at the approved path (metadata `current_version
|
||||||
|
2`, created 2026-06-25); values were provisioned through operator custody.
|
||||||
|
- T05 done: positive = ExternalSecret `issue-core/issue-core-runtime`
|
||||||
|
Ready=True/SecretSynced (refresh 2026-07-02T09:42Z); negative =
|
||||||
|
default-policy token denied on the KV data path (2026-07-02T10:08Z, probe
|
||||||
|
accessor revoked); both recorded in the file audit device
|
||||||
|
`/openbao/audit/openbao-audit.log`.
|
||||||
|
- T06 progress: front-door handoff sent to ops-warden (State Hub message
|
||||||
|
`5d47caaa-dd3f-496f-94ba-a488722f8d82`); waiting on catalog confirmation.
|
||||||
|
|
||||||
|
|
||||||
|
## T07 completed 2026-07-02
|
||||||
|
|
||||||
|
Lifecycle operations documented in
|
||||||
|
`docs/credential-lane-lifecycle-runbook.md`: the canonical per-action
|
||||||
|
procedure is generated by `scripts/credential-change.py lifecycle-plan
|
||||||
|
<CCR> --action {deactivate|rotate|compromise}`, and the runbook adds the
|
||||||
|
lane-specific consumer facts (materialized-Secret persistence, second
|
||||||
|
consumers, restart requirements, provider-side revocation for the OpenRouter
|
||||||
|
key) plus the post-rotate verification contract. Front-door disable comes
|
||||||
|
first in every action; audit evidence is never deleted; values stay in
|
||||||
|
OpenBao/operator custody.
|
||||||
@@ -0,0 +1,352 @@
|
|||||||
|
---
|
||||||
|
id: RAILIANCE-WP-0010
|
||||||
|
type: workplan
|
||||||
|
title: "llm-connect OpenRouter Provider Key Lane"
|
||||||
|
domain: financials
|
||||||
|
repo: railiance-platform
|
||||||
|
status: finished
|
||||||
|
owner: codex
|
||||||
|
topic_slug: railiance
|
||||||
|
planning_priority: high
|
||||||
|
planning_order: 10
|
||||||
|
created: "2026-06-29"
|
||||||
|
updated: "2026-07-02"
|
||||||
|
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`
|
||||||
|
|
||||||
|
Ops-warden already uses `openrouter-llm-connect` as the canonical draft
|
||||||
|
catalog id in its routing catalog and playbook. This workplan and
|
||||||
|
`CCR-2026-0003` now align to that selector so automated callers have
|
||||||
|
one stable route name before activation.
|
||||||
|
|
||||||
|
## 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 | `openrouter-llm-connect` |
|
||||||
|
| 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 openrouter-llm-connect --fetch OPENROUTER_API_KEY` |
|
||||||
|
|
||||||
|
## Tasks
|
||||||
|
|
||||||
|
## T01 - Review CCR scope and selector naming
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: RAILIANCE-WP-0010-T01
|
||||||
|
status: done
|
||||||
|
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:
|
||||||
|
`openrouter-llm-connect`.
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
**2026-07-01:** Resolved the selector naming ambiguity in favor of ops-warden
|
||||||
|
canon. The local ops-warden routing catalog and playbook define
|
||||||
|
`openrouter-llm-connect` as the draft OpenRouter/llm-connect route, so
|
||||||
|
`CCR-2026-0003` and this workplan now use that catalog id and command
|
||||||
|
shape. T01 remains `progress` until activity-core/platform approval moves
|
||||||
|
the CCR out of `proposed`.
|
||||||
|
|
||||||
|
## 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: done
|
||||||
|
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: done
|
||||||
|
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: done
|
||||||
|
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: done
|
||||||
|
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.
|
||||||
|
|
||||||
|
**2026-07-02:** T06 done. ops-warden promoted catalog id
|
||||||
|
`openrouter-llm-connect` from draft to active (ops-warden commit `364eb7d`)
|
||||||
|
following its own promotion checklist: concrete zero-placeholder handoff
|
||||||
|
(`warden route show openrouter-llm-connect --json` reports `status: active`,
|
||||||
|
`resolvable: true`), playbook gate marked met, draft tables updated, routing
|
||||||
|
tests passing (45/45). The entry carries pointers only — ops-warden proxies
|
||||||
|
reads as the caller and holds no provider key value. `CCR-2026-0003` recorded
|
||||||
|
the `frontdoor_activation` evidence and moved to `status: active` with
|
||||||
|
`readiness: ready`. Promotion happened only after the 2026-07-02
|
||||||
|
positive/negative verification.
|
||||||
|
|
||||||
|
## T07 - Record lifecycle operations
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: RAILIANCE-WP-0010-T07
|
||||||
|
status: done
|
||||||
|
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.
|
||||||
|
|
||||||
|
|
||||||
|
## Progress 2026-07-02 — approval and metadata apply
|
||||||
|
|
||||||
|
`CCR-2026-0003` approved by bernd.worsch (platform-operator +
|
||||||
|
activity-core-owner); T01 closes on that approval with the
|
||||||
|
`openrouter-llm-connect` selector already aligned.
|
||||||
|
|
||||||
|
- T03 done: policy `workload-kv-read-llm-connect-provider-secrets` and
|
||||||
|
kubernetes auth role applied via the constrained prod-applier child token;
|
||||||
|
State Hub apply evidence `04c70285`.
|
||||||
|
- T04 remains the live gate: the KV entry at
|
||||||
|
`platform/workloads/activity-core/llm-connect/llm-connect-provider-secrets`
|
||||||
|
does not exist yet — the operator must enter `OPENROUTER_API_KEY` through
|
||||||
|
OpenBao custody. The activity-core namespace also has no ExternalSecret
|
||||||
|
object for this lane yet. ops-warden checkpoint message: `6b058584`.
|
||||||
|
|
||||||
|
|
||||||
|
## Progress 2026-07-02 — value provisioned, lane live end-to-end
|
||||||
|
|
||||||
|
- T04 done: the operator entered `OPENROUTER_API_KEY` directly through OpenBao
|
||||||
|
custody (KV metadata: version 1, created 2026-07-02T10:18Z). The value never
|
||||||
|
passed through Git, State Hub, chat, or agent hands.
|
||||||
|
- T05 done: positive — new `ExternalSecret
|
||||||
|
activity-core/llm-connect-provider-secrets` (ClusterSecretStore
|
||||||
|
`openbao-activity-core`, creationPolicy Owner) reached `SecretSynced=True`
|
||||||
|
at 10:54Z, took ownership of the previously manual Secret, and the
|
||||||
|
llm-connect deployment rolled out cleanly on the OpenBao-delivered value
|
||||||
|
(pod ready, 0 restarts, /health probes passing). Negative — default-policy
|
||||||
|
token denied on the KV path (10:08Z probe, audit-logged). Manifest committed
|
||||||
|
in llm-connect `dfd2ce7`
|
||||||
|
(`deploy/k8s/activity-core-llm-connect/externalsecret.yaml`).
|
||||||
|
- T06 progress: activation update sent to ops-warden; `openrouter-llm-connect`
|
||||||
|
can now leave draft once the catalog confirmation lands.
|
||||||
|
- Scope note: this closes the CoulombCore lane the CCR describes. The separate
|
||||||
|
llm-connect instance on the railiance01 k3s cluster still consumes its
|
||||||
|
bootstrap-provisioned Secret; migrating it is railiance01-cluster work, not
|
||||||
|
part of CCR-2026-0003.
|
||||||
|
|
||||||
|
|
||||||
|
## T07 completed 2026-07-02
|
||||||
|
|
||||||
|
Lifecycle operations documented in
|
||||||
|
`docs/credential-lane-lifecycle-runbook.md`: the canonical per-action
|
||||||
|
procedure is generated by `scripts/credential-change.py lifecycle-plan
|
||||||
|
<CCR> --action {deactivate|rotate|compromise}`, and the runbook adds the
|
||||||
|
lane-specific consumer facts (materialized-Secret persistence, second
|
||||||
|
consumers, restart requirements, provider-side revocation for the OpenRouter
|
||||||
|
key) plus the post-rotate verification contract. Front-door disable comes
|
||||||
|
first in every action; audit evidence is never deleted; values stay in
|
||||||
|
OpenBao/operator custody.
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
id: RAIL-PL-WP-0002
|
id: RAIL-PL-WP-0002
|
||||||
type: workplan
|
type: workplan
|
||||||
title: "OpenBao Platform Secrets Service"
|
title: "OpenBao Platform Secrets Service"
|
||||||
domain: railiance
|
domain: financials
|
||||||
repo: railiance-platform
|
repo: railiance-platform
|
||||||
status: finished
|
status: finished
|
||||||
owner: codex
|
owner: codex
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
id: RAILIANCE-WP-0003
|
id: RAILIANCE-WP-0003
|
||||||
type: workplan
|
type: workplan
|
||||||
title: "Provision shared CNPG cluster apps-pg"
|
title: "Provision shared CNPG cluster apps-pg"
|
||||||
domain: railiance
|
domain: financials
|
||||||
repo: railiance-platform
|
repo: railiance-platform
|
||||||
status: finished
|
status: finished
|
||||||
owner: codex
|
owner: codex
|
||||||
@@ -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`.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
Reference in New Issue
Block a user