WARDEN-WP-0026 T01: capabilities-safe lane verification + incident note
T01 (done): canonical capabilities-based verify pattern in the fleet promotion checklist (catalog-lane-promotion.md) and applied to the railiance-backup and forgejo-admin lane playbooks. Verification proves allow/deny via `bao token capabilities` against the KV v2 data path, never `bao kv get`; a denied default-policy token-create is a pass, not a privileged-fallback trigger. T07 (progress): lessons-learned note for the 2026-07-16 CCR-2026-0004 disclosure (three root causes). Live re-verify + rotation block remain (depend on T06). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
2ad8a53781
commit
ea98d6bf39
5 changed files with 161 additions and 3 deletions
57
history/2026-07-16-credential-disclosure-lessons.md
Normal file
57
history/2026-07-16-credential-disclosure-lessons.md
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
# Credential disclosure lessons — 2026-07-16
|
||||
|
||||
**Context:** buildup mode. Exposure was accepted; the value here is the learnings,
|
||||
not blame. Rotation of the exposed values is the operator's optional call, not a
|
||||
blocker (see WP-0026 T07).
|
||||
|
||||
## What happened
|
||||
|
||||
While verifying `CCR-2026-0004` (railiance offsite backup lane), a negative policy
|
||||
test was run as:
|
||||
|
||||
```bash
|
||||
BAO_TOKEN=$(bao token create -policy=default -field=token) bao kv get <path>
|
||||
```
|
||||
|
||||
The `bao token create` was **denied** (the workload role lacks it), so `BAO_TOKEN`
|
||||
was left unset and `bao kv get` fell back to the caller's **privileged login
|
||||
token**. The read succeeded and printed all three field values —
|
||||
`NC_WEBDAV_TOKEN`, `NC_WEBDAV_URL`, `AGE_PRIVATE_KEY` — into an agent session
|
||||
transcript (a logged context).
|
||||
|
||||
## Root causes
|
||||
|
||||
1. **The deny-test read the secret data path at all.** A negative test should
|
||||
prove *deny*, and proving deny never requires reading the value.
|
||||
2. **Silent privileged-token fallback.** When the scoped token creation failed,
|
||||
the command quietly used the caller's privileged token instead of failing.
|
||||
3. **The read landed in a logged context.** An agent session transcript is not a
|
||||
safe sink for secret material.
|
||||
|
||||
## Corrections (WARDEN-WP-0026, Strand A)
|
||||
|
||||
- **Verification never reads secret data.** Prove allow/deny with
|
||||
`bao token capabilities`, not `bao kv get`. If `bao token create -policy=default`
|
||||
is itself denied, that is a *pass* for the deny direction — never fall back to a
|
||||
privileged token. Canonical pattern:
|
||||
`wiki/playbooks/catalog-lane-promotion.md#capabilities-safe-lane-verification`
|
||||
(WP-0026 T01, applied to the forgejo and railiance-backup lane playbooks).
|
||||
- **Safe transport** for values that must move: env var, file, or response-wrapping
|
||||
token (`-wrap-ttl`) — never a stdout table (WP-0026 T02).
|
||||
- **Masking** as defense-in-depth in the warden wrapper (WP-0026 T03).
|
||||
- **Agent read-boundary + EXPOSED taint** on high-risk lanes, and per-lane
|
||||
**rotation guidance** (WP-0026 T04–T06).
|
||||
|
||||
## Deferred (Strand B — WARDEN-WP-0027)
|
||||
|
||||
Executable one-command mass rotation, graded lockdown / break-glass with a designed
|
||||
trust-root, and tamper-evident policy governance + reconcile are captured in
|
||||
`WARDEN-WP-0027` (backlog, gated on an activation trigger).
|
||||
|
||||
## References
|
||||
|
||||
- `WARDEN-WP-0026` — disclosure hygiene (Strand A)
|
||||
- `WARDEN-WP-0027` — governance/lockdown (Strand B, deferred)
|
||||
- `CCR-2026-0004-railiance-backup-offsite-lane.yaml` (railiance-platform)
|
||||
- `wiki/playbooks/railiance-backup-offsite-lane.md`
|
||||
- `.claude/rules/credential-routing.md`
|
||||
|
|
@ -22,12 +22,50 @@ Before changing `status: draft` → `status: active`:
|
|||
| 5 | **Resolvable** | `warden route show <id> --json` shows `resolvable: true` when placeholders are documented |
|
||||
| 6 | **Tests** | Routing test or smoke proving lookup + handoff shape (no secret values in fixtures) |
|
||||
| 7 | **Review date** | Update `reviewed:` in catalog entry |
|
||||
| 8 | **Verification** | Positive + negative proof via **`bao token capabilities`** — never `bao kv get` (see below) |
|
||||
|
||||
Promotion PR touches: `registry/routing/catalog.yaml`, playbook, optional
|
||||
`tests/test_routing.py`, and a one-line note in `wiki/CredentialRouting.md` draft table.
|
||||
|
||||
---
|
||||
|
||||
## Capabilities-safe lane verification (WARDEN-WP-0026 T01)
|
||||
|
||||
**Verifying a lane must never read the secret *data*.** A negative deny-test that
|
||||
runs `bao kv get <path>` will, if the deny fails (e.g. a privileged token
|
||||
fallback), print the secret value into a logged context — this is exactly the
|
||||
2026-07-16 CCR-2026-0004 disclosure. Prove *allow/deny* with
|
||||
`bao token capabilities`, which returns the capability list, not the value.
|
||||
|
||||
For KV v2, capabilities are checked against the **API data path**
|
||||
(`<mount>/data/<lane-path>`), not the `kv get` logical path.
|
||||
|
||||
```bash
|
||||
# Positive: the lane's own OIDC identity can read the data path.
|
||||
bao login -method=oidc -path=netkingdom role=<lane-role> # caller identity
|
||||
bao token capabilities "$(bao print token)" platform/data/<lane-path>
|
||||
# → expect the list to include: read
|
||||
|
||||
# Negative: a default-only identity is denied — no value is ever read.
|
||||
DEFAULT_TOKEN=$(bao token create -policy=default -field=token) # if denied, STOP — do not fall back
|
||||
bao token capabilities "$DEFAULT_TOKEN" platform/data/<lane-path>
|
||||
# → expect: deny
|
||||
```
|
||||
|
||||
- **Never** substitute `bao kv get` for the checks above. Reading a value to
|
||||
"confirm it's there" is the anti-pattern; presence is proven by `read` in the
|
||||
capability list.
|
||||
- If `bao token create -policy=default` is itself denied for your identity, that
|
||||
is a *pass for the deny direction* — **do not** fall back to your privileged
|
||||
login token to force the read.
|
||||
- Fetching a value **for use** (`--field` into an env var or file, or
|
||||
`warden access … --field`) is a separate, intended action — not verification.
|
||||
|
||||
Record the capability lists (allow/deny) as the promotion evidence; they contain
|
||||
no secret material and are safe for CCRs, State Hub, and Git.
|
||||
|
||||
---
|
||||
|
||||
## Worked examples (already active)
|
||||
|
||||
**`ops-warden-warden-sign-token`** — promoted 2026-07-01 after RAILIANCE-WP-0005:
|
||||
|
|
|
|||
|
|
@ -95,6 +95,30 @@ After CCR approval and policy apply:
|
|||
|
||||
---
|
||||
|
||||
## Verify the lane (capabilities-safe — never read the value)
|
||||
|
||||
Prove allow/deny with `bao token capabilities`, **not** `bao kv get -field=…`.
|
||||
`bao kv metadata get` (above) is fine — it shows versions, not values. Reading the
|
||||
data field to "confirm" it is the anti-pattern
|
||||
(`wiki/playbooks/catalog-lane-promotion.md#capabilities-safe-lane-verification`).
|
||||
|
||||
```bash
|
||||
# Positive: lane OIDC identity can read the data path
|
||||
bao login -method=oidc -path=netkingdom role=forgejo-admin-workload-kv-read
|
||||
bao token capabilities "$(bao print token)" platform/data/workloads/forgejo/forgejo-admin
|
||||
# → expect: read
|
||||
|
||||
# Negative: default-only identity is denied
|
||||
DEFAULT_TOKEN=$(bao token create -policy=default -field=token) # if denied, that IS the pass — do NOT fall back
|
||||
bao token capabilities "$DEFAULT_TOKEN" platform/data/workloads/forgejo/forgejo-admin
|
||||
# → expect: deny
|
||||
```
|
||||
|
||||
Confirming the PAT works against Forgejo is a separate, value-using action — fetch
|
||||
`--field API_TOKEN` into an env var and call `/api/v1/user`; never paste the token.
|
||||
|
||||
---
|
||||
|
||||
## Consumers (downstream wiring — after lane verified)
|
||||
|
||||
| Consumer | Repo |
|
||||
|
|
|
|||
|
|
@ -56,4 +56,28 @@ Used by `railiance-backup` (workstation) and `forgejo-backup` (platform).
|
|||
tools/cmd/forgejo-backup
|
||||
```
|
||||
|
||||
`AGE_PRIVATE_KEY` in the same path is recovery escrow — fetch only for restore drills.
|
||||
`AGE_PRIVATE_KEY` in the same path is recovery escrow — fetch only for restore drills.
|
||||
|
||||
---
|
||||
|
||||
## Verify the lane (capabilities-safe — never read the value)
|
||||
|
||||
Prove allow/deny with `bao token capabilities`, **not** `bao kv get`. Reading the
|
||||
value to "confirm" it triggered the 2026-07-16 disclosure of `NC_WEBDAV_TOKEN` /
|
||||
`NC_WEBDAV_URL` / `AGE_PRIVATE_KEY` (see `history/2026-07-16-credential-disclosure-lessons.md`).
|
||||
|
||||
```bash
|
||||
# Positive: lane OIDC identity can read the data path
|
||||
bao login -method=oidc -path=netkingdom role=railiance-backup-workload-kv-read
|
||||
bao token capabilities "$(bao print token)" platform/data/workloads/railiance/backup/offsite-lane
|
||||
# → expect: read
|
||||
|
||||
# Negative: default-only identity is denied (no value is read)
|
||||
DEFAULT_TOKEN=$(bao token create -policy=default -field=token) # if this is denied, that IS the pass — do NOT fall back
|
||||
bao token capabilities "$DEFAULT_TOKEN" platform/data/workloads/railiance/backup/offsite-lane
|
||||
# → expect: deny
|
||||
```
|
||||
|
||||
The capability lists contain no secret material — safe to record on
|
||||
`CCR-2026-0004` as promotion evidence. Full pattern:
|
||||
`wiki/playbooks/catalog-lane-promotion.md#capabilities-safe-lane-verification`.
|
||||
|
|
@ -61,11 +61,19 @@ advisory knowledge held next to the routing catalog, not in OpenBao.
|
|||
|
||||
```task
|
||||
id: WARDEN-WP-0026-T01
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "9329e72d-c07c-41ce-88ce-e8602eb72c43"
|
||||
```
|
||||
|
||||
Done 2026-07-16: canonical capabilities-safe verification pattern added to
|
||||
`wiki/playbooks/catalog-lane-promotion.md` (fleet promotion checklist criterion 8
|
||||
+ dedicated section), and applied to the `railiance-backup-offsite-lane` and
|
||||
`forgejo-admin-api-token` playbook verify sections. Positive/negative proven via
|
||||
`bao token capabilities` against the KV v2 data path — never `bao kv get`; the
|
||||
denied `default` token-create is documented as a pass, not a fallback trigger.
|
||||
Live CCR-2026-0004 re-verify carried under T07.
|
||||
|
||||
Replace secret-reading verify flows with capability checks. Positive test:
|
||||
approved identity has `read` on the KV data path. Negative test: a `default`-only
|
||||
identity is `deny`. Both via `bao token capabilities <token> <path>` (or the
|
||||
|
|
@ -170,11 +178,18 @@ every active lane, and the scorecard fails if any active lane lacks guidance.
|
|||
|
||||
```task
|
||||
id: WARDEN-WP-0026-T07
|
||||
status: todo
|
||||
status: progress
|
||||
priority: medium
|
||||
state_hub_task_id: "9944f46d-3706-43a4-9300-7f63bf87c9ff"
|
||||
```
|
||||
|
||||
Lessons-learned note written 2026-07-16:
|
||||
`history/2026-07-16-credential-disclosure-lessons.md` (buildup context, exposure
|
||||
accepted, three root causes). Remaining: capabilities-based live re-verify of
|
||||
CCR-2026-0004 on `bao.coulomb.social` (uses T01 pattern) and its `rotation:` block
|
||||
(depends on T06 registry) so the lane can promote to `resolvable: true` and unblock
|
||||
RAILIANCE-WP-0015.
|
||||
|
||||
Write a short lessons-learned note (buildup context; exposure accepted; the three
|
||||
root causes). Apply T01 + T06 to `CCR-2026-0004` as the first worked lane:
|
||||
re-verify it the capabilities-safe way so it can finally promote to
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue