Create private evidence directories safely on fsGroup volumes
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
This commit is contained in:
parent
bda9381f07
commit
1a12223574
5 changed files with 57 additions and 2 deletions
5
SCOPE.md
5
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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue