diff --git a/.claude/rules/architecture.md b/.claude/rules/architecture.md index 1fa1d44..6e11407 100644 --- a/.claude/rules/architecture.md +++ b/.claude/rules/architecture.md @@ -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 diff --git a/SCOPE.md b/SCOPE.md index e6e926b..401afef 100644 --- a/SCOPE.md +++ b/SCOPE.md @@ -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 diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index 5dd702a..6867475 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -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 | diff --git a/docs/adr/ADR-0007-build-stage-stops-at-credential-disclosure.md b/docs/adr/ADR-0007-build-stage-stops-at-credential-disclosure.md new file mode 100644 index 0000000..b80d9ca --- /dev/null +++ b/docs/adr/ADR-0007-build-stage-stops-at-credential-disclosure.md @@ -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 diff --git a/docs/adr/README.md b/docs/adr/README.md index 7365cd0..8d0bc11 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -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 | diff --git a/registry/routing/catalog.yaml b/registry/routing/catalog.yaml index b8bf284..7e11138 100644 --- a/registry/routing/catalog.yaml +++ b/registry/routing/catalog.yaml @@ -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///: 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 diff --git a/workplans/WARDEN-WP-0032-security-zones.md b/workplans/WARDEN-WP-0032-security-zones.md index ddbe085..9160e44 100644 --- a/workplans/WARDEN-WP-0032-security-zones.md +++ b/workplans/WARDEN-WP-0032-security-zones.md @@ -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 --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