From 1a122235741d61b649e39ab86ddfbd317e54ad2f Mon Sep 17 00:00:00 2001 From: tegwick Date: Fri, 11 Sep 2026 12:41:04 +0200 Subject: [PATCH] Create private evidence directories safely on fsGroup volumes Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc --- SCOPE.md | 5 +++++ deploy/README.md | 4 +++- informed_decision/container.py | 15 ++++++++++++++- tests/test_container_runtime.py | 17 +++++++++++++++++ ...founding-specs-and-approver-ui-ownership.md | 18 ++++++++++++++++++ 5 files changed, 57 insertions(+), 2 deletions(-) diff --git a/SCOPE.md b/SCOPE.md index 10a698a..701ead2 100644 --- a/SCOPE.md +++ b/SCOPE.md @@ -45,6 +45,11 @@ actual Flex Auth, Approval Engine and Audit Core with synthetic identity/custody writer, backup/inspection commands and eight review-only Kubernetes objects. Network-isolated container restart/restore preserves unresolved submissions. +The 2026-09-11 fsGroup startup correction handles newly created private +directories on Kubernetes volumes; unsafe existing directories still refuse. +Its full local suite passes 335 tests with 39 optional checks skipped. The +earlier local image must be rebuilt with this correction before publication. + Remaining: native policy package/caller/assignment admission, registered human login and deployed binding, independent production audit custody, image publication/cutover, platform backup/restore and operator recovery admission. diff --git a/deploy/README.md b/deploy/README.md index 405b070..4374e31 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -56,7 +56,9 @@ Kubernetes projected ConfigMaps are root-owned and symlinked. The container entrypoint reads at most 16 KiB from `/configuration/runtime.json`, requires `evidence_db=/data/private/review.sqlite`, and copies the configuration into owned ephemeral storage at `/run/informed-decision/private/runtime.json` (0600). -It creates `/data/private` as the process user with mode 0700 and refuses unsafe +It creates `/data/private` as the process user with mode 0700, clearing the +inherited setgid bit only on directories it just created on fsGroup volumes, +and refuses unsafe existing ownership/modes. It does not silently chmod or take over existing data. The evidence database remains 0600. Credential callbacks read rotating projected files directly; bearer tokens are never copied into SQLite or the config snapshot. diff --git a/informed_decision/container.py b/informed_decision/container.py index 7b23670..6bf02b2 100644 --- a/informed_decision/container.py +++ b/informed_decision/container.py @@ -23,7 +23,20 @@ def private_directory(path): path = Path(path) if not path.is_absolute(): raise ValueError("absolute private directory required") - path.mkdir(mode=0o700, exist_ok=True) + try: + path.mkdir(mode=0o700) + except FileExistsError: + pass + else: + # Kubernetes fsGroup volumes make newly created children inherit setgid. + # Normalize only this new owned directory; never take over existing data. + fd = os.open(path, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW) + try: + info = os.fstat(fd) + if info.st_uid == os.getuid() and stat.S_IMODE(info.st_mode) == 0o2700: + os.fchmod(fd, 0o700) + finally: + os.close(fd) info = path.lstat() if (not stat.S_ISDIR(info.st_mode) or info.st_uid != os.getuid() or stat.S_IMODE(info.st_mode) != 0o700): diff --git a/tests/test_container_runtime.py b/tests/test_container_runtime.py index 7a61410..99dc48a 100644 --- a/tests/test_container_runtime.py +++ b/tests/test_container_runtime.py @@ -101,3 +101,20 @@ def test_listener_exposure_requires_explicit_supported_setting(monkeypatch,host) assert not observed else: web.main();assert observed[0]['host']==(host or '127.0.0.1') + + +def test_fsgroup_volume_creates_private_store_without_inherited_setgid(tmp_path): + volume=tmp_path/'volume';volume.mkdir();volume.chmod(0o2770) + directory=private_directory(volume/'private') + assert stat.S_IMODE(directory.stat().st_mode)==0o700 + store=Store(directory/'review.sqlite') + assert store.outbox()==[] + assert private_directory(directory)==directory + + +@pytest.mark.parametrize('mode',[0o2700,0o2770]) +def test_existing_setgid_directory_is_refused_without_chmod(tmp_path,mode): + directory=tmp_path/'private';directory.mkdir();directory.chmod(mode) + with pytest.raises(ValueError,match='unsafe ownership or mode'): + private_directory(directory) + assert stat.S_IMODE(directory.stat().st_mode)==mode diff --git a/workplans/INFD-WP-0001-founding-specs-and-approver-ui-ownership.md b/workplans/INFD-WP-0001-founding-specs-and-approver-ui-ownership.md index 5925ca7..b20b3dd 100644 --- a/workplans/INFD-WP-0001-founding-specs-and-approver-ui-ownership.md +++ b/workplans/INFD-WP-0001-founding-specs-and-approver-ui-ownership.md @@ -667,6 +667,24 @@ publication, owner service/namespace and exact peer admission, registration and real human/deployed binding, platform backup/restore and product acceptance. No cluster apply, native secret read, factory attempt or paid call occurred. +2026-09-11 — **native probe exposed fsGroup startup incompatibility.** +The native synthetic sender Job failed during private-store bootstrap, before +any audit HTTP request. A filesystem regression reproduced the same refusal: +Kubernetes fsGroup-style setgid volume roots cause `mkdir(0700)` to create a +`2700` child, which the evidence store correctly refuses. `private_directory` +now clears inherited setgid only on a directory this invocation just created, +through a non-symlink directory descriptor. Unsafe existing directories keep +their mode and remain refused. The private evidence contract stays 0700/0600. + +The new regression failed before the correction and passes after it; existing +2700/2770 directories remain unchanged and refused. The available full suite +passes 335 tests, with 39 optional checks skipped in this environment. The native +sender probe will consume this exact helper from pinned source before image +rebuild/publication. The previously built local image is superseded by this +source correction and must not be published as the final service candidate. +T08 stays progress for native acceptance and the existing deployment/human +binding/recovery gates; no service cutover or human disposition occurred. + ## Known risks - **T02 is a hard gate.** Writing the blueprint before the layer ruling risks