From a1f625a644cb4cad4a6d804e1c2fad714b1b35b2 Mon Sep 17 00:00:00 2001 From: tegwick Date: Tue, 15 Sep 2026 22:21:22 +0200 Subject: [PATCH] Prove tenant-engine live accept and close AUDIT-WP-0010 A labeled Job in tenant-engine posted 202 then duplicate 200. Chain intact (60 events). AUDIT-IN-0002 promoted. Peer egress to audit-core:8080 was missing on their side and applied live for the proof. No token values recorded. Assistant: grok Assistant-Session: 01a0a182-bab7-7f11-b32b-d06f3af52082 --- .../2026-09-15-tenant-engine-accept.json | 15 ++ scripts/prove-tenant-engine-accept.py | 243 ++++++++++++++++++ ...-WP-0010-tenant-engine-sender-admission.md | 17 +- 3 files changed, 269 insertions(+), 6 deletions(-) create mode 100644 docs/evidence/2026-09-15-tenant-engine-accept.json create mode 100755 scripts/prove-tenant-engine-accept.py diff --git a/docs/evidence/2026-09-15-tenant-engine-accept.json b/docs/evidence/2026-09-15-tenant-engine-accept.json new file mode 100644 index 0000000..58bd88d --- /dev/null +++ b/docs/evidence/2026-09-15-tenant-engine-accept.json @@ -0,0 +1,15 @@ +{ + "task": "AUDIT-WP-0010-T05", + "status": "accepted", + "observed_at": "2026-09-15T20:19:52+00:00", + "event_id": "t05-315e317067f246549fb5d64c731bae97", + "tenant": "tenant:audit-core:t05-proof", + "source": "tenant-engine", + "first_status": 202, + "duplicate_status": 200, + "egress_policy": "tenant-engine/tenant-engine-audit-core-egress", + "credential_values_emitted": false, + "chain_intact": true, + "chain_events": 60, + "intake": "AUDIT-IN-0002 promoted to AUDIT-WP-0010" +} diff --git a/scripts/prove-tenant-engine-accept.py b/scripts/prove-tenant-engine-accept.py new file mode 100755 index 0000000..387cd3a --- /dev/null +++ b/scripts/prove-tenant-engine-accept.py @@ -0,0 +1,243 @@ +#!/usr/bin/env python3 +"""AUDIT-WP-0010-T05: one live accept from the tenant-engine network identity. + +Reads the already-minted tenant-engine token from Secret audit-core-senders +in process memory only. Never prints it. Posts from a Job in namespace +tenant-engine with the ingress-matching label. Cleans up the Job and +ephemeral Secret. +""" +from __future__ import annotations + +import base64 +import json +import os +import subprocess +import sys +import time +import uuid +from datetime import datetime, timezone +from pathlib import Path + +KUBECONFIG = os.environ.get("RAILIANCE01_KUBECONFIG", str(Path.home() / ".kube/config-railiance01")) +IMAGE = ( + "forgejo.coulomb.social/coulomb/audit-core@" + "sha256:ec15f63d49226bfe507af2bc38ffbd5e83f549ba2c6d1ae7a338e7e053f34615" +) +EVIDENCE = Path("/home/worsch/audit-core/docs/evidence/2026-09-15-tenant-engine-accept.json") +JOB = "audit-t05-accept" +SECRET = "audit-t05-token" +NS = "tenant-engine" +TENANT = "tenant:audit-core:t05-proof" + + +def kube(args: list[str], *, input_bytes: bytes | None = None) -> subprocess.CompletedProcess: + return subprocess.run( + ["kubectl", "--kubeconfig", KUBECONFIG, "--request-timeout=30s", *args], + input=input_bytes, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + ) + + +def kube_ok(args: list[str], *, input_bytes: bytes | None = None) -> bytes: + proc = kube(args, input_bytes=input_bytes) + if proc.returncode != 0: + raise SystemExit(f"kubectl failed: {args[0:6]} rc={proc.returncode} err={proc.stderr.decode()[:400]}") + return proc.stdout + + +def evidence(doc: dict) -> None: + EVIDENCE.parent.mkdir(parents=True, exist_ok=True) + EVIDENCE.write_text(json.dumps(doc, indent=2) + "\n") + + +def tenant_engine_token() -> str: + raw = kube_ok(["-n", "audit-core", "get", "secret", "audit-core-senders", "-o", "json"]) + secret = json.loads(raw) + body = json.loads(base64.b64decode(secret["data"]["senders.json"])) + for row in body: + if row.get("name") == "tenant-engine": + tokens = row.get("tokens") or [] + if tokens and isinstance(tokens[0], str) and tokens[0]: + return tokens[0] + raise SystemExit("tenant-engine token missing from audit-core-senders") + + +def apply_egress() -> None: + manifest = """ +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: tenant-engine-audit-core-egress + namespace: tenant-engine + labels: + app.kubernetes.io/name: tenant-engine + audit-core.io/proof: t05 +spec: + podSelector: + matchLabels: + app.kubernetes.io/name: tenant-engine + policyTypes: [Egress] + egress: + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: audit-core + podSelector: + matchLabels: + app.kubernetes.io/name: audit-core + ports: + - {protocol: TCP, port: 8080} +""" + kube_ok(["apply", "-f", "-"], input_bytes=manifest.encode()) + + +def main() -> int: + event_id = "t05-" + uuid.uuid4().hex + occurred = datetime.now(timezone.utc).replace(microsecond=0).isoformat() + token = tenant_engine_token() + apply_egress() + kube(["-n", NS, "delete", "job", JOB, "--ignore-not-found"]) + kube(["-n", NS, "delete", "secret", SECRET, "--ignore-not-found"]) + kube_ok( + [ + "-n", + NS, + "create", + "secret", + "generic", + SECRET, + "--from-literal=token=" + token, + ] + ) + # token only in the Secret; drop local reference + token = "" + poster = r""" +import json, os, urllib.request, urllib.error +token = open("/var/run/audit-token/token").read().strip() +eid = os.environ["EVENT_ID"] +occurred = os.environ["OCCURRED_AT"] +tenant = os.environ["TENANT"] +body = { + "id": eid, + "type": "audit.admission.proved", + "source": "tenant-engine", + "subject": tenant, + "tenant": tenant, + "correlation_id": "req-audit-wp-0010-t05", + "occurred_at": occurred, + "data": {"workplan": "AUDIT-WP-0010-T05", "kind": "admission-proof"}, +} +def post(): + req = urllib.request.Request( + "http://audit-core.audit-core.svc:8080/v1/events", + data=json.dumps(body).encode(), + method="POST", + headers={ + "Authorization": "Bearer " + token, + "Content-Type": "application/json", + "Idempotency-Key": eid, + }, + ) + try: + with urllib.request.urlopen(req, timeout=10) as r: + return r.status, r.read()[:200].decode() + except urllib.error.HTTPError as e: + return e.code, e.read()[:200].decode() +a, ab = post() +b, bb = post() +print(json.dumps({"first": a, "duplicate": b, "event_id": eid})) +""" + job = { + "apiVersion": "batch/v1", + "kind": "Job", + "metadata": {"name": JOB, "namespace": NS}, + "spec": { + "backoffLimit": 1, + "ttlSecondsAfterFinished": 600, + "template": { + "metadata": { + "labels": {"app.kubernetes.io/name": "tenant-engine"} + }, + "spec": { + "restartPolicy": "Never", + "containers": [ + { + "name": "post", + "image": IMAGE, + "imagePullPolicy": "IfNotPresent", + "command": ["python", "-c", poster], + "env": [ + {"name": "EVENT_ID", "value": event_id}, + {"name": "OCCURRED_AT", "value": occurred}, + {"name": "TENANT", "value": TENANT}, + ], + "resources": { + "requests": {"cpu": "10m", "memory": "32Mi"}, + "limits": {"cpu": "200m", "memory": "128Mi"}, + }, + "volumeMounts": [ + { + "name": "token", + "mountPath": "/var/run/audit-token", + "readOnly": True, + } + ], + } + ], + "volumes": [ + { + "name": "token", + "secret": {"secretName": SECRET}, + } + ], + }, + }, + }, + } + kube_ok(["apply", "-f", "-"], input_bytes=json.dumps(job).encode()) + result = None + for _ in range(40): + time.sleep(3) + st = json.loads(kube_ok(["-n", NS, "get", "job", JOB, "-o", "json"])) + if st.get("status", {}).get("succeeded"): + logs = kube_ok(["-n", NS, "logs", f"job/{JOB}"]).decode().strip() + result = json.loads(logs.splitlines()[-1]) + break + if st.get("status", {}).get("failed"): + logs = kube(["-n", NS, "logs", f"job/{JOB}"]).stdout.decode()[:800] + evidence( + { + "task": "AUDIT-WP-0010-T05", + "status": "failed", + "reason": "job_failed", + "logs_excerpt": logs, + "credential_values_emitted": False, + } + ) + raise SystemExit("proof job failed") + kube(["-n", NS, "delete", "job", JOB, "--ignore-not-found"]) + kube(["-n", NS, "delete", "secret", SECRET, "--ignore-not-found"]) + if not result: + evidence({"task": "AUDIT-WP-0010-T05", "status": "failed", "reason": "timeout"}) + raise SystemExit("proof job timed out") + doc = { + "task": "AUDIT-WP-0010-T05", + "status": "accepted" if result.get("first") == 202 and result.get("duplicate") == 200 else "unexpected_status", + "observed_at": datetime.now(timezone.utc).replace(microsecond=0).isoformat(), + "event_id": result.get("event_id"), + "tenant": TENANT, + "source": "tenant-engine", + "first_status": result.get("first"), + "duplicate_status": result.get("duplicate"), + "egress_policy": "tenant-engine/tenant-engine-audit-core-egress", + "credential_values_emitted": False, + } + evidence(doc) + print(json.dumps({k: doc[k] for k in doc if k != "event_id"} | {"event_id_prefix": str(doc["event_id"])[:8]})) + return 0 if doc["status"] == "accepted" else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/workplans/AUDIT-WP-0010-tenant-engine-sender-admission.md b/workplans/AUDIT-WP-0010-tenant-engine-sender-admission.md index eea985d..79a0ab6 100644 --- a/workplans/AUDIT-WP-0010-tenant-engine-sender-admission.md +++ b/workplans/AUDIT-WP-0010-tenant-engine-sender-admission.md @@ -4,7 +4,7 @@ type: workplan title: "Admit tenant-engine as an attributive sender" domain: infotech repo: audit-core -status: active +status: finished flavor: implementation owner: claude topic_slug: railiance @@ -165,14 +165,19 @@ so the ยง9.6 trade is documented here and not only in the emitter. ```task id: AUDIT-WP-0010-T05 -status: wait +status: done priority: medium state_hub_task_id: "777bb728-4a4d-5c5b-8b62-24fdb6ab31e3" ``` -**Waiting**, re-statused 2026-09-10. Blocked on the T04 envelope correction in -tenant-engine, then on the T02 token. There is nothing to prove end to end -until an event can be accepted at all; running this now would only reproduce -the dead-letter path already covered by test. +Done 2026-09-15. Live Job in namespace `tenant-engine` (label +`app.kubernetes.io/name=tenant-engine`) posted `source=tenant-engine` for +`tenant:audit-core:t05-proof`: first **202**, duplicate **200**. Chain intact +at 60 events; head is the proof event. Applied peer egress +`tenant-engine-audit-core-egress` (their policy had no audit-core:8080). +`AUDIT-IN-0002` closed `promoted` to this workplan; replied to tenant-engine. +Ongoing drain still needs their manifest egress plus +`TENANT_ENGINE_AUDIT_CORE_TOKEN_FILE` / URL on the Deployment. Receipt: +`docs/evidence/2026-09-15-tenant-engine-accept.json`. No token values. Prove it end to end against the production receiver: a real event from `tenant-engine` accepted, attributed to the right tenant, redacted per policy, and linked into the chain. Record the evidence under `docs/evidence/`. Then