feat(release): add isolated journal profile installation

This commit is contained in:
2026-10-13 05:25:10 +08:00
parent 325ed08ed2
commit ad7eebee54
3 changed files with 372 additions and 0 deletions
+18
View File
@@ -304,3 +304,21 @@ operations and cleanup results. Root factory termination uses a signal notice an
the existing bounded guardian/recovery path; ordinary import termination finishes
its current authorized operation before claiming another. These interfaces require
actual independent-VM lifecycle acceptance and are not installation/safety proof.
`python3.13 -m scripts.release.journal` separately installs a fixed KF journal
namespace profile. It requires a preprovisioned dedicated 64-MiB ext4 mount at
`/var/log/journal/MACHINE_ID.kf-onlinejudge`, with the same private hard-volume
mount rules used for dependencies. It never creates/formats/mounts a disk, changes
the default system journal, adopts old unrecorded log data or starts a daemon.
Unknown configuration/overrides and partial installations are retained and refused.
The profile fixes 32-MiB persistent/8-MiB runtime rotation budgets, bounded files,
lines and seven-day retention. Its namespaced daemon override limits CPU, memory,
swap and PIDs. Rotation settings are not a hard filesystem guarantee; the dedicated
volume must also be observed. The `check_ready` interface reads actual namespace
service/process/cgroup state. Connecting service templates to this profile,
redaction and real retention/capacity behavior remain C+D/F installation acceptance
work; installing the profile alone does not route existing service logs or verify
their privacy. The profile relies on the pinned systemd 262
[namespace interface](https://github.com/systemd/systemd/blob/v262/man/systemd.exec.xml)
and [journal configuration](https://github.com/systemd/systemd/blob/v262/man/journald.conf.xml).
+324
View File
@@ -0,0 +1,324 @@
"""A separate bounded log namespace; never change the host's default journal."""
import argparse
import hashlib
import json
import os
import re
import stat
import subprocess
from pathlib import Path
from . import configuration, install, preflight
from .policy import DeliveryError, canonical, digest, fields, read_json
NAMESPACE = "kf-onlinejudge"
UNIT = "systemd-journald@kf-onlinejudge.service"
CONFIG = Path("/etc/systemd/journald@kf-onlinejudge.conf")
DROPIN = Path("/etc/systemd/system") / (UNIT + ".d") / "50-kf-bounds.conf"
RECEIPT = Path("/var/lib/kf-onlinejudge/journal-profile.json")
CAPACITY = 64 * 1024**2
PROFILE = b"""[Journal]
Storage=persistent
SplitMode=none
SystemMaxUse=32M
RuntimeMaxUse=8M
SystemMaxFileSize=4M
RuntimeMaxFileSize=2M
SystemMaxFiles=8
RuntimeMaxFiles=4
MaxFileSec=1h
MaxRetentionSec=7day
RateLimitIntervalSec=30s
RateLimitBurst=100
LineMax=8192
ForwardToSyslog=no
ForwardToKMsg=no
ForwardToConsole=no
ForwardToWall=no
"""
UNIT_BOUNDS = b"""[Unit]
RequiresMountsFor=/var/log/journal/%m.kf-onlinejudge
StartLimitIntervalSec=600
StartLimitBurst=3
[Service]
Slice=kfonlinejudge.slice
MemoryMax=64M
MemorySwapMax=0
TasksMax=16
CPUQuota=25%
LimitCORE=0
LimitFSIZE=8M
LimitNOFILE=256
Restart=on-failure
RestartSec=30
TimeoutStopSec=15
"""
def manager(*args: str) -> str:
# Import after service profiles are initialized so their future log-binding
# checks can call this independent installer without an import cycle.
from .service_install import systemctl
return systemctl(*args)
def machine_id() -> str:
path = Path("/etc/machine-id")
preflight.protected(path)
value = bounded_text(path, 64).strip()
if not re.fullmatch(r"[0-9a-f]{32}", value) or value == "0" * 32:
raise DeliveryError("fixed machine journal identity required")
return value
def bounded_text(path: Path, maximum: int) -> str:
with path.open() as source:
value = source.read(maximum + 1)
if len(value) > maximum:
raise DeliveryError("journal observation exceeds bound")
return value
def validate_storage(value: dict, path: Path) -> None:
fields(
value,
{"path", "root", "filesystem", "options", "shared", "capacity_bytes"},
"journal storage",
)
if (
value["path"] != str(path)
or value["root"] != "/"
or value["filesystem"] != "ext4"
or value["shared"] is not False
or not isinstance(value["options"], list)
or not all(isinstance(option, str) for option in value["options"])
or not {"rw", "nosuid", "nodev", "noexec"} <= set(value["options"])
or type(value["capacity_bytes"]) is not int
or not CAPACITY * 9 // 10 <= value["capacity_bytes"] <= CAPACITY
):
raise DeliveryError("dedicated journal hard storage budget differs")
def storage() -> dict:
path = Path("/var/log/journal") / (machine_id() + "." + NAMESPACE)
preflight.protected(path, directory=True)
rows = [
line.split() for line in bounded_text(Path("/proc/self/mountinfo"), 1024**2).splitlines()
]
selected = [row for row in rows if len(row) > 6 and row[4] == str(path)]
if len(selected) != 1:
raise DeliveryError("dedicated journal mount required; never created by installer")
row = selected[0]
separator = row.index("-")
info = os.statvfs(path)
value = {
"path": str(path),
"root": row[3],
"filesystem": row[separator + 1],
"options": row[5].split(","),
"shared": any(other[2] == row[2] and other != row for other in rows),
"capacity_bytes": info.f_frsize * info.f_blocks,
}
validate_storage(value, path)
return value
def validate_receipt(value: dict) -> None:
fields(
value,
{
"schema_version",
"namespace",
"machine_id",
"files",
"services_started",
"execution_verified",
},
"journal receipt",
)
if (
type(value["schema_version"]) is not int
or value["schema_version"] != 1
or value["namespace"] != NAMESPACE
or not isinstance(value["machine_id"], str)
or not re.fullmatch(r"[0-9a-f]{32}", value["machine_id"])
or value["machine_id"] == "0" * 32
or value["files"]
!= {
str(CONFIG): hashlib.sha256(PROFILE).hexdigest(),
str(DROPIN): hashlib.sha256(UNIT_BOUNDS).hexdigest(),
}
or value["services_started"] is not False
or value["execution_verified"] is not False
):
raise DeliveryError("journal receipt differs from fixed profile")
def check_profile() -> dict:
preflight.protected(RECEIPT)
configuration.private(RECEIPT)
receipt = read_json(RECEIPT, 4096)
validate_receipt(receipt)
if receipt["machine_id"] != machine_id():
raise DeliveryError("journal profile belongs to another machine")
for path, expected in receipt["files"].items():
member = Path(path)
preflight.protected(member)
if (
member.stat().st_gid != 0
or stat.S_IMODE(member.stat().st_mode) != 0o644
or digest(member) != expected
):
raise DeliveryError("protected journal profile changed")
# Refuse unrecorded instance-specific overrides; systemd's actual limits are
# checked separately, including overrides inherited from the vendor template.
if {p.name for p in DROPIN.parent.iterdir()} != {DROPIN.name}:
raise DeliveryError("unknown journal instance override")
check_configuration_overrides()
return storage()
def check_configuration_overrides() -> None:
for root in ("/etc/systemd", "/run/systemd", "/usr/local/lib/systemd", "/usr/lib/systemd"):
directory = Path(root) / ("journald@" + NAMESPACE + ".conf.d")
if directory.exists() or directory.is_symlink():
preflight.protected(directory, directory=True)
if next(directory.iterdir(), None) is not None:
raise DeliveryError("unrecorded namespace journal configuration override")
def check_ready() -> dict:
value = check_profile()
props = dict(
line.split("=", 1)
for line in manager(
"show",
UNIT,
"--property=ActiveState,SubState,MainPID,ControlGroup,FragmentPath,DropInPaths",
).splitlines()
if "=" in line
)
group = "/kfonlinejudge.slice/" + UNIT
if (
props.get("ActiveState") != "active"
or props.get("SubState") != "running"
or props.get("ControlGroup") != group
):
raise DeliveryError("bounded journal instance unavailable")
if props.get(
"FragmentPath"
) != "/usr/lib/systemd/system/systemd-journald@.service" or props.get(
"DropInPaths", ""
).split() != [str(DROPIN)]:
raise DeliveryError("unrecorded journal manager executable or override")
pid = int(props.get("MainPID", "0"))
if not 1 < pid < 2**31:
raise DeliveryError("journal process identity unavailable")
status = dict(
line.split(":", 1)
for line in bounded_text(Path(f"/proc/{pid}/status"), 16384).splitlines()
if ":" in line
)
if status.get("Uid", "").split() != ["0"] * 4:
raise DeliveryError("journal process authority differs")
directory = Path("/sys/fs/cgroup" + group)
limits = {
name: (directory / name).read_text().strip()
for name in ("memory.max", "memory.swap.max", "pids.max", "cpu.max")
}
if limits != {
"memory.max": str(64 * 1024**2),
"memory.swap.max": "0",
"pids.max": "16",
"cpu.max": "25000 100000",
}:
raise DeliveryError("effective journal budgets differ")
value["effective_limits"] = limits
return value
def install_profile() -> dict:
if os.getuid() != 0 or os.geteuid() != 0 or os.getgid() != 0:
raise DeliveryError("root journal operator required")
selected = install.selection()
payload = install.RELEASES / selected["release_id"] / "payload"
shipped = payload / "delivery/scripts/release/journal.py"
preflight.protected(shipped)
if digest(shipped) != digest(Path(__file__)):
raise DeliveryError("journal operator differs from selected delivery")
install.require_stopped()
observed = storage()
if RECEIPT.exists() or RECEIPT.is_symlink():
check_profile()
return {
"profile_installed": True,
"storage": observed,
"services_started": False,
"execution_verified": False,
}
props = dict(
line.split("=", 1)
for line in manager("show", UNIT, "--property=ActiveState,MainPID").splitlines()
if "=" in line
)
if props.get("ActiveState") != "inactive" or props.get("MainPID") != "0":
raise DeliveryError("unrecorded journal instance; refused adoption")
path = Path(observed["path"])
if any(
p.name != "lost+found"
or p.is_symlink()
or not p.is_dir()
or next(p.iterdir(), None) is not None
for p in path.iterdir()
):
raise DeliveryError("unrecorded journal data; refused adoption")
for member in (CONFIG, DROPIN):
if member.exists() or member.is_symlink():
raise DeliveryError("unrecorded journal configuration; refused overwrite")
check_configuration_overrides()
install.directory(DROPIN.parent, 0o755)
if next(DROPIN.parent.iterdir(), None) is not None:
raise DeliveryError("unknown journal override directory")
install.exclusive_file(CONFIG, PROFILE, 0o644)
install.exclusive_file(DROPIN, UNIT_BOUNDS, 0o644)
receipt = {
"schema_version": 1,
"namespace": NAMESPACE,
"machine_id": machine_id(),
"files": {
str(CONFIG): hashlib.sha256(PROFILE).hexdigest(),
str(DROPIN): hashlib.sha256(UNIT_BOUNDS).hexdigest(),
},
"services_started": False,
"execution_verified": False,
}
validate_receipt(receipt)
install.exclusive_file(RECEIPT, canonical(receipt), 0o600)
manager("daemon-reload")
check_profile()
return {
"profile_installed": True,
"storage": observed,
"services_started": False,
"execution_verified": False,
}
def main() -> None:
parser = argparse.ArgumentParser(description="Install only the fixed KF log namespace profile")
parser.parse_args()
try:
print(json.dumps(install_profile(), sort_keys=True))
except (OSError, ValueError, subprocess.SubprocessError) as error:
parser.exit(
1,
"Journal profile refused: "
+ (str(error) if isinstance(error, DeliveryError) else type(error).__name__)
+ "\n",
)
if __name__ == "__main__":
main()