diff --git a/history/2026-07-16-credential-disclosure-lessons.md b/history/2026-07-16-credential-disclosure-lessons.md new file mode 100644 index 0000000..19e2e35 --- /dev/null +++ b/history/2026-07-16-credential-disclosure-lessons.md @@ -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 +``` + +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` diff --git a/wiki/playbooks/catalog-lane-promotion.md b/wiki/playbooks/catalog-lane-promotion.md index 7efd811..36ebcad 100644 --- a/wiki/playbooks/catalog-lane-promotion.md +++ b/wiki/playbooks/catalog-lane-promotion.md @@ -22,12 +22,50 @@ Before changing `status: draft` → `status: active`: | 5 | **Resolvable** | `warden route show --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 ` 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** +(`/data/`), 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= # caller identity +bao token capabilities "$(bao print token)" platform/data/ +# → 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/ +# → 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: diff --git a/wiki/playbooks/forgejo-admin-api-token.md b/wiki/playbooks/forgejo-admin-api-token.md index 01d4f9c..18d1151 100644 --- a/wiki/playbooks/forgejo-admin-api-token.md +++ b/wiki/playbooks/forgejo-admin-api-token.md @@ -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 | diff --git a/wiki/playbooks/railiance-backup-offsite-lane.md b/wiki/playbooks/railiance-backup-offsite-lane.md index a352089..d000b82 100644 --- a/wiki/playbooks/railiance-backup-offsite-lane.md +++ b/wiki/playbooks/railiance-backup-offsite-lane.md @@ -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. \ No newline at end of file +`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`. \ No newline at end of file diff --git a/workplans/WARDEN-WP-0026-credential-disclosure-hygiene.md b/workplans/WARDEN-WP-0026-credential-disclosure-hygiene.md index 7725d58..ac716d4 100644 --- a/workplans/WARDEN-WP-0026-credential-disclosure-hygiene.md +++ b/workplans/WARDEN-WP-0026-credential-disclosure-hygiene.md @@ -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 ` (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