NK-WP-0056-T06 prep: admission helper, write-role window-open/close, service-client case; T15 evidence

- bounded-executor/prepare_admission.py: the live admission from the pilot example, pins read
  from the image's commit, window <= 2 h (audit-core's declared bound), validated.
- executor-lanes.py window-open/window-close: CCR-2026-0058's window-scoped write role, created
  for one admitted open window under attended platform-admin and deleted afterwards (idempotent).
- security-cases/nk-bind-executor-approval.yaml: the case of record, proposed (founder acceptance).
- docs/evidence/2026-11-04-nk56-t15-audit-sender: 8 checks against audit-core 876c3f6.

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
This commit is contained in:
tegwick 2026-11-04 01:49:40 +01:00
parent 9bd7bf0bdc
commit c43afd46a7
7 changed files with 368 additions and 6 deletions

View file

@ -0,0 +1,92 @@
#!/usr/bin/env python3
"""Prepare the live pilot admission (NK-WP-0056-T06) from the pilot example.
The executor checks every pinned file against the copy inside its image (source_root /app), so the pins are taken
from the commit the pinned image was built from (`--commit`, read with `git show`), not from the working tree.
The window opens at `--not-before` and lasts `--hours`. The budget, plan, checks and scope ceiling are the
example's, so the admission stays inside the executor's declared grants (make bounded-executor-test).
python3 bounded-executor/prepare_admission.py --id BEA-2026-0002 --commit 9bd7bf0 \\
--not-before 2026-11-05T09:00:00Z --hours 2 --out admissions/BEA-2026-0002.yaml
Prints the admission and plan digests. Writes nothing else and contacts nothing.
"""
from __future__ import annotations
import argparse
import datetime as dt
import hashlib
import importlib.util
import json
import pathlib
import subprocess
import sys
import yaml
HERE = pathlib.Path(__file__).resolve().parent
REPO = HERE.parent
EXAMPLE = REPO / "examples/bounded-execution-admission/bind-rotation-svc-authelia.yaml"
_spec = importlib.util.spec_from_file_location("bea_validate_prepare", REPO / "tools/bounded-execution-admission/validate.py")
bea = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(bea)
def _git(*args: str) -> bytes:
return subprocess.run(["git", "-C", str(REPO), *args], capture_output=True, check=True).stdout
def _z(t: dt.datetime) -> str:
return t.astimezone(dt.UTC).isoformat(timespec="seconds").replace("+00:00", "Z")
def prepare(admission_id: str, commit: str, not_before: dt.datetime, hours: float) -> dict:
full = _git("rev-parse", "--verify", commit + "^{commit}").decode().strip()
doc = bea.load(EXAMPLE)
spec = doc["spec"]
spec.update(id=admission_id, work_ref="NK-WP-0056-T06",
title="Rotate the svc-authelia directory bind (live pilot, one consumer)")
spec["window"] = {"not_before": _z(not_before), "not_after": _z(not_before + dt.timedelta(hours=hours))}
files = [{"path": f["path"], "sha256": hashlib.sha256(_git("show", f"{full}:{f['path']}")).hexdigest()}
for f in spec["plan"]["source"]["files"]]
spec["plan"]["source"].update(commit=full[:12], files=files)
pins = {f["path"]: f["sha256"] for f in files}
for check in spec["checks"]:
check["sha256"] = pins[check["ref"]]
spec["plan_digest"] = bea.plan_digest(doc)
report = bea.evaluate(doc, json.loads(bea.SCHEMA.read_text()), as_of=not_before)
if not report["valid"]:
raise ValueError("admission invalid: " + ",".join(sorted({f["rule"] for f in report["findings"]})))
return doc
def main(argv=None) -> int:
parser = argparse.ArgumentParser(description=__doc__.split("\n", 1)[0])
parser.add_argument("--id", required=True)
parser.add_argument("--commit", required=True, help="the commit the pinned executor image was built from")
parser.add_argument("--not-before", required=True, help="window start, RFC 3339 with offset")
parser.add_argument("--hours", type=float, default=2.0)
parser.add_argument("--out", required=True, type=pathlib.Path)
args = parser.parse_args(argv)
if args.out.exists():
raise SystemExit("output exists; admissions are immutable, choose a new id")
if not 0 < args.hours <= 2:
raise SystemExit("--hours must be in (0, 2]: audit-core's declared bound for a run without receipt is 2 h")
try:
doc = prepare(args.id, args.commit, dt.datetime.fromisoformat(args.not_before.replace("Z", "+00:00")),
args.hours)
except (ValueError, subprocess.CalledProcessError) as error:
print(f"refused: {error}" if isinstance(error, ValueError) else "refused: unknown commit or path",
file=sys.stderr)
return 4
args.out.parent.mkdir(parents=True, exist_ok=True)
args.out.write_text("# NK-WP-0056-T06 live pilot admission. Immutable once requested: a change is a new id.\n"
+ yaml.safe_dump(doc, sort_keys=False, width=120))
print(json.dumps({"admission": args.id, "admission_digest": bea.admission_digest(doc),
"plan_digest": doc["spec"]["plan_digest"], "window": doc["spec"]["window"],
"pinned_commit": doc["spec"]["plan"]["source"]["commit"]}, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -0,0 +1,55 @@
"""Live pilot admission preparation (NK-WP-0056-T06)."""
from __future__ import annotations
import datetime as dt
import hashlib
import importlib.util
import json
import pathlib
import subprocess
import sys
import tempfile
import unittest
HERE = pathlib.Path(__file__).resolve().parent
spec = importlib.util.spec_from_file_location("prepare_admission", HERE.parent / "prepare_admission.py")
P = importlib.util.module_from_spec(spec)
sys.modules["prepare_admission"] = P
spec.loader.exec_module(P)
START = dt.datetime(2026, 11, 5, 9, 0, tzinfo=dt.UTC)
@unittest.skipUnless((P.REPO / ".git").exists(), "needs a git checkout (CI tests a source tarball)")
class Prepare(unittest.TestCase):
def setUp(self):
self.commit = subprocess.run(["git", "-C", str(P.REPO), "rev-parse", "HEAD"], capture_output=True,
text=True, check=True).stdout.strip()
def test_pins_the_commits_content_and_is_valid(self):
doc = P.prepare("BEA-2026-0099", self.commit, START, 2)
spec = doc["spec"]
self.assertEqual(spec["window"], {"not_before": "2026-11-05T09:00:00Z", "not_after": "2026-11-05T11:00:00Z"})
self.assertEqual(spec["plan"]["source"]["commit"], self.commit[:12])
for f in spec["plan"]["source"]["files"]:
blob = subprocess.run(["git", "-C", str(P.REPO), "show", f"{self.commit}:{f['path']}"],
capture_output=True, check=True).stdout
self.assertEqual(f["sha256"], hashlib.sha256(blob).hexdigest())
report = P.bea.evaluate(doc, json.loads(P.bea.SCHEMA.read_text()), as_of=START)
self.assertTrue(report["valid"], report["findings"])
def test_refusals(self):
with tempfile.TemporaryDirectory() as tmp:
out = pathlib.Path(tmp) / "a.yaml"
out.write_text("exists")
with self.assertRaises(SystemExit):
P.main(["--id", "BEA-2026-0099", "--commit", self.commit, "--not-before", "2026-11-05T09:00:00Z",
"--out", str(out)])
with self.assertRaises(SystemExit):
P.main(["--id", "BEA-2026-0099", "--commit", self.commit, "--not-before", "2026-11-05T09:00:00Z",
"--hours", "3", "--out", str(pathlib.Path(tmp) / "b.yaml")])
self.assertEqual(P.main(["--id", "BEA-2026-0099", "--commit", "0" * 40, "--not-before",
"2026-11-05T09:00:00Z", "--out", str(pathlib.Path(tmp) / "c.yaml")]), 4)
if __name__ == "__main__":
unittest.main()

View file

@ -0,0 +1,44 @@
# NK-WP-0056-T15: executor audit sender against audit-core's real ingestion (2026-11-04)
Agent-run, local only, synthetic tokens, everything removed afterwards:
`python3 bounded-executor/exercise_audit_sender.py --receipt FILE`.
**Setup:**
- **audit-core:** source `876c3f6`. It runs in-process with `IngestionApplication` and the SQLite backend. The sender
registry is built the way production builds it: a Secret-shaped `AUDIT_CORE_SENDERS` entry, deliberately minted with
the defaults, plus the Git scope overlay `deploy/senders-scope.json`.
- **Executor:** the real code. `executor.Run.execute()` runs on in-memory operations and emits through
`clients.AuditCore` over HTTP with a token file. Two runs: one completed, one reverted.
Receipt: [`receipt.json`](receipt.json). It is value-free; the harness asserts that no generated token appears in it.
| Check | Result |
| --- | --- |
| Completed run: started and receipt | 202 each |
| Reverted run: started and receipt | 202 each |
| Both emitted pairs against receipt schema v0.1 (EAR-1..EAR-10) | valid |
| Retry of the same event | 200 duplicate; the client counts it as stored |
| Same id with another body | 409; the client reports "run id reused", never retried |
| Wrong source, wrong tenant (`tenant:platform`), another sender's token, no token, unknown token | refused, and nothing stored under that run id |
| A started event with no receipt, whose admission window closed 2 h ago | `opening_without_closing`, `bound: window.not_after` |
| The closed runs | not flagged |
**Built for this task** (`d7133e3`):
- `canon/schemas/execution-attribution-receipt_v0.1.schema.json`
- `tools/execution-attribution-receipt/`: the validator, and fixtures generated from the executor
- the started event now carries `data.window.not_after`, which audit-core asked for in AUDIT-WP-0018-T03
**Limitations:**
- audit-core runs from source, not from the released receiver image.
- The executor's operations are in-memory; the rotation itself is T12's synthetic pilot.
- Plain HTTP on loopback.
**Live path:**
- Custody of the sender bearer is CCR-2026-0063 (railiance-platform, proposed). It is delivered by ESO as
`sso/bounded-executor-tokens` key `audit-core`, which the run Job already mounts.
- Ingress is `audit-core-nk-bounded-executor-ingress` (T16).
- The founder seed is in audit-core `docs/bounded-executor-receipts.md`, "Founder runbook".

View file

@ -0,0 +1,32 @@
{
"schema": "netkingdom.bounded-executor.audit-sender-exercise/v1",
"task": "NK-WP-0056-T15",
"target": "disposable local processes; synthetic tokens",
"started_at": "2026-11-04T00:40:11+00:00",
"audit_core_commit": "876c3f6647798daff56086e03d413f4559bd32a1",
"senders_scope_sha256": "bd29068f9738e3f20939a1e42f96b3c8f836ef6c1bfbc60b39361ca7aa19ebfc",
"net_kingdom_commit": "d7133e3a0489373fa7c9fbf581e36509218e496b",
"executor_source_sha256": {
"executor.py": "e82cd03eba32f4542c1d4842d13f442af8db098deb380160c3fc6c158b1d0d58",
"clients.py": "f0ce580f1beba91b7154051a35c088205de01f26d63a119a23bd926fe1c772cb"
},
"receipt_schema_sha256": "5036bffc81380183e86a7c0c6199c8e72446f7e4e66a79ea4fc4123475d8ac3b",
"limitations": [
"audit-core runs from source in-process (SQLite backend), not the released receiver image",
"the executor's operations are in-memory; the bind rotation itself is T12's synthetic pilot",
"plain HTTP on loopback; the live path is the ESO-delivered bearer over the sso ingress (T16)"
],
"checks": {
"completed_run_started_and_receipt_accepted_202": true,
"reverted_run_started_and_receipt_accepted_202": true,
"emitted_pairs_pass_receipt_schema_v0_1": true,
"retry_is_duplicate_200": true,
"same_id_other_body_conflict_409_reported_as_defect": true,
"wrong_source_tenant_or_token_refused_and_not_stored": true,
"started_without_receipt_flagged_by_its_own_window": true,
"closed_runs_not_flagged": true
},
"status": "passed",
"cleanup_complete": true,
"finished_at": "2026-11-04T00:40:11+00:00"
}

View file

@ -0,0 +1,25 @@
# KeyCape service client for the bounded executor's approval calls (NK-WP-0056-T06 prerequisite).
# The case of record; identical parameters to the type's first example. Status proposed until the founder
# accepts it (type acceptance: owner and founder). Going live makes oidc-service-client 0.2 `implemented`.
apiVersion: netkingdom.io/security-case/v0.1
kind: SecurityCase
spec:
id: nk-bind-executor-approval
type: oidc-service-client@0.2
status: proposed
owner:
repo: net-kingdom
person: founder
parameters:
client_id: nk-bind-executor-approval
display_name: NetKingdom bounded executor (approval-engine)
audience: approval-engine
allowed_scopes: approval:create,approval:read,approval:consume
service_subject: service:nk-bind-executor
tenant: tenant:platform
roles: bounded-executor
token_lifetime: 15m
consuming_service: bounded-executor
consumer_namespace: sso
consumer_service_account: nk-bind-executor
consumer_owner_repo: rapp-bounded-executor

View file

@ -72,6 +72,8 @@ class FakeBao:
if path == JOURNEY and token == 'reader':
return out({'data': {'data': {'PASSWORD': 'SYNTHETIC-PW-VALUE', 'TOTP_SEED': 'SYNTHETIC-SEED-VALUE'}, 'metadata': {'version': 1}}})
return out({'data': self.store[path]}) if path in self.store else missing
if args[0] == 'delete':
self.store.pop(args[-1], None); return out({})
if args[0] == 'write':
path = args[-2] if args[-1] == '-' else args[-1]
if path == 'auth/token/revoke': self.revoked.append(payload['token']); return out({})
@ -175,3 +177,51 @@ def test_verify_refusals(tmp_path, monkeypatch, mutate, reason):
mutate(m, fake)
r = run(m, 'verify', d)
assert (r['status'], r['reason']) == ('failed', reason)
ADMISSION = ROOT / 'examples/bounded-execution-admission/bind-rotation-svc-authelia.yaml'
def admission_at(tmp_path, m):
"""The pilot example, its window around a fixed instant."""
import datetime as dt
bea = m.load('bea_for_test', m.BEA)
doc = bea.load(ADMISSION)
path = tmp_path / 'a.yaml'
path.write_text(json.dumps(doc))
start = bea.parse_time(doc['spec']['window']['not_before'])
return path, start + dt.timedelta(minutes=10)
def test_window_open_creates_the_exact_write_role_once_then_close_removes_it(tmp_path, monkeypatch):
m, fake, d = build(tmp_path, monkeypatch)
run(m, 'setup', d)
path, now = admission_at(tmp_path, m)
r = {}
m.window_open(d, path, r, now=now)
role = 'auth/kubernetes/role/' + m.LANES['write']['role']
assert r['status'] == 'window_opened' and r['admission'] == 'BEA-2026-0001' and role in fake.store
m.check_role(fake.store[role], m.LANES['write'])
with pytest.raises(m.Refused, match='write_role_already_open'):
m.window_open(d, path, {}, now=now)
c = {}
m.window_close(path, c)
assert c['status'] == 'window_closed' and c['write_role_was_present'] and role not in fake.store
c = {}
m.window_close(path, c) # idempotent, also late
assert c['status'] == 'window_closed' and not c['write_role_was_present']
@pytest.mark.parametrize('mutate, reason', [
(lambda m, f, now: (now - __import__('datetime').timedelta(hours=1)), 'admission_window_not_open'),
(lambda m, f, now: (f.identity.update(policies=['root']), now)[1], 'attended_platform_admin_required'),
(lambda m, f, now: (setattr(f, 'cas', False), now)[1], 'cas_required_not_set'),
(lambda m, f, now: (f.store.update({m.BOUNDARY: {'policy': ''}}), now)[1], 'agent_fence_missing'),
])
def test_window_open_refusals(tmp_path, monkeypatch, mutate, reason):
m, fake, d = build(tmp_path, monkeypatch)
run(m, 'setup', d)
path, now = admission_at(tmp_path, m)
with pytest.raises(m.Refused, match=reason):
m.window_open(d, path, {}, now=mutate(m, fake, now))
assert 'auth/kubernetes/role/' + m.LANES['write']['role'] not in fake.store

View file

@ -1,8 +1,8 @@
#!/usr/bin/env python3
"""Attended OpenBao setup and proof for the bounded executor's two lanes (NK-WP-0056-T10, T06).
write CCR-2026-0058 policy only. Its Kubernetes role is window-scoped: created per admitted run,
never here. Also sets cas_required on the svc-authelia key.
write CCR-2026-0058 policy only. Its Kubernetes role is window-scoped: created per admitted run by
window-open, deleted by window-close. setup sets cas_required on the svc-authelia key.
journey CCR-2026-0059 policy and the standing read role for sso/nk-bind-executor.
setup: under both approved, sha256-bound CCRs (binding confirmed) and an attended platform-admin session:
@ -11,6 +11,11 @@ setup: under both approved, sha256-bound CCRs (binding confirmed) and an attend
verify: read back policies, cas_required and the role; log in as sso/nk-bind-executor (audience openbao) and prove
that the token reads only the journey path, with exactly its policy and at most 15m. Prove that a wrong
ServiceAccount, a wrong audience and a login to the write role are all denied.
window-open --admission FILE: for one admitted run (T06): the admission is valid, names this executor and the write
lane, and its window is open now; the policy, cas_required and the fence hold; the write role is absent. Then
create the write role exactly as CCR-2026-0058 declares it.
window-close --admission FILE: after the run's receipt or at window end, whichever comes first: delete the write role
and prove it is gone. Always allowed, also late; needs no CCR digests.
Receipts hold closed booleans only. The journey values are read into memory to check their field names, and never
printed or written.
"""
@ -229,22 +234,81 @@ def verify(digests, kubeconfig, receipt):
source_ccrs={n: c['id'] for n, c in ccrs.items()}, ccr_sha256=digests)
BEA = REPO / 'tools/bounded-execution-admission/validate.py'
WRITE_SCOPE = 'openbao/' + LANES['write']['kv']
def admitted_window(admission_path, now=None):
"""The admission that opens the window: valid, for this executor, naming the write lane, window open now."""
bea = load('bea_validate_lanes', BEA)
doc = bea.load(Path(admission_path))
now = now or __import__('datetime').datetime.now(__import__('datetime').UTC)
if not bea.evaluate(doc, json.loads(bea.SCHEMA.read_text()), as_of=now)['valid']: raise Refused('admission_invalid')
spec = doc['spec']
if spec['executor']['identity'] != 'service:' + SA: raise Refused('admission_other_executor')
if not any(c['mode'] == 'OPERATE' and c['scope'] == WRITE_SCOPE for c in spec['scope_ceiling']):
raise Refused('admission_does_not_name_write_lane')
start, end = (bea.parse_time(spec['window'][k]) for k in ('not_before', 'not_after'))
if not start <= now < end: raise Refused('admission_window_not_open')
return {'admission': spec['id'], 'admission_digest': bea.admission_digest(doc),
'window': dict(spec['window'])}
def window_open(digests, admission_path, receipt, now=None):
"""Create the write role for one admitted window (CCR-2026-0058 lifecycle.role_window). Refuses if it exists."""
receipt['phase'] = 'contract'; ccrs = contracts(digests)
receipt['phase'] = 'admission'; receipt.update(admitted_window(admission_path, now))
receipt['phase'] = 'identity'; attended_platform_admin()
receipt['phase'] = 'preconditions'
write = LANES['write']
policy_check(write)(data(bao(['read', '-format=json', 'sys/policies/acl/' + write['policy']]))['data'])
if not cas_required(): raise Refused('cas_required_not_set')
if not fence_present(): raise Refused('agent_fence_missing')
if not write_role_absent(): raise Refused('write_role_already_open')
receipt['phase'] = 'create_role'
ensure('auth/kubernetes/role/' + write['role'], role_payload(write), lambda actual: check_role(actual, write))
receipt.update(phase='done', status='window_opened', write_role_created=True, exact_role=True,
close_by=receipt['window']['not_after'], source_ccrs={n: c['id'] for n, c in ccrs.items()},
ccr_sha256=digests)
def window_close(admission_path, receipt):
"""Delete the write role after the run's receipt or at window end (whichever comes first) and prove it is gone.
Needs no open window: closing late must always be possible."""
bea = load('bea_validate_lanes', BEA)
spec = bea.load(Path(admission_path))['spec']
receipt.update(admission=spec['id'])
receipt['phase'] = 'identity'; attended_platform_admin()
receipt['phase'] = 'delete_role'
was_open = not write_role_absent()
if was_open:
bao(['delete', 'auth/kubernetes/role/' + LANES['write']['role']])
if not write_role_absent(): raise Refused('write_role_still_present')
receipt.update(phase='done', status='window_closed', write_role_was_present=was_open, write_role_absent=True)
def main():
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
p.add_argument('action', choices=['setup', 'verify'])
p.add_argument('--write-ccr-sha256', required=True, help='sha256 of the CCR-2026-0058 file')
p.add_argument('--journey-ccr-sha256', required=True, help='sha256 of the CCR-2026-0059 file')
p.add_argument('action', choices=['setup', 'verify', 'window-open', 'window-close'])
p.add_argument('--write-ccr-sha256', help='sha256 of the CCR-2026-0058 file (all but window-close)')
p.add_argument('--journey-ccr-sha256', help='sha256 of the CCR-2026-0059 file (all but window-close)')
p.add_argument('--kubeconfig', help='admin kubeconfig; required for verify')
p.add_argument('--admission', help='the admitted BEA file; required for window-open and window-close')
p.add_argument('--receipt', required=True)
a = p.parse_args()
if a.action == 'verify' and not a.kubeconfig: p.error('--kubeconfig is required for verify')
if a.action.startswith('window-') and not a.admission: p.error('--admission is required for ' + a.action)
if a.action != 'window-close' and not (a.write_ccr_sha256 and a.journey_ccr_sha256):
p.error('--write-ccr-sha256 and --journey-ccr-sha256 are required for ' + a.action)
digests = {'write': a.write_ccr_sha256, 'journey': a.journey_ccr_sha256}
fd = os.open(a.receipt, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
r = {'status': 'failed', 'action': a.action, 'values_recorded': False,
'source_sha256': hashlib.sha256(Path(__file__).read_bytes()).hexdigest()}
try:
if a.action == 'setup': setup(digests, r)
else: verify(digests, a.kubeconfig, r)
elif a.action == 'verify': verify(digests, a.kubeconfig, r)
elif a.action == 'window-open': window_open(digests, a.admission, r)
else: window_close(a.admission, r)
except Exception as error:
r.update(status='failed', reason=str(error) if isinstance(error, Refused) else type(error).__name__)
finally: