fix(delivery): recognize privileged service groups in byte proofs

This commit is contained in:
2026-10-22 05:54:34 +08:00
parent 0cd94a20ec
commit 278337dd46
3 changed files with 91 additions and 20 deletions
+67 -2
View File
@@ -336,7 +336,9 @@ def live_byte_proof_model(ordinary_bundle, monkeypatch, tmp_path):
path = tmp_path / "byte-proof.json"
path.write_bytes(policy.canonical(record))
monkeypatch.setattr(operation_proof, "RECORD", path)
monkeypatch.setattr(operation_proof, "_root", lambda: True)
for name in ("getuid", "geteuid", "getgid", "getegid"):
monkeypatch.setattr(operation_proof.os, name, lambda: 0)
monkeypatch.setattr(operation_proof.os, "getgroups", lambda: [0])
monkeypatch.setattr(operation_proof.preflight, "protected", lambda *a, **kw: None)
monkeypatch.setattr(
operation_proof.preflight,
@@ -367,6 +369,69 @@ def test_live_parent_model_reuses_complete_proof_without_remembering_it(
assert not reads
@pytest.mark.parametrize(
("unit", "gid"),
[
("postgres", 1203),
("rabbitmq", 1204),
("sandbox-factory", 0),
("coordinator", 1201),
("api", 1200),
("outbox", 1200),
("problem-import", 1200),
("worker", 1202),
("proxy", 1205),
("node-health", 0),
],
)
def test_privileged_service_group_model_can_consume_but_not_publish(
live_byte_proof_model, monkeypatch, unit, gid
):
bundle, expected, record = live_byte_proof_model
monkeypatch.setattr(operation_proof.os, "getgroups", lambda: [gid])
monkeypatch.setattr(
operation_proof.preflight,
"current_cgroup",
lambda: Path(operation_proof.GROUP + "kf-" + unit + ".service"),
)
reads = observed_digests(monkeypatch)
assert manifest.verify(bundle, expected)["release_id"] == record["release_id"]
assert not reads
# Publication still requires Root's own groups, even in a consumer model.
assert operation_proof._root() is (gid == 0)
@pytest.mark.parametrize(
("unit", "groups", "ordinary_id"),
[
("api", [1201], None),
("worker", [1200], None),
("node-health", [1200], None),
("api", [0, 1200, 1201], None),
("api/child", [1200], None),
("api", [1200], "getuid"),
("api", [1200], "geteuid"),
("api", [1200], "getgid"),
("api", [1200], "getegid"),
],
)
def test_foreign_groups_and_each_ordinary_identity_model_require_full_bytes(
live_byte_proof_model, monkeypatch, unit, groups, ordinary_id
):
bundle, expected, _ = live_byte_proof_model
monkeypatch.setattr(operation_proof.os, "getgroups", lambda: groups)
if ordinary_id:
monkeypatch.setattr(operation_proof.os, ordinary_id, lambda: 1200)
monkeypatch.setattr(
operation_proof.preflight,
"current_cgroup",
lambda: Path(operation_proof.GROUP + "kf-" + unit + ".service"),
)
reads = observed_digests(monkeypatch)
manifest.verify(bundle, expected)
assert reads
@pytest.mark.parametrize(
("field", "value"),
[
@@ -415,7 +480,7 @@ def test_ineligible_parent_proof_model_requires_full_bytes(
):
bundle, expected, _ = live_byte_proof_model
if reason == "ordinary_uid":
monkeypatch.setattr(operation_proof, "_root", lambda: False)
monkeypatch.setattr(operation_proof, "_root", lambda *args: False)
elif reason in {"foreign_group", "parent"}:
unit = "unrelated.service" if reason == "foreign_group" else operation_proof.PARENT
monkeypatch.setattr(
+4
View File
@@ -434,6 +434,10 @@ identities again before returning. All readers are joined on success or failure.
During one `start`, `resume` or `restart`, the fixed Root maintenance helper reads
the complete bundle once. Its Root startup checks and node-health helpers can
reuse that proof while the same transient maintenance invocation is running.
Consumers require real and effective UID/GID 0 and the exact fixed service
cgroup. A privileged service precheck may retain only Root's group and that
service's fixed group; ordinary service processes still read the complete bytes.
The publishing maintenance helper accepts only Root's own group.
A Root-only systemd runtime directory holds a record of at most 4096 bytes,
binding the full metadata digest to the selected manifest, release, configuration,
boot, MainPID and InvocationID. Each consumer checks the live owner and every
+20 -18
View File
@@ -15,33 +15,32 @@ DIRECTORY = Path("/run/kf-node-verification")
RECORD = DIRECTORY / "byte-proof.json"
PARENT = "kf-node-maintenance.service"
GROUP = "/sys/fs/cgroup/kfonlinejudge.slice/"
CONSUMERS = frozenset(
"kf-" + name + ".service"
for name in (
"postgres",
"rabbitmq",
"sandbox-factory",
"coordinator",
"api",
"outbox",
"problem-import",
"worker",
"proxy",
"node-health",
)
)
# Privileged ExecStartPre checks retain their ordinary service's supplementary
# group. These are fixed unit identities, never groups supplied by a proof.
CONSUMERS = {
"kf-postgres.service": 1203,
"kf-rabbitmq.service": 1204,
"kf-sandbox-factory.service": 0,
"kf-coordinator.service": 1201,
"kf-api.service": 1200,
"kf-outbox.service": 1200,
"kf-problem-import.service": 1200,
"kf-worker.service": 1202,
"kf-proxy.service": 1205,
"kf-node-health.service": 0,
}
MAX_AGE_NS = 900 * 10**9
MAX_RECORD = 4096
_REPORTED: tuple[int, str] | None = None
def _root() -> bool:
def _root(allowed_groups: frozenset[int] = frozenset({0})) -> bool:
return (os.getuid(), os.geteuid(), os.getgid(), os.getegid()) == (
0,
0,
0,
0,
) and set(os.getgroups()) <= {0}
) and set(os.getgroups()) <= allowed_groups
def _now() -> int:
@@ -194,7 +193,10 @@ def _observed(value: dict) -> None:
def reuse(bundle: Path, expected: str) -> dict | None:
"""Missing/ineligible proofs use full bytes; eligible corrupt proofs refuse."""
if not _root() or str(preflight.current_cgroup()) not in {GROUP + unit for unit in CONSUMERS}:
allowed_groups = {GROUP + unit: frozenset({0, gid}) for unit, gid in CONSUMERS.items()}.get(
str(preflight.current_cgroup())
)
if allowed_groups is None or not _root(allowed_groups):
return None
try:
RECORD.lstat()