diff --git a/registry/routing/catalog.yaml b/registry/routing/catalog.yaml index ad11dd4..e4bba2e 100644 --- a/registry/routing/catalog.yaml +++ b/registry/routing/catalog.yaml @@ -48,6 +48,27 @@ entries: - "Sign: `warden sign --pubkey ` — cert is written to stdout (the cert_command contract)." - "TTL is enforced per actor type: adm 48h / agt 24h / atm 8h. No long-lived keys." + - id: ops-warden-warden-sign-token + title: Scoped OpenBao token for ops-warden SSH signing (warden-sign) + need_keywords: [vault_token, vault, token, warden-sign, warden, ops-warden, signing, sign, smoke, flex-auth, credential, broker, lease, openbao, ssh, production] + owner_repo: railiance-platform + subsystem: OpenBao credential broker + warden_executes: false + wiki_ref: wiki/playbooks/ops-warden-warden-sign-token.md#worker-checklist + canon_ref: net-kingdom/docs/platform-identity-security-architecture.md + reviewed: "2026-07-01" + status: active + # Concrete broker lane — RAILIANCE-WP-0005 pilot (live 2026-07-01): + # credential exec injects VAULT_TOKEN only into the child process; ops-warden + # issues SSH certs and never mints or holds OpenBao tokens. + auth_method: "railiance-platform credential broker (issuer via OPENBAO_TOKEN_FILE for apply; child tokens via grant)" + path_template: "credential-grants/catalog.yaml grant ops-warden/warden-sign" + fetch_command: "scripts/credential.py request --grant ops-warden/warden-sign --purpose ops-warden-sign --ttl 15m" + policy_ref: "flex-auth optional preflight per grant catalog" + exec_owner: railiance-platform + exec_command: "scripts/credential.py exec --grant ops-warden/warden-sign --ttl 15m -- " + pointer_command: "make credential-exec-ops-warden-smoke" + - id: openbao-api-key title: API key, DB credential, or dynamic lease need_keywords: [api, key, secret, database, db, password, token, lease, openbao, vault, kv, dynamic, credential, npm, npm_auth_token, registry] diff --git a/tests/test_routing.py b/tests/test_routing.py index eff25e7..2c18c17 100644 --- a/tests/test_routing.py +++ b/tests/test_routing.py @@ -76,6 +76,26 @@ def test_real_catalog_has_one_executed_lane(): assert [e.id for e in executed] == ["ssh-cert-host-access"] +def test_ops_warden_warden_sign_lane_has_native_exec(): + """RAILIANCE-WP-0005 T08 — broker lane routes to railiance-platform credential exec.""" + catalog = load_catalog(_repo_catalog()) + e = catalog.get("ops-warden-warden-sign-token") + assert e is not None and e.is_active and e.owner_repo == "railiance-platform" + assert e.has_native_exec is True + assert e.exec_owner == "railiance-platform" + assert "credential.py exec" in e.exec_command + assert "ops-warden/warden-sign" in e.exec_command + assert "credential-exec-ops-warden-smoke" in e.pointer_command + assert e.warden_executes is False + assert e.resolvable is False # broker lane — owner exec, not warden access --fetch + + +def test_route_find_vault_token_ops_warden_prefers_broker_lane(): + catalog = load_catalog(_repo_catalog()) + matches = catalog.find("VAULT_TOKEN ops-warden warden sign", limit=3) + assert matches[0].id == "ops-warden-warden-sign-token" + + def test_whynot_design_npm_lane_is_concrete_and_resolvable(): """The provisioned npm publish lane has no placeholders and reports resolvable.""" catalog = load_catalog(_repo_catalog()) @@ -125,6 +145,16 @@ def test_cli_show_native_exec_json(repo_catalog_env): assert "primary" in data["next_action"] and "secrets-engine" in data["next_action"] +def test_cli_show_warden_sign_broker_json(repo_catalog_env): + result = runner.invoke(app, ["route", "show", "ops-warden-warden-sign-token", "--json"]) + assert result.exit_code == 0 + data = json.loads(result.stdout) + assert data["owner_repo"] == "railiance-platform" + assert data["exec_owner"] == "railiance-platform" + assert "credential.py exec" in data["exec_command"] + assert "primary" in data["next_action"] and "railiance-platform" in data["next_action"] + + def test_no_double_source_rule_rejects_routed_steps(tmp_path): bad = dict(ROUTED_ENTRY) bad["steps"] = ["do a thing on OpenBao"] # non-SSH entry must not carry steps diff --git a/wiki/CredentialRouting.md b/wiki/CredentialRouting.md index c4573e6..b09994e 100644 --- a/wiki/CredentialRouting.md +++ b/wiki/CredentialRouting.md @@ -86,6 +86,7 @@ run the owner's tool as the caller and preserve owner custody. | Catalog `id` | What ops-warden answers | What the worker does next | | --- | --- | --- | | `ssh-cert-host-access` | **Issues** the cert (`warden sign`) | Use the cert / wire it into `cert_command` | +| `ops-warden-warden-sign-token` | "railiance-platform broker owns the `warden-sign` lease — use `credential exec`" | `railiance-platform/scripts/credential.py exec --grant ops-warden/warden-sign` (see playbook) | | `openbao-api-key` | "OpenBao owns this — here is the path/command shape" | Call OpenBao directly, or use `warden access --fetch/--exec` as yourself when the lane is `exec_capable` | | `flex-auth-policy-check` | "flex-auth decides — here is the policy doc" | Query flex-auth / embed the PEP | | `key-cape-oidc-login` | "key-cape / Keycloak owns identity" | Authenticate via IAM Profile, or use the `warden access` login lane as yourself | @@ -113,6 +114,7 @@ value; the owner remains OpenBao, key-cape, flex-auth, or the routed subsystem. | Request | Correct path | | --- | --- | +| "`VAULT_TOKEN` for ops-warden production sign / policy-gate smoke" | `railiance-platform` credential broker — `warden route show ops-warden-warden-sign-token` | | "Populate `OPENROUTER_API_KEY` for llm-connect" | Operator → OpenBao/K8s Secret in `activity-core` namespace | | "Store Inter-Hub admin key for bootstrap" | Operator → OpenBao or `IHUB_OPERATOR_KEY_FILE` (`CUST-WP-0049`) | | "Give me Vault root token" | Break-glass ceremony → `railiance-platform/docs/openbao.md` | diff --git a/wiki/OpsWardenConfig.md b/wiki/OpsWardenConfig.md index 2696211..689db6f 100644 --- a/wiki/OpsWardenConfig.md +++ b/wiki/OpsWardenConfig.md @@ -114,22 +114,30 @@ paths. ### Authentication -Export a token with permission to sign against the mapped roles: +**Preferred:** use the railiance-platform credential broker so `VAULT_TOKEN` is +injected only into the child process (no manual export): ```bash -# After OIDC login or policy-issued token (OpenBao CLI) -export VAULT_TOKEN="" - -# Or HashiCorp Vault CLI against a Vault-compatible endpoint -vault login +cd ~/railiance-platform +scripts/credential.py exec --grant ops-warden/warden-sign --ttl 15m -- \ + warden sign --pubkey ``` -`warden` reads the token from the env var named in `vault.token_env` (default -`VAULT_TOKEN`). OpenBao uses the same header; you do not need a separate -`BAO_TOKEN` unless you configure `token_env` that way. +`warden route show ops-warden-warden-sign-token` · +`wiki/playbooks/ops-warden-warden-sign-token.md`. -See `wiki/playbooks/operator-openbao-token-hygiene.md` for scoped `warden-sign` -tokens, OIDC routing, and HTTP 403 recovery. +**Manual fallback** — export a scoped token for the current shell only: + +```bash +export VAULT_TOKEN="" +``` + +`warden` reads the env var named in `vault.token_env` (default `VAULT_TOKEN`). +OpenBao uses the same header; you do not need a separate `BAO_TOKEN` unless you +configure `token_env` that way. + +See `wiki/playbooks/operator-openbao-token-hygiene.md` for hygiene rules, OIDC +routing, and HTTP 403 recovery. On failure, `warden sign` suggests falling back to `--backend local` only for lab recovery — not as a production substitute. diff --git a/wiki/PolicyGatedSigning.md b/wiki/PolicyGatedSigning.md index bfe6a83..2ac93d6 100644 --- a/wiki/PolicyGatedSigning.md +++ b/wiki/PolicyGatedSigning.md @@ -186,7 +186,9 @@ Smoke (non-secret): ```bash ./scripts/policy_gate_production_smoke.sh -# OpenBao-backed when VAULT_TOKEN is valid: +# OpenBao-backed — preferred: credential broker (no manual VAULT_TOKEN): +cd ~/railiance-platform && make credential-exec-ops-warden-smoke +# Manual fallback when broker unavailable: SMOKE_VAULT=1 ./scripts/policy_gate_production_smoke.sh ``` @@ -207,7 +209,7 @@ with `fail_closed: true`, unreachable flex-auth blocks all signs. | 2 | flex-auth | Load production registry + policy package (`~/flex-auth/examples/ops-warden/`) | | 3 | ops-warden | Regenerate registry from inventory: `scripts/build_flex_auth_registry.py` | | 4 | ops-warden | Local smoke: `./scripts/policy_gate_production_smoke.sh` | -| 5 | operator | Vault smoke: `SMOKE_VAULT=1 ./scripts/policy_gate_production_smoke.sh` (valid `VAULT_TOKEN`) | +| 5 | operator | Vault smoke: `make credential-exec-ops-warden-smoke` in `railiance-platform` (or manual `SMOKE_VAULT=1` fallback) | | 6 | operator | Set `policy.flex_auth_url` in `~/.config/warden/warden.yaml` | | 7 | operator | Set `policy.enabled: true`; keep `fail_closed: true` | | 8 | operator | Allow smoke: `warden sign ` — `signatures.log` has `policy_decision_id` | diff --git a/wiki/playbooks/operator-openbao-token-hygiene.md b/wiki/playbooks/operator-openbao-token-hygiene.md index c821c48..e0e1129 100644 --- a/wiki/playbooks/operator-openbao-token-hygiene.md +++ b/wiki/playbooks/operator-openbao-token-hygiene.md @@ -3,8 +3,33 @@ Date: 2026-06-24 Workplan: WARDEN-WP-0013 T4 -Daily `warden sign` against production OpenBao requires a **scoped** API token in -`VAULT_TOKEN` — not the cluster root token. +Production `warden sign` against OpenBao needs a **scoped** `warden-sign` token in +`VAULT_TOKEN` — not the cluster root token. Prefer the credential broker so you +never paste or export the raw token manually. + +--- + +## Preferred path (credential broker) + +Use the railiance-platform broker to mint a short-lived child token and inject it +only into the command that needs it: + +```bash +cd ~/railiance-platform +make credential-exec-ops-warden-smoke # policy-gate smoke, no manual VAULT_TOKEN + +# Or for a single sign: +scripts/credential.py exec \ + --grant ops-warden/warden-sign \ + --purpose ops-warden-production-sign-smoke \ + --ttl 15m -- \ + warden sign --pubkey +``` + +Routing: `warden route show ops-warden-warden-sign-token --json` · playbook: +`wiki/playbooks/ops-warden-warden-sign-token.md`. + +ops-warden does not mint OpenBao tokens — the broker in `railiance-platform` does. --- @@ -46,7 +71,10 @@ OpenBao admin runbooks). --- -## Session pattern +## Session pattern (manual fallback) + +Use only when the broker is unavailable and you already hold a scoped token +out-of-band: ```bash # Set for current shell only — do not add to ~/.bashrc with a literal token @@ -100,6 +128,7 @@ from daily shell profile. ## See also +- `wiki/playbooks/ops-warden-warden-sign-token.md` — preferred broker path - `wiki/OpenBaoSshEngineChecklist.md` - `wiki/OpsWardenConfig.md` — Authentication section - `examples/warden.production.example.yaml` \ No newline at end of file diff --git a/wiki/playbooks/ops-warden-warden-sign-token.md b/wiki/playbooks/ops-warden-warden-sign-token.md new file mode 100644 index 0000000..46d8bdf --- /dev/null +++ b/wiki/playbooks/ops-warden-warden-sign-token.md @@ -0,0 +1,109 @@ +# ops-warden warden-sign OpenBao token + +Date: 2026-07-01 +Catalog: `ops-warden-warden-sign-token` (status `active`) +Owner: `railiance-platform` (credential broker / OpenBao) · grant `ops-warden/warden-sign` +Workplan: `RAILIANCE-WP-0005` T08 · live-verified 2026-07-01 + +A short-lived OpenBao child token with only the `warden-sign` policy — enough for +production `warden sign` and the flex-auth policy-gate smoke. ops-warden **does not +mint, store, or vend this token**; it issues SSH certificates only. Token issuance +belongs to the railiance-platform credential broker. + +--- + +## Owner-confirmed lane + +| Field | Value | +| --- | --- | +| Grant id | `ops-warden/warden-sign` | +| OpenBao token role | `warden-sign` | +| Issuer policy | `credential-broker-warden-sign-issuer` | +| Workload policy | `warden-sign` (SSH sign paths only) | +| Default TTL | 15 minutes (max 1 hour) | +| Preferred delivery | `exec-env` — inject into one child process, then revoke | +| Grant catalog | `railiance-platform/credential-grants/catalog.yaml` | +| Owner docs | `railiance-platform/docs/credential-broker.md` | + +--- + +## Worker checklist + +1. **Route here first** — do not paste `VAULT_TOKEN` into chat, State Hub, Git, or workplans: + ```bash + warden route find "VAULT_TOKEN ops-warden warden sign" --json + warden route show ops-warden-warden-sign-token --json + ``` + +2. **Preferred: one-command exec injection** (no manual `export VAULT_TOKEN`): + ```bash + cd ~/railiance-platform + make credential-exec-ops-warden-smoke + ``` + For an arbitrary child command: + ```bash + cd ~/railiance-platform + scripts/credential.py exec \ + --grant ops-warden/warden-sign \ + --purpose ops-warden-production-sign-smoke \ + --ttl 15m -- \ + warden sign --pubkey + ``` + The helper mints a bounded child token, sets `VAULT_TOKEN` only in the child + environment, redacts token-looking output, and revokes the lease when the child exits. + +3. **Policy-gate production smoke** (FLEX-WP-0007 T4): + ```bash + cd ~/railiance-platform + scripts/credential.py exec \ + --grant ops-warden/warden-sign \ + --purpose ops-warden-production-sign-smoke \ + --ttl 15m -- \ + SMOKE_VAULT=1 ~/ops-warden/scripts/policy_gate_production_smoke.sh + ``` + +4. **Attended handoff or local file** (when exec-env is not suitable): + ```bash + cd ~/railiance-platform + scripts/credential.py request \ + --grant ops-warden/warden-sign \ + --purpose flex-auth-openbao-smoke \ + --ttl 15m + # default: mode-0600 file under .local/credential-leases/ + non-secret accessor metadata + scripts/credential.py status + scripts/credential.py revoke + ``` + +5. **Manual shell export (fallback only)** — when the broker is unavailable and an + operator already holds a scoped token out-of-band: + ```bash + export VAULT_TOKEN="" # current shell only + warden sign --pubkey + ``` + See `wiki/playbooks/operator-openbao-token-hygiene.md` for hygiene rules. Do not + add literals to `~/.bashrc`. + +6. **ops-warden role boundary** — `warden access` does not proxy this lane. The + owner-native front door is `railiance-platform/scripts/credential.py` (or the + Make wrappers). ops-warden routes and documents; it never holds the token value. + +--- + +## Troubleshooting + +| Symptom | Likely cause | Action | +| --- | --- | --- | +| OpenBao sealed | Cluster restarted | Operator unseal (2/3 shares) — `railiance-platform/docs/openbao.md` | +| `403` on grant apply | Pod token helper lacks ACL write | Use `OPENBAO_TOKEN_FILE` + platform-admin for one-time apply, not `--use-token-helper` | +| `Vault token not found` in child | Broker did not inject env | Use `credential exec`, not bare `warden sign` | +| `HTTP 403` during sign | Expired child token | Re-run `credential exec` with a fresh TTL | +| Broker mint denied | Issuer policy/role missing | `make openbao-configure-token-grants` in `railiance-platform` | + +--- + +## See also + +- `wiki/playbooks/operator-openbao-token-hygiene.md` — fallback manual token hygiene +- `wiki/PolicyGatedSigning.md` — flex-auth policy gate and smoke +- `wiki/CredentialRouting.md` — generic OpenBao routing (`openbao-api-key`) +- `railiance-platform/docs/credential-broker.md` — broker ownership and threat model \ No newline at end of file