audit-core/docs/bounded-executor-receipts.md
tegwick 3157454b84
All checks were successful
CI Smoke / host-smoke (push) Successful in 1s
CI Smoke / container-smoke (push) Successful in 2s
AUDIT-WP-0018-T04: align seed with settled CCR-2026-0063 (railiance-platform f3b0c4f)
- sso delivery is ExternalSecret/Secret sso/bounded-executor-tokens key
  audit-core (the Secret the rapp run Job already mounts), not
  nk-bind-executor-audit/audit-token.
- CCR check now also pins the new reader shape (audience openbao, explicit
  max TTL 15m, token_self_lifecycle) and that the CCR names this script as
  its custody procedure and the bounded-executor-tokens delivery.
- Docs/workplan: this script is the single seeder; railiance-platform
  fences the boundary (nk_bind_executor_custody.py fence) and owns
  manifests/nk-bind-executor-eso.yaml.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 523928@bnt-lap001
Assistant-Session: 6c737afb-9c14-4152-9f02-477214b9c350
2026-11-04 02:05:18 +01:00

19 KiB

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:<run_id>:started run:<run_id>:receipt
source net-kingdom.bounded-executor (exact; emitter confirms) same
subject admission:<spec.id> same
tenant the admission's tenant, exact string (owner-bound, never inferred from namespace) same; must equal data.tenant
correlation_id <run_id> <run_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:

