--- id: RISK-F-0009 type: finding title: "agent-high-risk-boundary denies 6 of 17 high-risk lanes; the direct bao path is unprotected for the rest" status: fixed owner: risk-nexus reported_by: ops-warden reported_via: ops-warden routed_by: ops-warden date_reported: "2026-08-20" system: railiance-platform environment: production fix_owner: railiance-platform fix_tracking: RPF-WP-0013 / RAILIANCE-WP-0022 (finished 2026-08-22) filed_as: "RISK-F-0004 by ops-warden; renumbered by risk-nexus 2026-08-20 (id collision)" answers: RISK-F-0003 related: [RISK-F-0003] # Graded by risk-nexus 2026-08-20 — docs/rulings/2026-08-20-openbao-layer.md severity: high severity_at_production: high impact: I4 likelihood: L2 fidelity_modifier: false production_rescore: false disclosure: public publication: pending-handover publication_id: risk-f-0009-openbao-high-risk-deny-coverage publication_path: "findings/openbao-high-risk-deny-coverage/v1/index.html" publication_subtitle: "OpenBao's agent boundary covered only a fraction of high-risk credential lanes; generated source-to-live coverage and deny-wins proof now close the gap." revision: "fixed-1" last_reviewed: "2026-09-01" review_interval: 6m embargo_lifted: "2026-08-22 — every concrete high-risk KV path is generated into the live deny policy and a dedicated agent identity proved deny-wins" embargo_was_since: "2026-08-20" escalation: none date_fixed: "2026-08-22" last_checked: "2026-09-01T00:32:44Z" next_check: "2026-09-01T00:32:44Z" cadence: instant clean_streak: 0 graded_by: risk-nexus ruling: RISK-RULING-2026-08-20 checked_by: "codex/risk-nexus" --- # RISK-F-0009 — the OpenBao half of the agent read-boundary covers a third of the lanes ## What is true `ADR-0004` (ops-warden) is enforced in two independent layers: 1. **ops-warden's CLI** — `warden access --fetch` exits 7 when `WARDEN_AGENT_ID` is set and the lane is high-risk. Verified working for all 17 high-risk lanes as of 2026-08-20 (`WARDEN-WP-0032-T05`/`T06`). 2. **OpenBao policy** `agent-high-risk-boundary` (`railiance-platform/openbao/policies/agent-high-risk-boundary.hcl`) — denies `data` read and permits `metadata` read for agent tokens. Layer 1 protects the ops-warden path. **Layer 2 is the one that protects the path an agent actually takes when it bypasses ops-warden** — a direct `bao kv get`, which is the 2026-07-16 disclosure vector. Comparing the policy's deny list against ops-warden's catalog: - **17** lanes graded `risk: high` - **5** data paths denied by the policy - **6** high-risk lanes covered (one policy path serves two lanes) - **8** high-risk lanes with a concrete KV path that the policy does **not** deny - **3** high-risk lanes with no KV path (policy not applicable) Not covered: | Lane | KV path | Newly graded 2026-08-19? | | --- | --- | --- | | `rapp-qonto-keycape-client` | `platform/workloads/rapp-qonto/keycape-client` | no — already `high` | | `agent-harness-forgejo-deploy` | `platform/workloads/agent-harness/forgejo-deploy-key` | no — already `high` | | `email-connect-transactional` | `platform/workloads/email-connect/transactional` | no — already `high` | | `scaleway-bootstrap` | `platform/workloads/railiance/scaleway/bootstrap` | no — already `high` | | `whynot-design-npm-publish` | `platform/workloads/coulomb/whynot-design/npm-publish` | yes | | `audit-core-senders` | `platform/workloads/audit-core/senders` | yes | | `ops-warden-warden-sign-token` | credential-broker grant, not a KV path | yes | | `openbao-api-key` | `platform/workloads///` — a **pattern**, not a path | yes | **Four of the eight were already graded `high` before this analysis.** The divergence is therefore pre-existing and not a consequence of the 2026-08-19 regrade; the regrade only made it visible by making the two lists comparable. ## Why it matters ops-warden's layer can be bypassed by not using ops-warden. An agent session holding a `workload-kv-read-*` policy can `bao kv get` these paths directly, and for the eight above nothing denies it. The playbook (`ops-warden/wiki/playbooks/agent-read-boundary.md`) already states that an agent token carrying `agent-high-risk-boundary` **must not** also carry the lane's `workload-kv-read-*` policy — but that is an instruction to whoever mints the token, not a mechanism. ## Exposure — stated only as far as ops-warden can support it - The comparison above is **static**, from the policy file in the `railiance-platform` checkout and ops-warden's catalog. It has **not** been confirmed against the live OpenBao. The operator token available to ops-warden is expired (`bao token lookup` → 403), so `bao policy read` and `bao token capabilities` could not be run. **The deployed policy may differ from the file.** - Whether any agent token currently carries `agent-high-risk-boundary` at all, and whether any carries it *together with* a `workload-kv-read-*` policy, is **not established**. If no agent tokens exist, exposure is theoretical. - `openbao-api-key` is a routing lane whose `path_template` is a pattern rather than an address; it is listed for completeness but probably cannot be expressed as a policy deny and should be handled by grading the concrete lanes it resolves to. - No evidence of an actual disclosure through these lanes has been sought or found. ## Suggested direction, not a prescription The two lists should be **generated from one source rather than maintained in two places** — that is what let them drift for four lanes without anyone noticing. ops-warden's catalog already carries `risk` and `path_template` per lane and could emit the deny set; railiance-platform owns whether to consume it. A check that fails when a high-risk lane has no corresponding deny would turn this from an audit into an invariant. ## Live verification — 2026-08-21 (ops-warden) The exposure section above says the comparison is static and that ops-warden's token was expired. **The token was not expired** — `bao token lookup` returned a valid `platform-admin` token from an OIDC login, and `bao policy read agent-high-risk-boundary` succeeded against `https://bao.coulomb.social`. The deployed policy has now been read. Reported by the same repo that filed the static version; correcting our own record. Three changes, one of them against ourselves: **1. Six lanes are uncovered, not eight.** The table above lists `ops-warden-warden-sign-token` (a credential-broker grant, not a KV path) and `openbao-api-key` (a `//` pattern) among the "concrete KV path" uncovered set — the prose already said the second was a pattern, and the table counted it anyway. Neither can be expressed as a policy deny. Corrected split: **6 covered, 6 uncovered with a concrete path, 5 with no address.** The six that stand are `rapp-qonto-keycape-client`, `agent-harness-forgejo-deploy`, `email-connect-transactional`, `scaleway-bootstrap`, `whynot-design-npm-publish`, `audit-core-senders`. **2. Coverage confirmed at 6 of 17 against the live policy.** The headline was right and is no longer an inference from a checkout. **3. New — the deployed policy has drifted from the file.** The file denies `platform/workloads/core-hub/runtime`; the server does not. No ops-warden catalog lane maps to that path, so nothing above changes. It matters as evidence rather than as exposure: this finding flagged "the deployed policy may differ from the file" as an unconfirmed risk, and it does differ. Any conclusion drawn from that file alone — by anyone, about any lane — is unsafe. Whether the drift is an undeployed change or a reverted one is `railiance-platform`'s to say, and this is not an assertion that `core-hub/runtime` should be denied. **The suggested direction is now half-built.** `ops-warden/scripts/check_agent_read_boundary.py` is the check that "fails when a high-risk lane has no corresponding deny" — run against the deployed policy, falling back to the file only with a loud warning, exit 1 on any uncovered lane. It is capabilities-only by construction: it reads the policy document and lane metadata, never a secret value, and never mints a token. What is still missing is the other half — `railiance-platform` consuming a generated deny set rather than hand-maintaining one. That half is theirs, and ops-warden does not amend another repo's control. **Not established, still.** Whether any agent token carries `agent-high-risk-boundary`, and whether any carries it together with a `workload-kv-read-*` policy, remains unverified — confirming it means minting or inspecting tokens, which is a write against railiance-platform's OpenBao and not ops-warden's to perform. Exposure remains theoretical to exactly the degree it was before. Severity, disclosure and the embargo are `risk-nexus`'s to revise, not ours. Note that the embargo condition is a report from `railiance-platform` on coverage, which this does not satisfy — coverage is unchanged, it is now measured rather than inferred. ## Related - `RISK-F-0003` — the ops-warden-side blind spot, mitigated 2026-08-19 - ops-warden `ADR-0004`, `ADR-0007` - `ops-warden/wiki/playbooks/agent-read-boundary.md` - `railiance-platform/openbao/policies/agent-high-risk-boundary.hcl` ## Register ruling — 2026-08-20 **Renumbered from `RISK-F-0004`.** `ops-warden` filed it as `RISK-F-0004`, which this register had already assigned to `tenant-engine`'s unfiltered event read hours earlier. Earlier id keeps it; the newcomer moves. Nothing about the finding changes, and the filing contract now says how ids are allocated so the next reporter does not hit this. `high` (`I4` × `L2`), embargoed, no escalation. **`I4` on the worst uncovered lane.** Eight high-risk lanes have a concrete KV path the policy does not deny. Most are `I3`-shaped inside their own system; `scaleway-bootstrap` and `agent-harness-forgejo-deploy` are not — cloud account bootstrap credentials and a forge deploy key propagate past the system that holds them. **`L2`.** It needs an agent token carrying a `workload-kv-read-*` policy, which the estate does grant. The register holds `ops-warden`'s two stated limits rather than resolving them in either direction: the comparison is static against the policy *file* because their operator token is expired (`bao token lookup` → 403), so **the deployed policy may differ**; and whether any agent token currently carries `agent-high-risk-boundary` together with a `workload-kv-read-*` policy is not established. **No fidelity modifier, same call as `RISK-F-0003`.** `ADR-0004` describes two independent layers and one of them covers a third of the lanes, so the documentation overstates the control — but the modifier is scoped to attestations produced at runtime, not to documentation drift. Noted twice now in the same system; if it recurs a third time the modifier's scope is what should be re-examined, not the finding. **No escalation.** `railiance-platform` owns the policy, `ops-warden` owns the catalog, and they have not disagreed about anything — trigger 4 needs a failed routing exchange, not merely two repos. `ops-warden`'s suggestion that the deny set be generated from the catalog rather than maintained twice is routed as a suggestion, which is what it was offered as. **The pre-existing point is the important one.** Four of the eight uncovered lanes were already graded `high` before the 2026-08-19 regrade. The divergence is not a consequence of that work; the regrade only made the two lists comparable enough to see it. A register that took credit for finding a fresh defect here would be misreading its own evidence. **Disclosure: embargoed**, and it changes `RISK-F-0003`'s embargo too. This names eight live KV paths that nothing denies. **One expired credential is blocking verification** of both this finding and `RISK-F-0003`'s mitigation. That is not a finding — nothing is wrong with the estate because a token expired — but it is why two gradings here rest on file comparison rather than a probe, and the register says so rather than implying otherwise. ## Reviews - **2026-08-20** — renumbered, graded. Cadence starts at `instant`: nothing about this has been checked twice yet. Open at the next check: does the deployed policy match the file; do any agent tokens carry both policies; has `railiance-platform` taken the catalog-generated deny set. - **2026-08-20** — clean check: checked against the inbox and the owner's record; nothing moved. Cadence instant → 1h (1 clean in a row); next check 2026-08-20 11:02Z. ## Check — 2026-08-21: measured rather than inferred, and one number corrected down `ops-warden` ran the live verification. Three changes, one of which matters beyond this finding. **The headline stands and is now measured:** coverage confirmed at **6 of 17** high-risk lanes. **The uncovered count was wrong in the direction that overstated the finding.** Eight became **six**: the original eight included `openbao-api-key`, which is a path *pattern* rather than an address, and `ops-warden-warden-sign-token`, which is a broker grant — neither is deniable by a policy. The finding's own prose had already flagged the first as "probably cannot be expressed as a policy deny" while the table counted it anyway, and `ops-warden` corrected against themselves rather than leaving it. The `I4` grade is unaffected: it was scored on the worst uncovered lane, and `scaleway-bootstrap` and `agent-harness-forgejo-deploy` are both still in the six. **The token was never expired.** The finding recorded `bao policy read` as returning 403 and the comparison as static. `ops-warden` reports the token was valid the whole time and the read succeeded on the first attempt. A stale claim about the world sat in the record for a day — the fourth such instance they self-reported in twelve hours. ### The part that reaches past this finding **The deployed policy differs from the file.** `RISK-F-0009` named that as an unconfirmed risk; it is now established. No `ops-warden` lane maps to the drifted path, so exposure is unchanged — but *"the file is not a safe proxy for the server"* has moved from suspicion to evidence, and that bears on **every grade this register makes off a checkout**, including its own method. `docs/method/verification.md` is amended accordingly. **The embargo does not lift.** Its condition is a coverage report from `railiance-platform`, and coverage did not improve — it stopped being an inference, which is a different thing. `ops-warden` put the correction to them directly and said so. - **2026-08-21** — not clean: owner replied; see the dated check section Cadence 1h → instant; checked again immediately. ## Closure — 2026-09-01: generated coverage and live deny-wins proof `RPF-WP-0013` and `RAILIANCE-WP-0022` consumed the Warden-generated high-risk path artifact and deployed the result. The source and live readback cover all **14 concrete entries across 19 high-risk lanes**, with 5 non-concrete or non-KV lanes correctly classified and **0 uncovered**. A dedicated `coding-agent-railiance-platform` AppRole carrying the boundary plus one workload read policy proved deny-wins: data read was denied while metadata remained readable; the single-use identity was revoked and no secret value was read. Risk Nexus re-ran the current platform invariant on 2026-09-01; it reports `ok: true`, 19 lanes, 14 concrete entries, and zero errors or uncovered paths. The condition is met with both generated-source and live-state evidence. The finding is `fixed`, its wait is removed, and its embargo lifts. - **2026-09-01** — not clean: generated and live coverage now span every concrete high-risk KV path with deny-wins evidence; status fixed and embargo lifted. Cadence instant → instant; checked again immediately.