WARDEN-WP-0026: credential disclosure hygiene + rotation guidance (Strand A)
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 3s

Follow-up to the 2026-07-16 CCR-2026-0004 verify disclosure incident. Strand A:
capabilities-based verification, safe access transport, masking (defense-in-depth),
agent read-boundary, EXPOSED taint convention, and a structured-but-advisory
rotation/re-establishment guidance registry surfaced via warden. Strand B deferred.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
tegwick 2026-07-16 01:41:39 +02:00
parent a8adb9c2c5
commit 7e0789ab0d

View file

@ -0,0 +1,188 @@
---
id: WARDEN-WP-0026
type: workplan
title: "Credential disclosure hygiene + rotation guidance (Strand A)"
domain: infotech
repo: ops-warden
status: ready
owner: codex
topic_slug: custodian
planning_priority: high
planning_order: 26
created: "2026-07-16"
updated: "2026-07-16"
---
# Credential disclosure hygiene + rotation guidance (Strand A)
## Origin
Follow-up to a credential-disclosure incident on 2026-07-16 while verifying
`CCR-2026-0004` (railiance offsite backup lane). A negative policy test was run
as `BAO_TOKEN=$(bao token create -policy=default -field=token) bao kv get <path>`.
The `token create` was denied (workload role lacks it), `BAO_TOKEN` fell back to
the caller's privileged login token, and `bao kv get` printed all three field
values (`NC_WEBDAV_TOKEN`, `NC_WEBDAV_URL`, `AGE_PRIVATE_KEY`) into an agent
session transcript. Buildup mode — exposure accepted, learnings captured.
**Root causes:** (1) a deny-test that read the secret *data* path at all;
(2) a silent privileged-token fallback; (3) the read landed in a logged context.
This is **Strand A** (disclosure hygiene). Tamper-resistant policy governance and
one-command mass rotation/lockdown (Strand B) are deliberately **deferred** — see
"Out of scope" below.
## Goal
Make accidental secret disclosure structurally hard, and make ops-warden the
authoritative source for **how each secret is rotated or re-established**
advisory knowledge held next to the routing catalog, not in OpenBao.
## Design guardrails (binding on acceptance)
- **Verification never reads secret data.** Use `bao token capabilities` (allow/deny)
instead of `kv get` for positive/negative lane tests.
- **Masking is defense-in-depth, not a boundary.** Any display filter is a wrapper
convenience; raw `bao kv get <path>` remains the documented anti-pattern.
- **Rotation guidance covers both `rotate` (provider re-mint) and `re-establish`
(regenerate from source, e.g. a new age keypair).**
- **Coverage gate:** every `active` (and newly promoted) catalog lane must carry
rotation guidance, enforced by a scorecard check.
## Out of scope (Strand B — deferred, not built here)
- Executable one-command rotation of all tainted secrets.
- Graded lockdown / break-glass seal + re-key with a designed trust-root.
- Policy time-travel / policy-as-code reconcile-to-past-commit. (Drift *detection*
may be revisited separately; the reconcile machinery is out.)
## Task: Capabilities-based lane verification
```task
id: WARDEN-WP-0026-T01
status: todo
priority: high
```
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
self endpoint) — **never** `bao kv get`. Update
`wiki/playbooks/*lane*.md` verify sections and any lane-verify helper.
**Done when:** the documented and tooled verify path for any lane proves
allow/deny without reading a secret value, and the CCR-2026-0004 re-verify uses
it (see T07).
## Task: Safe access transport (no stdout values)
```task
id: WARDEN-WP-0026-T02
status: todo
priority: high
```
`warden access` / fetch paths must emit values only into an env var, a file, or
a **response-wrapping token** (`bao … -wrap-ttl`), never a stdout table. Add a
wrapping-token transport for values that must move between processes. Record in
canon (`credential-routing` rules) that raw `bao kv get <path>` (full table) is
the anti-pattern; the sanctioned path is `warden access … --field <F>` into env.
**Done when:** the sanctioned fetch path cannot print a value to a terminal, and
the anti-pattern is documented fleet-wide.
## Task: Masking display filter (defense-in-depth)
```task
id: WARDEN-WP-0026-T03
status: todo
priority: medium
```
In the warden wrapper, mask KV data values by default when any listing/status is
shown — display presence, length, and a short non-reversible hash instead of the
value. Explicitly labelled as defense-in-depth (raw bao bypasses it).
**Done when:** wrapper-mediated output never shows a raw KV value, with the
limitation documented.
## Task: Agent read-boundary on high-risk lanes
```task
id: WARDEN-WP-0026-T04
status: todo
priority: high
```
**Repo: railiance-platform (OpenBao policy/roles).** Agent identities receive
`capabilities`/metadata and wrapping tokens on high-risk lanes, not raw data
reads. Align with the existing credential-routing rule ("ops-warden proxies reads
as the caller and must not retain values"). Classify which lanes are high-risk
(recovery escrow like `AGE_PRIVATE_KEY`, upload tokens).
**Done when:** at least the high-risk lanes deny raw data reads to agent roles
while still allowing wrapped/proxied access, verified via capabilities checks.
## Task: EXPOSED taint convention
```task
id: WARDEN-WP-0026-T05
status: todo
priority: medium
```
**Repo: railiance-platform (OpenBao) + ops-warden surface.** Establish a KV v2
`custom_metadata` convention to mark a tainted secret: `exposed_at=<datetime>`
and the affected `version`. Identify semi-automatic candidates from the OpenBao
audit log (reads from agent/shared contexts). `warden` surfaces taint status for
a lane (advisory; no auto-rotation here).
**Done when:** a secret can be marked EXPOSED via a documented convention and
`warden` reports whether a lane is currently tainted.
## Task: Rotation / re-establishment guidance registry
```task
id: WARDEN-WP-0026-T06
status: todo
priority: high
```
Give every catalog lane **structured-but-advisory** renewal guidance, held in the
ops-warden registry (not in OpenBao). Add a `rotation:` block per catalog entry
capturing: `method` (rotate | re-establish), ordered `steps` (provider re-mint /
keygen / OpenBao write / re-encrypt-and-reupload where relevant), `owner`, and
`automatable` (bool, for future Strand-B). Surface via `warden rotate-guide <id>`
(and/or `warden route … --rotate`). Add a **scorecard coverage check**: every
`active` lane must have a `rotation:` block; flag any that don't.
**Done when:** `warden rotate-guide <id>` returns actionable renewal steps for
every active lane, and the scorecard fails if any active lane lacks guidance.
## Task: Incident lessons + first worked lane (CCR-2026-0004)
```task
id: WARDEN-WP-0026-T07
status: todo
priority: medium
```
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
`resolvable: true` (unblocking `RAILIANCE-WP-0015`), and ensure its `rotation:`
block (rotate Nextcloud token; re-establish age keypair + re-encrypt artifacts)
is present. **Rotation of the exposed values is the operator's optional call, not
a blocker** (buildup).
**Done when:** the lessons note exists, CCR-2026-0004 has capabilities-based
verify + rotation guidance, and its promotion path is unblocked.
## References
- `CCR-2026-0004-railiance-backup-offsite-lane.yaml` (railiance-platform)
- `wiki/playbooks/railiance-backup-offsite-lane.md`
- `.claude/rules/credential-routing.md` (the-custodian, fleet-inlined)
- `RAILIANCE-WP-0015` (railiance-apps) — cnpg backup coverage, gated on CCR-2026-0004
- Strand B (deferred): tamper-resistant governance, one-command rotation/lockdown,
policy time-travel — capture separately if/when justified.