ops-warden filed this static, saying its OpenBao token was expired. It was not -- bao policy read succeeded, so the deployed policy has now been compared directly. Coverage confirmed at 6 of 17. But the uncovered count was wrong: eight included a path pattern and a broker grant, neither of which a policy can deny, and the finding's own prose already said so about the first. Six stand. New: the deployed policy differs from the file in railiance-platform -- the file denies core-hub/runtime, the server does not. No ops-warden lane maps there, so the numbers are unchanged. It matters because this finding named "the deployed policy may differ from the file" as unconfirmed, and it does. Severity, disclosure and embargo left untouched -- risk-nexus's to set. The embargo condition is a coverage report from railiance-platform, which this does not satisfy. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
241 lines
12 KiB
Markdown
241 lines
12 KiB
Markdown
---
|
||
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: open
|
||
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: unset
|
||
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: embargoed
|
||
embargo_condition: "railiance-platform reports the deny set covers every high-risk lane with a KV path (live verification refines the grade, it is not the condition)"
|
||
embargo_since: "2026-08-20"
|
||
escalation: none
|
||
last_checked: "2026-08-20T10:02:42Z"
|
||
next_check: "2026-08-20T11:02:42Z"
|
||
cadence: 1h
|
||
clean_streak: 1
|
||
waiting_on:
|
||
- who: railiance-platform
|
||
what: "report whether the deny set covers every high-risk lane with a KV path"
|
||
since: "2026-08-20"
|
||
would_change: "embargo lifts on coverage; live verification would refine the grade but is not required for it"
|
||
default: "the eight uncovered paths stand as recorded and the finding is re-raised"
|
||
default_at: "2026-09-03"
|
||
graded_by: risk-nexus
|
||
ruling: RISK-RULING-2026-08-20
|
||
---
|
||
|
||
# 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 <lane> --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/<domain>/<workload>/<bundle>` — 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 `<domain>/<workload>/<bundle>` 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.
|