fix(release): validate service-only systemd credential ACLs

This commit is contained in:
2026-10-14 03:54:07 +08:00
parent be22dd9c12
commit 5ff88047ff
5 changed files with 174 additions and 30 deletions
+61
View File
@@ -0,0 +1,61 @@
"""Strict systemd credential metadata, including service-only POSIX ACLs."""
import os
import stat
import struct
from pathlib import Path
from . import preflight
from .policy import DeliveryError
ACL_INPUTS = {
"kf-coordinator.service": (1201, {"coordinator.json"}),
"kf-worker.service": (1202, {"worker.json"}),
"kf-postgres-initialize.service": (1203, {"bootstrap"}),
"kf-postgres-recover.service": (1203, {"bootstrap", "connections"}),
}
def validate_acl(contents: bytes, uid: int) -> None:
# Linux POSIX ACL xattr v2: owner and exactly this user may read;
# the owning group and other users have no rights. The group mode bits
# describe the ACL mask, rather than an owning-group permission grant.
undefined = 0xFFFFFFFF
entries = [
(1, 4, undefined),
(2, 4, uid),
(4, 0, undefined),
(16, 4, undefined),
(32, 0, undefined),
]
expected = struct.pack("<I", 2) + b"".join(struct.pack("<HHI", *row) for row in entries)
if contents != expected:
raise DeliveryError("service-only credential ACL differs")
def validate_metadata(path: Path, uid: int, maximum: int) -> os.stat_result:
"""Call after the fixed service identity/cgroup/budgets have been verified."""
info = path.lstat()
if (
not stat.S_ISREG(info.st_mode)
or info.st_nlink != 1
or info.st_uid not in {0, uid}
or not 1 <= info.st_size <= maximum
):
raise DeliveryError("private bounded systemd credential required")
mode = stat.S_IMODE(info.st_mode)
if mode in {0o400, 0o600}:
return info
bound_uid, members = ACL_INPUTS.get(path.parent.name, (None, set()))
if (
mode != 0o440
or (info.st_uid, info.st_gid) != (0, 0)
or path.parent.parent != Path("/run/credentials")
or uid != bound_uid
or path.name not in members
or not os.statvfs(path).f_flag & os.ST_RDONLY
):
raise DeliveryError("private bounded systemd credential required")
preflight.protected(path.parent, directory=True)
validate_acl(os.getxattr(path, "system.posix_acl_access"), uid)
return info
+17 -10
View File
@@ -9,7 +9,15 @@ import subprocess
import time
from pathlib import Path
from . import configuration, dependencies, dependency_install, install, preflight, service_install
from . import (
configuration,
credentials,
dependencies,
dependency_install,
install,
preflight,
service_install,
)
from .policy import DeliveryError, canonical, digest, fields, read_json, sha256
from .services import python_environment
@@ -93,14 +101,8 @@ def run_in_unit() -> dict:
interfaces = Path("/proc/net/dev").read_text().splitlines()[2:]
if {line.split(":", 1)[0].strip() for line in interfaces} != {"lo"}:
raise DeliveryError("network-isolated database initializer required")
credential = CREDENTIAL.lstat()
if (
not stat.S_ISREG(credential.st_mode)
or credential.st_nlink != 1
or credential.st_uid not in {0, 1203}
or stat.S_IMODE(credential.st_mode) not in {0o400, 0o600}
or credential.st_size != 65
):
credential = credentials.validate_metadata(CREDENTIAL, 1203, 65)
if credential.st_size != 65:
raise DeliveryError("private database initialization credential required")
validate_password(CREDENTIAL.read_bytes())
if DATA.exists() or DATA.is_symlink():
@@ -173,7 +175,12 @@ def initialize(password_file: Path, password_sha: str) -> dict:
validate_password(password_file.read_bytes())
selected, original = service_install.current_receipt()
payload = install.RELEASES / selected["release_id"] / "payload"
for name in ("dependencies.py", "dependency_install.py", "dependency_initialize.py"):
for name in (
"credentials.py",
"dependencies.py",
"dependency_install.py",
"dependency_initialize.py",
):
shipped = payload / "delivery/scripts/release" / name
preflight.protected(shipped)
if digest(shipped) != digest(Path(__file__).parent / name):
+2 -10
View File
@@ -11,7 +11,7 @@ import time
from pathlib import Path
from urllib.parse import urlsplit
from . import configuration, database, dependencies, dependency_initialize, preflight
from . import configuration, credentials, database, dependencies, dependency_initialize, preflight
from .policy import DeliveryError, canonical, read_json
UNIT = "kf-postgres-recover.service"
@@ -300,15 +300,7 @@ def run_in_unit() -> dict:
raise DeliveryError("identity recovery refuses standby/replay signal files")
dependency_initialize.cluster_identity()
for name, maximum in (("connections", 16384), ("bootstrap", 65)):
info = (CREDENTIALS / name).lstat()
if (
not stat.S_ISREG(info.st_mode)
or info.st_nlink != 1
or info.st_uid not in {0, 1203}
or stat.S_IMODE(info.st_mode) not in {0o400, 0o600}
or info.st_size > maximum
):
raise DeliveryError("private recovery credential identity differs")
credentials.validate_metadata(CREDENTIALS / name, 1203, maximum)
endpoints = configuration.validate_connections(read_json(CREDENTIALS / "connections", 16384))
raw = (CREDENTIALS / "bootstrap").read_bytes()
dependency_initialize.validate_password(raw)
+2 -10
View File
@@ -8,7 +8,7 @@ import resource
import stat
from pathlib import Path
from . import preflight
from . import credentials, preflight
from .policy import DeliveryError, sha256
from .services import PROFILES
@@ -193,15 +193,7 @@ def python_command(role: str, release: str) -> tuple[list[str], dict]:
def read_credential(path: Path, expected: str, uid: int) -> bytes:
info = path.lstat()
if (
not stat.S_ISREG(info.st_mode)
or info.st_nlink != 1
or info.st_uid not in {0, uid}
or stat.S_IMODE(info.st_mode) not in {0o400, 0o600}
or not 1 <= info.st_size <= 32768
):
raise DeliveryError("private bounded systemd credential required")
info = credentials.validate_metadata(path, uid, 32768)
content = path.read_bytes()
if len(content) != info.st_size or hashlib.sha256(content).hexdigest() != sha256(expected):
raise DeliveryError("service credential fingerprint mismatch")