ops-warden/wiki/OperatorAccessAssist.md
tegwick 7ce58ae638
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
feat: adopt security zones and explicit workload refs
Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a0291a-1e87-7151-9934-fcbfe3f65eb1
2026-08-22 15:36:37 +02:00

124 lines
6.8 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.

# Operator Access Assist — `warden access`
> The operator front door for **every** NetKingdom credential need. ops-warden
> issues the SSH lane directly and **assists** with the rest: it tells you exactly
> how to obtain a credential and — for `exec_capable` lanes — proxies the fetch
> *as you*, without ever holding, persisting, or logging the value.
Shipped in WARDEN-WP-0014. This extends the routing charter from a **pointer layer**
("who owns it") to an **assist layer** ("here is exactly how to get it, gated and
audited"). It does **not** move secret custody into ops-warden.
---
## Three roles, one front door
| Role | Lane | Command | What ops-warden does |
| --- | --- | --- | --- |
| **Issue** | SSH cert (`adm`/`agt`/`atm`) | `warden access ssh…``warden sign` | Executes — signs the cert |
| **Assist (advise)** | any credential need | `warden access <need>` | Renders the owner, auth method, path, command skeleton, policy gate |
| **Assist (proxy)** | `exec_capable` lanes (OpenBao, login) | `warden access <need> --fetch / --exec` | Runs the owner's tool **as the caller**; value never touches warden |
```console
# advisory — works with no config; never fetches a value
$ warden access "npm token" --domain coulomb_social
# --- sanctioned transports (WP-0026 T02): value never hits stdout ---
# write the value to a mode-0600 file
$ warden access "npm token" --domain coulomb_social --field NPM_AUTH_TOKEN --path <p> --fetch --out ./npm.token
# run a child command with the secret in its env only (à la `op run`)
$ warden access "npm token" --field NPM_AUTH_TOKEN --exec -- npm publish
# return a single-use OpenBao wrapping token to unwrap in your own context
$ warden access "npm token" --path <p> --wrap # then: bao unwrap <token>
# interactive login (login lane): no token required, no secret-read gate
$ warden access "login oidc" --domain coulomb_social --fetch
# masked status: presence, length, short hash — never the value (WP-0026 T03)
$ warden access "npm token" --path <p> --fingerprint # hidden len=40 sha256:1a2b3c4d
```
> **Raw `--fetch` to stdout is the anti-pattern.** It is refused when stdout is
> captured or piped (a logged-context disclosure risk); pass `--unsafe-stdout` only
> for an interactive human terminal. Prefer `--out` / `--exec` / `--wrap`.
> **`--fingerprint` is defense-in-depth, not a boundary.** It masks warden-mediated
> output so two parties can compare `sha256` prefixes to confirm they hold the same
> value (e.g. that a rotation landed) without either seeing it. It only masks
> *warden's* output — raw `bao kv get <path>` bypasses it entirely. The real boundary
> is OpenBao policy plus capabilities-safe verify (T01) and the no-stdout transports.
`--json` gives a stable, secret-free shape for agentic operators.
---
## The conduit-vs-broker boundary (the security model)
There are two very different things "secret transits warden" can mean. One is
sanctioned; the other is forbidden by the NetKingdom responsibility model
(`net-kingdom/docs/responsibility-map.md`: ops-warden *"must not become a universal
secret broker — runtime secrets remain OpenBao; authorization remains flex-auth"*).
**Sanctioned — transparent conduit.** ops-warden runs the owner's tool with the
**caller's own identity**, streams the value straight to the caller, and retains
nothing. It holds no standing credential and stores no value. This is the `vault exec`
/ `op run` shape.
**Forbidden — standing broker.** ops-warden holding its own long-lived secret-read
token, caching fetched values, becoming a service every operator's secrets flow
through and rest in. That recreates the single high-value target the model exists to
prevent, and duplicates OpenBao.
`warden access` is built as the first and forbids the second by construction.
---
## The three guardrails (enforced in code)
| | Guardrail | How it is enforced |
| --- | --- | --- |
| **G1** | **Caller identity, never warden's** | The proxy runs the owner's tool with the caller's own environment; ops-warden injects no token of its own. Secret lanes require the caller to already hold a credential (`caller_auth_present`), else they fail with the auth pointer. |
| **G2** | **Transit only — no persistence/logging of values** | Sanctioned transports keep the value off stdout: `--out` writes it to a mode-0600 file, `--exec` injects it into a child process env, `--wrap` returns a single-use OpenBao wrapping token (not the value). Raw `--fetch` to stdout is refused for captured/piped output (`--unsafe-stdout` overrides for a human terminal). warden never writes the value to disk or log; the audit record is **metadata only**. (WP-0026 T02) |
| **G3** | **Policy gate before fetch** | `check_fetch_policy` (flex-auth) runs before every secret-lane fetch. Zone membership selects stance and local failure mode; an unresolved workload uses the explicit `unknown` profile. The retired `--no-policy` flag is rejected, so this gate has no CLI bypass. |
The catalog side enforces a fourth, upstream guard: **handoff fields are templates,
never values.** `_assert_no_secret_material` rejects any known token prefix or
high-entropy run in a catalog handoff field, so a secret can never leak into the
git-tracked, agent-visible catalog.
---
## Lanes
Each catalog entry declares a `lane`:
- **`secret`** (default) — read a value. Requires caller auth (G1) and runs the
flex-auth secret-read gate (G3). Value transits via inherit-stdout (`--fetch`) or
child env (`--exec`).
- **`login`** — interactive auth bootstrap (OIDC/MFA). **No** caller-auth precheck
(you have no token yet — that is the point) and **no** secret-read gate (it
establishes the identity the gate would need). Runs interactively as the caller;
`--exec` is rejected; the token lands in the caller's own store and warden never
captures it.
---
## What proxying requires
- An `exec_capable` catalog entry with a resolvable `fetch_command`.
- For `secret` lanes: the caller already authenticated (`VAULT_TOKEN`/`BAO_TOKEN` or
`~/.vault-token`) and a loadable `warden.yaml` (for policy posture + audit sink).
- All `<…>` placeholders resolved — `warden access` **refuses to run a half-templated
command** rather than guess an owner-confirmed resource name. Supply `--domain`,
`--field`, and `--path` as needed.
Audit lands in `state_dir/access-audit.log` (JSON lines, metadata only: who, need id,
owner, domain, action, policy decision id — never a value).
---
## See also
- `wiki/AccessRouting.md` — issue / route / assist roles
- `wiki/CredentialRouting.md` — which subsystem owns each need
- `registry/routing/catalog.yaml` — handoff fields + lanes
- `wiki/PolicyGatedSigning.md` — the flex-auth gate (shared with the SSH lane)
- `.claude/rules/credential-routing.md` — agent-facing routing + anti-patterns
- `history/2026-06-27-operator-access-assist-charter.md` — the proxy-mode decision