feat(delivery): install protected factory service snapshots
This commit is contained in:
@@ -165,3 +165,10 @@ same-boot loss or conflicting record refuses. New-boot recreation requires empty
|
||||
persistent resources. No journal/lease is deleted and preparation grants no
|
||||
execution permit. Neither composition nor lexical tests are Root/kernel/service
|
||||
acceptance; install, restart, failure recovery and actual execution remain VM-only.
|
||||
The separate operator step `factory_service --install DIRECTORY --receipt-sha256 SHA`
|
||||
requires that generated receipt, the selected protected configuration and matching
|
||||
installer bytes. It keeps immutable Root-unit batches under `/etc/kf-judge/units/`,
|
||||
updates only recorded Root-owned pointers with all app units stopped, and checks
|
||||
loaded/inactive systemd metadata. Foreign files/links and partial unknown batches
|
||||
are refused, old batches remain retained. This step neither starts the factory nor
|
||||
prepares its cgroup; only the later bounded startup invokes Sandbox preparation.
|
||||
|
||||
@@ -3,10 +3,106 @@
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import secrets
|
||||
import stat
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
from . import configuration, install, preflight, service_install, services
|
||||
from .policy import DeliveryError, canonical, digest
|
||||
from .policy import DeliveryError, canonical, digest, fields, read_json, sha256
|
||||
|
||||
UNIT = "kf-sandbox-factory.service"
|
||||
UNITS = Path("/etc/kf-judge/units")
|
||||
|
||||
|
||||
def validate_receipt(value: dict) -> dict:
|
||||
fields(
|
||||
value,
|
||||
{
|
||||
"schema_version",
|
||||
"source_sha",
|
||||
"configuration_id",
|
||||
"configuration_receipt_sha256",
|
||||
"unit_sha256",
|
||||
"installed",
|
||||
"services_started",
|
||||
"execution_verified",
|
||||
},
|
||||
"factory unit receipt",
|
||||
)
|
||||
if (
|
||||
type(value["schema_version"]) is not int
|
||||
or value["schema_version"] != 1
|
||||
or not isinstance(value["source_sha"], str)
|
||||
or not re.fullmatch(r"[0-9a-f]{40}", value["source_sha"])
|
||||
or not isinstance(value["configuration_id"], str)
|
||||
or not re.fullmatch(r"cfg-[0-9a-f]{24}", value["configuration_id"])
|
||||
or any(
|
||||
value[name] is not False
|
||||
for name in (
|
||||
"installed",
|
||||
"services_started",
|
||||
"execution_verified",
|
||||
)
|
||||
)
|
||||
):
|
||||
raise DeliveryError("factory unit binding differs")
|
||||
sha256(value["configuration_receipt_sha256"])
|
||||
sha256(value["unit_sha256"])
|
||||
return value
|
||||
|
||||
|
||||
def batch_directory(receipt: dict) -> Path:
|
||||
value = validate_receipt(receipt)
|
||||
return UNITS / ("sd-" + value["source_sha"][:12] + "." + value["configuration_id"])
|
||||
|
||||
|
||||
def check_batch(directory: Path, receipt: dict) -> None:
|
||||
preflight.protected(directory, directory=True)
|
||||
if stat.S_IMODE(directory.stat().st_mode) != 0o700:
|
||||
raise DeliveryError("private factory unit batch required")
|
||||
if {p.name for p in directory.iterdir()} != {UNIT, "receipt.json"}:
|
||||
raise DeliveryError("unknown factory batch member")
|
||||
path = directory / "receipt.json"
|
||||
preflight.protected(path)
|
||||
configuration.private(path)
|
||||
if canonical(validate_receipt(read_json(path, 8192))) != canonical(receipt):
|
||||
raise DeliveryError("factory batch receipt differs")
|
||||
path = directory / UNIT
|
||||
preflight.protected(path)
|
||||
if (
|
||||
path.stat().st_size > 32768
|
||||
or stat.S_IMODE(path.stat().st_mode) != 0o644
|
||||
or digest(path) != receipt["unit_sha256"]
|
||||
):
|
||||
raise DeliveryError("factory batch bytes/mode differ")
|
||||
|
||||
|
||||
def known_link(path: Path) -> Path | None:
|
||||
try:
|
||||
info = path.lstat()
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
if not stat.S_ISLNK(info.st_mode) or info.st_uid != 0 or info.st_gid != 0:
|
||||
raise DeliveryError("unknown factory unit; refused overwrite")
|
||||
target = os.readlink(path)
|
||||
candidate = Path(target)
|
||||
if (
|
||||
candidate.as_posix() != target
|
||||
or candidate.name != UNIT
|
||||
or candidate.parent.parent != UNITS
|
||||
or not re.fullmatch(r"sd-[0-9a-f]{12}\.cfg-[0-9a-f]{24}", candidate.parent.name)
|
||||
):
|
||||
raise DeliveryError("unknown factory unit pointer")
|
||||
receipt_path = candidate.parent / "receipt.json"
|
||||
preflight.protected(receipt_path)
|
||||
configuration.private(receipt_path)
|
||||
receipt = validate_receipt(read_json(receipt_path, 8192))
|
||||
if batch_directory(receipt) != candidate.parent:
|
||||
raise DeliveryError("factory pointer/receipt binding differs")
|
||||
check_batch(candidate.parent, receipt)
|
||||
return candidate.parent
|
||||
|
||||
|
||||
def render(receipt: dict) -> bytes:
|
||||
@@ -96,13 +192,122 @@ def generate(output: Path) -> dict:
|
||||
return result
|
||||
|
||||
|
||||
def install_unit(source: Path, expected: str) -> dict:
|
||||
if os.getuid() != 0 or os.geteuid() != 0:
|
||||
raise DeliveryError("factory unit installation requires root operator")
|
||||
preflight.protected(source, directory=True)
|
||||
path = source / "receipt.json"
|
||||
preflight.protected(path)
|
||||
configuration.private(path)
|
||||
if digest(path) != sha256(expected):
|
||||
raise DeliveryError("factory source receipt differs")
|
||||
receipt = validate_receipt(read_json(path, 8192))
|
||||
selected, original = service_install.current_receipt()
|
||||
if (
|
||||
receipt["source_sha"] != original["source_sha"]
|
||||
or receipt["configuration_id"] != selected["configuration_id"]
|
||||
or receipt["configuration_receipt_sha256"] != selected["receipt_sha256"]
|
||||
):
|
||||
raise DeliveryError("factory unit does not belong to selected configuration")
|
||||
payload = install.RELEASES / selected["release_id"] / "payload"
|
||||
for name in ("factory_service.py", "services.py"):
|
||||
shipped = payload / "delivery/scripts/release" / name
|
||||
preflight.protected(shipped)
|
||||
if digest(shipped) != digest(Path(__file__).parent / name):
|
||||
raise DeliveryError("factory installer differs from selected delivery")
|
||||
if {p.name for p in source.iterdir()} != {UNIT, "receipt.json"}:
|
||||
raise DeliveryError("unknown factory source member")
|
||||
path = source / UNIT
|
||||
preflight.protected(path)
|
||||
configuration.private(path)
|
||||
contents = render(original)
|
||||
if (
|
||||
path.stat().st_size > 32768
|
||||
or digest(path) != receipt["unit_sha256"]
|
||||
or path.read_bytes() != contents
|
||||
):
|
||||
raise DeliveryError("factory source is not the fixed privileged composition")
|
||||
install.require_stopped()
|
||||
system = Path("/etc/systemd/system")
|
||||
preflight.protected(system, directory=True)
|
||||
link = system / UNIT
|
||||
known_link(link)
|
||||
install.directory(UNITS, 0o700)
|
||||
directory = batch_directory(receipt)
|
||||
install.directory(directory, 0o700)
|
||||
for p in directory.iterdir():
|
||||
if p.name not in {UNIT, "receipt.json"}:
|
||||
raise DeliveryError("unknown partial factory batch member")
|
||||
preflight.protected(p)
|
||||
if p.stat().st_size > (8192 if p.name == "receipt.json" else 32768):
|
||||
raise DeliveryError("partial factory member exceeds bound")
|
||||
install.exclusive_file(directory / UNIT, contents, 0o644)
|
||||
install.exclusive_file(directory / "receipt.json", canonical(receipt), 0o600)
|
||||
check_batch(directory, receipt)
|
||||
if install.selection() != selected:
|
||||
raise DeliveryError("selected configuration changed during factory preparation")
|
||||
install.require_stopped()
|
||||
if known_link(link) != directory:
|
||||
temporary = system / (".kf-factory-" + secrets.token_hex(16))
|
||||
created = None
|
||||
try:
|
||||
temporary.symlink_to(directory / UNIT)
|
||||
info = temporary.lstat()
|
||||
created = (info.st_dev, info.st_ino)
|
||||
os.replace(temporary, link)
|
||||
install.sync_directory(system)
|
||||
finally:
|
||||
if created is not None and temporary.is_symlink():
|
||||
info = temporary.lstat()
|
||||
if (info.st_dev, info.st_ino) == created:
|
||||
temporary.unlink()
|
||||
service_install.systemctl("daemon-reload")
|
||||
loaded = dict(
|
||||
line.split("=", 1)
|
||||
for line in service_install.systemctl(
|
||||
"show",
|
||||
UNIT,
|
||||
"--property=LoadState,ActiveState,MainPID,FragmentPath",
|
||||
).splitlines()
|
||||
if "=" in line
|
||||
)
|
||||
if (
|
||||
loaded.get("LoadState") != "loaded"
|
||||
or loaded.get("ActiveState") != "inactive"
|
||||
or loaded.get("MainPID") != "0"
|
||||
or loaded.get("FragmentPath") not in {str(link), str(directory / UNIT)}
|
||||
or known_link(link) != directory
|
||||
or install.selection() != selected
|
||||
):
|
||||
raise DeliveryError("factory unit load readback differs")
|
||||
return {
|
||||
"source_sha": receipt["source_sha"],
|
||||
"configuration_id": receipt["configuration_id"],
|
||||
"unit_installed": True,
|
||||
"unit_manager_readback": loaded,
|
||||
"services_started": False,
|
||||
"execution_verified": False,
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Protected Root factory unit composition")
|
||||
parser.add_argument("--output", required=True, type=Path)
|
||||
action = parser.add_mutually_exclusive_group(required=True)
|
||||
action.add_argument("--output", type=Path)
|
||||
action.add_argument("--install", type=Path)
|
||||
parser.add_argument("--receipt-sha256")
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
print(json.dumps(generate(args.output), sort_keys=True))
|
||||
except (OSError, ValueError) as error:
|
||||
if args.output is not None:
|
||||
if args.receipt_sha256 is not None:
|
||||
raise DeliveryError("unexpected generator receipt override")
|
||||
result = generate(args.output)
|
||||
else:
|
||||
if args.receipt_sha256 is None:
|
||||
raise DeliveryError("explicit factory receipt fingerprint required")
|
||||
result = install_unit(args.install, args.receipt_sha256)
|
||||
print(json.dumps(result, sort_keys=True))
|
||||
except (OSError, ValueError, subprocess.SubprocessError) as error:
|
||||
parser.exit(
|
||||
1,
|
||||
"Factory composition refused: "
|
||||
|
||||
Reference in New Issue
Block a user