diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index a7309fa..f9ef2e2 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -28,6 +28,7 @@ | workplan | AUDIT-WP-0015 | blocked | — | workplans/AUDIT-WP-0015-independent-evidence-returns.md | | workplan | AUDIT-WP-0016 | finished | — | workplans/AUDIT-WP-0016-wave2-resource-right-sizing.md | | workplan | AUDIT-WP-0017 | proposed | — | workplans/AUDIT-WP-0017-production-audit-sources-and-correlation.md | +| workplan | AUDIT-WP-0018 | proposed | — | workplans/AUDIT-WP-0018-bounded-executor-receipt-custody.md | | task | ADHOC-2026-09-14-T01 | done | — | workplans/ADHOC-2026-09-14.md | | task | ADHOC-2026-09-24-T01 | done | — | workplans/ADHOC-2026-09-24.md | | task | ADHOC-2026-10-15-T01 | done | — | workplans/ADHOC-2026-10-15.md | @@ -111,6 +112,9 @@ | task | AUDIT-WP-0017-T05 | todo | — | workplans/AUDIT-WP-0017-production-audit-sources-and-correlation.md | | task | AUDIT-WP-0017-T06 | todo | — | workplans/AUDIT-WP-0017-production-audit-sources-and-correlation.md | | task | AUDIT-WP-0017-T07 | wait | — | workplans/AUDIT-WP-0017-production-audit-sources-and-correlation.md | +| task | AUDIT-WP-0018-T01 | done | — | workplans/AUDIT-WP-0018-bounded-executor-receipt-custody.md | +| task | AUDIT-WP-0018-T02 | wait | — | workplans/AUDIT-WP-0018-bounded-executor-receipt-custody.md | +| task | AUDIT-WP-0018-T03 | wait | — | workplans/AUDIT-WP-0018-bounded-executor-receipt-custody.md | | intake | AUDIT-IN-0001 | closed | — | intakes/intakes.md | | intake | AUDIT-IN-0002 | closed | — | intakes/intakes.md | | intake | AUDIT-IN-0003 | closed | — | intakes/intakes.md | diff --git a/docs/bounded-executor-receipts.md b/docs/bounded-executor-receipts.md new file mode 100644 index 0000000..6ad282e --- /dev/null +++ b/docs/bounded-executor-receipts.md @@ -0,0 +1,165 @@ +# Bounded-executor execution-attribution receipts — owner answer + +Answers NetKingdom NK-WP-0056-T02 (owner request +`net-kingdom/docs/proposals/2026-10-31-nk56-t02-owner-requests.md`, section +"audit-core"; hub message `755cbeed-ecbd-46d9-883e-958fbd88378b`). Reviewed +2026-10-31 against `net-kingdom/canon/standards/bounded-execution-admission_v0.1.md` +section 6 and `playbook-capability-contract_v0.2.md` "Execution Attribution +Receipt (proposed)". Work: AUDIT-WP-0018. Canonical receipt validation remains +AUDIT-WP-0014-T02. + +This is an owner proposal. It activates nothing: registration, ingress and the +sender bearer require the owner deliveries and the approved live change listed +at the end. + +## Answer 1 — will audit-core hold the receipts? + +Yes. Audit-core accepts the evidence-holder role for the bounded executor's +receipts, as it already did for NK-WP-0040 (`docs/cross-repo-evidence-admission.md`, +"Execution-attribution receipts"). What changes is the emitter: no longer a +Railiance component but the NetKingdom-written executor, deployed by the new +`rapp-*` package, running as `service:nk-bind-executor` in namespace `sso`. + +Holding means what it means for every other sender (`docs/event-envelope.md`): +audit-core authenticates the sender, checks source and tenant against the +registered identity, stores the event verbatim, and chains it. It records that +the executor *said* the run happened this way. It does not decide whether the +`decision_refs` authorized the run, and it does not authenticate the initiator +chain. That check belongs to the independent verifier in BEA section 6 +("Verify"), which reads the receipt from audit-core and compares it with the +carrier record. + +Until AUDIT-WP-0014-T02 ships canonical schema validation, an accepted receipt +is generic JSON custody, not a schema-validated receipt. Audit-core will not +describe it otherwise. + +## Answer 2 — one event, or separate events by `run_id`? + +Both, in a fixed shape: **one receipt event per run carries all per-step detail**, +plus **one `started` event** emitted when the executor has consumed the approval and +before it takes its first step. Do not split steps into separate events. + +Why the steps stay inside the receipt: + +- The envelope stores `data` verbatim as one object (`docs/event-envelope.md`). + Per-step outcomes, check results, revert yes/no and budget used are the + sender's claims. They belong in `data`, where they read as claims. The stored + `outcome` is always `recorded` and `actor` is always `null` + (`docs/event-envelope.md`, "What the receiver derives"). +- One receipt is the retained evidence unit. BEA section 6 says the receipt is + "kept by hash". If steps were split into separate events, nobody could tell + whether a step event was missing without a declared count. The receipt would + then become a reconstruction, not a record. +- The validator then sees one document, so `decision_refs`, budget and step + list are checked together. + +Why a separate `started` event is still needed: a receipt is emitted at the end +of a run. If the executor crashes mid-run after it has consumed its single +approval, it emits nothing. Without a second event, a run that partly executed +leaves no record in custody. With a `started` event, a `started` that has no +matching receipt after the admission window is detectable. Both events share the +`run_id`. + +## Event mapping (proposal) + +Both events use the existing eight-field envelope. No envelope change is needed. + +| Envelope field | `started` event | Receipt event | +| --- | --- | --- | +| `type` | `execution.run.started` | `execution.attribution.receipt` | +| `id` / `Idempotency-Key` | `run::started` | `run::receipt` | +| `source` | `net-kingdom.bounded-executor` (exact; emitter confirms) | same | +| `subject` | `admission:` | same | +| `tenant` | the admission's tenant, exact string (owner-bound, never inferred from namespace) | same; must equal `data.tenant` | +| `correlation_id` | `` | `` | +| `occurred_at` | consume time, offset-aware | `time_interval.end`, offset-aware | +| `data` | `run_id`, `admission_id`, `admission_digest`, `approval_ref`, `executing_workload`, `runtime_principal`, `plan_digest`, `steps_planned` | the canonical receipt (below) | + +Each `id` is derived deterministically from `run_id`. A retry is therefore a +`200` duplicate. A second, different body under the same id is a `409` conflict +(`audit_core/ingestion.py` response table), which exposes reuse of a run id. + +Proposed receipt `data`. The schema belongs to NetKingdom (NK-WP-0040-T02); +this lists what audit-core needs to see in it: + +```json +{ + "receipt_version": "", + "run_id": "…", + "initiator": {"iss": "…", "sub": "admin"}, + "executing_workload": "service:nk-bind-executor", + "runtime_principal": "system:serviceaccount:sso:", + "tenant": "…", + "environment": "…", + "source_commit": "", + "artifact_digest": "", + "target": "", + "action": "bounded-execution", + "decision_refs": [ + {"kind": "admission", "id": ""}, + {"kind": "admission_digest", "id": "sha256:…"}, + {"kind": "approval", "id": ""} + ], + "time_interval": {"start": "…+00:00", "end": "…+00:00", "clock_source": "…"}, + "steps": [ + {"index": 1, "operation": "…", "target": "…", "action": "…", + "outcome": "succeeded|failed|skipped", + "checks": [{"id": "…", "result": "pass|fail|unknown"}]} + ], + "reverted": false, + "budget": {"limit": {"…": 0}, "used": {"…": 0}}, + "outcome": "succeeded|failed|reverted" +} +``` + +The points that fit the existing model and need NetKingdom to decide: + +1. **`target`/`action` are singular in the canonical field list but per-step in + BEA section 6.** Proposal: the receipt-level `target` is the admission's + target and the receipt-level `action` is `bounded-execution`. Each step's + target and action go in `steps[]`. Audit-core needs one answer in the schema. + It will not pick one. +2. **`decision_refs` must be typed.** The admission id, the admission digest and + the approval reference are three different things. An untyped list cannot + be checked against the carrier. +3. **`delegation` stays absent** when there is none. `time_interval.bound` stays + absent when no bound is established. Unknown attribution stays the explicit + `unknown`. This is unchanged from `docs/cross-repo-evidence-admission.md`. +4. **Check results carry an id and a result only.** They carry no response + bodies, tokens, password values or LLDAP/OpenBao payloads. + +## Registration (proposal) + +| Registry field | Proposed value | Reason | +| --- | --- | --- | +| `name` | `nk-bounded-executor` | one identity per executor deployment | +| `sources` | `["net-kingdom.bounded-executor"]` | exact, no wildcard | +| `tenants` | the exact admission tenant(s) | not `*`; the executor acts for named tenants only | +| `may_write` / `may_read` | `true` / `false` | the verifier reads with its own read identity | +| `secret_policy` | `reject` | a receipt is kept by hash. If audit-core redacted it silently, the stored body would no longer match the hash the executor computed, so a secret-shaped field must refuse the event (`400 secret_shaped_field`). The executor treats that as a defect | +| `evidence_kind` | `load-bearing`, if the `started` + receipt pair is adopted. Otherwise `attributive` with an explicit `completeness_trade` | the pair is what makes a missing receipt detectable | + +Ingress: a new NetworkPolicy in `deploy/networkpolicies.yaml` admitting +namespace `sso` and the executor's exact pod labels, following +`audit-core-tenant-engine-ingress`. Do not admit the whole `sso` namespace, +because Authelia, LLDAP and KeyCape run there too. The bearer is +ESO-projected by railiance-platform, like the other in-cluster senders. No token +value appears in this repository. + +## What audit-core needs back + +- **NetKingdom (NK-WP-0040-T02 / NK-WP-0056):** + - the versioned receipt schema, validator, path and digest, with positive and + negative fixtures, covering `steps`, `reverted`, `budget` and the typed + `decision_refs`; + - an answer to point 1; + - the exact `source` and event types; + - the tenant binding; + - a decision on the `started` event and the emission guarantee. +- **The rapp-\* package owner:** the exact pod labels and ServiceAccount, and + the image revision that emits. +- **railiance-platform:** the ESO projection of the sender bearer + (catalog/route via `warden route`), and the attended seed of the registry + entry. +- **Founder:** the live registry entry and NetworkPolicy rollout are an approved + live change. Audit-core prepares them; the founder runs them. diff --git a/docs/cross-repo-evidence-admission.md b/docs/cross-repo-evidence-admission.md index bdfa428..52a75fb 100644 --- a/docs/cross-repo-evidence-admission.md +++ b/docs/cross-repo-evidence-admission.md @@ -139,6 +139,10 @@ ordered offset-aware start/end if that is the canonical rule. No independent local receipt schema is shipped ahead of the owner schema, and generic JSON acceptance is not advertised as receipt-schema validation. +NK-WP-0056 names a new emitter, the NetKingdom bounded executor in `sso`. Its +mapping (one receipt per run plus a `started` event, correlated by `run_id`) is in +`docs/bounded-executor-receipts.md` (AUDIT-WP-0018). + ## Independent chain-head custody — AUDIT-WP-0015-T01 RESOURCE-WP-0002-T06 is done for encrypted logical database copies. That diff --git a/workplans/AUDIT-WP-0018-bounded-executor-receipt-custody.md b/workplans/AUDIT-WP-0018-bounded-executor-receipt-custody.md new file mode 100644 index 0000000..ec24ec7 --- /dev/null +++ b/workplans/AUDIT-WP-0018-bounded-executor-receipt-custody.md @@ -0,0 +1,85 @@ +--- +id: AUDIT-WP-0018 +type: workplan +title: "Hold bounded-executor execution-attribution receipts" +domain: infotech +repo: audit-core +status: proposed +flavor: implementation +owner: audit-core +topic_slug: audit-evidence-dependencies +created: "2026-10-31" +updated: "2026-10-31" +related: + - NK-WP-0056 + - NK-WP-0040 + - AUDIT-WP-0014 +--- + +# AUDIT-WP-0018 — Hold bounded-executor execution-attribution receipts + +NetKingdom's Bounded Execution Admission v0.1 makes a non-human executor +(`service:nk-bind-executor`, namespace `sso`) emit one execution-attribution +receipt per run. Audit-core holds those receipts. The owner answer and the event +mapping are in `docs/bounded-executor-receipts.md`. Canonical schema validation +is still AUDIT-WP-0014-T02 and is not duplicated here. + +## Answer the NK-WP-0056-T02 owner request + +```task +id: AUDIT-WP-0018-T01 +status: done +priority: high +``` + +Hub message 755cbeed-ecbd-46d9-883e-958fbd88378b. Audit-core accepts custody. +Each run produces one receipt event that carries the per-step outcomes, check +results, `reverted` and `budget`. Each run also produces an +`execution.run.started` event, correlated with the receipt by `run_id`. Event +ids are deterministic from `run_id`. The proposed `secret_policy` is `reject`. +NetKingdom has to decide how a singular `target`/`action` relates to per-step +targets and actions, and has to make `decision_refs` typed. + +## Prepare sender registration and ingress for the executor + +```task +id: AUDIT-WP-0018-T02 +status: wait +priority: high +blocking_reason: "NetKingdom must confirm source, event types, tenant binding and the started event; the rapp-* package must name pod labels and ServiceAccount; railiance-platform must route the ESO-projected bearer." +blocked_on: message-from:net-kingdom +``` + +Prepare the following: + +- a non-secret registry entry (`nk-bounded-executor`: exact source, exact + tenants, write only, `secret_policy: reject`, and either `evidence_kind` + `load-bearing` or `attributive` with a `completeness_trade`); +- a NetworkPolicy that admits only the executor's pods in `sso`. + +Then test on synthetic fixtures: + +- accept on 202; +- a retry returns a 200 duplicate; +- a reused `run_id` with a different body is a 409 conflict; +- wrong source or tenant is refused; +- a secret-shaped step check is refused with 400. + +The live registry seed and the rollout are founder-run. + +## Detect runs that started without a receipt + +```task +id: AUDIT-WP-0018-T03 +status: wait +priority: medium +blocking_reason: "Depends on NetKingdom adopting the execution.run.started event and stating the admission window bound after which a missing receipt is a finding." +blocked_on: message-from:net-kingdom +``` + +Report every `execution.run.started` that has no +`execution.attribution.receipt` with the same `correlation_id` after the +declared window. Reuse the existing stream-completeness/cadence surface +(`docs/stream-completeness.md`, `docs/cadence-registration.md`) instead of +adding a new monitor. The report is a finding for the verifier and the +founder. Audit-core does not infer what the run did.