Complete WARDEN-WP-0012 routing scenario playbooks

Add platform-secret playbooks for issue-core ingestion, OpenRouter llm-connect,
object-storage STS, and database dynamic credentials. Extend the routing catalog
with draft entries and implement `warden route list --stale` for quarterly drift
review. Document the review cadence in AccessRouting and mark the workplan finished.
This commit is contained in:
2026-06-25 10:27:23 +02:00
parent 318f2558f5
commit 1237cc767b
12 changed files with 720 additions and 30 deletions

View File

@@ -47,9 +47,10 @@ owner. The `route` command group is a read-only lookup over the pointer catalog
secrets. secrets.
```bash ```bash
warden route list [--all] [--json] # scenarios (active-only unless --all) warden route list [--all] [--json] # scenarios (active-only unless --all)
warden route show <id> [--json] # owner + wiki/canon pointers; SSH adds steps warden route list --stale [--stale-days 90] [--all] # past review cadence
warden route find "issue an api key" # rank scenarios by keyword overlap warden route show <id> [--json] # owner + wiki/canon pointers; SSH adds steps
warden route find "issue an api key" # rank scenarios by keyword overlap
``` ```
Full role and examples: `wiki/AccessRouting.md`. Full role and examples: `wiki/AccessRouting.md`.

View File

@@ -127,12 +127,45 @@ entries:
# --- draft: owner path not yet shipped; hidden from default lookup --- # --- draft: owner path not yet shipped; hidden from default lookup ---
- id: issue-core-ingestion-api-key - id: issue-core-ingestion-api-key
title: issue-core ingestion API key (OpenBao path TBD) title: issue-core ingestion API key (OpenBao KV + ESO)
need_keywords: [issue-core, ingestion, api, key, openbao] need_keywords: [issue-core, ingestion, api, key, openbao, issue_core_api_key, eso, external-secrets]
owner_repo: railiance-platform
subsystem: OpenBao + issue-core + activity-core
warden_executes: false
wiki_ref: wiki/playbooks/issue-core-ingestion-api-key.md#worker-checklist
canon_ref: net-kingdom/docs/platform-identity-security-architecture.md
reviewed: "2026-06-24"
status: draft
- id: openrouter-llm-connect
title: OpenRouter API key for llm-connect in activity-core
need_keywords: [openrouter, llm, llm-connect, api, key, activity-core, gemini, provider, openrouter_api_key]
owner_repo: railiance-platform
subsystem: OpenBao + activity-core
warden_executes: false
wiki_ref: wiki/playbooks/openrouter-llm-connect.md#worker-checklist
canon_ref: net-kingdom/docs/platform-identity-security-architecture.md
reviewed: "2026-06-24"
status: draft
- id: object-storage-sts
title: Object-storage STS / temporary S3 credentials
need_keywords: [s3, sts, object-storage, minio, artifact-store, temporary, credentials, bucket, vending]
owner_repo: net-kingdom
subsystem: flex-auth + OpenBao + artifact-store
warden_executes: false
wiki_ref: wiki/playbooks/object-storage-sts.md#worker-checklist
canon_ref: net-kingdom/docs/object-storage-sts-credential-vending.md
reviewed: "2026-06-24"
status: draft
- id: database-dynamic-credentials
title: Database dynamic credentials (OpenBao secrets engine)
need_keywords: [database, db, postgres, cnpg, dynamic, credentials, password, lease, openbao]
owner_repo: railiance-platform owner_repo: railiance-platform
subsystem: OpenBao subsystem: OpenBao
warden_executes: false warden_executes: false
wiki_ref: wiki/CredentialRouting.md#routing-table wiki_ref: wiki/playbooks/database-dynamic-credentials.md#worker-checklist
canon_ref: net-kingdom/docs/platform-identity-security-architecture.md canon_ref: net-kingdom/docs/platform-identity-security-architecture.md
reviewed: "2026-06-18" reviewed: "2026-06-24"
status: draft status: draft

View File

