WARDEN-WP-0026 T06: rotation guidance registry + warden rotate-guide
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

- 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:
tegwick 2026-07-16 14:40:30 +02:00
parent ac09f21ad3
commit c3eb59ea04
9 changed files with 385 additions and 12 deletions

View file

@ -152,6 +152,43 @@ def check_file_permissions(state_dir: Path) -> CheckResult:
)
def check_catalog_rotation_coverage() -> CheckResult:
"""Every active secret-vending catalog lane must carry rotation guidance (T06).
A lane an operator can obtain a *secret value* through must also tell them how
to renew or re-establish it. Scoped to ``vends_secret`` lanes: this exempts the
SSH lane (short-lived certs renewal is re-issuance), ``login`` lanes (re-auth,
no stored value), and pure routing pointers (tunnel, principals, emission
sinks, policy checks) with no secret to rotate. Draft lanes are exempt until
promoted.
"""
try:
from warden.routing import load_catalog
catalog = load_catalog()
except Exception as e: # noqa: BLE001 — catalog missing/invalid is its own signal
return CheckResult(
name="catalog_rotation_coverage",
passed=False,
detail=f"could not load routing catalog: {e}",
)
missing = [
e.id
for e in catalog.entries
if e.is_active and e.vends_secret and not e.has_rotation
]
return CheckResult(
name="catalog_rotation_coverage",
passed=len(missing) == 0,
detail=(
f"active lanes lacking rotation guidance: {missing} — add a `rotation:` "
"block (see WARDEN-WP-0026 T06)"
if missing
else "all active lanes carry rotation guidance"
),
)
def run_scorecard(state_dir: Path, inventory: PrincipalsInventory) -> List[CheckResult]:
"""Run all cert-side scorecard checks. Returns list of CheckResult."""
return [
@ -161,4 +198,5 @@ def run_scorecard(state_dir: Path, inventory: PrincipalsInventory) -> List[Check
check_no_stale_certs(state_dir),
check_ttl_policy(state_dir, inventory),
check_file_permissions(state_dir),
check_catalog_rotation_coverage(),
]