FTI audit log hash-chain extension (Phase B)
Background
Phase A tracked under fti-audit-hash-chain-test.adoc adds chain-verification tests to audit_events in canopy-security — the table that already has hash-chain columns and an advisory-lock-serialised writer.
Phase A's errata calls out that the original version of that plan conflated two separate audit logs: the centralised audit_events (hash chain ✓) and the per-program fti_audit_log in canopy-tanf and canopy-medicaid (hash chain ✗). ADR-004 currently requires the latter to be separate, retained, access-controlled, and FTI-scrubbed at the event-bus boundary — but does not require hash-chain integrity. Adding it is this issue.
Starting-point sketch
Carried forward from the original plan body. Treat as inputs, not settled scope.
- Migrations. New migrations adding
previous_hash TEXTandevent_hash TEXTcolumns tofti_audit_loginservices/canopy-tanf/migrations/andservices/canopy-medicaid/migrations/. Align thefti_audit_log_archiveschema the same way (precedent:services/canopy-security/migrations/20260409000000_align_archive_hash_columns.sql). - Logger. Update
PostgresFtiAuditLogger::log_accessat crates/canopy-common/src/fti_audit.rs#L225 to compute a SHA-256 hash and write both columns, serialised by a per-database advisory lock (distinct lock ID from the canopy-security lock so they don't interfere). - Shared helper. New
verify_fti_chainincanopy-common::fti_audit— mirrorscanopy-security::store::verify_chainbut overfti_audit_logwith the FTI-appropriate hash inputs (see Open Design Questions). - Scheduled job. New
services/canopy-security/src/jobs/fti_chain_verify.rs. Runs daily against canopy-tanf and canopy-medicaid. Emitsfti.audit_chain.verifiedorfti.audit_chain.breach_detectedevents that the existing wildcard subscriber records. - Auditor endpoint. New
GET /v1/security/fti/chain-status?service=tanf&window=24h. RBAC:role::auditororrole::admin. Returns the most recent verification result.
Open design questions (resolve before implementation)
- What is hashed?
audit_eventshashes(previous_hash, event_id, event_type, timestamp). FTI audit rows have noevent_id; the row'sidis the natural substitute. Do we includeaccessed_by,purpose_code,data_elements_accessed? Including them is strictly more tamper-evident; excluding them keeps parity with the audit_events model. Pick one and commit. - Advisory-lock strategy. Per-service lock ID (lock 2 for canopy-tanf, lock 3 for canopy-medicaid), or a separate write worker that batches FTI audit writes? High-volume program services may not tolerate serialised writes on the request path.
- Archive boundary.
archive_expired_recordsat fti_audit.rs#L406 moves rows tofti_audit_log_archive. Unless the archive is also chained, verification breaks at the boundary every time an archive run completes. Plan needs to cover this explicitly. - Failure mode. When
verify_fti_chainfails in production, is that a 500 on the endpoint, an alert to auditors only, or both? This is a Pub 1075 §9 reporting question — defer to the runbook and cite it in the ADR amendment. - ADR amendment. ADR-004 §"FTI audit" needs to be amended to require hash-chain integrity on FTI logs once Phase B lands. Alternatively, a new ADR-012 if the scope grows.
Acceptance criteria
- All five Open Design Questions resolved in the Phase B plan (new file under
docs/modules/ROOT/pages/plans/) or an ADR amendment before any code lands. - Migrations are forward-only and tested via
cargo xtask validate. PostgresFtiAuditLogger::log_accesswrites chain columns; the existingfti_auditedhelper inherits this automatically.- New
verify_fti_chainhelper has happy-path + tamper-detection tests co-located with the existing Phase A tests style (see services/canopy-security/src/store/mod.rs). - Per-service integration tests in
services/canopy-tanf/tests/andservices/canopy-medicaid/tests/that drive a real determination → FTI access → row appended → chain verifies. - Scheduled job wired in canopy-security with logs visible under
docker logs canopy-security | grep fti.audit_chain. - Endpoint documented with utoipa + CLI command + runbook entry in
security-operations.adoc.
Not in scope for Phase B
- Phase A's test coverage (see fti-audit-hash-chain-test.adoc plan) — this is the precondition, not an output.
- Changes to
audit_events(canopy-security). Phase B lives entirely in canopy-common + program services + canopy-security's jobs/API layer.
Labels
workflow::needs-spec intentional — no code lands until the Phase B plan is written and the Open Design Questions are resolved.