From 387f8b1c8b2518f3cd5cff12707afb4d1c8da6d6 Mon Sep 17 00:00:00 2001 From: tegwick Date: Sat, 31 Oct 2026 18:27:30 +0100 Subject: [PATCH] NK-WP-0056: sso ingress for the bounded executor; README and handoff follow-up netpol-sso.yaml admits only executor pods to LLDAP 17170, KeyCape 8080 and Authelia 9091 (never LDAP 3890); test_netpol.py pins it. Prepared, not applied. README reflects T13. Live reads: OpenBao open to all namespaces; approval-engine and audit-core need pod-scoped rules (requested: hub messages 39a3bffa, deea5bf8). Co-Authored-By: Claude Opus 5.5 Assistant: claude-code Assistant-Model: opus Assistant-Process: 5502@bnt-lap001 Assistant-Session: 68b3b02f-5d0f-4e71-8421-796f62e50196 --- bounded-executor/README.md | 11 ++- sso-mfa/k8s/bounded-executor/test_netpol.py | 45 +++++++++++++ sso-mfa/k8s/network-policies/netpol-sso.yaml | 67 +++++++++++++++++++ workplans/NK-WP-0056-bounded-executor-path.md | 13 ++++ 4 files changed, 134 insertions(+), 2 deletions(-) create mode 100644 sso-mfa/k8s/bounded-executor/test_netpol.py diff --git a/bounded-executor/README.md b/bounded-executor/README.md index 1a30b95..d91d73d 100644 --- a/bounded-executor/README.md +++ b/bounded-executor/README.md @@ -2,7 +2,7 @@ Runs one founder-approved Bounded Execution Admission (`canon/standards/bounded-execution-admission_v0.1.md`) exactly once. Founder decision 6d1adce9: net-kingdom writes the executor; `rapp-bounded-executor` deploys it as one -Job per admission in `sso` (T12). +Job per admission in `sso`. `executor.py` is the core runner (T11): @@ -44,7 +44,14 @@ without guessing. `synthetic_pilot.py` runs a rotation and a deliberate revert on the T08 stack plus OpenBao: `uv run bounded-executor/synthetic_pilot.py`. -T13 adds the CLI, the Containerfile, the image workflow and the rapp package. +`cli.py` (T13) is the Job's entry point: one admission, one approval id, a non-secret config. Exit codes: `0` +completed, `3` reverted, `4` refused before any effect, `5` stopped, unknown or failed revert, `6` consumed but not in +custody. `Containerfile` builds the image, which `.forgejo/workflows/bounded-executor-image.yaml` publishes by digest. + +Deployment is in `rapp-bounded-executor`, which pins the digest and renders one ConfigMap and Job per admission +(`make run`, founder-run). That package also holds the ServiceAccount and Role, copied from +`sso-mfa/k8s/bounded-executor/rbac.yaml`, and the executor's egress-only NetworkPolicy. The receiving side in `sso` is +`allow-bounded-executor-to-*` in `sso-mfa/k8s/network-policies/netpol-sso.yaml`. ```bash python3 -m pytest bounded-executor/tests # or: make bounded-executor-test diff --git a/sso-mfa/k8s/bounded-executor/test_netpol.py b/sso-mfa/k8s/bounded-executor/test_netpol.py new file mode 100644 index 0000000..72a3a9d --- /dev/null +++ b/sso-mfa/k8s/bounded-executor/test_netpol.py @@ -0,0 +1,45 @@ +"""The sso namespace admits the bounded executor exactly where its pilot steps go (NK-WP-0056). No cluster.""" + +import pathlib +import unittest + +import yaml + +NETPOL = pathlib.Path(__file__).resolve().parents[1] / "network-policies" / "netpol-sso.yaml" +EXECUTOR = {"app.kubernetes.io/name": "bounded-executor"} +EXPECTED = {("lldap", 17170), ("keycape", 8080), ("authelia", 9091)} + + +def policies(): + return [d for d in yaml.safe_load_all(NETPOL.read_text()) if d] + + +def executor_ingress(): + for pol in policies(): + for rule in pol["spec"].get("ingress", []): + if any(peer.get("podSelector", {}).get("matchLabels") == EXECUTOR for peer in rule["from"]): + yield pol, rule + + +class ExecutorIngress(unittest.TestCase): + def test_exactly_the_pilot_paths(self): # in particular never LDAP :3890 + found = set() + for pol, rule in executor_ingress(): + target = pol["spec"]["podSelector"]["matchLabels"]["app.kubernetes.io/name"] + found |= {(target, p["port"]) for p in rule["ports"]} + self.assertEqual(found, EXPECTED) + + def test_peers_are_in_namespace_executor_pods_only(self): + for pol, rule in executor_ingress(): + self.assertEqual(pol["metadata"]["namespace"], "sso") + self.assertEqual(pol["spec"]["policyTypes"], ["Ingress"]) + for peer in rule["from"]: + self.assertEqual(peer, {"podSelector": {"matchLabels": EXECUTOR}}) + + def test_default_deny_is_kept(self): + deny = [p for p in policies() if p["metadata"]["name"] == "default-deny-all"] + self.assertEqual(deny[0]["spec"], {"podSelector": {}, "policyTypes": ["Ingress", "Egress"]}) + + +if __name__ == "__main__": + unittest.main() diff --git a/sso-mfa/k8s/network-policies/netpol-sso.yaml b/sso-mfa/k8s/network-policies/netpol-sso.yaml index 15177ec..c45d994 100644 --- a/sso-mfa/k8s/network-policies/netpol-sso.yaml +++ b/sso-mfa/k8s/network-policies/netpol-sso.yaml @@ -11,6 +11,7 @@ # user-engine → authelia :9091 (read GET /api/state for an existing sign-in) # Traefik → authelia :9091 (login portal, user-facing) # Traefik → lldap :17170 (admin web UI; IP-restricted at Traefik layer) +# bounded-executor → lldap :17170, keycape :8080, authelia :9091 (NK-WP-0056 Job) # # Allowed egress paths: # keycape → authelia :9091 (OIDC callback orchestration) @@ -374,6 +375,72 @@ spec: protocol: TCP --- +# ── Bounded executor → LLDAP :17170, KeyCape :8080, Authelia :9091 ─────────── +# NK-WP-0056: the per-admission executor Job (rapp-bounded-executor, label +# app.kubernetes.io/name=bounded-executor) self-rotates a bind through the LLDAP +# web API and runs the synthetic journey in-cluster against KeyCape and Authelia. +# The executor's own egress-only policy lives in rapp-bounded-executor. No pod +# exists unless an admitted Job is running. +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: allow-bounded-executor-to-lldap-api + namespace: sso +spec: + podSelector: + matchLabels: + app.kubernetes.io/name: lldap + policyTypes: + - Ingress + ingress: + - from: + - podSelector: + matchLabels: + app.kubernetes.io/name: bounded-executor + ports: + - port: 17170 + protocol: TCP +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: allow-bounded-executor-to-keycape + namespace: sso +spec: + podSelector: + matchLabels: + app.kubernetes.io/name: keycape + policyTypes: + - Ingress + ingress: + - from: + - podSelector: + matchLabels: + app.kubernetes.io/name: bounded-executor + ports: + - port: 8080 + protocol: TCP +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: allow-bounded-executor-to-authelia + namespace: sso +spec: + podSelector: + matchLabels: + app.kubernetes.io/name: authelia + policyTypes: + - Ingress + ingress: + - from: + - podSelector: + matchLabels: + app.kubernetes.io/name: bounded-executor + ports: + - port: 9091 + protocol: TCP +--- # ── Allow egress DNS (all pods) ────────────────────────────────────────────── apiVersion: networking.k8s.io/v1 kind: NetworkPolicy diff --git a/workplans/NK-WP-0056-bounded-executor-path.md b/workplans/NK-WP-0056-bounded-executor-path.md index 72dd9b4..b593ec1 100644 --- a/workplans/NK-WP-0056-bounded-executor-path.md +++ b/workplans/NK-WP-0056-bounded-executor-path.md @@ -576,6 +576,7 @@ Left: - DONE 2026-10-31, founder: full server dry run with the admin kubeconfig. The ServiceAccount, Role, RoleBinding and NetworkPolicy were applied server-side as a dry run, and the synthetic BEA-2026-0001 ConfigMap and Job were created as a dry run. Nothing was persisted. + **Done 2026-10-31.** - The image workflow for `ba9d266` succeeded, and the digest @@ -589,6 +590,18 @@ Follow-up outside the done criteria: - Founder: create the Forgejo remote `coulomb/rapp-bounded-executor` and push. +Follow-up 2026-10-31 for T10/T06 reachability: + +- `netpol-sso.yaml` gained `allow-bounded-executor-to-{lldap-api,keycape,authelia}`, which admit only executor pods, + on 17170, 8080 and 9091; never LDAP 3890. Tests: `test_netpol.py`, plus a rapp test that its in-namespace + egress equals these admissions. Prepared in source, not applied. +- Live reads (limited identity): + - OpenBao's chart policy admits every namespace on 8200, so nothing is needed there. + - approval-engine is deployed but admits only secrets-engine and namespaces labelled + `railiance.io/approval-engine-client=true`, and none are. + - audit-core has no ingress from `sso`. + - Both owners were asked for pod-scoped rules; see the rapp's `docs/platform-handoffs.md`. + ## Pilot: unattended bind rotation under one admission ```task