ops-warden/docs/adr/ADR-0002-conduit-not-broker.md
tegwick 35aff380a3
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Lift ops-warden's binding rules into owned ADRs
Five rules that governed this repo lived in wiki prose, a workplan, and a
comment at the top of catalog.yaml. All were followed; none was addressable.
A reader outside ops-warden could not cite one, could not tell whether it was
current, and — the point of this change — could not tell whether it was ours
to change or someone else's that we merely obey.

  ADR-0001  The routing catalog is a pointer layer, never a second copy
  ADR-0002  ops-warden is a transparent conduit, never a secret broker
  ADR-0003  Cover gaps, but never silently own them
  ADR-0004  High-risk lanes refuse raw value streaming to agent sessions
  ADR-0005  Implement one lane narrowly, route everything else

Each carries owner: ops-warden, which is the load-bearing field. It says we
follow the rule AND we are responsible for changing it — by superseding ADR,
never an in-place edit. The failure this prevents runs both ways: a rule we own
mistaken for inherited canon never gets fixed, because we wait for an owner who
does not exist; inherited canon mistaken for ours gets quietly bent, and the
drift is invisible until it breaks a repo that trusted the canonical version.

Rules we follow but do not own — NetKingdom canon, the IAM profile, the
credential-management standard, the-custodian's ADR-001 workplan convention —
are cited, never copied into docs/adr/. Copying them would recreate exactly the
second-source-of-truth failure ADR-0001 exists to prevent. architecture.md also
now flags the three-digit/four-digit ADR-001 vs ADR-0001 collision, which is
itself an ours-versus-inherited confusion waiting to happen.

Publication verified rather than assumed: all five render through policy-nexus
tools/render.py, and owner reaches the reader in three places — the page eyebrow
(render.py:346), the index Owner column (build_site.py:123,137), and the
publication manifest. build_site.py:179 makes title/status/owner required, so
ownership cannot be dropped on the way out. policy-nexus publishes and never
writes back; docs/adr/ stays the source of truth.

Documentation adapted: SCOPE.md gains a Governing rules section and an orientation
entry; .claude/rules/architecture.md replaced its stub with the ADR index, the
owned-versus-inherited rule, and ADR-over-wiki precedence; finding-routing.md's
ADR-gap section closed; catalog.yaml's no-double-source header now cites ADR-0001
rather than the originating workplan.

uv run pytest -q → 338 passed, 4 deselected.

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

90 lines
3.8 KiB
Markdown

---
id: ops-warden-adr-0002
type: adr
title: "ADR-0002 — ops-warden is a transparent conduit, never a secret broker"
domain: infotech
repo: ops-warden
status: accepted
version: "1.0"
revision: "1"
owner: ops-warden
binds: "ops-warden"
created: "2026-06-26"
updated: "2026-08-18"
last_reviewed: "2026-08-18"
review_interval: 6m
enforced_by: "src/warden/access.py; wiki/OperatorAccessAssist.md"
supersedes: ""
successor: ""
---
# ADR-0002 — ops-warden is a transparent conduit, never a secret broker
## Status
Accepted. Decided during WARDEN-WP-0014 (operator access assist), tightened by
WARDEN-WP-0026 (disclosure hygiene).
## Context
`warden access` is the operator front door for every credential need in the estate.
For lanes marked `exec_capable` it does more than advise: it runs the owner's tool
and returns the value. Anything that fetches secrets on request looks like a broker,
and the gravity toward becoming one is strong — a broker is more convenient at every
individual call site.
The distinction is not stylistic. A broker holds authority; a conduit borrows the
caller's. Only one of those creates a new thing worth attacking.
## Decision
**ops-warden runs the owner's tool with the caller's own identity, and takes no
custody of the value.** The caller's credentials do the work. ops-warden holds
nothing after the command returns, stores nothing, and caches nothing.
**Forbidden: a standing broker.** ops-warden must not hold its own long-lived
secret-read credential in order to serve values to callers who could not have
fetched them themselves. If the caller lacks authority, the correct outcome is a
denial from the owner's system — not a fetch performed on their behalf by a more
privileged intermediary.
The test is a question: *could the caller have run this themselves?* If yes, we are
a conduit and may proxy. If no, proxying is privilege laundering and is refused.
**Owner-native front doors outrank the proxy.** Where an owner has shipped their own
exec surface — `secrets-engine exec`, the railiance-platform credential broker — we
route there and do not proxy. The proxy is a fallback for lanes nobody fronts yet,
not a preferred path. This is why `whynot-design-npm-publish` and
`ops-warden-warden-sign-token` are `native` rather than `interim`.
**The value must not land somewhere it will be logged.** Sanctioned transports are
`--out` (mode-0600 file), `--exec` (child process env), and `--wrap` (a single-use
OpenBao wrapping token). Streaming to a non-terminal stdout is refused without an
explicit `--unsafe-stdout`, which exists for interactive humans only.
## Consequences
**ops-warden never becomes a credential store, and gains no value by being
compromised beyond the SSH CA it already holds.** This is the whole point. An
attacker who owns ops-warden gets the SSH signing lane — serious, bounded, and
already the thing this repo is hardened around — not a key to every secret in the
estate.
**Some requests cannot be served, and that is the correct answer.** When a caller
lacks authority, ops-warden routes and explains rather than fetching. This reads as
unhelpfulness at the moment it happens; it is the property that makes the front door
safe to point every agent at.
**Every proxied fetch is auditable and attributable to the caller**, because it ran
as them. `audit.jsonl` records metadata only — never values, guarded in code.
**The `--unsafe-stdout` escape hatch is a known liability.** It exists because
humans in terminals legitimately need to see values. It is also exactly the shape of
the 2026-07-16 disclosure, where a value reached a captured stdout. `ADR-0004`
constrains it further for agent sessions.
## Related
- `wiki/OperatorAccessAssist.md#the-conduit-vs-broker-boundary-the-security-model`
- `ADR-0004` — the agent-session read boundary built on top of this
- `ADR-0003` — why proxied lanes are tracked as interim rather than owned