ops-warden/wiki/AccessRouting.md
tegwick e18894ee5b
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Scale the blocker window by lane risk, converging with risk-nexus
risk-nexus accepted the offer to match their convention rather than grow a second
one, and published it: 14d critical/high, 30d medium, 60d low, nothing auto-closing
on staleness alone. Their preference — point warden route gaps at those windows and
the two registers agree without a shared mechanism — is better than a joint tool.

blocker_stale_days() now maps lane risk onto those windows. A flat 14 would have
been wrong in both directions: too aggressive for a low-risk pointer, and it treated
an admin PAT lane the same as one.

ungraded takes the shortest window, not the longest. ADR-0007 makes an absent grade
a defect and ADR-0008 makes a grade cover the whole path, so a lane nobody graded is
the one whose blocker deserves least trust. Encoding that as 60 days would have been
the fail-open default this repo already fixed once.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 13:29:29 +02:00

286 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Access Routing — what ops-warden answers
Date: 2026-06-18
ops-warden **issues short-lived SSH certificates**, **routes every other credential
need to the subsystem that owns it**, and **assists** with obtaining it through the
`warden access` front door. This page states that role plainly so it cannot be
misread as a desk that wraps the platform.
- **What ops-warden executes:** the SSH certificate lane only (`warden sign`,
`cert_command`, `ops-ssh-wrapper`).
- **What ops-warden answers:** *where* a credential need belongs and *who owns it*
pointing at the owner's docs, never restating their procedure.
- **What ops-warden assists with:** `warden access` renders the exact auth/path/command
for any need and, for `exec_capable` lanes, **proxies the fetch as the caller** — a
transparent, policy-gated, audited conduit that holds, caches, and logs nothing.
- **What ops-warden never does:** *own* a secret store, *establish* identity, *decide*
policy, open tunnels, or deploy hosts. The assist conduit uses **your** identity and
owns none of these. See `OperatorAccessAssist.md`.
For the worker-facing decision tree see `CredentialRouting.md`; for component
literacy see `NetKingdomSecurityMap.md`. This page is the steward's statement of
**role and boundary**.
---
## Issue vs route
| Need | Subsystem | ops-warden role | Who acts |
| --- | --- | --- | --- |
| SSH cert for host/ops access (`adm`/`agt`/`atm`) | **ops-warden** | **Issue** (`warden sign`) | ops-warden signs; worker uses cert |
| API key / DB cred / dynamic lease | OpenBao | Route — point at path | Worker calls OpenBao |
| "May I perform action X?" | flex-auth (+ Topaz PDP) | Route — point at policy | Worker/PEP calls flex-auth |
| Login / OIDC token / MFA | key-cape / Keycloak | Route — point at IAM Profile | Worker authenticates |
| Object-storage STS / S3 creds | net-kingdom + flex-auth + OpenBao | Route — point at vending path | Worker follows NK-WP-0007 |
| SSH tunnel / port forward | ops-bridge | Route — supply `cert_command` | ops-bridge opens tunnel |
| Host principal / force-command | railiance-infra | Route — point at Ansible | infra deploys host |
| OpenBao cluster init / unseal | railiance-platform | Route — point at ceremony | platform operates |
Only the first row is something ops-warden **executes**. Every other row is a
**pointer**: ops-warden names the owner and the doc, and the worker acts on the
owning system directly.
**Assist layer (`warden access`).** For routed rows, ops-warden goes beyond the
pointer: it renders the exact auth method, path template, and command, and — where the
catalog marks a lane `exec_capable` (today: OpenBao secret reads, key-cape login) —
**proxies the call as the caller**. This does not change ownership: the secret stays in
OpenBao, the decision stays in flex-auth, the identity stays in key-cape. ops-warden is
a transparent conduit using the caller's identity, never a custodian of the value. The
boundary that keeps this sound is in `OperatorAccessAssist.md#the-conduit-vs-broker-boundary`.
---
## Anti-patterns (not coming to ops-warden)
ops-warden does not **own** custody, identity, authorization, or transport — those
belong to other subsystems. The assist layer (`warden access`) may *proxy* a call as
the caller, but it never becomes the owner. Don't reach for a command that implies
ownership:
| Tempting command | Why it's wrong | Right path |
| --- | --- | --- |
| `warden secret` / `warden bao` (as a store/vend) | ops-warden owns no secret store and vends nothing | OpenBao; to obtain *as yourself*, `warden access <need> --fetch` |
| `warden login` (as an identity owner) | ops-warden does not establish identity | key-cape / Keycloak; to run the login *as yourself*, `warden access <login need> --fetch` (login lane) |
| `warden policy` (as a decision) | ops-warden does not decide authorization | flex-auth makes the call; ops-warden only gates its own proxy on it |
| `warden tunnel` | ops-warden does not manage transport | ops-bridge |
The distinction: a **standing broker** (warden's own secret-read token, a cache of
values) is forbidden; a **transparent conduit** (`warden access --fetch`, caller's
identity, nothing retained) is sanctioned. ops-warden authors step-by-step procedure
for exactly one lane — SSH issuance — because it owns it. For everything else it
carries a **pointer** (and, for `exec_capable` lanes, a conduit), not a fork of the
owner's runbook. See the no-double-source rule in
`workplans/WARDEN-WP-0010-access-routing-charter.md` and the conduit-vs-broker
boundary in `OperatorAccessAssist.md`.
---
## Interim custodianship
ops-warden **works with, and never replaces or duplicates**, the NetKingdom
components that own identity, custody, authorization, and tenant/user lifecycle
(`INTENT.md` §9). Covering a gap is legitimate. Silently becoming the owner of
that gap is not.
The only lane ops-warden executes with its own authority is **SSH issuance**
(`ssh-cert-host-access`). Every other execution position — including every
`warden access` proxy — is **interim by default**. A catalog entry without a
`delegation:` block is treated as interim with an unknown owner: absence is a
question, not a settlement.
| `delegation.mode` | Meaning |
| --- | --- |
| `permanent` | ops-warden is the designed owner of this front door (SSH only, today) |
| `native` | the intended owner already fronts it; ops-warden routes (and may keep a proxy as fallback) |
| `interim` | ops-warden covers the gap until `intended_owner` ships the missing front door named in `blocked_on` |
Delegation targets — the components that should own a front door once they have
one:
| Target | What they should front |
| --- | --- |
| **secrets-engine** | owner-native secret-exec (`secrets-engine exec --catalog <id>`) |
| **tenant-engine** | tenant/client secret custody and tenant-lane front door |
| **user-engine** | end-user / account-lifecycle secrets that belong with user identity |
| **railiance-platform** | OpenBao cluster, credential broker, platform workload procedure |
| **flex-auth** | authorization decisions (already native — ops-warden only points) |
| **key-cape** | identity login and client-credential protocol (OIDC, `client_secret_basic`) |
Query the register with `warden route gaps`. An interim lane is retired by
setting `exec_owner` / `exec_command` (the WP-0019 pattern) and flipping
`delegation.mode` to `native` once the owner's front door is proven. Do not
delete a working proxy on the way.
---
## Routing lookup CLI (`warden route`)
Agents and operators query the pointer catalog directly instead of re-deriving
routing from wiki prose. The command group is **read-only** — it never calls
OpenBao, flex-auth, key-cape, or any other subsystem, and never returns secret
material.
```bash
warden route list [--json] [--all] [--tag <keyword>] # active-only unless --all
warden route list --stale [--stale-days 90] [--all] [--json] # past review cadence
warden route gaps [--json] [--all] # interim register (owner + blocker)
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:
```bash
# "I need an API key" — find the owner, get a pointer, act there yourself
warden route find "openrouter api key" --json
warden route show openbao-api-key --json
# → {"warden_executes": false, "next_action": "next action on `railiance-platform` — see `wiki/CredentialRouting.md#routing-table`"}
# The one lane ops-warden executes: SSH. `show` appends the authored steps + cert pattern.
warden route show ssh-cert-host-access --json
# → {"warden_executes": true, "cert_command": "warden sign <actor> --pubkey <path>", "steps": [...]}
```
`show` on a routed (non-SSH) need always ends with **"next action on
`<owner_repo>` — see `<wiki_ref>`"** and never implies ops-warden performed
anything. Draft scenarios (owner path not yet shipped) are hidden unless `--all`.
---
## Audience notes
- **Human operators** read this page and `CredentialRouting.md` to choose the
right subsystem, then follow that subsystem's own docs.
- **Agents / CI** read the machine-readable routing catalog
(`registry/routing/catalog.yaml`) via `warden route` (above) so routing does
not have to be re-derived from wiki prose each session.
- **Same truth, two shapes:** humans read the wiki; agents read the catalog. The
catalog references wiki sections by anchor so the two cannot drift apart — a
test (`tests/test_routing.py`) fails CI if any `wiki_ref` anchor stops resolving.
---
## How this stays aligned
NetKingdom security architecture is canonical in `net-kingdom`. ops-warden tracks
it: when canon changes, the wiki section is updated and the catalog pointer
(`wiki_ref` + `canon_ref`) follows. ops-warden never overrides canon and never
silently forks it.
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 |
| **Fortnightly** (default 14 days) | Run `warden route gaps` — re-check each `blocked_on` against the intended owner; flip to `native` when their front door exists |
### Two cadences, because they are two different claims
A catalog pointer and an interim blocker both carry a `reviewed:` date, and for a
while they shared one 90-day threshold. They should not.
| Claim | Question | Default | Where |
| --- | --- | --- | --- |
| Pointer freshness | Is this still the right owner and page? | **90 days** | `warden route list --stale` |
| Interim blocker | Has the intended owner answered / can they front this yet? | **risk-scaled, 1460 days** | `warden route gaps` |
The blocker window scales with what the lane holds, matching `risk-nexus`'s stall
windows (`docs/method/check-procedure.md`) so the two registers agree without a
shared tool:
| Lane `risk` | Window | risk-nexus equivalent |
| --- | --- | --- |
| `high`, `ungraded` | **14 days** | critical / high |
| `standard` | **30 days** | medium |
| `low`, `accepted` | **60 days** | low |
`ungraded` takes the *shortest* window, not the longest. `ADR-0007` makes an
absent grade a defect and `ADR-0008` makes a grade cover the whole path — a lane
nobody has graded is the one whose blocker deserves least trust.
A pointer genuinely is a quarterly question. A blocker is not: it is a claim
about another repo's state at a date, and this estate invalidates those in days.
`RISK-F-0001` invalidated an ops-warden blocker in one. The `secrets-engine`
lanes carried one for ten while it was answerable from that repo's source the
whole time. A `FLEX-WP-0007` blocker was repeated by two repos for about seven
weeks after that workplan read `finished`.
Sharing 90 days did not make the blocker check loose, it made it **inert**: the
delegation register was created 2026-08-15, so the threshold could not have fired
before November and never had.
### Reviewed is not verified
The more important half. A `reviewed:` date records when someone last *touched*
the entry, which looks identical to re-checking it. So every interim lane also
carries `verified:`, saying how the claim was established:
| Value | Meaning | Counts as verification |
| --- | --- | --- |
| `owner-confirmed` | the intended owner stated the blocker's status | **yes** |
| `source-read` | re-derived from the owner's code, canon, or a CCR | **yes** |
| `asked-and-waiting` | a question is outstanding | **no** |
| `unverified` | carried forward without a check | **no** |
`asked-and-waiting` deliberately does not reset the clock. A lane asked today
reads as reviewed today, and that is precisely how a blocker stays fresh-looking
while nobody answers it. `warden route gaps` flags such a lane as stale at zero
days old.
**Re-check the blocker; do not bump the date.** Bumping `reviewed` without
re-establishing the claim is the failure this section exists to prevent, and it
is cheap to avoid — most of these are answerable by reading the owner's repo.
```bash
warden route gaps # risk-scaled cadence, plus unverified lanes
warden route gaps --stale-days 30 # flat override for every lane
warden route gaps --fail-on-stale # exit 3 — for a cron job or a gate
```
### 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
- `NetKingdomSecurityMap.md` — component literacy
- `INTENT.md` — steward mission ("issue SSH, route the rest"); §9 interim custodianship
- `workplans/WARDEN-WP-0030-delegation-register.md` — delegation register
- `workplans/WARDEN-WP-0010-access-routing-charter.md` — charter + no-double-source rule
- `net-kingdom/docs/platform-identity-security-architecture.md` — platform canon