- proxy.py: proxy_fetch_to_file (mode-0600 file), build_wrapped_fetch + proxy_fetch_wrapped (single-use OpenBao response-wrapping token), _capture_value helper, is_bao_kv_fetch. - warden access: --out FILE, --wrap [--wrap-ttl], --unsafe-stdout. Raw --fetch to a non-TTY stdout is refused (exit 6) — captured/piped output is the disclosure risk; sanctioned transports are --out / --exec / --wrap. - canon: anti-pattern (secret value onto captured stdout) + transport table in .claude/rules/credential-routing.md; OperatorAccessAssist.md examples + G2 updated. - tests: file/wrap/build + stdout-guard in tests/test_proxy.py. 293 pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
116 lines
6.1 KiB
Markdown
116 lines
6.1 KiB
Markdown
# 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
|
|
```
|
|
|
|
> **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`.
|
|
|
|
`--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 any secret-lane fetch. With `policy.enabled: false` the proxy refuses unless `--no-policy` is given to acknowledge proxying ungated. |
|
|
|
|
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
|