Implement AUDIT-WP-0006 honest operational custody.
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s

Postgres now reports custody_class=operational with a cited 30-day
recoverable window. Join ITC-CAP operations.audit at D4, publish the
interface card, and overlay user-engine tenants [*] from Git so an
ExternalSecret refresh cannot shrink it.
This commit is contained in:
tegwick 2026-08-16 00:24:33 +02:00
parent 0a3d05ff1c
commit ded432a63f
25 changed files with 832 additions and 94 deletions

View file

@ -117,20 +117,26 @@ Compatibility rules (not yet implemented):
| Field | Meaning |
| --- | --- |
| `custody_class` | `development`, `archive`, or `hot_search` |
| `retention_days` | Maximum age before eligible deletion; `None` means indefinite |
| `custody_class` | `development`, `operational`, `archive`, or `hot_search` |
| `retention_days` | Maximum age before eligible deletion; `None` is a lifecycle statement (no expiry), not a recovery guarantee |
| `immutable` | Whether stored records are protected from in-place alteration |
| `tamper_evidence` | Whether manifests, hash chains, or signatures exist |
| `durable` | Whether survival is expected across process restarts and host reboots |
| `recoverable_days` | Cited platform backup window; `None` if not declared |
| `recoverable_source` | Where the recoverable window is cited from |
| `recoverable_basis` | ITC-GOV EvidenceBasis of that citation (`measured`, `quoted`, …) |
### Custody classes
| Class | Purpose | Guarantees |
| --- | --- | --- |
| `development` | Local integration and bootstrap wiring | Ephemeral local files; best-effort cleanup; **not audit custody** |
| `archive` | Long-term evidence (planned) | Durable object storage, batch manifests, explicit retention |
| `operational` | Durable production custody | Append-only Postgres; recoverable through the platform `data.backup` provision; not ITC-CAP `data.archive` |
| `archive` | Long-term evidence (future sink) | Reserved for a backend that can satisfy `data.archive` hooks (retention policy, integrity verification, retrieval test) |
| `hot_search` | Operational investigation (planned) | Shorter retention; searchable; not the evidence record |
`AUDIT_CORE_REQUIRE_CUSTODY_CLASS=operational` is the production fail-closed gate. `archive` is accepted as an alias of `operational` for one mixed rollout so an old manifest cannot refuse a new image. A `development` backend satisfies neither.
### Mock file backend policy
`MockFileAuditBackend.retention_policy`:
@ -152,15 +158,20 @@ Enforcement:
**Not guaranteed:** crash-safe writes, replication, encryption, tenant isolation,
integrity proofs, or survival of `/tmp` across reboots.
### Production archive policy (planned)
### Production operational policy (Postgres, live)
Target guarantees for the first durable backend:
`PostgresAuditBackend.retention_policy`:
- `custody_class`: `archive`
- `retention_days`: scope policy (often years, sometimes indefinite)
- `immutable`: true (WORM / object lock where available)
- `tamper_evidence`: true (batch manifests with content hashes)
- `custody_class`: `operational`
- `retention_days`: unset in production (the service does not expire rows)
- `immutable`: true (trigger `events_append_only`; not a claim against the database owner)
- `tamper_evidence`: false (a superuser can drop the trigger; no hash-chain)
- `durable`: true
- `recoverable_days`: 30, cited from the platform `data.backup` provision
- `recoverable_source`: `resource-control/data/capability/platform-audit-storage.json#provisions[capability=data.backup]`
- `recoverable_basis`: `measured`
`None` retention is a lifecycle statement, not unbounded archive. Rows older than the recoverable window are not promised after a restore. A future `archive` backend that satisfies ITC-CAP `data.archive` is not implemented.
## Migration path: mock file → durable backend
@ -189,15 +200,15 @@ backend.emit(AuditEvent(source="...", action="...", resource="...", outcome="suc
1. Register a durable archive backend implementing `AuditBackend`.
2. Configure routing: development scopes may keep mock; production scopes require
`custody_class=archive`.
`custody_class=operational` (the live Postgres backend).
3. Readiness checks compare `backend.retention_policy` against scope policy and
fail closed when custody is insufficient.
### Phase 2 — archive primary (planned)
### Phase 2 — operational primary (live)
1. Point `emit` calls (or HTTP ingestion) at the archive backend.
1. HTTP ingestion writes through `PostgresAuditBackend` (`custody_class=operational`).
2. Retain mock only for local `make mock-audit-smoke` and unit tests.
3. Export historical mock JSONL into archive batches with manifest generation.
3. A future `data.archive` sink is a separate backend, not a rename of Postgres.
### Phase 3 — hot search adjunct (planned)
@ -220,7 +231,9 @@ Archive remains the evidence record; hot search may use shorter `retention_days`
| Backend | Module | Custody class |
| --- | --- | --- |
| Mock file JSONL | `audit_core.mock_file_backend.MockFileAuditBackend` | `development` |
| Archive (planned) | TBD | `archive` |
| SQLite (local / test) | `audit_core.sqlite_backend.SQLiteAuditBackend` | `development` |
| PostgreSQL (production) | `audit_core.postgres_backend.PostgresAuditBackend` | `operational` |
| Archive (planned `data.archive` sink) | TBD | `archive` |
| Hot search (planned) | TBD | `hot_search` |
## Related documents