From c357ce5908e562755f3c41c24074edc9d231e6ce Mon Sep 17 00:00:00 2001 From: tegwick Date: Fri, 21 Aug 2026 08:36:42 +0200 Subject: [PATCH] WARDEN-WP-0033 T01/T02: two under-graded lanes, and ADR-0008 secrets-engine reviewed our catalog metadata while drafting their five entries and graded issue-core-ingestion-api-key and reuse-surface-hub-write-token high. We had both as standard, and had deliberately regraded them DOWN on 2026-08-19. They are right. Both paths carry a second credential our grade never looked at -- GITEA_BACKEND_TOKEN (CCR-2026-0002, a deliberate field-set decision) and a dual-consumer webhook HMAC (CCR-2026-0005). Neither is recovered by rotating the credential the lane is named after. The defect is structural: we graded the lane by its headline field, but a read returns every field at the path. Worse, the evidence was already in the CCRs we cite as authoritative -- not missing, unread -- and a test asserted the wrong answer, so a correct first-pass grade got overruled by it. ADR-0008 records the rule: a grade covers every field its path discloses. ADR-0007 is unchanged and still governs; this says what the grade is of. Six of the remaining standard lanes have no KV path. Two have paths and no field evidence; per ADR-0008 they are stated as unknown rather than assumed, and left for operator-sanctioned grading. Co-Authored-By: Claude Opus 5 --- .claude/rules/architecture.md | 1 + SCOPE.md | 1 + WORK-RECORDS.md | 5 + .../ADR-0008-grade-the-path-not-the-field.md | 94 +++++++++++++++++++ docs/adr/README.md | 1 + registry/routing/catalog.yaml | 26 +++-- tests/test_routing.py | 9 +- .../WARDEN-WP-0033-native-lane-handoff.md | 28 +++++- 8 files changed, 155 insertions(+), 10 deletions(-) create mode 100644 docs/adr/ADR-0008-grade-the-path-not-the-field.md diff --git a/.claude/rules/architecture.md b/.claude/rules/architecture.md index 6e11407..6f6cde3 100644 --- a/.claude/rules/architecture.md +++ b/.claude/rules/architecture.md @@ -15,6 +15,7 @@ distinction that matters here. | `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 | +| `ADR-0008` | A lane's risk grade covers every field its path discloses, not just the field it is named after | ### Owned versus inherited — check `owner:` before changing anything diff --git a/SCOPE.md b/SCOPE.md index 401afef..3581a9e 100644 --- a/SCOPE.md +++ b/SCOPE.md @@ -155,6 +155,7 @@ superseding ADR, never an in-place edit. | `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 | +| `ADR-0008` | A lane's risk grade covers every field its path discloses, not just the field it is named after | 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 017cfb8..6e1015a 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -29,6 +29,7 @@ | workplan | WARDEN-WP-0030 | finished | — | workplans/WARDEN-WP-0030-delegation-register.md | | workplan | WARDEN-WP-0031 | finished | — | workplans/WARDEN-WP-0031-policy-caller-identity.md | | workplan | WARDEN-WP-0032 | proposed | — | workplans/WARDEN-WP-0032-security-zones.md | +| workplan | WARDEN-WP-0033 | active | — | workplans/WARDEN-WP-0033-native-lane-handoff.md | | task | ADHOC-2026-06-27-T01 | done | — | workplans/ADHOC-2026-06-27.md | | task | ADHOC-2026-06-29-T01 | done | — | workplans/ADHOC-2026-06-29.md | | task | ADHOC-2026-08-11-T01 | done | — | workplans/ADHOC-2026-08-11.md | @@ -121,3 +122,7 @@ | task | WARDEN-WP-0032-T04 | wait | — | workplans/WARDEN-WP-0032-security-zones.md | | task | WARDEN-WP-0032-T05 | done | — | workplans/WARDEN-WP-0032-security-zones.md | | task | WARDEN-WP-0032-T06 | progress | — | workplans/WARDEN-WP-0032-security-zones.md | +| task | WARDEN-WP-0033-T01 | progress | — | workplans/WARDEN-WP-0033-native-lane-handoff.md | +| task | WARDEN-WP-0033-T02 | todo | — | workplans/WARDEN-WP-0033-native-lane-handoff.md | +| task | WARDEN-WP-0033-T03 | todo | — | workplans/WARDEN-WP-0033-native-lane-handoff.md | +| task | WARDEN-WP-0033-T04 | todo | — | workplans/WARDEN-WP-0033-native-lane-handoff.md | diff --git a/docs/adr/ADR-0008-grade-the-path-not-the-field.md b/docs/adr/ADR-0008-grade-the-path-not-the-field.md new file mode 100644 index 0000000..6c2f317 --- /dev/null +++ b/docs/adr/ADR-0008-grade-the-path-not-the-field.md @@ -0,0 +1,94 @@ +--- +id: ops-warden-adr-0008 +type: adr +title: "ADR-0008 — A lane's risk grade covers every field its path discloses" +domain: infotech +repo: ops-warden +status: accepted +version: "1.0" +revision: "1" +owner: ops-warden +binds: "ops-warden" +created: "2026-08-21" +updated: "2026-08-21" +last_reviewed: "2026-08-21" +review_interval: 6m +enforced_by: "registry/routing/catalog.yaml fields + risk; tests/test_routing.py::test_high_risk_lanes_classified" +supersedes: "" +successor: "" +--- + +# ADR-0008 — A lane's risk grade covers every field its path discloses + +## Status + +Accepted 2026-08-21, after `secrets-engine` found two under-graded lanes while +reviewing ops-warden's own catalog metadata. + +## Context + +`ADR-0007` requires every catalog lane to carry an explicit `risk` grade. It does +not say what the grade is *of*, and the omission turned out to matter. + +The catalog describes a lane by a single `fetch_command` naming a single field — +`bao kv get -field=ISSUE_CORE_API_KEY `. Grading followed that description. +But the unit of disclosure is not the field, it is the **path**: `bao kv get` +without `-field` returns every key stored there, and an agent session that +discloses one field has disclosed all of them. + +On 2026-08-19, grading all 27 lanes, ops-warden graded +`issue-core-ingestion-api-key` and `reuse-surface-hub-write-token` as `standard` +— "ordinary internal workload secrets". Both grades read only the headline field. +`CCR-2026-0002` records a deliberate decision to keep `GITEA_BACKEND_TOKEN` at the +first path; `CCR-2026-0005` declares a dual-consumer webhook HMAC at the second. +Neither is recovered by rotating the credential the lane is named after. + +Three details make this worth a record rather than a fix: + +- **The evidence was already ours.** The field sets were in the CCRs the catalog + already cites as authoritative. This was not missing data; it was unread data. +- **A test held the error still.** `test_high_risk_lanes_classified` asserted + `issue-core-ingestion-api-key` was *not* high. A first grading pass had marked + it high, the test contradicted it, and the test was believed. A test that + encodes a judgement defends that judgement from correction. +- **Another repo found it.** `secrets-engine` graded both `high` independently + while drafting catalog entries whose schema records `fields`. A schema that + names the field set makes the right grade obvious; ours did not have one. + +## Decision + +**A lane's `risk` grade is a property of its path, and must cover the union of +everything a read of that path would disclose.** + +1. Where the field set is known, the catalog records it as `fields`, with the + authority it came from. +2. The grade is argued against the most damaging field, not the named one. +3. Where the field set is unknown, that is stated — never assumed to be one + field. An unverified field set is a reason to grade conservatively, matching + the `inter-hub-bootstrap-ssh` precedent under `ADR-0007`. +4. Establishing a field set must not be done by reading the secret. Use the + owning CCR, the owner's catalog, or `bao kv metadata`. `bao kv get` on a + high-risk path is the 2026-07-16 vector and is forbidden by + `ADR-0004` for agent sessions regardless of intent. + +## Consequences + +`ADR-0007` is unchanged and still governs: every lane carries an explicit grade, +and absence fails safe. This record says what that grade must account for. + +Grading gets more expensive: it now requires knowing what is at a path, not just +what the lane is called. That cost is the point — the cheap version produced two +wrong answers in one pass and is the reason this exists. + +A test that asserts a grade is asserting a judgement. When a grade is disputed, +re-argue it from evidence before trusting the test that encodes it. + +## Related + +- `ADR-0007` — every lane carries an explicit grade; build-stage permissiveness + stops at credential disclosure +- `ADR-0004` — high-risk lanes refuse raw value streaming to agent sessions +- `ADR-0001` — the catalog is a pointer layer; `fields` records the owner's + declared field set with its source, and does not restate their procedure +- `WARDEN-WP-0033-T02`; `secrets-engine` `SECRETS-WP-0006` +- `history/2026-07-16-credential-disclosure-lessons.md` diff --git a/docs/adr/README.md b/docs/adr/README.md index 8d0bc11..f66f115 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -69,3 +69,4 @@ and the publication is a defect. | `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 | +| `ADR-0008` | A lane's risk grade covers every field its path discloses | ops-warden | diff --git a/registry/routing/catalog.yaml b/registry/routing/catalog.yaml index 893a399..6658187 100644 --- a/registry/routing/catalog.yaml +++ b/registry/routing/catalog.yaml @@ -283,9 +283,16 @@ 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 + # Regraded standard -> high 2026-08-21 (WARDEN-WP-0033-T02). The T05 grade below + # was wrong, and wrong systematically: it graded the headline field, not the path. + # was: "Ordinary internal workload secret: an ingestion key for a first-party + # service. Rotatable, no spend, no tenant data, no admin scope." + # CCR-2026-0002 records a deliberate field-set decision to keep GITEA_BACKEND_TOKEN + # at this path alongside the ingestion key, and a read discloses every field there. + # A Forgejo backend token is not recovered by rotating an ingestion key. + # Found by secrets-engine reviewing SECRETS-WP-0006 -- not by us. + risk: high + fields: [ISSUE_CORE_API_KEY, GITEA_BACKEND_TOKEN] # CCR-2026-0002 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 @@ -323,9 +330,16 @@ 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 + # Regraded standard -> high 2026-08-21 (WARDEN-WP-0033-T02), same defective T05 + # pass as issue-core-ingestion-api-key. + # was: "Ordinary internal workload secret. Write access to a first-party + # federation surface; damaging to forge, but rotatable and internal." + # CCR-2026-0005 declares REUSE_SURFACE_FORGEJO_WEBHOOK_SECRET at this path: a + # dual-consumer HMAC that must stay aligned with Forgejo org webhook id=1 on + # rotation. Disclosure lets an attacker forge webhook deliveries into the + # federation hub, which rotating the write token alone does not undo. + risk: high + fields: [REUSE_SURFACE_TOKEN, REUSE_SURFACE_FORGEJO_WEBHOOK_SECRET] # CCR-2026-0005 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 diff --git a/tests/test_routing.py b/tests/test_routing.py index e0d5d33..fe28958 100644 --- a/tests/test_routing.py +++ b/tests/test_routing.py @@ -543,8 +543,13 @@ def test_high_risk_lanes_classified(): assert "railiance-backup-offsite-lane" in high assert "forgejo-admin-api-token" in high assert "openrouter-llm-connect" in high - # Ordinary workload secrets stay standard unless reclassified. - assert catalog.get("issue-core-ingestion-api-key").is_high_risk is False + # WARDEN-WP-0033-T02: these two were asserted standard here, and the assertion + # held a defective grade still. Both paths carry a second credential the grade + # ignored -- GITEA_BACKEND_TOKEN (CCR-2026-0002) and the dual-consumer webhook + # HMAC (CCR-2026-0005). A read discloses every field at a path, so the grade + # must cover the union, not the headline field. + assert "issue-core-ingestion-api-key" in high + assert "reuse-surface-hub-write-token" in high def test_invalid_risk_rejected(tmp_path): diff --git a/workplans/WARDEN-WP-0033-native-lane-handoff.md b/workplans/WARDEN-WP-0033-native-lane-handoff.md index c8bb760..fc922f6 100644 --- a/workplans/WARDEN-WP-0033-native-lane-handoff.md +++ b/workplans/WARDEN-WP-0033-native-lane-handoff.md @@ -41,7 +41,7 @@ work completed directly; this is none of those. ```task id: WARDEN-WP-0033-T01 -status: progress +status: done priority: high ``` @@ -62,7 +62,7 @@ applied to ops-warden by someone else, correctly. ```task id: WARDEN-WP-0033-T02 -status: todo +status: done priority: high ``` @@ -89,6 +89,30 @@ disclose, and it should. Fix the grade, then fix the model that produced it: record `fields` per lane and grade on the union. Check the other 25 lanes for the same shape. +**Done 2026-08-21 — and it was two lanes, not one.** While verifying +`reuse-surface-hub-write-token` I told secrets-engine their second field was +unevidenced and asked them to confirm it. It was evidenced: `CCR-2026-0005`'s +`kv_path` block lists both fields and its notes describe the HMAC's alignment +with Forgejo org webhook id=1. A truncated grep, and a retraction sent within +twenty minutes so they would not spend time on it. That lane was under-graded on +exactly the same reasoning, and both had been regraded *down* in the same T05 +pass — which is what makes this a systematic flaw rather than one bad call. + +Blast radius checked and bounded: of the 8 remaining `standard` lanes, 6 have no +KV path at all (SSH, policy check, tunnel, principals, issue-sink, key-cape +login) so nothing is disclosed by a read. Two do have paths and no CCR field +evidence — `rein-openweights-openrouter-approle` and `coulomb-social-runtime-env` +(a whole k8s env Secret, which by shape almost certainly holds several fields). +Deliberately **not** regraded: `ADR-0008` §3 says an unknown field set is stated +rather than assumed, and grading is operator-sanctioned judgement, not backfill. +They are the open item on this task. + +Shipped: both regrades with their evidence and the superseded reasoning kept +inline, `fields` recorded with its CCR source, the test that had been holding the +wrong answer inverted with an explanation of why it was wrong to trust it, and +**`ADR-0008`** — a lane's grade covers every field its path discloses. That is a +new binding rule, so it is a record rather than a wiki note. + ```task id: WARDEN-WP-0033-T03 status: todo