{
  "receipt_version": "<canonical schema version>",
  "run_id": "…",
  "initiator": {"iss": "…", "sub": "admin"},
  "executing_workload": "service:nk-bind-executor",
  "runtime_principal": "system:serviceaccount:sso:<sa>",
  "tenant": "…",
  "environment": "…",
  "source_commit": "<plan.source.commit>",
  "artifact_digest": "<plan_digest>",
  "target": "<admission-level target>",
  "action": "bounded-execution",
  "decision_refs": [
    {"kind": "admission", "id": "<spec.id>"},
    {"kind": "admission_digest", "id": "sha256:…"},
    {"kind": "approval", "id": "<carrier approval reference>"}
  ],
  "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.

Status 2026-10-31 — T02 prepared after NetKingdom's answers

NetKingdom answered on hub messages ed03ba30 and deea5bf8. The answers are based on executor.py at net-kingdom a020758 and rapp-bounded-executor 9826a24.

  • Source net-kingdom.bounded-executor. Event types execution.run.started and execution.attribution.receipt. Tenant platform, which is the exact string. Note that it is not tenant:platform, which approval-engine and informed-decision use. The started event is adopted, so the stream is load-bearing.
  • Registry entry (prepared, not seeded): nk-bounded-executor in deploy/senders-scope.json and deploy/senders-scope.yaml. The entry admits nothing until the Secret carries a token for that name. The overlay may now tighten secret_policy from redact to reject, but it may never relax it (audit_core/senders.py). Even if the Secret entry is minted with the default, the result is therefore reject.
  • Ingress (prepared, not applied): audit-core-nk-bounded-executor-ingress in deploy/networkpolicies.yaml admits only namespace sso combined (AND) with pod label app.kubernetes.io/name=bounded-executor, on TCP 8080.
  • Tests: tests/test_bounded_executor_sender.py runs on the executor's envelope shape. It checks: 202 accept; 200 duplicate on retry; 409 on a reused run id with a different body; 400 source_not_allowed and tenant_not_allowed; 400 secret_shaped_field for a secret-shaped step check.
  • Defect found in the emitter: bounded-executor/clients.py AuditCore.emit accepts only 200 and 201. Audit-core answers a first accept with 202, so the executor would treat a stored started event as a failure and stop with audit_unavailable. NetKingdom has to accept 202 (accepted) and 200 (duplicate). It should treat 409 as a run-id-reuse defect and must not retry it.
  • T03 window bound: a started event that has no receipt with the same correlation_id becomes a finding after the admission's window.not_after. Pilot windows are 2 h.

Remaining work:

  • Founder-run: mint and seed the Secret entry, then roll out the ConfigMap and the NetworkPolicy.
  • NetKingdom: the versioned receipt schema with fixtures (NK-WP-0040-T02) and the 202 fix.
  • railiance-platform: custody of the sender bearer (ESO projection to sso).

Status 2026-11-03 — T03 detection implemented

A started event that has no receipt is reported on GET /v1/stream-findings as opening_without_closing (see docs/stream-completeness.md, "Opening events that never close"). The pair is declared on nk-bounded-executor in deploy/senders-scope.{json,yaml} with a 7200 s bound, which matches the pilot windows. The finding appears 3 h after acceptance at the latest, because the heartbeat grace allowance is added.

The executor's started body (net-kingdom bounded-executor/executor.py) does not yet carry the admission window. If NetKingdom adds data.window.not_after, each run is held to its own window wherever that is earlier than the declared bound. Without it, admissions longer than 2 h rely on the declared ceiling.

The executor's emitter now accepts 202 (accepted) and 200 (duplicate) (clients.AuditCore.emit), which closes the defect recorded on 2026-10-31.

Released 2026-11-03: receiver sha256:5f3d01c8… (built from 2b8c909) and the scope ConfigMap carrying closing_classes are live on railiance01. The detection is inert until the nk-bounded-executor token is seeded and the executor emits.

Status 2026-11-04 — what is still needed live (NK-WP-0056-T15/T16)

Checked read-only on 2026-11-04 with the default (limited) identity:

  • Ingress: not live. audit-core-nk-bounded-executor-ingress is the only object in deploy/networkpolicies.yaml that is absent on railiance01. The nine other policies in that file match the live specs exactly. The executor's own egress (sso/bounded-executor, to audit-core pods on 8080) is already live.
  • Scope ConfigMap: the limited identity cannot read ConfigMaps in audit-core. The release on 2026-11-03 is recorded in 06d0029. The seed script below refuses unless the live entry equals deploy/senders-scope.json.
  • Bearer: not minted. The registry is OpenBao platform/workloads/audit-core/senders (field senders.json). ExternalSecret audit-core/audit-core-senders projects it into the receiver. The same pattern served tenant-engine and the CCR-2026-0021/0022 senders.

How the token reaches both sides. One founder-run process, scripts/seed-nk-bounded-executor-sender.py, does the following:

  1. It mints one bearer.
  2. It writes the bearer with cas 0 to platform/workloads/net-kingdom/bounded-executor/audit-sender (AUDIT_TOKEN, CUSTODY_REQUEST=CCR-2026-0063).
  3. It appends the nk-bounded-executor entry to the registry. The entry is the declared scope plus the token, written with cas on the version read. Every other sender stays byte-equal.
  4. It force-syncs audit-core-senders and compares the token by SHA-256 only.
  5. It restarts the receiver.
  6. It proves the bearer without writing an event. Inside the receiver pod, with the bearer on stdin:
    • a read gets 403 (write-only);
    • reconciliation for its own source and tenant platform gets 200;
    • tenant tenant:platform gets 403;
    • another source gets 403. An unknown bearer gets 401.
  7. It force-syncs sso/bounded-executor-tokens and compares key audit-core by hash, but only if that ExternalSecret exists. That is the Secret the rapp run Job already mounts.

The script is silent. The receipt holds versions, booleans and a 12-hex fingerprint. A rerun reuses its own custody and writes nothing new. It refuses foreign or drifted custody, a sender registered without custody, a token collision and a concurrent registry write. Tests: tests/test_seed_nk_bounded_executor_sender.py.

The script refuses before any write unless all of the following hold:

  • CCR-2026-0063 is approved or later, with all three owner reviews. Its reader shape must be audience openbao, explicit max TTL and token self-lifecycle. Its delivery must be sso/bounded-executor-tokens key audit-core, and it must name this script as its custody procedure;
  • the cluster is railiance01 (kube-system UID);
  • the receiver runs the pinned image;
  • the live scope ConfigMap matches;
  • the ingress policy exists;
  • the coding-agent boundary already denies the custody data and metadata paths.

CCR-2026-0063 (railiance-platform f3b0c4f) names this script as its single seeder. The script does not create the reader policy, the role or the boundary. railiance-platform owns those: its credential-change applier creates the policy and role, and scripts/nk_bind_executor_custody.py fence creates the boundary. Nor does it create ClusterSecretStore openbao-nk-bind-executor-audit or ExternalSecret sso/bounded-executor-tokens, which are railiance-platform's manifests/nk-bind-executor-eso.yaml.

Founder runbook — nk-bounded-executor sender

Run these in order. Steps 1 and 2 can run now. Step 3 depends on CCR-2026-0063: railiance-platform must have it approved by platform-owner, audit-core-owner and net-kingdom-owner, must have applied its reader policy and role, and must have run nk_bind_executor_custody.py fence for the coding-agent boundary entries.

1. Ingress (NK-WP-0056-T16). This applies exactly one object. Run the server dry-run first, then the apply:

cd /home/worsch/audit-core && python3 -c 'import sys,yaml; d=[x for x in yaml.safe_load_all(open("deploy/networkpolicies.yaml")) if x and x["metadata"]["name"]=="audit-core-nk-bounded-executor-ingress"]; assert len(d)==1; yaml.safe_dump(d[0],sys.stdout)' | KUBECONFIG=/home/worsch/.kube/config-railiance01 kubectl apply --dry-run=server -f -
cd /home/worsch/audit-core && python3 -c 'import sys,yaml; d=[x for x in yaml.safe_load_all(open("deploy/networkpolicies.yaml")) if x and x["metadata"]["name"]=="audit-core-nk-bounded-executor-ingress"]; assert len(d)==1; yaml.safe_dump(d[0],sys.stdout)' | KUBECONFIG=/home/worsch/.kube/config-railiance01 kubectl apply -f -

2. Read-only preflight (prints no secrets):

KUBECONFIG=/home/worsch/.kube/config-railiance01 kubectl -n audit-core get configmap audit-core-senders-scope -o jsonpath='{.data.senders-scope\.json}' | python3 -c 'import sys,json; print([e for e in json.load(sys.stdin) if e["name"]=="nk-bounded-executor"])'
KUBECONFIG=/home/worsch/.kube/config-railiance01 kubectl -n audit-core get deploy audit-core -o jsonpath='{.spec.template.spec.containers[0].image}{"\n"}'
bridge status

The image must end in sha256:5f3d01c8a1f90411a1429451b46c66a9ac9262fc7fce68790c16e22eeb7871a6. The openbao-ui-railiance01 tunnel must be up.

3. Seed and reload (NK-WP-0056-T15). Needs CCR-2026-0063 approved and applied. This is one silent command. Read the result from the receipt, not from the terminal:

BAO_ADDR=http://127.0.0.1:18200 VAULT_ADDR=http://127.0.0.1:18200 warden access openbao-platform-admin-login --exec -- /home/worsch/audit-core/scripts/attended-seed-nk-bounded-executor-sender.sh /home/worsch/audit-core/docs/evidence/nk-bounded-executor-sender-seed-r1.json

The receipt is created exclusively. If you rerun, use -r2.json. The receiver restart is part of this step. No separate rollout is needed.

4. Verify (read-only):

python3 -c 'import json; r=json.load(open("/home/worsch/audit-core/docs/evidence/nk-bounded-executor-sender-seed-r1.json")); print(r["status"], r.get("error"), r.get("receiver_probe"), r.get("registry_secret_hash_match"), r.get("sso_secret_hash_match"), r["credential_values_emitted"])'
KUBECONFIG=/home/worsch/.kube/config-railiance01 kubectl -n audit-core get networkpolicy audit-core-nk-bounded-executor-ingress

The expected status is seeded_pending_sso_delivery, or seeded_and_delivered once railiance-platform's ExternalSecret sso/bounded-executor-tokens exists. The expected probe is {"read": 403, "own_tenant": 200, "other_tenant": 403, "other_source": 403}. If railiance-platform applies manifests/nk-bind-executor-eso.yaml after the seed, rerun step 3 with -r2.json. It writes no custody or registry and confirms the sso copy by hash.