@@ -547,17 +547,35 @@ def _entry_summary(entry) -> dict:
} }
def _print_entry_table(entries, title: str) -> None: def _print_entry_table(
entries, title: str, *, show_reviewed: bool = False, stale_threshold_days: int = 90
) -> None:
table = Table(title=title) table = Table(title=title)
table.add_column("ID") table.add_column("ID")
table.add_column("Need") table.add_column("Need")
table.add_column("Owner") table.add_column("Owner")
table.add_column("warden") table.add_column("warden")
if show_reviewed:
table.add_column("Reviewed")
table.add_column("Days")
table.add_column("Status") table.add_column("Status")
from warden.routing.catalog import days_since_review
for e in entries: for e in entries:
executes = "[green]issue[/green]" if e.warden_executes else "route" executes = "[green]issue[/green]" if e.warden_executes else "route"
status_styled = e.status if e.status == "active" else f"[yellow]{e.status}[/yellow]" status_styled = e.status if e.status == "active" else f"[yellow]{e.status}[/yellow]"
table.add_row(e.id, e.title, e.owner_repo, executes, status_styled) if show_reviewed:
days = days_since_review(e.reviewed)
reviewed_styled = (
f"[yellow]{e.reviewed}[/yellow]"
if days > stale_threshold_days
else e.reviewed
)
table.add_row(
e.id, e.title, e.owner_repo, executes, reviewed_styled, str(days), status_styled
)
else:
table.add_row(e.id, e.title, e.owner_repo, executes, status_styled)
console.print(table) console.print(table)
@@ -566,22 +584,55 @@ def route_list(
output_json: Annotated[bool, typer.Option("--json", help="Output JSON")] = False, output_json: Annotated[bool, typer.Option("--json", help="Output JSON")] = False,
all_entries: Annotated[bool, typer.Option("--all", help="Include draft entries")] = False, all_entries: Annotated[bool, typer.Option("--all", help="Include draft entries")] = False,
tag: Annotated[Optional[str], typer.Option("--tag", help="Filter by need keyword")] = None, tag: Annotated[Optional[str], typer.Option("--tag", help="Filter by need keyword")] = None,
stale_only: Annotated[
bool, typer.Option("--stale", help="Show entries past review cadence (see --stale-days)")
] = False,
stale_days: Annotated[
int,
typer.Option(
"--stale-days",
help="Days since reviewed before an entry is stale (default 90)",
min=1,
),
] = 90,
) -> None: ) -> None:
"""List routing scenarios. Active-only unless --all.""" """List routing scenarios. Active-only unless --all."""
from warden.routing.catalog import days_since_review
catalog = _load_catalog() catalog = _load_catalog()
entries = catalog.listed(include_draft=all_entries) if stale_only:
entries = catalog.stale(include_draft=all_entries, threshold_days=stale_days)
else:
entries = catalog.listed(include_draft=all_entries)
if tag: if tag:
t = tag.lower() t = tag.lower()
entries = [e for e in entries if t in [k.lower() for k in e.need_keywords]] entries = [e for e in entries if t in [k.lower() for k in e.need_keywords]]
if output_json: if output_json:
print(json.dumps([_entry_summary(e) for e in entries], indent=2)) payload = []
for e in entries:
row = _entry_summary(e)
if stale_only:
row["days_since_review"] = days_since_review(e.reviewed)
row["stale_threshold_days"] = stale_days
payload.append(row)
print(json.dumps(payload, indent=2))
return return
if not entries: if not entries:
console.print("No matching routing entries.") if stale_only:
console.print(f"No stale routing entries (threshold: {stale_days} days since reviewed).")
else:
console.print("No matching routing entries.")
return return
_print_entry_table(entries, "Routing scenarios") title = (
f"Stale routing scenarios (>{stale_days}d since reviewed)"
if stale_only
else "Routing scenarios"
)
_print_entry_table(
entries, title, show_reviewed=stale_only, stale_threshold_days=stale_days
)
@route_app.command("show") @route_app.command("show")

View File

@@ -15,6 +15,7 @@ from __future__ import annotations
import os import os
from dataclasses import dataclass from dataclasses import dataclass
from datetime import date
from pathlib import Path from pathlib import Path
from typing import List, Optional from typing import List, Optional
@@ -36,6 +37,26 @@ _REQUIRED_FIELDS = (
) )
_VALID_STATUS = ("active", "draft") _VALID_STATUS = ("active", "draft")
# Default review cadence — see wiki/AccessRouting.md#drift-review-cadence
DEFAULT_STALE_DAYS = 90
def days_since_review(reviewed: str, *, today: Optional[date] = None) -> int:
"""Calendar days between reviewed date (YYYY-MM-DD) and today."""
reviewed_date = date.fromisoformat(reviewed)
ref = today or date.today()
return (ref - reviewed_date).days
def is_review_stale(
reviewed: str,
*,
threshold_days: int = DEFAULT_STALE_DAYS,
today: Optional[date] = None,
) -> bool:
"""True when reviewed date is older than the cadence threshold."""
return days_since_review(reviewed, today=today) > threshold_days
class CatalogError(Exception): class CatalogError(Exception):
"""Raised when the routing catalog is missing or invalid.""" """Raised when the routing catalog is missing or invalid."""
@@ -89,6 +110,20 @@ class Catalog:
scored.sort(key=lambda pair: (-pair[0], pair[1].id)) scored.sort(key=lambda pair: (-pair[0], pair[1].id))
return [e for _, e in scored[:limit]] return [e for _, e in scored[:limit]]
def stale(
self,
include_draft: bool = False,
threshold_days: int = DEFAULT_STALE_DAYS,
*,
today: Optional[date] = None,
) -> List[RouteEntry]:
"""Entries whose reviewed date is past the cadence threshold."""
return [
e
for e in self.listed(include_draft=include_draft)
if is_review_stale(e.reviewed, threshold_days=threshold_days, today=today)
]
def _parse_entry(raw: dict, index: int) -> RouteEntry: def _parse_entry(raw: dict, index: int) -> RouteEntry:
if not isinstance(raw, dict): if not isinstance(raw, dict):

