From 1321361d85cf773aa6d051360be0159ac672eff2 Mon Sep 17 00:00:00 2001 From: tegwick Date: Mon, 5 Oct 2026 00:05:35 +0200 Subject: [PATCH] Prepare private dispatcher queue on Kubernetes fsGroup volumes Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a10780-6d41-7952-9718-dfbc1f41236f --- deploy/informed-decision/README.md | 6 +++ .../dispatcher.candidate.json | 42 +++++++++++++++++-- tests/test_memo_dispatch.py | 23 ++++++++++ ...STATE-WP-0096-private-decision-dispatch.md | 15 ++++++- 4 files changed, 81 insertions(+), 5 deletions(-) diff --git a/deploy/informed-decision/README.md b/deploy/informed-decision/README.md index 552603b..d4ba206 100644 --- a/deploy/informed-decision/README.md +++ b/deploy/informed-decision/README.md @@ -65,3 +65,9 @@ Live observation: State Hub's API Service selects app=state-hub. This worker does not carry that label, so it cannot enter the API endpoints. At inspection there were no other State Hub NetworkPolicies; recheck effective selection before activation. Its own policy allows only DNS and the exact intake peer on 8081. + +The approved Kubernetes deployment prepares the private queue with a non-root +init container using the identical image. It removes inherited setgid only on +an empty, owned 2700 directory and otherwise requires 0700. It never changes +existing nonempty evidence custody. This covers local-path fsGroup behavior +that a plain Docker volume does not reproduce. diff --git a/deploy/informed-decision/dispatcher.candidate.json b/deploy/informed-decision/dispatcher.candidate.json index cd7197b..0e7e046 100644 --- a/deploy/informed-decision/dispatcher.candidate.json +++ b/deploy/informed-decision/dispatcher.candidate.json @@ -9,7 +9,7 @@ "name": "state-hub-memo-dispatch", "namespace": "state-hub", "annotations": { - "state-hub.coulomb.social/admission": "proposal; STATE-WP-0096 / INFD-WP-0007 approved rollout required" + "state-hub.coulomb.social/admission": "approved; INFD-WP-0007 cutover" } }, "spec": { @@ -31,7 +31,7 @@ "name": "state-hub-memo-dispatch", "namespace": "state-hub", "annotations": { - "state-hub.coulomb.social/admission": "proposal; STATE-WP-0096 / INFD-WP-0007 approved rollout required" + "state-hub.coulomb.social/admission": "approved; INFD-WP-0007 cutover" } }, "spec": { @@ -131,6 +131,42 @@ ] } } + ], + "initContainers": [ + { + "name": "prepare-private-queue", + "image": "forgejo.coulomb.social/coulomb/state-hub-memo-dispatch@sha256:410fb6b6c9fd7a7ee0b63d727fd3249fb3ae13943421d6a5ce88b7ad2520bd2f", + "command": [ + "python", + "-c", + "import os, stat\np='/data/private'\ntry:\n os.mkdir(p, 0o700)\nexcept FileExistsError:\n pass\nfd=os.open(p, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)\ntry:\n info=os.fstat(fd)\n if info.st_uid != os.getuid():\n raise SystemExit('queue owner refused')\n mode=stat.S_IMODE(info.st_mode)\n if mode == 0o2700 and not os.listdir(fd):\n os.fchmod(fd, 0o700)\n elif mode != 0o700:\n raise SystemExit('queue mode refused')\nfinally:\n os.close(fd)\n" + ], + "securityContext": { + "allowPrivilegeEscalation": false, + "readOnlyRootFilesystem": true, + "capabilities": { + "drop": [ + "ALL" + ] + } + }, + "resources": { + "requests": { + "cpu": "5m", + "memory": "24Mi" + }, + "limits": { + "cpu": "100m", + "memory": "96Mi" + } + }, + "volumeMounts": [ + { + "name": "queue", + "mountPath": "/data" + } + ] + } ] } } @@ -143,7 +179,7 @@ "name": "state-hub-memo-dispatch", "namespace": "state-hub", "annotations": { - "state-hub.coulomb.social/admission": "proposal; STATE-WP-0096 / INFD-WP-0007 approved rollout required" + "state-hub.coulomb.social/admission": "approved; INFD-WP-0007 cutover" } }, "spec": { diff --git a/tests/test_memo_dispatch.py b/tests/test_memo_dispatch.py index 3bc6c7a..45989d4 100644 --- a/tests/test_memo_dispatch.py +++ b/tests/test_memo_dispatch.py @@ -124,3 +124,26 @@ def test_transport_suppresses_proxy_redirect_and_oversized_response(monkeypatch) transport('POST',origin+'/v1/memos',{}, {}) finally: server.shutdown();server.server_close();thread.join(timeout=2) + + +def test_kubernetes_queue_init_handles_setgid_without_taking_over_data(tmp_path): + manifest = json.loads((Path(__file__).parents[1] / 'deploy/informed-decision/dispatcher.candidate.json').read_text()) + pod = next(item for item in manifest['items'] if item['kind'] == 'Deployment')['spec']['template']['spec'] + init = pod['initContainers'][0] + assert init['image'] == pod['containers'][0]['image'] + tmp_path.chmod(0o2770) + private = tmp_path / 'private' + code = init['command'][2].replace('/data/private', str(private)) + def run(): + return subprocess.run([sys.executable, '-c', code], capture_output=True).returncode + assert run() == 0 + assert stat.S_IMODE(private.stat().st_mode) == 0o700 + retained = private / 'retained' + retained.write_text('existing evidence') + assert run() == 0 + private.chmod(0o2700) + assert run() != 0 # Existing nonempty custody is never repaired silently. + retained.unlink() + assert run() == 0 # Empty directory left by the initial failed startup. + private.chmod(0o755) + assert run() != 0 diff --git a/workplans/STATE-WP-0096-private-decision-dispatch.md b/workplans/STATE-WP-0096-private-decision-dispatch.md index afad321..2c52879 100644 --- a/workplans/STATE-WP-0096-private-decision-dispatch.md +++ b/workplans/STATE-WP-0096-private-decision-dispatch.md @@ -8,7 +8,7 @@ status: active owner: codex topic_slug: netkingdom created: "2026-10-04" -updated: "2026-10-04" +updated: "2026-10-05" related: [INFD-WP-0005, INFD-WP-0007] state_hub_workstream_id: "4cca4a91-3b21-51f0-a61f-902ccb1289c6" --- @@ -53,7 +53,7 @@ caller identity. ```task id: STATE-WP-0096-T03 -status: todo +status: progress priority: high state_hub_task_id: "1aeb4da7-5411-5634-ae6a-2e4bf0e8738b" ``` @@ -85,3 +85,14 @@ Final Forgejo image 410fb6b (source 4a59b23) passed five installed-container checks and Trivy HIGH/CRITICAL scan (zero findings). Owner tests: 28 passed. Cross-service suite: 663 passed, one skipped. Both projected audiences and final image pin passed server dry-run. Receipts: docs/evidence/2026-10-04-memo-dispatch/. + +## Approved native cutover — 2026-10-05 + +Founder approved the cutover (State Hub decision d2eaa2e9-6d11-4676-a07f-e3b7e59a88e3). +The worker was deployed by a pinned manual Argo application. Native startup +exposed inherited setgid on the newly created private queue (2700 instead of +0700). A same-image, non-root init container normalizes only an empty owned +2700 directory; existing nonempty or unsafe custody fails closed. Regression +coverage exercises first start under setgid, restart preservation, the empty +failed-start repair, and rejection of nonempty or world-readable custody. +Human acceptance remains pending.