Assistant: codex Assistant-Model: gpt-5.6-luna Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
311 lines
14 KiB
Markdown
311 lines
14 KiB
Markdown
---
|
||
id: AUDIT-WP-0009
|
||
type: workplan
|
||
title: "Evidence-role conformance under Security Layer Model v0.7"
|
||
domain: infotech
|
||
repo: audit-core
|
||
status: active
|
||
owner: claude
|
||
topic_slug: railiance
|
||
created: "2026-08-29"
|
||
updated: "2026-08-29"
|
||
depends_on:
|
||
- AUDIT-WP-0007
|
||
state_hub_workstream_id: "46a96b03-bc08-53b5-9c93-4071adabf734"
|
||
---
|
||
|
||
# AUDIT-WP-0009 — Evidence-role conformance under Security Layer Model v0.7
|
||
|
||
## Goal
|
||
|
||
Make audit-core dischargeable as the estate's **Evidence engine** under
|
||
NetKingdom Security Layer Model v0.7 (accepted), and stop it claiming a property
|
||
its own contract does not permit.
|
||
|
||
Two things drive this workplan.
|
||
|
||
**First, a live overclaim.** `audit_core/postgres_backend.py` returns
|
||
`tamper_evidence=True` as a constant, while `docs/integrity.md` permits that
|
||
claim only when a live external chain-head attestation exists. The one
|
||
attestation on record is 2026-08-16 and nothing renews it. Audit Core is
|
||
currently telling every sender it has a property whose precondition is
|
||
unverified — the §9.6 defect it twice corrected in gate-house's doctrine, turned
|
||
inward.
|
||
|
||
**Second, an obligation audit-core argued for and cannot yet support.** v0.7
|
||
§9.6 makes a declared emission cadence a **MUST** for load-bearing sources, with
|
||
reconciliation or a heartbeat as the required form for low-volume classes.
|
||
audit-core raised both, moving cadence from SHOULD to MUST. A source cannot
|
||
declare a cadence to a system with nowhere to put it. Having won the obligation,
|
||
audit-core owes the surface.
|
||
|
||
Full assessment: `history/2026-08-29-v0.7-alignment-and-scope-assessment.md`.
|
||
Standard: `net-kingdom/canon/standards/security-layer-model_v0.7.md`, companion
|
||
`net-kingdom/SECURITY-COMPANION.md`.
|
||
|
||
## Non-goals
|
||
|
||
Fixed by the statute; not deferred, not ours:
|
||
|
||
- No authorization decision surface, and no approval-validity query (§6, §9.4).
|
||
- No operative approval state (§9.4).
|
||
- No WORM, object lock, or ITC-CAP `data.archive` — §16 ruled the
|
||
stronger-custody gap closed, a different store with a different owner.
|
||
- Emission atomicity at the source, which is the emitter's obligation (§9.6).
|
||
|
||
## Tasks
|
||
|
||
```task
|
||
id: AUDIT-WP-0009-T01
|
||
status: done
|
||
priority: high
|
||
state_hub_task_id: "f545b0e4-8c99-5186-affd-ce9a41209ed7"
|
||
```
|
||
Make `tamper_evidence` conditional on live attestation state. Derive the flag
|
||
rather than hard-coding it: the backend reports `True` only when a chain-head
|
||
attestation exists, verifies against the live chain, and is within a declared
|
||
freshness window. Stale or absent attestation degrades the claim — it does not
|
||
leave it standing. Assert the degradation with a test, and state the freshness
|
||
window in `docs/integrity.md` alongside the two existing preconditions, which
|
||
today are documented but unenforced.
|
||
|
||
Done 2026-09-06. `evaluate_tamper_evidence` in `audit_core/integrity.py` derives
|
||
the claim from a `ChainReport` and the mounted attestation; `PostgresAuditBackend`
|
||
reads it per `retention_policy` call through `tamper_evidence_state()`, briefly
|
||
cached because `/readyz` probes it. Seven degradation reasons are distinguished
|
||
and asserted — `no_attestation`, `attestation_stale`, `attestation_mismatch`,
|
||
`attestation_undated`, `chain_break`, `chain_unreadable`, against `attested`.
|
||
Unreadable is treated as absent deliberately: a malformed file must not hold up
|
||
a claim a missing file would drop. The freshness window is **168h** against an
|
||
intended daily cadence, declared in `docs/integrity.md` with the reasoning that
|
||
the window is seven cadences so missed runs degrade rather than flap. Twelve
|
||
tests in `tests/test_integrity.py`; `docs/audit-backend-contract.md` now states
|
||
the field is derived rather than declared.
|
||
|
||
Consequence to state plainly: production `/readyz` will report
|
||
`tamper_evidence: false` until T02 mounts a renewed attestation. The 2026-08-16
|
||
attestation is 21 days old against a 7-day window. That is the overclaim being
|
||
removed, not a regression — the claim was false before and now says so.
|
||
|
||
```task
|
||
id: AUDIT-WP-0009-T02
|
||
status: todo
|
||
priority: high
|
||
state_hub_task_id: "6de9945f-4dd1-57dd-898a-f59c35b1df6c"
|
||
```
|
||
Schedule chain-head attestation so the precondition T01 enforces is normally
|
||
met. `attest-chain` exists and is operator-run; `deploy/` has no job. Add one,
|
||
write the attestation to the logical-offsite path already used by
|
||
`RESOURCE-WP-0002-T06` (`rapp-postgres` / Nextcloud + age) and **not** the
|
||
Barman prefix, per `docs/integrity.md` — that copy is restored with the table
|
||
and proves nothing. Record the cadence in `docs/integrity.md`.
|
||
|
||
```task
|
||
id: AUDIT-WP-0009-T03
|
||
status: done
|
||
priority: high
|
||
state_hub_task_id: "acae6085-ada7-51b2-8dc0-4abac7f7e7b3"
|
||
```
|
||
Represent the §9.6 evidence kind per source. Add `evidence_kind`
|
||
(`load-bearing` | `attributive`) to `SenderIdentity` and the sender registration
|
||
schema, defaulting to `attributive` — a source that has not declared is not
|
||
silently treated as load-bearing. Where a source declares `attributive` and has
|
||
deliberately traded away atomicity, carry the declared trade with it, because
|
||
§9.6 requires the trade be declared where the trail is documented. Prerequisite
|
||
for T04–T06.
|
||
|
||
Done 2026-09-06. `evidence_kind` and `completeness_trade` on `SenderIdentity`,
|
||
the `AUDIT_CORE_SENDERS` schema, and the non-secret scope overlay. Two
|
||
asymmetries are deliberate and asserted:
|
||
|
||
- **Default is `attributive`.** The other default would have audit-core imply a
|
||
completeness obligation no source ever accepted.
|
||
- **The overlay may raise the kind, never lower it.** Same principle that stops
|
||
an ExternalSecret refresh shrinking `user-engine`'s tenants: a ConfigMap
|
||
refresh must not be able to drop a source's atomicity and detection
|
||
obligations without anyone deciding to. Downgrading raises.
|
||
|
||
A load-bearing source may not carry a `completeness_trade` — §9.6 requires
|
||
atomicity of it, so recording a trade would store a contradiction as policy.
|
||
`evidence_declaration()` states `completeness_claimed: False` for **both**
|
||
kinds, because neither licenses the claim that the archive proves an event
|
||
occurred. Twelve tests in `tests/test_senders.py`.
|
||
|
||
```task
|
||
id: AUDIT-WP-0009-T04
|
||
status: todo
|
||
priority: high
|
||
state_hub_task_id: "f36470af-4019-54b2-96d7-e049d867c7db"
|
||
```
|
||
Heartbeat ingestion and missing-heartbeat findings. The required form for
|
||
low-volume load-bearing classes, and the only control covering adversarial
|
||
omission for exactly the rare negative events — revocation, denial, containment
|
||
— where suppression is most valuable and least visible. Accept a signed
|
||
*nothing to report* assertion per source and class; raise a finding when an
|
||
expected heartbeat does not arrive. The point is a positive claim that can
|
||
itself go missing, which rate monitoring can never produce.
|
||
|
||
```task
|
||
id: AUDIT-WP-0009-T05
|
||
status: wait
|
||
priority: medium
|
||
state_hub_task_id: "382575ca-1801-5a32-a93d-90a8b9c8adbf"
|
||
```
|
||
Accept and evaluate a declared emission cadence per source, and raise a finding
|
||
when the stream falls below it. **Waiting on** the §17 emission-cadence
|
||
declaration schema, which `kings-guard` is drafting and Taxonomy will own; do
|
||
not invent a competing shape. Cadence covers high-volume streams; T04 covers the
|
||
low-volume load-bearing case, and the two are complementary rather than
|
||
alternatives.
|
||
|
||
```task
|
||
id: AUDIT-WP-0009-T06
|
||
status: todo
|
||
priority: medium
|
||
state_hub_task_id: "0a8d6eed-75dd-5aaa-bf86-60be9dadca03"
|
||
```
|
||
Reconciliation surface: per-source, per-class event counts over a bounded
|
||
window, readable by the source itself, so an emitter can compare audit-core's
|
||
count against its own state transitions and treat divergence as a finding.
|
||
`GH-WP-0002-T04` is the reference instance on the `approval-engine` side; this
|
||
is its counterpart here. Respect the existing read model — `may_read`, tenant
|
||
scoping, and no payloads in a counts response.
|
||
|
||
```task
|
||
id: AUDIT-WP-0009-T07
|
||
status: todo
|
||
priority: medium
|
||
state_hub_task_id: "f572dfeb-0d1b-58d6-be83-405125189028"
|
||
```
|
||
Give stream-completeness findings a home. `/v1/dead-letters` and
|
||
`/v1/secret-findings` exist; a cadence miss (T05) and a missing heartbeat (T04)
|
||
have nowhere to surface. INTENT principle 10 already says a degraded audit
|
||
stream is itself an audit and operations event — the principle is in place and
|
||
the mechanism is not. Follow the existing findings-surface conventions rather
|
||
than adding a new shape.
|
||
|
||
```task
|
||
id: AUDIT-WP-0009-T08
|
||
status: done
|
||
priority: medium
|
||
state_hub_task_id: "f7b513ee-af48-5c5a-a34e-3e9922d29b76"
|
||
```
|
||
Assert the §9.4 approval-validity prohibition with a negative test. It is
|
||
currently honoured by absence, which is not the estate's idiom: §6.4 obligation
|
||
3 requires a PEP's published stance map equal shipped behaviour *asserted by
|
||
test*. A prohibition worth stating in `INTENT.md`, `SCOPE.md`, and `layer.yaml`
|
||
is worth asserting in `tests/`.
|
||
|
||
```task
|
||
id: AUDIT-WP-0009-T09
|
||
status: progress
|
||
priority: high
|
||
state_hub_task_id: "fd4a4ac3-e525-57c1-9179-e0fcd0226913"
|
||
```
|
||
Register `approval-engine` as a distinct source under §9.4 and `AUDIT-IN-0001`:
|
||
sender registration, the four event classes (issuance, use, supersession,
|
||
revocation), tenancy mapping, retention profile, `secret_policy`, and
|
||
`evidence_kind: load-bearing` from T03. Onboarded under INTENT principle 4 —
|
||
declared ownership, retention, access, export and evidence policy, not merely
|
||
events arriving.
|
||
|
||
**Re-priced low → high, 2026-09-06.** `glas-harness` reports (message
|
||
`85c68e66`) that `approval-engine` production startup now *requires* audit
|
||
delivery, putting this on the deployment critical path. The old note — "not
|
||
blocking: `approval-engine` is not yet emitting" — read the dependency
|
||
backwards: audit-core is not waiting for a producer, the producer is waiting
|
||
for a sender identity. Corrected.
|
||
|
||
What this does not change: T09 still depends on **T03**, because §9.4 makes
|
||
approval evidence load-bearing and there is nowhere to record that until
|
||
`evidence_kind` exists. Registering the sender ahead of T03 would admit a
|
||
load-bearing source into a system that would silently treat it as attributive
|
||
— the precise defect §9.6 names. T03 is therefore promoted with it. Cadence
|
||
and reconciliation (T04/T06) remain open gates on the *detection* half and are
|
||
not prerequisites for admission; they bound what may be claimed of the stream
|
||
once it flows, and `approval-engine` should be told so rather than discovering
|
||
it later.
|
||
|
||
Token provisioning goes by the approved custody path
|
||
(`approval-engine/approval-engine-audit`, key `audit-token`) via `warden
|
||
route`; no sender token passes through Git, State Hub, or a workplan.
|
||
|
||
**In progress 2026-09-06.** T03 landed, so the registration inputs are now
|
||
expressible. Prepared, non-secret, and recorded in
|
||
`docs/approval-engine-source-registration.md`:
|
||
|
||
- Non-secret scope in `deploy/senders-scope.json` and `senders-scope.yaml`,
|
||
`evidence_kind: load-bearing`, `may_read: false`, exact source match. Landed
|
||
ahead of the credential deliberately: the overlay only applies to a sender
|
||
the Secret already carries, so the entry admits nothing until the token
|
||
exists. A test asserts that inertness rather than trusting the reading.
|
||
- `audit-core-approval-engine-ingress` in `deploy/networkpolicies.yaml`:
|
||
namespace **and** pod label in one `from` peer, so both are ANDed. Narrower
|
||
than `user-engine`'s namespace-only rule on purpose — a new sender should not
|
||
inherit an older rule's breadth. `user-engine`'s policy is unchanged, and a
|
||
test asserts it stays that way.
|
||
- The four §9.4 classes, retention (no expiry; 30-day `measured` recoverable
|
||
window), custody class, and the claim bound.
|
||
|
||
**Two inputs are owed by `approval-engine`, not by audit-core.** `tenants` is
|
||
proposed as `["*"]` and needs confirmation with reasoning or a list —
|
||
`senders.py` says a missing tenant restriction must be justified per sender,
|
||
and audit-core cannot justify it on another repo's behalf. `secret_policy` is
|
||
recommended `redact` (losing a revocation record is worse than storing it with
|
||
one field redacted) but should be chosen explicitly rather than inherited.
|
||
Applying the manifests is an operator action audit-core does not take
|
||
unprompted.
|
||
|
||
**T02 is not a gate on this.** Asked directly by `glas-harness`: attestation
|
||
freshness governs what audit-core may *claim*, not whether it accepts and
|
||
durably stores. The append-only trigger and hash chain operate regardless. And
|
||
it is not even the relevant control — §9.6 is explicit that the acute risk for
|
||
approvals is omission, which tamper evidence does not address at all. T04 does.
|
||
A consumer waiting on T02 before trusting approval evidence would be waiting on
|
||
the wrong thing.
|
||
|
||
**2026-09-08 source return:** approval-engine now records the exact existing
|
||
store scope `["tenant:platform"]` and explicitly selects `redact` in
|
||
`docs/audit-source-registration.md`. This supersedes the two input waits above.
|
||
The JSON and ConfigMap overlays are narrowed together. The overlay preserves
|
||
secret_policy from the protected registry, so provisioning must set it there.
|
||
T09 remains progress for the platform custody record, credential, admitted
|
||
manifest application and positive/negative live ingestion evidence. No secret
|
||
was created and no production manifest was applied by this source change.
|
||
|
||
```task
|
||
id: AUDIT-WP-0009-T10
|
||
status: todo
|
||
priority: low
|
||
state_hub_task_id: "ac3464fd-3df9-51a1-b1f4-3bf3c60e4265"
|
||
```
|
||
Make the §5 conformance check total. `layer.yaml` declares
|
||
`tooling_contacts: []`, true under §5 as written — audit-core is an Engine and
|
||
holds no `key-cape` or OpenBao client. Companion §4 asks that uncatalogued
|
||
infrastructure be listed anyway, and that carve-out sunsets within two review
|
||
intervals for a store another layer reads, which `platform-pg` is. List it and
|
||
the State Hub now. Add a conformance script and test in the shape ops-warden
|
||
offered estate-wide.
|
||
|
||
## Acceptance
|
||
|
||
- No custody claim is returned unconditionally where `docs/integrity.md`
|
||
conditions it, and a stale attestation visibly degrades the claim.
|
||
- A source can declare its evidence kind, and a load-bearing source can
|
||
discharge the §9.6 cadence MUST against a real audit-core surface.
|
||
- A suppressed low-volume load-bearing class produces a finding rather than
|
||
silence.
|
||
- The approval-validity prohibition is asserted by test, not by absence.
|
||
- `layer.yaml` lists every infrastructure contact, catalogued or not, and a test
|
||
checks it.
|
||
|
||
## Notes
|
||
|
||
T01 and T02 are separable and T01 is the one that matters: scheduling
|
||
attestation without making the claim conditional would leave the overclaim in
|
||
place whenever the job fails, which is precisely the case the claim is supposed
|
||
to cover.
|
||
|
||
T05's dependency is real and should not be worked around. audit-core argued the
|
||
cadence obligation up to a MUST; inventing a local cadence shape ahead of the
|
||
Taxonomy schema would fragment the very interface it asked for.
|