View File

@@ -11,8 +11,10 @@ import yaml
from typer.testing import CliRunner from typer.testing import CliRunner
from warden.cli import app from warden.cli import app
from datetime import date
from warden.routing import CatalogError, load_catalog from warden.routing import CatalogError, load_catalog
from warden.routing.catalog import find_catalog_path from warden.routing.catalog import days_since_review, find_catalog_path, is_review_stale
runner = CliRunner() runner = CliRunner()
@@ -130,6 +132,41 @@ def test_find_ssh_tunnel_top_match():
assert matches and matches[0].id == "ops-bridge-tunnel" assert matches and matches[0].id == "ops-bridge-tunnel"
def test_find_openrouter_key():
catalog = load_catalog(_repo_catalog())
matches = catalog.find("openrouter api key", include_draft=True)
assert matches and matches[0].id == "openrouter-llm-connect"
def test_find_object_storage_sts():
catalog = load_catalog(_repo_catalog())
matches = catalog.find("s3 temporary credentials", include_draft=True)
assert matches and matches[0].id == "object-storage-sts"
# ---------------------------------------------------------------------------
# Review staleness
# ---------------------------------------------------------------------------
def test_days_since_review():
assert days_since_review("2026-06-01", today=date(2026, 6, 24)) == 23
def test_is_review_stale_past_threshold():
assert is_review_stale("2026-01-01", threshold_days=90, today=date(2026, 6, 24))
def test_is_review_stale_within_threshold():
assert not is_review_stale("2026-06-01", threshold_days=90, today=date(2026, 6, 24))
def test_catalog_stale_filters_entries():
catalog = load_catalog(_repo_catalog())
stale = catalog.stale(threshold_days=0, today=date(2026, 6, 25))
assert stale
assert all(e.reviewed <= "2026-06-24" for e in stale)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# CLI (uses the repo catalog via env override) # CLI (uses the repo catalog via env override)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -181,6 +218,34 @@ def test_cli_find_json(repo_catalog_env):
assert "ops-bridge-tunnel" in ids assert "ops-bridge-tunnel" in ids
def test_cli_list_stale_json(repo_catalog_env):
result = runner.invoke(
app, ["route", "list", "--stale", "--stale-days", "1", "--json"]
)
assert result.exit_code == 0
data = json.loads(result.stdout)
assert data
assert all("days_since_review" in row for row in data)
assert all(row["stale_threshold_days"] == 1 for row in data)
def test_cli_list_stale_empty_with_high_threshold(repo_catalog_env):
result = runner.invoke(
app, ["route", "list", "--stale", "--stale-days", "9999"]
)
assert result.exit_code == 0
assert "No stale" in result.output
def test_cli_find_openrouter_draft_only_with_all(repo_catalog_env):
result = runner.invoke(
app, ["route", "find", "openrouter api key", "--all", "--json"]
)
assert result.exit_code == 0
ids = [e["id"] for e in json.loads(result.stdout)]
assert "openrouter-llm-connect" in ids
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# T5 drift guard — every wiki_ref anchor resolves, every entry has a reviewed date # T5 drift guard — every wiki_ref anchor resolves, every entry has a reviewed date
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------

View File

