Closes 36 findings from the project audit. The headline win is correctness:
the daemon now actually does what the README claims, all three audit-log
integrity layers verify, and the rotation control loop terminates.
Critical fixes:
- Orchestrator bumps Credential.last_rotated_at on success and writes a
sealed credential_versions row, so the policy evaluator stops re-firing
the same rotation every tick
- Envelope encryption (AES-256-GCM, KEK-wrapped DEK, AAD-bound) is now
wired into the rotation success path; the credential_versions table is
no longer dead schema
- cre audit verify checks all three layers (hash chain + HMAC ratchet +
Merkle batches) instead of only the chain
- BatchSealerScheduler runs every 300s and on shutdown so audit_batches
actually fills with signed Merkle roots
- Compliance bundle exports the real audit_batches list and covers
public_key.pem under the manifest's checksum
- cre run/watch hard-fail without CRE_HMAC_KEY_HEX + CRE_KEK_HEX rather
than silently using zero defaults
Quality fixes:
- HTTP::Client wrapper with connect/read timeouts + bounded retry-with-
jitter on 408/429/5xx; Telegram error logs redact the bot token
- EventBus dispatch isolates Block subscribers behind select+timeout so
one stuck subscriber can't pin the bus
- RotationWorker checks rotations.in_flight before dispatching to dedupe
duplicate schedules; PG enforces it at the DB via partial unique index
- Commit-step failures transition rotations to Inconsistent (a terminal
state) and raise a critical alert instead of pretending success
- env_file rotator uses per-PID pending paths plus an exclusive flock on
a sibling .lock file so two daemons can't race on the rename
- Versioned migration runner replaces the soup of CREATE IF NOT EXISTS;
SQLite gains BEFORE UPDATE / BEFORE DELETE triggers on audit_events
- AuditSubscriber publishes a critical alert + panics by default when
log writes fail, instead of silently dropping
- Engine.stop drains via an ack channel with a 2s deadline rather than
the previous magic 50ms sleep
- Policy DSL moved into CRE::Policy::Dsl module (consumers `include` it);
multiple matching policies raise PolicyConflictError
- Evaluator uses credentials.overdue() per-max_age group instead of
loading every credential each tick
- AuditRepo gains each_in_range streaming and all_batches enumeration;
bundle export streams instead of buffering
- New cre verify-bundle <zip> CLI re-runs every check the bundle README
documents; new cre tui-demo for synthetic event preview
Tests: 179 unit (up from 159), all passing. New rotation_worker_spec,
new SigV4 regression vector, new orchestrator success-path coverage
(envelope write + last_rotated_at bump + Inconsistent state).
Docs: README + learn/00..04 aligned with the post-audit behavior;
removed /snooze (was a stub) and the AwsIamKey/Database CredentialKind
variants that had no rotators. Added required-env-var documentation.
Justfile mirrors the haskell-reverse-proxy pattern (grouped recipes:
build, run, demo, test, quality, db, util). 33 recipes covering full
lifecycle - including parameterized recipes (cre rotate ID, cre policy
show NAME) that read more naturally as 'just rotate ...' than as
make targets.
User-edited docker-compose.yml port mappings preserved
(postgres:18 on 6022, localstack on 45666, vault on 18201,
fake-github on 7115).