Grade every catalog lane; record ADR-0007
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s

Closes the RISK-F-0003 exposure. All 14 ungraded lanes now carry an explicit
risk grade with its justification in the entry: 17 high, 10 standard, 0
ungraded. The agent read-boundary now fires (exit 7) on lanes that were silently
outside it.

Graded on merit rather than defensively. A first pass marked two ordinary
internal workload secrets high; test_high_risk_lanes_classified asserted the
opposite and was right, so both were regraded down. high means disclosure into a
logged context is damaging beyond what rotation recovers. inter-hub-bootstrap-ssh
is high conservatively, with the reason in the entry so it is regraded with
evidence rather than assumed down.

ADR-0007 records the rule the grading rests on: build-stage permissiveness
applies to controls that gate work, not to controls that prevent credential
disclosure. The test is friction, not severity — the read-boundary blocks nobody,
since --out/--exec/--wrap remain available, so relaxing it buys nothing. A
blocked operation is recovered by retrying; a credential in a logged transcript
is not recovered by rotation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
tegwick 2026-08-19 23:44:58 +02:00
parent 7b8dd3467b
commit ac85259c20
7 changed files with 158 additions and 1 deletions

View file

@ -14,6 +14,7 @@ distinction that matters here.
| `ADR-0004` | High-risk lanes refuse raw value streaming to agent sessions |
| `ADR-0005` | Implement one lane narrowly, route everything else |
| `ADR-0006` | Enforcement is zone-scoped, never a global flag |
| `ADR-0007` | Build-stage permissiveness stops at credential disclosure; every lane carries an explicit `risk` grade |
### Owned versus inherited — check `owner:` before changing anything

View file

@ -154,6 +154,7 @@ superseding ADR, never an in-place edit.
| `ADR-0004` | High-risk lanes refuse raw value streaming to agent sessions |
| `ADR-0005` | Implement one lane narrowly, route everything else |
| `ADR-0006` | Enforcement is zone-scoped, never a global flag (`policy.enabled` stays off until WP-0032) |
| `ADR-0007` | Build-stage permissiveness stops at credential disclosure; every lane carries an explicit `risk` grade |
Rules we follow but do not own — NetKingdom canon, the IAM profile, the
credential-management standard, the-custodian's ADR-001 workplan convention — are

View file

@ -119,3 +119,5 @@
| task | WARDEN-WP-0032-T02 | wait | — | workplans/WARDEN-WP-0032-security-zones.md |
| task | WARDEN-WP-0032-T03 | wait | — | workplans/WARDEN-WP-0032-security-zones.md |
| task | WARDEN-WP-0032-T04 | wait | — | workplans/WARDEN-WP-0032-security-zones.md |
| task | WARDEN-WP-0032-T05 | todo | — | workplans/WARDEN-WP-0032-security-zones.md |
| task | WARDEN-WP-0032-T06 | wait | — | workplans/WARDEN-WP-0032-security-zones.md |

View file