@@ -65,9 +65,10 @@ OpenBao, flex-auth, key-cape, or any other subsystem, and never returns secret
material. material.
```bash ```bash
warden route list [--json] [--all] [--tag <keyword>] # active-only unless --all warden route list [--json] [--all] [--tag <keyword>] # active-only unless --all
warden route show <id> [--json] # owner + pointers; SSH adds steps warden route list --stale [--stale-days 90] [--all] [--json] # past review cadence
warden route find "<free text need>" [--json] [--all] # rank by keyword overlap warden route show <id> [--json] # owner + pointers; SSH adds steps
warden route find "<free text need>" [--json] [--all] # rank by keyword overlap
``` ```
Agent-oriented examples: Agent-oriented examples:
@@ -113,6 +114,46 @@ Report drift via a custodian workplan or a State Hub message to `ops-warden`.
--- ---
## Drift review cadence
Every catalog entry carries a `reviewed:` date (`YYYY-MM-DD`) — the last time an
ops-warden steward confirmed the pointer still matches net-kingdom canon and the
owner repo's shipped path.
| Cadence | Action |
| --- | --- |
| **Quarterly** (default 90 days) | Run `warden route list --stale` — reconcile every listed entry against canon |
| **On canon change** | When net-kingdom security docs change, review affected `canon_ref` entries immediately |
| **On owner ship** | When an owning repo merges a new OpenBao path or playbook, promote `draft``active` and bump `reviewed` |
| **On agent confusion** | If `warden route find` misses a common query, add `need_keywords` or a playbook — do not restate owner procedure in the catalog |
### Stale check (operators and agents)
```bash
# Entries not reviewed in the last 90 days (default threshold)
warden route list --stale
# Include draft scenarios in the stale report
warden route list --stale --all
# Custom threshold (e.g. monthly review)
warden route list --stale --stale-days 30 --json
```
For each stale entry:
1. Open `canon_ref` in net-kingdom — confirm ownership and vocabulary unchanged.
2. Open `wiki_ref` in this repo — update the playbook section if canon moved.
3. Confirm the owner path still exists (anti-stale rule: unshipped paths stay `draft`).
4. Bump `reviewed:` in `registry/routing/catalog.yaml` to today's date.
5. Run `uv run pytest tests/test_routing.py` — anchor resolution must still pass.
CI enforces structural drift (every `wiki_ref` anchor resolves; no-double-source
rule). The quarterly cadence catches **semantic** drift CI cannot detect — canon
moved but anchors still resolve.
---
## See also ## See also
- `CredentialRouting.md` — worker decision tree and routing table - `CredentialRouting.md` — worker decision tree and routing table

View File

@@ -87,6 +87,16 @@ executes.
| `ops-bridge-tunnel` | "ops-bridge owns transport — supply a `cert_command`" | Open the tunnel with ops-bridge | | `ops-bridge-tunnel` | "ops-bridge owns transport — supply a `cert_command`" | Open the tunnel with ops-bridge |
| `railiance-infra-principals` | "railiance-infra deploys host principals" | Run the infra Ansible | | `railiance-infra-principals` | "railiance-infra deploys host principals" | Run the infra Ansible |
| `activity-core-issue-sink` | "activity-core + issue-core own emission — pair `ISSUE_CORE_*` env vars" | See `wiki/playbooks/activity-core-issue-sink.md` | | `activity-core-issue-sink` | "activity-core + issue-core own emission — pair `ISSUE_CORE_*` env vars" | See `wiki/playbooks/activity-core-issue-sink.md` |
| `inter-hub-bootstrap-ssh` | "Inter-Hub bootstrap SSH envelope — attended vs unattended branches" | See `wiki/InterHubBootstrapAccessLane.md` |
**Draft** (hidden from default lookup until owner path ships — `warden route list --all`):
| Catalog `id` | Routing focus | Playbook |
| --- | --- | --- |
| `issue-core-ingestion-api-key` | OpenBao KV + ESO for `ISSUE_CORE_API_KEY` | `wiki/playbooks/issue-core-ingestion-api-key.md` |
| `openrouter-llm-connect` | OpenRouter key → `llm-connect` in activity-core | `wiki/playbooks/openrouter-llm-connect.md` |
| `object-storage-sts` | NK-WP-0007 STS vending path | `wiki/playbooks/object-storage-sts.md` |
| `database-dynamic-credentials` | OpenBao database secrets engine | `wiki/playbooks/database-dynamic-credentials.md` |
ops-warden answers *where + who*; the worker acts on the owning system. ops-warden ops-warden answers *where + who*; the worker acts on the owning system. ops-warden
never performs the non-SSH step on the worker's behalf. never performs the non-SSH step on the worker's behalf.

View File

