fix(delivery): recognize privileged service groups in byte proofs
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user