diff --git a/README.md b/README.md index daa5144..d14c238 100644 --- a/README.md +++ b/README.md @@ -47,9 +47,10 @@ owner. The `route` command group is a read-only lookup over the pointer catalog secrets. ```bash -warden route list [--all] [--json] # scenarios (active-only unless --all) -warden route show [--json] # owner + wiki/canon pointers; SSH adds steps -warden route find "issue an api key" # rank scenarios by keyword overlap +warden route list [--all] [--json] # scenarios (active-only unless --all) +warden route list --stale [--stale-days 90] [--all] # past review cadence +warden route show [--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`. diff --git a/registry/routing/catalog.yaml b/registry/routing/catalog.yaml index bdd9712..65ea9fb 100644 --- a/registry/routing/catalog.yaml +++ b/registry/routing/catalog.yaml @@ -127,12 +127,45 @@ entries: # --- draft: owner path not yet shipped; hidden from default lookup --- - id: issue-core-ingestion-api-key - title: issue-core ingestion API key (OpenBao path TBD) - need_keywords: [issue-core, ingestion, api, key, openbao] + title: issue-core ingestion API key (OpenBao KV + ESO) + 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 subsystem: OpenBao 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 - reviewed: "2026-06-18" + reviewed: "2026-06-24" status: draft diff --git a/src/warden/cli.py b/src/warden/cli.py index 492fa7d..f635d93 100644 --- a/src/warden/cli.py +++ b/src/warden/cli.py @@ -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.add_column("ID") table.add_column("Need") table.add_column("Owner") table.add_column("warden") + if show_reviewed: + table.add_column("Reviewed") + table.add_column("Days") table.add_column("Status") + from warden.routing.catalog import days_since_review + for e in entries: executes = "[green]issue[/green]" if e.warden_executes else "route" 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) @@ -566,22 +584,55 @@ def route_list( output_json: Annotated[bool, typer.Option("--json", help="Output JSON")] = 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, + 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: """List routing scenarios. Active-only unless --all.""" + from warden.routing.catalog import days_since_review + 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: t = tag.lower() entries = [e for e in entries if t in [k.lower() for k in e.need_keywords]] 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 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 - _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") diff --git a/src/warden/routing/catalog.py b/src/warden/routing/catalog.py index 7111bd7..21a4b96 100644 --- a/src/warden/routing/catalog.py +++ b/src/warden/routing/catalog.py @@ -15,6 +15,7 @@ from __future__ import annotations import os from dataclasses import dataclass +from datetime import date from pathlib import Path from typing import List, Optional @@ -36,6 +37,26 @@ _REQUIRED_FIELDS = ( ) _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): """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)) 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: if not isinstance(raw, dict): diff --git a/tests/test_routing.py b/tests/test_routing.py index 1ee596c..9a58a24 100644 --- a/tests/test_routing.py +++ b/tests/test_routing.py @@ -11,8 +11,10 @@ import yaml from typer.testing import CliRunner from warden.cli import app +from datetime import date + 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() @@ -130,6 +132,41 @@ def test_find_ssh_tunnel_top_match(): 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) # --------------------------------------------------------------------------- @@ -181,6 +218,34 @@ def test_cli_find_json(repo_catalog_env): 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 # --------------------------------------------------------------------------- diff --git a/wiki/AccessRouting.md b/wiki/AccessRouting.md index 4463819..9f72594 100644 --- a/wiki/AccessRouting.md +++ b/wiki/AccessRouting.md @@ -65,9 +65,10 @@ OpenBao, flex-auth, key-cape, or any other subsystem, and never returns secret material. ```bash -warden route list [--json] [--all] [--tag ] # active-only unless --all -warden route show [--json] # owner + pointers; SSH adds steps -warden route find "" [--json] [--all] # rank by keyword overlap +warden route list [--json] [--all] [--tag ] # active-only unless --all +warden route list --stale [--stale-days 90] [--all] [--json] # past review cadence +warden route show [--json] # owner + pointers; SSH adds steps +warden route find "" [--json] [--all] # rank by keyword overlap ``` 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 - `CredentialRouting.md` — worker decision tree and routing table diff --git a/wiki/CredentialRouting.md b/wiki/CredentialRouting.md index f3c6d60..436498f 100644 --- a/wiki/CredentialRouting.md +++ b/wiki/CredentialRouting.md @@ -87,6 +87,16 @@ executes. | `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 | | `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 never performs the non-SSH step on the worker's behalf. diff --git a/wiki/playbooks/database-dynamic-credentials.md b/wiki/playbooks/database-dynamic-credentials.md new file mode 100644 index 0000000..c4bf019 --- /dev/null +++ b/wiki/playbooks/database-dynamic-credentials.md @@ -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/ +``` + +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 (minutes–hours, not days) +- [ ] Path registered under `platform/databases/` +- [ ] 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` \ No newline at end of file diff --git a/wiki/playbooks/issue-core-ingestion-api-key.md b/wiki/playbooks/issue-core-ingestion-api-key.md new file mode 100644 index 0000000..cd42d0d --- /dev/null +++ b/wiki/playbooks/issue-core-ingestion-api-key.md @@ -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` \ No newline at end of file diff --git a/wiki/playbooks/object-storage-sts.md b/wiki/playbooks/object-storage-sts.md new file mode 100644 index 0000000..6763ab1 --- /dev/null +++ b/wiki/playbooks/object-storage-sts.md @@ -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/ +``` + +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` \ No newline at end of file diff --git a/wiki/playbooks/openrouter-llm-connect.md b/wiki/playbooks/openrouter-llm-connect.md new file mode 100644 index 0000000..8c14094 --- /dev/null +++ b/wiki/playbooks/openrouter-llm-connect.md @@ -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` \ No newline at end of file diff --git a/workplans/WARDEN-WP-0012-routing-scenario-playbooks.md b/workplans/WARDEN-WP-0012-routing-scenario-playbooks.md index d03aa8f..875eb45 100644 --- a/workplans/WARDEN-WP-0012-routing-scenario-playbooks.md +++ b/workplans/WARDEN-WP-0012-routing-scenario-playbooks.md @@ -4,7 +4,7 @@ type: workplan title: "Routing Scenario Playbooks" domain: infotech repo: ops-warden -status: active +status: finished owner: codex topic_slug: custodian 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). -**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 id: WARDEN-WP-0012-T01 -status: todo +status: done priority: high state_hub_task_id: "830bb512-0288-4dba-9dd4-ccfd28a4921f" ``` -- [ ] Coordinate with railiance-platform to canonicalize the OpenBao path first. -- [ ] Then write `wiki/playbooks/issue-core-ingestion-api-key.md` (prerequisites, +- [x] Coordinate with railiance-platform to canonicalize the OpenBao path first. + (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 - 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 @@ -103,26 +106,26 @@ state_hub_task_id: "9fb397f0-0abb-48f5-bb62-7e77edae93bb" ```task id: WARDEN-WP-0012-T04 -status: todo +status: done priority: low state_hub_task_id: "edcf4ed7-f18d-4a92-a42d-8cc7ca0ab792" ``` -- [ ] Playbooks for OpenRouter, object-storage STS, DB dynamic creds. -- [ ] Each ends with an owner-repo action; no warden secret code; pointers to canon. +- [x] Playbooks for OpenRouter, object-storage STS, DB dynamic creds. +- [x] Each ends with an owner-repo action; no warden secret code; pointers to canon. ### T5 — Drift review cadence ```task id: WARDEN-WP-0012-T05 -status: todo +status: done priority: low state_hub_task_id: "db98d655-8551-487b-9413-41bf97fc06e1" ``` -- [ ] Document a review cadence against net-kingdom canon. -- [ ] `warden route list --stale` keyed off the `reviewed:` date field. -- [ ] Process note in `wiki/AccessRouting.md`. +- [x] Document a review cadence against net-kingdom canon. +- [x] `warden route list --stale` keyed off the `reviewed:` date field. +- [x] Process note in `wiki/AccessRouting.md`. ---