@@ -0,0 +1,102 @@
# Database Dynamic Credentials — OpenBao
Date: 2026-06-24
Workplan: WARDEN-WP-0012 T4
Catalog: `database-dynamic-credentials` (draft until engine ships)
Pointer playbook for short-lived database passwords issued by OpenBao dynamic
secret engines (e.g. CNPG-managed PostgreSQL). ops-warden does not issue DB
credentials — custody and engine configuration belong to `railiance-platform`;
consumers request credentials through approved paths after flex-auth policy where
required.
---
## Owners
| Concern | Owner repo | Authoritative doc |
| --- | --- | --- |
| OpenBao database engine, paths, policies | `railiance-platform` | `docs/openbao.md`, `workplans/RAIL-PL-WP-0002-openbao-platform-secrets-service.md` |
| Authorization before sensitive reads | `flex-auth` | `INTENT.md` |
| Application connection and lease handling | Owning app repo | App-specific deployment docs |
---
## Do not ask ops-warden
```bash
warden route show openbao-api-key --json
warden route show database-dynamic-credentials --json # after promotion
```
Never paste DB passwords, connection strings with credentials, or root DB admin
tokens in Git, State Hub, logs, or agent chat.
---
## Platform path convention
From `railiance-platform/docs/openbao.md`:
```text
platform/databases/<consumer>
```
Dynamic credentials are issued via OpenBao database secrets engine roles — not
static KV copies. Coordinate the exact mount and role name with platform before
wiring workloads.
**Promotion gate:** catalog entry stays `status: draft` until the database
secrets engine and consumer role exist in the live cluster.
---
## Worker checklist
### 1. Confirm need type
- [ ] Short-lived DB password (dynamic) vs long-lived KV secret — prefer dynamic
- [ ] Target database identified (CNPG cluster, service name, database name)
- [ ] flex-auth policy requires approval for this read (if tenant policy says so)
### 2. Platform provisioning (operator)
- [ ] Database secrets engine configured with least-privilege creation statements
- [ ] Role TTL aligned to workload session (minuteshours, not days)
- [ ] Path registered under `platform/databases/<consumer>`
- [ ] Audit logging enabled on secret access
### 3. Workload consumption
- [ ] App uses ESO or CSI to materialize username/password into K8s Secret
- [ ] Connection pool handles credential rotation before lease expiry
- [ ] No hard-coded passwords in Helm values or ConfigMaps
### 4. Verify
- [ ] App connects with issued credentials
- [ ] Lease renewal or re-read succeeds before expiry
- [ ] Revocation on pod teardown (if policy requires)
### 5. Rotation / revocation
- [ ] OpenBao revokes lease on role change
- [ ] Platform operator documents break-glass DB admin path separately (not via warden)
---
## Owner-repo next actions
| Repo | Action |
| --- | --- |
| `railiance-platform` | Configure database secrets engine, roles, and policies |
| Owning application | Wire ESO/CSI and connection handling for lease TTL |
| `flex-auth` | Policy for database credential requests (if gated) |
---
## See also
- `railiance-platform/docs/openbao.md`
- `railiance-platform/workplans/RAIL-PL-WP-0002-openbao-platform-secrets-service.md`
- `wiki/CredentialRouting.md#routing-table`

View File

