Migration 0006 applied on railiance01. Verify-against the dated chain-head evidence is intact for 30 events.
7.3 KiB
| id | type | title | domain | repo | status | owner | topic_slug | created | updated | depends_on | state_hub_workstream_id | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| AUDIT-WP-0007 | workplan | Integrity verification for operational custody | infotech | audit-core | finished | grok | railiance | 2026-08-16 | 2026-08-16 |
|
97946512-eb0a-4282-8c1f-f53d9ee25bfa |
AUDIT-WP-0007 — Integrity verification for operational custody
Goal
Make the operations.audit integrity claim checkable.
AUDIT-WP-0006 made custody honest: the live receiver reports
operational, not archive, and cites the 30-day platform backup.
tamper_evidence is still False. The append-only trigger rejects
UPDATE/DELETE for the runtime role, and each row stores payload_hash,
but a database owner can drop the trigger and rewrite rows. Nothing
would prove they had. ITC-CAP hook integrity_verification is
therefore unknown.
This workplan adds a hash chain on accept, a verify surface that can
fail, and a chain-head attestation stored outside platform-pg. Only
then may RetentionPolicy.tamper_evidence become true, and only for
what those artefacts actually detect.
It does not raise the provision to D5. D5 also needs reliability measured and controlled (one replica, no drill cadence). Leave maturity at D4 unless those land elsewhere.
Why this is the next improvement
data.archive still needs a founder decision and a resource-control
procurement. A second sender expands the product. Integrity is the
remaining honesty gap on the live provision and is owned here.
Boundaries
This workplan owns
- Per-event hash chain in the Postgres store.
- A verify CLI and a
may_readHTTP surface. - A chain-head attestation outside
audit_coreonplatform-pg. - Updating
tamper_evidenceand the ITC-CAPintegrity_verificationhook only when verify can detect a rewritten payload.
This workplan does not own
- A
data.archive/ WORM object-store sink. - Booked cost,
rapp.yaml, public ingest, Barman/S3. - A public transparency log.
- Raising provision maturity to D5.
- Replacing the append-only trigger (it stays).
T01 — Specify the chain and the proof bound
id: AUDIT-WP-0007-T01
status: done
priority: high
state_hub_task_id: "d4423fd5-ad78-47d0-b85e-7ae6c582ec2b"
Write a short contract in docs/audit-backend-contract.md (or a sibling
docs/integrity.md) that states:
- Each accepted event carries
payload_hash(already stored) and a newchain_hash= H(previouschain_hash||payload_hash||event_id). First event uses a documented genesis value. - The chain is per schema / database, not per tenant. Tenant splits would hide a cross-tenant rewrite.
verifywalks accepted_at/chain order, recomputes, and reports the first break. A break is a custody defect, not a sender error.- Proof bound: a chain inside the same database detects a rewritten
payload if the attacker does not also recompute the suffix. A
database owner can. Tamper evidence against that class of attacker
requires a chain-head attestation stored outside
platform-pg. tamper_evidence=Trueis allowed only when both verify and an external head exist. It still does not mean WORM ordata.archive.
Done when the contract is written and the tests to be added are named.
Done 2026-08-16: docs/integrity.md names the chain, genesis, proof bound,
and the six tests.
T02 — Persist the chain on accept
id: AUDIT-WP-0007-T02
status: done
priority: high
state_hub_task_id: "5830bb1a-27b7-4eed-b09f-75ba8cc9f7f1"
Add an additive migration (0006) for chain_hash (and chain_prev if
the walk needs it). Backfill existing rows in accept-order so a live
database with the AUDIT-WP-0005/0006 events verifies after migrate.
accept must write the next link in the same transaction as the
idempotent insert. A duplicate accept must not mint a second link.
A conflict must not advance the head.
SQLite (dev) should implement the same chain so conformance stays one
suite. Mock file backend stays tamper_evidence=False.
Done when conformance tests show: first accept sets genesis; second links; duplicate does not fork; verify is clean on a fresh store.
Done 2026-08-16: migration 0006 plus backfill; SQLite and Postgres accept
write the next link under a lock. Conformance + tests/test_integrity.py.
T03 — Verify surface
id: AUDIT-WP-0007-T03
status: done
priority: high
state_hub_task_id: "fee1f93c-dcac-4fc3-9e41-17bb4436e8d6"
- CLI:
python -m audit_core verify-chain(or equivalent) exits 0 only if the walk is intact. Print first-break event id on failure. - HTTP: a
may_readroute (suggestedGET /v1/integrity) returns{intact, events, head, first_break}without event payloads.
A test must break a payload_hash (or chain_hash) using a superuser connection and show verify fails. That is the evidence the trigger alone never gave us.
Done when CLI and HTTP agree, and the broken-row test is red-then-green as a detector, not as a repair.
Done 2026-08-16: verify-chain CLI, GET /v1/integrity, SQLite rewrite
test and Postgres superuser rewrite test both fail verify.
T04 — Attest the chain head outside platform-pg
id: AUDIT-WP-0007-T04
status: done
priority: medium
state_hub_task_id: "b6d03d6a-0605-4982-a9a3-e18c89319ba9"
Publish {chain_hash, event_id, accepted_at, event_count, observed_at}
to a location that is not a table in audit_core on platform-pg.
First implementation: an operator command that writes a dated evidence
file, plus a documented second copy (for example the existing
logical-offsite / Nextcloud path used by RESOURCE-WP-0002-T06 — cite
it, do not invent a new bucket).
Do not write the head into the Barman prefix. That copy dies with the same restore as the table.
Verify must be able to compare the live head to a cited attestation and report mismatch as a break.
Done when one production walk produces an attestation file in
docs/evidence/ and verify-against-attestation is tested.
Done 2026-08-16: live walk 30 events, intact, head
03fd1773…. Evidence
docs/evidence/chain-head-20260816.json. Verify-against matched.
Do not copy this file into the Barman prefix; a second copy may follow
RESOURCE-WP-0002-T06 logical-offsite.
T05 — Declare tamper_evidence only as far as the proof
id: AUDIT-WP-0007-T05
status: done
priority: medium
state_hub_task_id: "9fd70d3c-5a64-4b26-967f-4ef45394d81c"
After T03 and T04:
- Postgres
RetentionPolicy.tamper_evidencebecomesTrue. /readyzmay report it. Do not changecustody_classback toarchive.- Update
data/capability/audit-core-operational.json:integrity_verificationbasismeasured, pointing at the broken-row test and the attestation walk. Leave maturity at D4. Keepdata.archiveunmet. - Refresh the reuse-surface card and operator runbook (how to verify, what a break means, that reboot unreadiness is unrelated).
Done when capability-review still ok and the hook is no longer
unknown.
Done 2026-08-16: Postgres tamper_evidence=True; live /readyz reports
it; capability-review ok; integrity_verification is measured.
Maturity stays D4. data.archive stays unmet.
Acceptance
- A rewritten stored payload makes
verifyfail. - A chain-head attestation exists outside
platform-pg. tamper_evidenceis true only after those two exist.- No
data.archivesink, no D5 inflation, no new cost stream.