@ -0,0 +1,100 @@
---
id: ops-warden-adr-0007
type: adr
title: "ADR-0007 — Build-stage permissiveness stops at credential disclosure"
domain: infotech
repo: ops-warden
status: accepted
version: "1.0"
revision: "1"
owner: ops-warden
binds: "ops-warden"
created: "2026-08-19"
updated: "2026-08-19"
last_reviewed: "2026-08-19"
review_interval: 6m
enforced_by: "registry/routing/catalog.yaml risk grades; src/warden/cli.py agent read-boundary; WARDEN-WP-0032-T06"
supersedes: ""
successor: ""
---
# ADR-0007 — Build-stage permissiveness stops at credential disclosure
## Status
Accepted 2026-08-19, alongside grading the last 14 ungraded catalog lanes.
## Context
`ADR-0006` deferred a global fail-closed authorization gate because uniform
enforcement across an estate under deep refactor hardens the access needed to
perform the refactor. The organization's declared posture is `build`
(WP-0029), and the operator has confirmed the estate need not be tight yet.
That is correct, and it is also the kind of principle that quietly generalises
past its warrant. Read loosely, "we are in build stage" argues for relaxing
every control, including the ones that stop a credential landing in a logged
agent transcript. Those are not the same class of control, and the difference
is not severity — it is **cost**.
`RISK-F-0003` made the distinction concrete. `ADR-0004` reads as a categorical
rule: high-risk lanes refuse raw value streaming to agent sessions. The
implementation was `risk == "high"` against an **optional** field, so 14 of 27
lanes never reached the control at all — five of them `exec_capable`. The
control had not been relaxed by anyone's decision. It had simply never been
reached, which is worse, because nothing announced it.
## Decision
**Build-stage permissiveness applies to controls that gate work. It does not
apply to controls that prevent credential disclosure.**
The test is friction, not severity:
- A control that can **block a legitimate operation** — a fail-closed
authorization gate, an enforcement stance — is a candidate for relaxation
while the organization is in `build`, and `ADR-0006` scopes that relaxation
to zones.
- A control that **redirects how a value moves without preventing the work**
the agent read-boundary, which refuses raw stdout but leaves `--out`,
`--exec`, `--wrap` and `--fingerprint` fully available — is not relaxed by
build posture, because relaxing it buys nothing. Nobody is unblocked by it.
The asymmetry that settles it: a blocked operation is recovered by retrying.
A credential written into a logged transcript is not recovered by rotation —
rotation limits the damage, it does not unwrite the log. The 2026-07-16
disclosure is the case in point.
**Therefore, regardless of `organization_posture`:**
1. Every catalog lane carries an explicit `risk` grade. **Absence is not a
grade**, and a lane that omits it is a defect, not a default.
2. Grading is done on merit, per lane. This decision is not licence to grade
everything `high` — an over-broad grade is its own inaccuracy, and
`tenancy-posture` §6's *accuracy, not altitude* applies to this field too.
3. Minimum credential-handling standards — the read-boundary, the safe fetch
transports, the no-secret audit guard — hold in every posture.
## Consequences
**We accept** the grading cost, now and on every new lane. That is the point:
`WARDEN-WP-0032-T06` makes an ungraded lane impossible rather than merely
discouraged, because a rule enforced by remembering is not enforced.
**We reject** "build stage" as a general argument in credential-handling
discussions. It is a real and useful argument about *gating*, and citing it
against a disclosure control is a category error this record exists to name.
**We note what this decision is not.** It does not set severity for
`RISK-F-0003` — that is `risk-nexus`'s. It does not make ops-warden the judge of
other repos' controls. And it does not survive contact with a zone model that
says otherwise: when `zone-engine` defines admission standards, a zone may
legitimately require *more* than this floor. It may not require less.
## Related
- `ADR-0004` — high-risk lanes refuse raw value streaming to agent sessions
- `ADR-0006` — enforcement is zone-scoped, never a global flag
- `RISK-F-0003` — the read-boundary blind spot that prompted this
- `WARDEN-WP-0032-T05` / `T06` — the grading, and making absence impossible
- `zone-engine` `ZONE-WP-0001` — where admission standards will be defined

View file

@ -68,3 +68,4 @@ and the publication is a defect.
| `ADR-0004` | High-risk lanes refuse raw value streaming to agent sessions | ops-warden, and any agent runtime calling `warden access` |
| `ADR-0005` | Implement one lane narrowly, route everything else | ops-warden |
| `ADR-0006` | Enforcement is zone-scoped, never a global flag | ops-warden |
| `ADR-0007` | Build-stage permissiveness stops at credential disclosure | ops-warden |

View file

@ -37,6 +37,8 @@ version: 1
entries:
- id: ssh-cert-host-access
title: Short-lived SSH certificate for host / ops reachability
# Emits a signed certificate — a public artifact. The private key never leaves the caller (WARDEN-WP-0032-T05).
risk: standard
need_keywords: [ssh, certificate, cert, host, access, sign, adm, agt, atm, reachability, ops]
owner_repo: ops-warden
subsystem: ops-warden
@ -57,6 +59,8 @@ entries:
- id: ops-warden-warden-sign-token
title: Scoped OpenBao token for ops-warden SSH signing (warden-sign)
# A scoped VAULT_TOKEN is a credential in its own right. Graded on what the value is, not on whether ops-warden currently proxies it (WARDEN-WP-0032-T05).
risk: high
need_keywords: [vault_token, vault, token, warden-sign, warden, ops-warden, signing, sign, smoke, flex-auth, credential, broker, lease, openbao, ssh, production]
owner_repo: railiance-platform
subsystem: OpenBao credential broker
@ -90,6 +94,8 @@ entries:
- id: openbao-api-key
title: API key, DB credential, or dynamic lease
# Wildcard lane over platform/workloads/<domain>/<workload>/<bundle>: its ceiling is the most dangerous bundle it can resolve to (WARDEN-WP-0032-T05).
risk: high
need_keywords: [api, key, secret, database, db, password, token, lease, openbao, vault, kv, dynamic, credential, npm, npm_auth_token, registry]
owner_repo: railiance-platform
subsystem: OpenBao
@ -123,6 +129,8 @@ entries:
- id: whynot-design-npm-publish
title: whynot-design npm publish token (@whynot/design → coulomb Gitea registry)
# Publish rights to the package registry — a leaked token is a supply-chain write, not a read (WARDEN-WP-0032-T05).
risk: high
need_keywords: [whynot-design, whynot, npm, publish, npm_auth_token, gitea, registry, coulomb, package]
owner_repo: railiance-platform
subsystem: OpenBao
@ -164,6 +172,8 @@ entries:
- id: flex-auth-policy-check
title: Authorization decision — may this actor perform this action
# Returns an authorization decision; no credential flows (WARDEN-WP-0032-T05).
risk: standard
need_keywords: [authorization, policy, permission, allow, deny, may, flex-auth, topaz, pdp, decision]
owner_repo: flex-auth
subsystem: flex-auth
@ -179,6 +189,8 @@ entries:
- id: key-cape-oidc-login
title: Interactive login, OIDC token, or MFA
# Interactive browser OIDC: a login flow, not a KV read. No stored value is fetched, and warden access already excludes is_login from raw-value streaming (WARDEN-WP-0032-T05).
risk: standard
need_keywords: [login, oidc, identity, mfa, token, jwt, sso, keycloak, key-cape, iam, claims, authenticate, signin]
owner_repo: key-cape
subsystem: key-cape / Keycloak
@ -203,6 +215,8 @@ entries:
- id: ops-bridge-tunnel
title: SSH tunnel or port forward
# Routes to ops-bridge and supplies a cert_command; no secret value flows (WARDEN-WP-0032-T05).
risk: standard
need_keywords: [tunnel, port, forward, bridge, ops-bridge, reverse, transport, ssh-tunnel, cert_command]
owner_repo: ops-bridge
subsystem: ops-bridge
@ -218,6 +232,8 @@ entries:
- id: railiance-infra-principals
title: Host SSH principal file or force-command deployment
# Principal-file deployment via Ansible; no secret value flows (WARDEN-WP-0032-T05).
risk: standard
need_keywords: [principal, auth_principals, force-command, host, sshd, hardening, railiance-infra, ansible]
owner_repo: railiance-infra
subsystem: railiance-infra
@ -233,6 +249,8 @@ entries:
- id: inter-hub-bootstrap-ssh
title: Inter-Hub bootstrap SSH envelope
# Graded high conservatively: ops-warden could not establish from the lane definition that no key material moves in the envelope. Regrade with evidence, do not assume down (WARDEN-WP-0032-T05).
risk: high
need_keywords: [inter-hub, interhub, bootstrap, ops-hub, agt-interhub-bootstrap, envelope, force-command, CUST-WP-0049]
owner_repo: ops-warden
subsystem: ops-warden + railiance-infra
@ -248,6 +266,8 @@ entries:
- id: activity-core-issue-sink
title: activity-core IssueSink → issue-core REST emission
# Emission routing only — the API key is a separate lane (WARDEN-WP-0032-T05).
risk: standard
need_keywords: [activity-core, issue-sink, issue-core, emission, issue_core_url, issue_core_api_key, tasks, ingest, rest, issuesink]
owner_repo: activity-core
subsystem: activity-core + issue-core
@ -263,6 +283,9 @@ entries:
- id: issue-core-ingestion-api-key
title: issue-core ingestion API key (OpenBao KV + ESO)
# Ordinary internal workload secret: an ingestion key for a first-party
# service. Rotatable, no spend, no tenant data, no admin scope (WARDEN-WP-0032-T05).
risk: standard
need_keywords: [issue-core, ingestion, api, key, openbao, issue_core_api_key, eso, external-secrets]
owner_repo: railiance-platform
subsystem: OpenBao + issue-core + activity-core
@ -300,6 +323,9 @@ entries:
- id: reuse-surface-hub-write-token
title: reuse-surface federation hub write bearer token
# Ordinary internal workload secret. Write access to a first-party federation
# surface; damaging to forge, but rotatable and internal (WARDEN-WP-0032-T05).
risk: standard
need_keywords: [reuse-surface, reuse_surface, hub, register, federation, write, token, bearer, REUSE_SURFACE_TOKEN, REUSE_SURFACE_FORGEJO_WEBHOOK_SECRET, reuse.coulomb.social]
owner_repo: railiance-platform
subsystem: OpenBao + reuse-surface
@ -635,6 +661,8 @@ entries:
- id: object-storage-sts
title: Object-storage STS / temporary S3 credentials
# Temporary S3 credentials are still credentials (WARDEN-WP-0032-T05).
risk: high
need_keywords: [s3, sts, object-storage, minio, artifact-store, temporary, credentials, bucket, vending]
owner_repo: net-kingdom
subsystem: flex-auth + OpenBao + artifact-store
@ -760,6 +788,8 @@ entries:
- id: audit-core-senders
title: audit-core sender registry (write and operator-read tokens)
# Vends write and operator-read tokens (WARDEN-WP-0032-T05).
risk: high
need_keywords: [audit-core, senders, sender registry, ingest token, AUDIT_CORE_SENDERS]
owner_repo: ops-mason
subsystem: OpenBao + audit-core