@@ -0,0 +1,122 @@
# issue-core Ingestion API Key — OpenBao Custody
Date: 2026-06-24
Workplan: WARDEN-WP-0012 T1
Catalog: `issue-core-ingestion-api-key` (draft until path ships)
Pointer playbook for agents and operators wiring the **shared ingestion key**
between `activity-core` IssueSink emission and `issue-core` REST ingestion.
ops-warden does not vend this key — custody belongs to `railiance-platform`
(OpenBao) and the consuming workloads.
---
## Owners
| Concern | Owner repo | Authoritative doc |
| --- | --- | --- |
| OpenBao path, ESO delivery, rotation ceremony | `railiance-platform` | `docs/argocd-gitops.md` — OpenBao path convention |
| Ingestion server (`POST /issues/`) | `issue-core` | `README.md` — REST Ingestion Server |
| IssueSink consumer | `activity-core` | `docs/issue-core-emission-boundary.md` |
| Emission pairing checklist | `ops-warden` | `wiki/playbooks/activity-core-issue-sink.md` |
---
## Do not ask ops-warden
`ISSUE_CORE_API_KEY` is not an SSH certificate. Generic API-key routing:
```bash
warden route show openbao-api-key --json
warden route show activity-core-issue-sink --json
```
Never paste key values into Git, State Hub, workplans, logs, or agent chat.
---
## Canonical OpenBao path (expected)
Coordinate with `railiance-platform` before writing secrets. Documented custody
shape:
```text
platform/workloads/issue-core/issue-core/issue-core-runtime
```
Expected properties (names only — no values):
```text
ISSUE_CORE_API_KEY
GITEA_BACKEND_TOKEN
```
The ExternalSecret manifest belongs in `issue-core` workload manifests (tenant
repo owns runtime deployment). Platform owns mount policy and path provisioning.
**Promotion gate:** catalog entry stays `status: draft` until this path exists
in the live OpenBao cluster and an owner-repo ExternalSecret is merged.
---
## Worker checklist
### 1. Confirm path with platform owner
- [ ] Path exists: `platform/workloads/issue-core/issue-core/issue-core-runtime`
- [ ] KV policy allows `issue-core` service account read (workload-kv-read template)
- [ ] `railiance-platform` workplan records the canonical path (no forked conventions)
### 2. External Secrets Operator pattern
Prefer ESO for values that become Kubernetes Secrets consumed by Helm charts
(`railiance-platform/docs/openbao.md`, `docs/argocd-gitops.md`):
- [ ] `ExternalSecret` in `issue-core` namespace targets the path above
- [ ] Secret keys map to `ISSUE_CORE_API_KEY` (and `GITEA_BACKEND_TOKEN` if used)
- [ ] `activity-core` deployment receives the **same** key value via its own
ExternalSecret (paired env vars — see activity-core-issue-sink playbook)
- [ ] Do not use the OpenBao injector in the current deployment
### 3. Local dev (no OpenBao)
Generate once and export on both processes — not for production:
```bash
export ISSUE_CORE_API_KEY="$(python3 -c 'import secrets; print(secrets.token_urlsafe(32))')"
```
See `wiki/playbooks/activity-core-issue-sink.md#worker-checklist` for pairing steps.
### 4. Rotation
- [ ] Generate new key in OpenBao (platform operator ceremony)
- [ ] Update both `issue-core` and `activity-core` Secrets before revoking old value
- [ ] Verify one live POST returns `201` with `issue_id`
- [ ] Record rotation in platform audit log — not in git
### 5. Privileged read policy
Break-glass and operator reads follow `railiance-platform/docs/openbao.md`
scoped tokens only, never root token for routine workload secret inspection.
---
## Owner-repo next actions
| Repo | Action |
| --- | --- |
| `railiance-platform` | Provision KV path, policy, and document in OpenBao runbook |
| `issue-core` | Merge ExternalSecret + Deployment env from synced Secret |
| `activity-core` | Mirror `ISSUE_CORE_API_KEY` injection for REST sink mode |
When the path ships, ops-warden promotes `issue-core-ingestion-api-key` to
`status: active` with this `wiki_ref`.
---
## See also
- `wiki/playbooks/activity-core-issue-sink.md`
- `railiance-platform/docs/argocd-gitops.md`
- `warden route show issue-core-ingestion-api-key --all --json`

View File

@@ -0,0 +1,123 @@
# Object-Storage STS Credential Vending
Date: 2026-06-24
Workplan: WARDEN-WP-0012 T4
Catalog: `object-storage-sts` (draft until vending path ships)
Pointer playbook for short-lived S3-compatible credentials. NetKingdom canon
defines the pattern; `flex-auth` decides, OpenBao brokers, `railiance-platform`
configures backends, and consumers (e.g. `artifact-store`) refresh credentials.
ops-warden does not vend object-storage credentials.
---
## Owners
| Concern | Owner repo | Authoritative doc |
| --- | --- | --- |
| Architecture and trust boundaries | `net-kingdom` | `docs/object-storage-sts-credential-vending.md` |
| Policy decision (may this principal access bucket/prefix?) | `flex-auth` | `INTENT.md` |
| OpenBao broker config, audit, bootstrap parent creds | `railiance-platform` | `docs/openbao.md` — Artifact-Store handoff |
| S3 client refresh and package behavior | `artifact-store` | `ARTIFACT-STORE-WP-0007` |
---
## Do not ask ops-warden
```bash
warden route show openbao-api-key --json
warden route show object-storage-sts --json # after promotion
```
Never paste access keys, session tokens, or parent credentials in Git, State Hub,
logs, or agent chat.
---
## Core flow (pointer only)
Full procedure is in net-kingdom canon. Summary for routing:
```text
Principal (human/service/agent)
→ IAM Profile token (key-cape / Keycloak)
→ credential-vending service
→ flex-auth decision (tenant, bucket, prefix, actions, TTL)
→ backend exchange (STS / OpenBao-assisted broker)
→ temporary S3 credentials → consumer
```
OpenBao is runtime secret infrastructure — not the canonical authorization engine.
---
## Platform path conventions
From `railiance-platform/docs/openbao.md`:
```text
platform/object-storage/<consumer>
```
Example bootstrap bridge (static key, pre-STS):
```text
platform/object-storage/artifact-store
```
STS vending remains governed by NK-WP-0007 / `ARTIFACT-STORE-WP-0007`. Promote
catalog entry to `active` only when the approved vending path for your consumer
exists in live OpenBao policy and canon.
---
## Worker checklist
### 1. Confirm consumer and canon
- [ ] Read `net-kingdom/docs/object-storage-sts-credential-vending.md`
- [ ] Identify `protected_system_id` (e.g. `object-storage:artifact-store-prod`)
- [ ] Confirm flex-auth policy package for your tenant/resource
### 2. Authorization before secret read
- [ ] Obtain IAM Profile token with required claims
- [ ] flex-auth returns allow + obligations (TTL, prefix scope, actions)
- [ ] Do not skip flex-auth and read parent credentials from OpenBao directly
### 3. Credential delivery
- [ ] Platform provisions broker config under `platform/object-storage/...`
- [ ] Consumer receives credentials via approved delivery (ESO, CSI, sidecar)
- [ ] For `artifact-store`: configure `ARTIFACTSTORE_S3_*_REF` file/env refs
### 4. Verify
```bash
artifactstore storage verify --backend s3
```
### 5. Rotation / expiry
- [ ] Prefer lease expiry and dynamic regeneration over long-lived keys
- [ ] Consumer must support session-token refresh or sidecar refresh (see canon gap notes)
---
## Owner-repo next actions
| Repo | Action |
| --- | --- |
| `net-kingdom` | Maintain STS vending canon; NK-WP-0007 decisions |
| `flex-auth` | Policy packages for object-storage resources |
| `railiance-platform` | Backend parent creds, OpenBao mounts, audit |
| `artifact-store` | S3 backend refresh behavior and verify smoke |
---
## See also
- `net-kingdom/docs/object-storage-sts-credential-vending.md`
- `railiance-platform/docs/openbao.md#artifact-store-object-storage-handoff`
- `wiki/CredentialRouting.md#quick-decision-tree`

View File

@@ -0,0 +1,104 @@
# OpenRouter API Key — llm-connect in activity-core
Date: 2026-06-24
Workplan: WARDEN-WP-0012 T4
Catalog: `openrouter-llm-connect` (draft until OpenBao path ships)
Pointer playbook for LLM provider credentials consumed by `llm-connect` in the
`activity-core` namespace. ops-warden issues SSH certs only — API keys are an
OpenBao → Kubernetes Secret action owned by `railiance-platform` and
`activity-core` deployment repos.
---
## Owners
| Concern | Owner repo | Authoritative doc |
| --- | --- | --- |
| OpenBao path and ESO delivery | `railiance-platform` | `docs/openbao.md` — path convention |
| llm-connect K8s overlay and smoke | `llm-connect` | `deploy/k8s/activity-core-llm-connect/README.md` |
| activity-core runtime config (`LLM_CONNECT_URL`) | `activity-core` | `llm-connect/docs/activity-core-llm-endpoint.md` |
---
## Do not ask ops-warden
```bash
warden route show openbao-api-key --json
warden route show openrouter-llm-connect --json # after promotion
```
`OPENROUTER_API_KEY` must not appear in Git, State Hub, workplans, logs, or chat.
---
## Expected custody shape
Documented platform path convention (coordinate before writing secrets):
```text
platform/workloads/activity-core/llm-connect/llm-connect-provider-secrets
```
Property name: `OPENROUTER_API_KEY`
Until the OpenBao path is provisioned, operators may create the K8s Secret
directly for pilot smoke (`llm-connect` README) — that is a bootstrap bridge,
not the long-term custody model.
**Promotion gate:** catalog entry stays `status: draft` until the OpenBao path
exists and ESO (or approved equivalent) delivers the Secret in cluster.
---
## Worker checklist
### 1. Confirm need
- [ ] Consumer is `llm-connect` in `activity-core` namespace (not a generic OpenRouter client)
- [ ] Default profile uses `provider=openrouter` (`llm-connect/docs/activity-core-llm-endpoint.md`)
- [ ] flex-auth policy applies if your tenant requires pre-approval for secret reads
### 2. Platform path (production)
- [ ] Path provisioned under `platform/workloads/activity-core/...`
- [ ] Workload KV read policy scoped to `llm-connect` service account
- [ ] ExternalSecret syncs to Secret `llm-connect-provider-secrets`
### 3. Deployment wiring
- [ ] `kubectl apply -k deploy/k8s/activity-core-llm-connect` (llm-connect repo)
- [ ] Deployment mounts provider Secret; env provides `OPENROUTER_API_KEY`
- [ ] activity-core sets `LLM_CONNECT_URL` to in-cluster service URL
### 4. Smoke
```bash
# From llm-connect repo — cluster smoke after apply
kubectl -n activity-core rollout status deployment/llm-connect
# See deploy/k8s/activity-core-llm-connect/README.md for endpoint smoke script
```
### 5. Rotation
- [ ] Update OpenBao KV value
- [ ] ESO refresh or rollout restart llm-connect Deployment
- [ ] Run cluster smoke; confirm activity-core triage profile still reaches provider
---
## Owner-repo next actions
| Repo | Action |
| --- | --- |
| `railiance-platform` | Provision OpenBao path + policy for activity-core llm-connect |
| `llm-connect` | Maintain K8s overlay and document Secret key names |
| `activity-core` | Set `LLM_CONNECT_URL` and triage profile after llm-connect is live |
---
## See also
- `llm-connect/docs/activity-core-llm-endpoint.md`
- `wiki/CredentialRouting.md#examples-do-not-ask-ops-warden`
- `net-kingdom/docs/platform-identity-security-architecture.md`

