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
|
|
@ -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(),
|
||||
]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue