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

3.8 KiB

id type title domain repo status version revision owner binds created updated last_reviewed review_interval enforced_by supersedes successor
ops-warden-adr-0002 adr ADR-0002 — ops-warden is a transparent conduit, never a secret broker infotech ops-warden accepted 1.0 1 ops-warden ops-warden 2026-06-26 2026-08-18 2026-08-18 6m src/warden/access.py; wiki/OperatorAccessAssist.md

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.

  • 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