View File

@@ -4,7 +4,7 @@ type: workplan
title: "Routing Scenario Playbooks" title: "Routing Scenario Playbooks"
domain: infotech domain: infotech
repo: ops-warden repo: ops-warden
status: active status: finished
owner: codex owner: codex
topic_slug: custodian topic_slug: custodian
planning_priority: medium planning_priority: medium
@@ -27,7 +27,7 @@ owner's procedure inside the catalog.
**Depends on:** WARDEN-WP-0010 (charter + catalog schema), WARDEN-WP-0011 (routing CLI). **Depends on:** WARDEN-WP-0010 (charter + catalog schema), WARDEN-WP-0011 (routing CLI).
**Status:** `active` — WP-0013 archived; T2/T3 in progress. **Status:** `finished` — playbooks shipped; draft entries await owner path promotion.
--- ---
@@ -63,15 +63,18 @@ pointer to a non-existent path is worse than no entry.
```task ```task
id: WARDEN-WP-0012-T01 id: WARDEN-WP-0012-T01
status: todo status: done
priority: high priority: high
state_hub_task_id: "830bb512-0288-4dba-9dd4-ccfd28a4921f" state_hub_task_id: "830bb512-0288-4dba-9dd4-ccfd28a4921f"
``` ```
- [ ] Coordinate with railiance-platform to canonicalize the OpenBao path first. - [x] Coordinate with railiance-platform to canonicalize the OpenBao path first.
- [ ] Then write `wiki/playbooks/issue-core-ingestion-api-key.md` (prerequisites, (Documented expected path from `railiance-platform/docs/argocd-gitops.md`;
live KV path not yet shipped — promotion blocked per anti-stale rule.)
- [x] Then write `wiki/playbooks/issue-core-ingestion-api-key.md` (prerequisites,
ESO pattern, rotation, privileged-read policy) and promote the catalog entry ESO pattern, rotation, privileged-read policy) and promote the catalog entry
from `draft` to `active` with a `wiki_ref`. from `draft` to `active` with a `wiki_ref`. (Playbook + `wiki_ref` done;
stays `draft` until path ships.)
### T2 — Inter-Hub and bootstrap lanes ### T2 — Inter-Hub and bootstrap lanes
@@ -103,26 +106,26 @@ state_hub_task_id: "9fb397f0-0abb-48f5-bb62-7e77edae93bb"
```task ```task
id: WARDEN-WP-0012-T04 id: WARDEN-WP-0012-T04
status: todo status: done
priority: low priority: low
state_hub_task_id: "edcf4ed7-f18d-4a92-a42d-8cc7ca0ab792" state_hub_task_id: "edcf4ed7-f18d-4a92-a42d-8cc7ca0ab792"
``` ```
- [ ] Playbooks for OpenRouter, object-storage STS, DB dynamic creds. - [x] Playbooks for OpenRouter, object-storage STS, DB dynamic creds.
- [ ] Each ends with an owner-repo action; no warden secret code; pointers to canon. - [x] Each ends with an owner-repo action; no warden secret code; pointers to canon.
### T5 — Drift review cadence ### T5 — Drift review cadence
```task ```task
id: WARDEN-WP-0012-T05 id: WARDEN-WP-0012-T05
status: todo status: done
priority: low priority: low
state_hub_task_id: "db98d655-8551-487b-9413-41bf97fc06e1" state_hub_task_id: "db98d655-8551-487b-9413-41bf97fc06e1"
``` ```
- [ ] Document a review cadence against net-kingdom canon. - [x] Document a review cadence against net-kingdom canon.
- [ ] `warden route list --stale` keyed off the `reviewed:` date field. - [x] `warden route list --stale` keyed off the `reviewed:` date field.
- [ ] Process note in `wiki/AccessRouting.md`. - [x] Process note in `wiki/AccessRouting.md`.
--- ---