WARDEN-WP-0026 T06: rotation guidance registry + warden rotate-guide
- routing model: RotationGuide (method rotate|re-establish, steps, owner, automatable), RouteEntry.rotation + has_rotation + vends_secret. - catalog parser: validate rotation block; secret-material screen gains a prose-safe mode (high-entropy detector only) so authored steps aren't tripped by substrings like "s."/"exists.". - CLI: `warden rotate-guide <id>` (human + --json); route show --json now carries has_rotation + rotation. - scorecard: catalog_rotation_coverage — every active secret-vending lane must carry a rotation block (SSH/login/pointer lanes exempt). Promotion checklist criterion 9. - data: rotation blocks for all 7 active vending lanes + the draft railiance-backup lane (re-establish: age keypair regen + re-encrypt). - fix pre-existing collision: bare `npm` keyword on forgejo-admin -> forgejo-npm so "npm token" routes to the generic lane (restores test_access expectations). - tests: rotation parse/coverage/prose-screen/CLI in tests/test_routing.py; scorecard count 6 -> 7. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
ac09f21ad3
commit
c3eb59ea04
9 changed files with 385 additions and 12 deletions
|
|
@ -68,6 +68,14 @@ entries:
|
|||
exec_owner: railiance-platform
|
||||
exec_command: "scripts/credential.py exec --grant ops-warden/warden-sign --ttl 15m -- <cmd>"
|
||||
pointer_command: "make credential-exec-ops-warden-smoke"
|
||||
rotation:
|
||||
method: rotate
|
||||
owner: railiance-platform
|
||||
automatable: true
|
||||
steps:
|
||||
- "This lane vends a short-lived (15m) child VAULT_TOKEN — routine renewal is just re-running `credential.py exec`; the token auto-expires, nothing to revoke."
|
||||
- "To rotate the underlying grant/issuer: railiance-platform edits `credential-grants/catalog.yaml` for `ops-warden/warden-sign` (scope/policy) and re-mints the issuer token behind OPENBAO_TOKEN_FILE."
|
||||
- "Verify capabilities-safe: `make credential-exec-ops-warden-smoke` and a `bao token capabilities` check on ssh/sign/{adm,agt,atm}-role (never read a value)."
|
||||
|
||||
- id: openbao-api-key
|
||||
title: API key, DB credential, or dynamic lease
|
||||
|
|
@ -87,6 +95,15 @@ entries:
|
|||
fetch_command: "bao kv get -field=<FIELD> <path_template>"
|
||||
policy_ref: "flex-auth check secret.read:<domain>"
|
||||
exec_capable: true
|
||||
rotation:
|
||||
method: rotate
|
||||
owner: railiance-platform
|
||||
automatable: false
|
||||
steps:
|
||||
- "Generic template lane — rotate per the concrete workload's own catalog entry when one exists."
|
||||
- "Provider re-mint (or OpenBao dynamic-secret rotation): mint a fresh value at the source, then `bao kv put <path> <FIELD>=@file` (value from a mode-0600 file, never on argv)."
|
||||
- "For dynamic-lease secrets, revoke the old lease (`bao lease revoke`) instead of a KV put."
|
||||
- "Verify capabilities-safe (`bao token capabilities` on the data path); notify consumers to re-fetch."
|
||||
|
||||
- id: whynot-design-npm-publish
|
||||
title: whynot-design npm publish token (@whynot/design → coulomb Gitea registry)
|
||||
|
|
@ -116,6 +133,14 @@ entries:
|
|||
exec_owner: secrets-engine
|
||||
exec_command: "secrets-engine exec --catalog whynot-design-npm-publish -- <cmd>"
|
||||
pointer_command: "secrets-engine route whynot-design-npm-publish --json"
|
||||
rotation:
|
||||
method: rotate
|
||||
owner: railiance-platform
|
||||
automatable: false
|
||||
steps:
|
||||
- "In the coulomb Gitea/Forgejo registry, revoke the current @whynot/design publish token and generate a new one (scope: package read/write) for the whynot-design publish identity."
|
||||
- "Write it back: `bao kv put platform/workloads/coulomb/whynot-design/npm-publish NPM_AUTH_TOKEN=@file` (value from a mode-0600 file)."
|
||||
- "Verify capabilities-safe, then confirm publish works via `secrets-engine exec --catalog whynot-design-npm-publish -- npm whoami` (value used, not printed)."
|
||||
|
||||
- id: flex-auth-policy-check
|
||||
title: Authorization decision — may this actor perform this action
|
||||
|
|
@ -213,6 +238,15 @@ entries:
|
|||
policy_ref: "flex-auth check secret.read:issue-core"
|
||||
exec_capable: true
|
||||
lane: secret
|
||||
rotation:
|
||||
method: rotate
|
||||
owner: railiance-platform
|
||||
automatable: false
|
||||
steps:
|
||||
- "Mint a new issue-core ingestion API key at the issue-core admin surface; keep the old one until consumers cut over."
|
||||
- "`bao kv put platform/workloads/issue-core/issue-core/issue-core-runtime ISSUE_CORE_API_KEY=@file` (value from a mode-0600 file)."
|
||||
- "ESO re-syncs ExternalSecret issue-core/issue-core-runtime; roll consumers, then revoke the old key at the source."
|
||||
- "Verify capabilities-safe on the data path (`bao token capabilities`); never read the value to confirm."
|
||||
|
||||
- id: reuse-surface-hub-write-token
|
||||
title: reuse-surface federation hub write bearer token
|
||||
|
|
@ -236,6 +270,15 @@ entries:
|
|||
exec_capable: true
|
||||
resolvable: true
|
||||
lane: secret
|
||||
rotation:
|
||||
method: rotate
|
||||
owner: railiance-platform
|
||||
automatable: false
|
||||
steps:
|
||||
- "Generate a new reuse-surface federation hub write bearer token at reuse.coulomb.social; if the Forgejo webhook secret rotates too, regenerate REUSE_SURFACE_FORGEJO_WEBHOOK_SECRET alongside it."
|
||||
- "`bao kv put platform/workloads/reuse/reuse-surface/runtime-secrets REUSE_SURFACE_TOKEN=@file` (and the webhook field if changed)."
|
||||
- "ESO re-syncs reuse/reuse-surface-runtime → reuse-surface-env on Railiance01; roll the consumer; update the Forgejo webhook config if the secret changed; revoke the old token."
|
||||
- "Verify capabilities-safe on the data path (`bao token capabilities`)."
|
||||
|
||||
- id: openrouter-llm-connect
|
||||
title: OpenRouter API key for llm-connect in activity-core
|
||||
|
|
@ -260,6 +303,15 @@ entries:
|
|||
policy_ref: "flex-auth check secret.read:llm-connect"
|
||||
exec_capable: true
|
||||
lane: secret
|
||||
rotation:
|
||||
method: rotate
|
||||
owner: railiance-platform
|
||||
automatable: false
|
||||
steps:
|
||||
- "Create a new key in the OpenRouter dashboard for the llm-connect identity; keep the old key active until rollout completes."
|
||||
- "`bao kv put platform/workloads/activity-core/llm-connect/llm-connect-provider-secrets OPENROUTER_API_KEY=@file` (value from a mode-0600 file)."
|
||||
- "ESO re-syncs activity-core/llm-connect-provider-secrets; roll out llm-connect on the new value; then delete the old OpenRouter key."
|
||||
- "Verify capabilities-safe on the data path (`bao token capabilities`)."
|
||||
|
||||
- id: railiance-backup-offsite-lane
|
||||
title: Railiance offsite backup Nextcloud WebDAV credentials
|
||||
|
|
@ -278,10 +330,24 @@ entries:
|
|||
fetch_command: "bao kv get -field=<FIELD> platform/workloads/railiance/backup/offsite-lane"
|
||||
exec_capable: true
|
||||
lane: secret
|
||||
# Mixed lane: NC_WEBDAV_TOKEN rotates (provider re-mint); AGE_PRIVATE_KEY
|
||||
# re-establishes (new keypair + re-encrypt existing artifacts). Method reflects
|
||||
# the more involved re-establish path. Marked exposed 2026-07-16 (see
|
||||
# history/2026-07-16-credential-disclosure-lessons.md); rotation is the operator's
|
||||
# optional call (buildup), not a blocker for promotion.
|
||||
rotation:
|
||||
method: re-establish
|
||||
owner: railiance-platform
|
||||
automatable: false
|
||||
steps:
|
||||
- "Rotate NC_WEBDAV_TOKEN: regenerate the Nextcloud WebDAV app password/token; `bao kv put platform/workloads/railiance/backup/offsite-lane NC_WEBDAV_TOKEN=@file`. NC_WEBDAV_URL changes only if the host/share moves."
|
||||
- "Re-establish AGE_PRIVATE_KEY: generate a new keypair (`age-keygen`), decrypt existing offsite artifacts with the old key and re-encrypt to the new recipient, then `bao kv put ... AGE_PRIVATE_KEY=@file` (mode-0600 file, shred after)."
|
||||
- "Because AGE_PRIVATE_KEY is recovery escrow, retain the old key offline until re-encryption of all retained backups is confirmed."
|
||||
- "Verify capabilities-safe on the data path (`bao token capabilities`); run a restore drill against a re-encrypted artifact."
|
||||
|
||||
- id: forgejo-admin-api-token
|
||||
title: Forgejo operator/admin API token (PAT)
|
||||
need_keywords: [forgejo, admin, pat, package, prune, FORGEJO_ADMIN_TOKEN, forgejo-package-prune, forgejo-tegwick, webhook, npm]
|
||||
need_keywords: [forgejo, admin, pat, package, prune, FORGEJO_ADMIN_TOKEN, forgejo-package-prune, forgejo-tegwick, webhook, forgejo-npm]
|
||||
owner_repo: railiance-platform
|
||||
subsystem: OpenBao + Forgejo
|
||||
warden_executes: false
|
||||
|
|
@ -303,6 +369,15 @@ entries:
|
|||
fetch_command: "bao kv get -field=API_TOKEN platform/workloads/forgejo/forgejo-admin"
|
||||
exec_capable: true
|
||||
lane: secret
|
||||
rotation:
|
||||
method: rotate
|
||||
owner: railiance-platform
|
||||
automatable: false
|
||||
steps:
|
||||
- "As Forgejo user tegwick (site admin): Settings → Applications → generate a new token with the current scopes (read/write:package, read/write:repository, plus admin scopes for operator-bootstrap); keep the old token until cutover."
|
||||
- "Store via the provisioning helper: `~/railiance-platform/scripts/forgejo-admin-pat-provision.sh <mode-0600-file>` writes field API_TOKEN to platform/workloads/forgejo/forgejo-admin; shred the input file."
|
||||
- "Confirm the new PAT works (fetch --field API_TOKEN into env, call /api/v1/user — never print it), then delete the old token in Forgejo."
|
||||
- "Verify capabilities-safe on the data path (`bao token capabilities`); notify consumers (package-prune, operator-bootstrap, npm-smoke, reuse-webhook) to re-fetch."
|
||||
|
||||
# --- draft: owner path not yet shipped; hidden from default lookup ---
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue