feat(warden): implement WARDEN-WP-0002 correctness and operational completeness

T1 — TTL max enforcement:
  - models.py: MAX_TTL_HOURS policy constant
  - ca.py: _enforce_ttl() raises CAError when spec.ttl_hours > type max
  - Called at top of LocalCA.sign() and VaultCA.sign()
  - scorecard.py: check_ttl_policy() — flags certs with issued TTL > type max
  - run_scorecard() now returns 5 checks

T2 — Stale cert cleanup:
  - ca.py: _evict_cert() removes existing cert before writing new one (no accumulation)
  - cli.py: warden cleanup [actor] [--dry-run] command
  - check_no_stale_certs detail suggests 'warden cleanup' when stale certs found

T3 — Outgoing signatures log:
  - ca.py: _append_signature_log() writes JSONL to state_dir/signatures.log
  - Called after every successful sign() in LocalCA and VaultCA
  - cli.py: warden log [actor] [--last N] [--json] command
  - parse_cert_metadata now also returns valid_from (needed for TTL policy check)

61 tests passing, ruff clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-15 15:53:10 +02:00
parent 66e93e5e5c
commit 9857ed1424
9 changed files with 494 additions and 37 deletions

View File

@@ -8,7 +8,7 @@ from pathlib import Path
import httpx
from warden.ca import CABackend, CAError, parse_cert_metadata
from warden.ca import CABackend, CAError, _append_signature_log, _enforce_ttl, _evict_cert, parse_cert_metadata
from warden.config import VaultConfig
from warden.models import CertRecord, CertSpec
@@ -31,6 +31,7 @@ class VaultCA(CABackend):
def sign(self, spec: CertSpec) -> CertRecord:
"""Sign the public key via Vault SSH engine. Returns a CertRecord."""
_enforce_ttl(spec)
pubkey_path = Path(os.path.expanduser(str(spec.pubkey_path)))
if not pubkey_path.exists():
raise CAError(f"Public key not found: {pubkey_path}")
@@ -72,6 +73,7 @@ class VaultCA(CABackend):
cert_text = response.json()["data"]["signed_key"].strip()
self._state_dir.mkdir(parents=True, exist_ok=True)
_evict_cert(spec.actor_name, self._state_dir)
dest = self._state_dir / f"{spec.actor_name}-cert.pub"
dest.write_text(cert_text + "\n")
@@ -87,7 +89,7 @@ class VaultCA(CABackend):
finally:
tmp_cert.unlink(missing_ok=True)
return CertRecord(
record = CertRecord(
identity=meta["identity"] or spec.identity,
valid_before=meta["valid_before"],
cert_path=dest,
@@ -95,3 +97,5 @@ class VaultCA(CABackend):
principals=meta["principals"],
actor_name=spec.actor_name,
)
_append_signature_log(record, spec, self._state_dir, "vault")
return record