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.

  1. Migrations. New migrations adding previous_hash TEXT and event_hash TEXT columns to fti_audit_log in services/canopy-tanf/migrations/ and services/canopy-medicaid/migrations/. Align the fti_audit_log_archive schema the same way (precedent: services/canopy-security/migrations/20260409000000_align_archive_hash_columns.sql).
  2. Logger. Update PostgresFtiAuditLogger::log_access at 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).
  3. Shared helper. New verify_fti_chain in canopy-common::fti_audit — mirrors canopy-security::store::verify_chain but over fti_audit_log with the FTI-appropriate hash inputs (see Open Design Questions).
  4. Scheduled job. New services/canopy-security/src/jobs/fti_chain_verify.rs. Runs daily against canopy-tanf and canopy-medicaid. Emits fti.audit_chain.verified or fti.audit_chain.breach_detected events that the existing wildcard subscriber records.
  5. Auditor endpoint. New GET /v1/security/fti/chain-status?service=tanf&window=24h. RBAC: role::auditor or role::admin. Returns the most recent verification result.

Open design questions (resolve before implementation)

  • What is hashed? audit_events hashes (previous_hash, event_id, event_type, timestamp). FTI audit rows have no event_id; the row's id is the natural substitute. Do we include accessed_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_records at fti_audit.rs#L406 moves rows to fti_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_chain fails 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

  1. 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.
  2. Migrations are forward-only and tested via cargo xtask validate.
  3. PostgresFtiAuditLogger::log_access writes chain columns; the existing fti_audited helper inherits this automatically.
  4. New verify_fti_chain helper has happy-path + tamper-detection tests co-located with the existing Phase A tests style (see services/canopy-security/src/store/mod.rs).
  5. Per-service integration tests in services/canopy-tanf/tests/ and services/canopy-medicaid/tests/ that drive a real determination → FTI access → row appended → chain verifies.
  6. Scheduled job wired in canopy-security with logs visible under docker logs canopy-security | grep fti.audit_chain.
  7. 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.