View file

@ -175,8 +175,9 @@ owning it — a superseding record, never an in-place edit. Update `SCOPE.md`,
```task
id: WARDEN-WP-0032-T05
status: todo
status: done
priority: high
state_hub_task_id: "4d5d2756-c880-4e90-8fb7-1f03674a4cd9"
```
**Grade the five exposed lanes now — do not wait for the model.**
@ -196,10 +197,31 @@ having them inferred. Verify separately whether OpenBao's
`agent-high-risk-boundary` policy covers these paths; `RISK-F-0003` deliberately
does not assume it does, because those paths were graded by the same omission.
**Done 2026-08-19, operator-sanctioned.** All 14 ungraded lanes graded on merit,
each with its justification in the entry: 17 `high`, 10 `standard`, **0
ungraded**. `warden access <lane> --fetch` with `WARDEN_AGENT_ID` set now exits 7
on lanes that were silently outside the control an hour earlier.
Graded on merit, not defensively. A first pass marked
`issue-core-ingestion-api-key` and `reuse-surface-hub-write-token` `high`; the
existing test `test_high_risk_lanes_classified` asserted the opposite and was
right — ordinary internal workload secrets are `standard`. Both were regraded
down. `high` means disclosure into a logged context is damaging beyond what
rotation recovers: provider keys with spend, admin PATs, tenant commercial data,
supply-chain publish rights. `inter-hub-bootstrap-ssh` is `high` **conservatively**
— ops-warden could not establish that no key material moves in the envelope, and
that is recorded in the entry so it is regraded with evidence rather than assumed
down.
The rule is now `ADR-0007`: build-stage permissiveness stops at credential
disclosure. Not yet verified: whether OpenBao's `agent-high-risk-boundary`
policy covers these paths (T06).
```task
id: WARDEN-WP-0032-T06
status: wait
priority: medium
state_hub_task_id: "8c082416-0ff9-45dc-8cbd-e9ca7913a6ef"
```
**Make absence impossible, once the model says what absence means.** Waits on