From 8e6133a8d00b1097dca19ab70d315c3be7d8221b Mon Sep 17 00:00:00 2001 From: kronecker <2026692687@qq.com> Date: Tue, 13 Oct 2026 06:36:14 +0800 Subject: [PATCH] feat(release): install and supervise the bounded HTTPS entry --- backend/tests/test_delivery.py | 109 +++++++++ scripts/release/README.md | 33 ++- scripts/release/build.py | 21 +- scripts/release/lifecycle.py | 10 +- scripts/release/node_health.py | 36 ++- scripts/release/policy.py | 1 + scripts/release/proxy.py | 7 +- scripts/release/proxy_guard.py | 177 ++++++++++++++ scripts/release/proxy_health.py | 64 +++++ scripts/release/proxy_install.py | 408 +++++++++++++++++++++++++++++++ scripts/release/proxy_runtime.py | 111 +++++++++ scripts/release/proxy_service.py | 156 ++++++++++++ 12 files changed, 1120 insertions(+), 13 deletions(-) create mode 100644 scripts/release/proxy_guard.py create mode 100644 scripts/release/proxy_health.py create mode 100644 scripts/release/proxy_install.py create mode 100644 scripts/release/proxy_runtime.py create mode 100644 scripts/release/proxy_service.py diff --git a/backend/tests/test_delivery.py b/backend/tests/test_delivery.py index f675232..7d62f5f 100644 --- a/backend/tests/test_delivery.py +++ b/backend/tests/test_delivery.py @@ -27,6 +27,9 @@ node_health = importlib.import_module("scripts.release.node_health") broker_health = importlib.import_module("scripts.release.broker_health") lifecycle = importlib.import_module("scripts.release.lifecycle") journal = importlib.import_module("scripts.release.journal") +proxy_runtime = importlib.import_module("scripts.release.proxy_runtime") +proxy_service = importlib.import_module("scripts.release.proxy_service") +proxy_guard = importlib.import_module("scripts.release.proxy_guard") def test_delivery_authorities_and_layout(): @@ -1044,3 +1047,109 @@ def test_failed_gate_closure_requires_known_api_stop_without_forging_owner_clean ) maintenance.close_failed_admission() assert calls == [("--no-block", "stop", "kf-api.service"), ("prove_empty", "api")] + + +def proxy_catalog_shape(): + # Structural example, not trusted executable bytes or ELF/version evidence. + names = ["bin/nginx", "lib/libc.so.6", "lib/ld-linux-x86-64.so.2"] + return { + "schema_version": 1, + "format": "kf.nginx.runtime.v1", + "version": "1.30.5", + "architecture": "x86_64", + "package_sha256": "b" * 64, + "directories": ["bin", "lib"], + "files": [ + { + "path": name, + "bytes": 1, + "sha256": "b" * 64, + "mode": "0555" if name == "bin/nginx" else "0444", + } + for name in names + ], + "payload_bytes": 3, + } + + +@pytest.mark.parametrize( + "field,value", [("version", "latest"), ("schema_version", True), ("architecture", "arm64")] +) +def test_proxy_runtime_cannot_silently_select_another_binary_profile(field, value): + value0 = proxy_catalog_shape() + proxy_runtime.validate(value0) + value0[field] = value + with pytest.raises(policy.DeliveryError): + proxy_runtime.validate(value0) + + +def test_proxy_runtime_cannot_smuggle_tls_or_configuration_into_public_binary_bundle(): + value = proxy_catalog_shape() + value["files"].append({"path": "lib/key.pem", "bytes": 1, "sha256": "b" * 64, "mode": "0444"}) + value["payload_bytes"] += 1 + with pytest.raises(policy.DeliveryError): + proxy_runtime.validate(value) + value = proxy_catalog_shape() + value["files"][1]["mode"] = "0555" + with pytest.raises(policy.DeliveryError): + proxy_runtime.validate(value) + + +def proxy_process_shape(): + return { + "uids": ["1205"] * 4, + "gids": ["1205"] * 4, + "groups": ["1205"], + "no_new_privileges": "1", + "capabilities": ["0000000000000400"] * 5, + "pid": 1, + "cgroup": "/sys/fs/cgroup/kfonlinejudge.slice/kf-proxy.service", + "limits": { + "memory.max": str(128 * 1024**2), + "memory.swap.max": "0", + "pids.max": "16", + "cpu.max": "50000 100000", + }, + "node_limits": { + "memory.max": str(3840 * 1024**2), + "memory.swap.max": "0", + "pids.max": "384", + "cpu.max": "200000 100000", + }, + "process_limits": { + "core": [0, 0], + "file": [8 * 1024**2] * 2, + "files": [256] * 2, + "stack": [16 * 1024**2] * 2, + }, + } + + +@pytest.mark.parametrize( + "field,value", + [ + ("pid", True), + ("pid", 2), + ("uids", ["0"] * 4), + ("groups", ["1205", "0"]), + ("capabilities", ["0000000000200400"] * 5), + ], +) +def test_proxy_cannot_trade_the_listener_capability_for_other_authority(field, value): + observed = proxy_process_shape() + proxy_guard.validate_runtime(observed) + observed[field] = value + with pytest.raises(policy.DeliveryError): + proxy_guard.validate_runtime(observed) + + +def test_proxy_render_is_fixed_to_a_selected_snapshot_without_business_credentials(): + selected = { + "release_id": "sd-" + "b" * 12, + "configuration_id": "cfg-" + "c" * 24, + "manifest_sha256": "d" * 64, + } + text = proxy_service.render(selected).decode() + assert "User=kf-proxy" in text and "CapabilityBoundingSet=CAP_NET_BIND_SERVICE" in text + assert "PrivatePIDs=yes" in text and "LogNamespace=kf-onlinejudge" in text + assert "EnvironmentFile=" not in text and "LoadCredential=" not in text diff --git a/scripts/release/README.md b/scripts/release/README.md index b810e6f..cc9e647 100644 --- a/scripts/release/README.md +++ b/scripts/release/README.md @@ -83,7 +83,7 @@ these require the recorded independent VM evidence. `generate-proxy` is a separate, offline composition entry. Its private 0600 profile has exactly `schema_version: 1`, one canonical HTTPS DNS `origin`, `certificate_sha256` and `key_sha256`. TLS inputs are independently supplied -later at `/etc/kf-onlinejudge/tls/fullchain.pem` and `key.pem`; the generator +later at `/etc/kf-proxy/tls/fullchain.pem` and `key.pem`; the generator does not read, create or verify a private key. It emits a new private NGINX configuration and receipt tied to the verified release. No arbitrary upstream, include, path, credentials or resource overrides are accepted. Only browser API @@ -340,3 +340,34 @@ are redirected through that formatter; installed API launchers disable access logging so request query strings are not copied into service logs. Development diagnostics remain separate. These ordinary formatting checks are not evidence of actual daemon, HTTP or upload log privacy; that is still a VM acceptance case. + +Build-input version 2 adds an explicitly fingerprinted public NGINX 1.30.5 +runtime catalog. Version 1 remains available for older engineering bundles without +proxy support; there is no implicit proxy default. The separate `proxy_runtime` +module permits only the binary and bounded shared libraries, excludes TLS and +configuration files, verifies every byte/mode and ships them under `payload/proxy`. +This separate library search tree avoids silently replacing existing application, +database or language-image libraries. Binary version/signature/ELF-closure evidence +is recorded by the operator; accepting a structural catalog is not TLS/server or +Linux-safety acceptance. Actual proxy installation and execution remain separate. + +`python3.13 -m scripts.release.proxy_install --source DIRECTORY +--receipt-sha256 SHA --certificate PRIVATE_CERTIFICATE --private-key PRIVATE_KEY` +installs the explicitly supplied protected proxy snapshot and matching unencrypted +TLS files. It requires the same release/API-origin batch, a distinct locked +UID/GID 1205 (`kf-proxy`) without supplementary groups, and an already provisioned +private 128-MiB ext4 volume at `/var/lib/kf-onlinejudge/proxy`. Provision that +volume's ownership/mode explicitly; the installer does not format, mount or +recursively reassign unknown data. Existing TLS material must match its fingerprint; +this entry does not silently rotate keys or claim DNS/public-browser trust. + +Proxy modules separate public runtime verification, fixed service rendering, +Root installation, ordinary launch guarding and read-only HTTPS observation. +The ordinary process retains only `CAP_NET_BIND_SERVICE` for 443, no-new-privileges, +private PID visibility and actual CPU/memory/swap/PIDs/file bounds. It receives +no application DB/MQ/control credentials. Installation preserves older snapshots, +refuses foreign system-unit pointers, reads back loaded/inactive metadata and +starts no server. Maintenance includes the proxy in start/drain/stop ordering. +Readiness uses fixed loopback TLS with the supplied certificate as an explicit +local trust anchor and checks hostname/leaf identity plus the API live response; +it does not substitute for actual browser CA/HTTPS/security acceptance in the VM. diff --git a/scripts/release/build.py b/scripts/release/build.py index 0457de4..4b8b0a2 100644 --- a/scripts/release/build.py +++ b/scripts/release/build.py @@ -9,7 +9,7 @@ import stat import subprocess from pathlib import Path -from . import manifest +from . import manifest, proxy_runtime from .policy import DeliveryError, contract, digest, fields, read_json, relative_name, sha256 ENTRYPOINTS = { @@ -227,9 +227,19 @@ def build(repo: Path, inputs: Path, output: Path, work: Path) -> dict: "runtime_manifest_sha256", "images", } - spec = fields(read_json(inputs, 16384), fields_spec, "build inputs") - if type(spec["schema_version"]) is not int or spec["schema_version"] != 1: + spec = read_json(inputs, 16384) + if ( + not isinstance(spec, dict) + or type(spec.get("schema_version")) is not int + or spec["schema_version"] not in (1, 2) + ): raise DeliveryError("unsupported build inputs") + if spec["schema_version"] == 2: + fields_spec.add("proxy") + fields(spec, fields_spec, "build inputs") + if spec["schema_version"] == 2: + fields(spec["proxy"], {"root", "catalog_sha256"}, "proxy build input") + proxy_runtime.verify(Path(spec["proxy"]["root"]), spec["proxy"]["catalog_sha256"]) root = Path(spec["runtime_root"]).resolve(strict=True) trusted_manifest = Path(spec["runtime_manifest"]) if digest(trusted_manifest) != sha256(spec["runtime_manifest_sha256"]): @@ -391,6 +401,11 @@ def build(repo: Path, inputs: Path, output: Path, work: Path) -> dict: target = payload / "bin" / name target.write_text(wrapper) target.chmod(0o555) + if spec["schema_version"] == 2: + fields(spec["proxy"], {"root", "catalog_sha256"}, "proxy build input") + proxy_runtime.copy_to_payload( + Path(spec["proxy"]["root"]), spec["proxy"]["catalog_sha256"], payload / "proxy" + ) for directory, directories, _ in os.walk(payload, topdown=False): for name in directories: (Path(directory) / name).chmod(0o555) diff --git a/scripts/release/lifecycle.py b/scripts/release/lifecycle.py index d421f28..2b086e3 100644 --- a/scripts/release/lifecycle.py +++ b/scripts/release/lifecycle.py @@ -21,6 +21,8 @@ from . import ( node_health, operator_units, preflight, + proxy_install, + proxy_service, service_install, services, ) @@ -28,7 +30,7 @@ from .policy import DeliveryError, canonical, digest, fields, read_json, sha256 STATE = Path("/var/lib/kf-onlinejudge/operations") LAST = STATE / "maintenance.last.json" -APPS = ("api", "outbox", "worker", "problem-import", "coordinator", "sandbox-factory") +APPS = ("proxy", "api", "outbox", "worker", "problem-import", "coordinator", "sandbox-factory") DEPENDENCIES = ("postgres", "rabbitmq") START_ORDER = ( "sandbox-factory", @@ -37,6 +39,7 @@ START_ORDER = ( "outbox", "problem-import", "worker", + "proxy", ) DRAIN_SECONDS = 360 @@ -263,6 +266,9 @@ class Maintenance: if name in services.PROFILES: directory = service_install.known_link(path) base = service_install.UNITS + elif name == "proxy": + directory = proxy_install.known_link(path) + base = proxy_service.UNITS elif name == "sandbox-factory": directory = factory_service.known_link(path) base = factory_service.UNITS @@ -419,7 +425,7 @@ class Maintenance: time.sleep(2) # Every process/child and final Root resource proof remains required. # No row, notification, identity lease or replay journal is removed here. - for name in ("api", "outbox", "problem-import", "worker"): + for name in ("proxy", "api", "outbox", "problem-import", "worker"): self.change(name, "stop") final = self.snapshot() if not projection_empty(final): diff --git a/scripts/release/node_health.py b/scripts/release/node_health.py index f7cb8fb..2d5a890 100644 --- a/scripts/release/node_health.py +++ b/scripts/release/node_health.py @@ -21,6 +21,9 @@ from . import ( journal, operator_units, preflight, + proxy_health, + proxy_install, + proxy_service, service_install, services, ) @@ -68,6 +71,11 @@ def binding() -> tuple[dict, dict, Path]: "operator_units.py", "outbox_health.py", "journal.py", + "proxy_service.py", + "proxy_install.py", + "proxy_guard.py", + "proxy_runtime.py", + "proxy_health.py", ): shipped = payload / "delivery/scripts/release" / name preflight.protected(shipped) @@ -102,6 +110,8 @@ def binding() -> tuple[dict, dict, Path]: def service_profile(name: str) -> tuple[int, int, int, int, int]: + if name == "proxy": + return 1205, 128, 16, 50, 8 if name in services.PROFILES: user, memory, pids, cpu, _, _ = services.PROFILES[name] return preflight.ACCOUNTS[user], memory, pids, cpu, 64 @@ -121,6 +131,11 @@ def inspect_unit(name: str, selected: dict) -> dict: expected = service_install.UNITS / ( selected["release_id"] + "." + selected["configuration_id"] ) + elif name == "proxy": + directory = proxy_install.known_link(link) + expected = proxy_service.UNITS / ( + selected["release_id"] + "." + selected["configuration_id"] + ) elif name == "sandbox-factory": directory = factory_service.known_link(link) expected = factory_service.UNITS / ( @@ -166,7 +181,7 @@ def inspect_unit(name: str, selected: dict) -> dict: raise DeliveryError("effective service identity differs") if not set(info.get("Groups", "").split()) <= {str(uid)}: raise DeliveryError("unexpected service supplementary authority") - if name != "sandbox-factory" and ( + if name not in {"sandbox-factory", "proxy"} and ( info.get("NoNewPrivs", "").strip() != "1" or any( not info.get(k, "").strip() or int(info[k].strip(), 16) != 0 @@ -174,11 +189,21 @@ def inspect_unit(name: str, selected: dict) -> dict: ) ): raise DeliveryError("effective ordinary privilege boundary differs") + if name == "proxy" and ( + info.get("NoNewPrivs", "").strip() != "1" + or any( + not info.get(k, "").strip() or int(info[k].strip(), 16) != proxy_service.CAPABILITY + for k in ("CapInh", "CapPrm", "CapEff", "CapBnd", "CapAmb") + ) + ): + raise DeliveryError("proxy acquired excess or missing listener capability") if name == "sandbox-factory" and ( info.get("NoNewPrivs", "").strip() != "0" or info.get("Seccomp", "").strip() != "0" ): raise DeliveryError("factory inherited incompatible hardening") - if name in {"api", "outbox", "problem-import"} and info.get("NSpid", "").split()[-1:] != ["1"]: + if name in {"api", "outbox", "problem-import", "proxy"} and info.get("NSpid", "").split()[ + -1: + ] != ["1"]: raise DeliveryError("Python service private PID namespace missing") group = Path("/sys/fs/cgroup" + value["ControlGroup"]) actual = { @@ -464,7 +489,12 @@ def observe(*, probe: bool) -> dict: components["journal"] = journal.check_ready() except (OSError, ValueError, subprocess.SubprocessError): errors.append("bounded_journal_unavailable") - for name in [*services.PROFILES, "postgres", "rabbitmq", "sandbox-factory"]: + try: + proxy_install.check_binding(selected["release_id"], selected["configuration_id"]) + components["https_entry"] = proxy_health.observe(selected) + except (OSError, ValueError, subprocess.SubprocessError): + errors.append("https_entry_unavailable") + for name in [*services.PROFILES, "postgres", "rabbitmq", "sandbox-factory", "proxy"]: try: components[name] = inspect_unit(name, selected) except (OSError, ValueError): diff --git a/scripts/release/policy.py b/scripts/release/policy.py index b8ad78c..dbc62c4 100644 --- a/scripts/release/policy.py +++ b/scripts/release/policy.py @@ -23,6 +23,7 @@ ROOTS = ( "images/", "deploy/", "delivery/", + "proxy/", ) FORBIDDEN = {".git", ".localplan", "docs", "__pycache__", ".env", ".ssh"} diff --git a/scripts/release/proxy.py b/scripts/release/proxy.py index 54c9755..c574a75 100644 --- a/scripts/release/proxy.py +++ b/scripts/release/proxy.py @@ -75,8 +75,7 @@ def render(release_id: str, value: dict) -> bytes: add_header Cache-Control "no-store" always; """.replace("HOST", host) return ( - """user kf-proxy; -worker_processes 1; + """worker_processes 1; pid /run/kf-proxy/nginx.pid; error_log /dev/null crit; events { worker_connections 128; } @@ -108,8 +107,8 @@ http { server { listen 443 ssl; server_name HOST; - ssl_certificate /etc/kf-onlinejudge/tls/fullchain.pem; - ssl_certificate_key /etc/kf-onlinejudge/tls/key.pem; + ssl_certificate /etc/kf-proxy/tls/fullchain.pem; + ssl_certificate_key /etc/kf-proxy/tls/key.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_session_cache shared:TLS:1m; ssl_session_timeout 5m; diff --git a/scripts/release/proxy_guard.py b/scripts/release/proxy_guard.py new file mode 100644 index 0000000..bcb10bf --- /dev/null +++ b/scripts/release/proxy_guard.py @@ -0,0 +1,177 @@ +"""Ordinary proxy guard and fixed exec; never read protected application credentials.""" + +import argparse +import os +import resource +import stat +from pathlib import Path + +from . import install, journal, preflight, proxy_install, proxy_runtime, proxy_service +from .policy import DeliveryError, digest, fields, read_json + + +def validate_runtime(value: dict) -> None: + fields( + value, + { + "uids", + "gids", + "groups", + "no_new_privileges", + "capabilities", + "pid", + "cgroup", + "limits", + "node_limits", + "process_limits", + }, + "proxy process observation", + ) + uid = str(proxy_service.UID) + expected = f"{proxy_service.CAPABILITY:016x}" + if ( + value["uids"] != [uid] * 4 + or value["gids"] != [uid] * 4 + or not set(value["groups"]) <= {uid} + or value["no_new_privileges"] != "1" + or type(value["pid"]) is not int + or value["pid"] != 1 + or value["cgroup"] != "/sys/fs/cgroup/kfonlinejudge.slice/kf-proxy.service" + or value["capabilities"] != [expected] * 5 + ): + raise DeliveryError("ordinary proxy identity/capability boundary differs") + if value["limits"] != { + "memory.max": str(128 * 1024**2), + "memory.swap.max": "0", + "pids.max": "16", + "cpu.max": "50000 100000", + } or value["node_limits"] != { + "memory.max": str(3840 * 1024**2), + "memory.swap.max": "0", + "pids.max": "384", + "cpu.max": "200000 100000", + }: + raise DeliveryError("proxy effective node/service budgets differ") + if value["process_limits"] != { + "core": [0, 0], + "file": [8 * 1024**2] * 2, + "files": [256] * 2, + "stack": [16 * 1024**2] * 2, + }: + raise DeliveryError("proxy effective process limits differ") + + +def observe() -> dict: + status = dict( + line.split(":", 1) + for line in journal.bounded_text(Path("/proc/self/status"), 16384).splitlines() + if ":" in line + ) + group = preflight.current_cgroup() + node = Path("/sys/fs/cgroup/kfonlinejudge.slice") + names = ("memory.max", "memory.swap.max", "pids.max", "cpu.max") + return { + "uids": status.get("Uid", "").split(), + "gids": status.get("Gid", "").split(), + "groups": status.get("Groups", "").split(), + "no_new_privileges": status.get("NoNewPrivs", "").strip(), + "capabilities": [ + status.get(k, "").strip() for k in ("CapInh", "CapPrm", "CapEff", "CapBnd", "CapAmb") + ], + "pid": os.getpid(), + "cgroup": str(group), + "limits": {k: (group / k).read_text().strip() for k in names}, + "node_limits": {k: (node / k).read_text().strip() for k in names}, + "process_limits": { + k: list(resource.getrlimit(r)) + for k, r in [ + ("core", resource.RLIMIT_CORE), + ("file", resource.RLIMIT_FSIZE), + ("files", resource.RLIMIT_NOFILE), + ("stack", resource.RLIMIT_STACK), + ] + }, + } + + +def run(release: str, batch: str) -> None: + selected = {"release_id": release, "configuration_id": batch} + directory = proxy_service.batch(selected) + validate_runtime(observe()) + proxy_install.account() + proxy_install.storage() + public = directory / "launch.json" + preflight.protected(public) + info = public.stat() + if stat.S_IMODE(info.st_mode) != 0o644 or info.st_gid != 0: + raise DeliveryError("protected public proxy launch spec required") + spec = read_json(public, 4096) + fields( + spec, + { + "schema_version", + "release_id", + "configuration_id", + "origin", + "configuration_sha256", + "certificate_sha256", + "key_sha256", + }, + "proxy launch spec", + ) + if ( + spec["schema_version"] != 1 + or type(spec["schema_version"]) is not int + or (spec["release_id"], spec["configuration_id"]) != (release, batch) + ): + raise DeliveryError("proxy public launch binding differs") + config = directory / "nginx.conf" + preflight.protected(config) + if digest(config) != spec["configuration_sha256"]: + raise DeliveryError("proxy configuration bytes changed") + for name, key in [("fullchain.pem", "certificate_sha256"), ("key.pem", "key_sha256")]: + path = proxy_service.ROOT / "tls" / name + preflight.protected(path) + if path.stat().st_size > 65536 or digest(path) != spec[key]: + raise DeliveryError("proxy TLS bytes changed") + payload = install.RELEASES / release / "payload" + root = payload / "proxy" + preflight.protected(root, directory=True) + # This guard is the already-installed, Root-protected launcher selected by + # the fixed unit; catalog checks are additional integrity checks only. + proxy_runtime.verify(root, digest(root / "manifest.json")) + executable = root / "bin/nginx" + preflight.protected(executable) + os.execve( + executable, + [ + str(executable), + "-p", + "/run/kf-proxy/", + "-c", + str(config), + "-g", + "daemon off; master_process on;", + ], + {"PATH": "/usr/bin:/bin", "LC_ALL": "C.UTF-8", "LD_LIBRARY_PATH": str(root / "lib")}, + ) + + +def main() -> None: + parser = argparse.ArgumentParser(description="Fixed ordinary HTTPS launcher") + parser.add_argument("--release-id", required=True) + parser.add_argument("--configuration-id", required=True) + args = parser.parse_args() + try: + run(args.release_id, args.configuration_id) + except (OSError, ValueError) as error: + parser.exit( + 1, + "Proxy launch refused: " + + (str(error) if isinstance(error, DeliveryError) else type(error).__name__) + + "\n", + ) + + +if __name__ == "__main__": + main() diff --git a/scripts/release/proxy_health.py b/scripts/release/proxy_health.py new file mode 100644 index 0000000..46d1c1c --- /dev/null +++ b/scripts/release/proxy_health.py @@ -0,0 +1,64 @@ +"""Read-only fixed HTTPS reachability, pinned certificate and hostname check.""" + +import hashlib +import http.client +import socket +import ssl +from urllib.parse import urlsplit + +from . import proxy_service +from .policy import DeliveryError, read_json + + +def observe(selected: dict) -> dict: + directory = proxy_service.batch(selected) + spec = read_json(directory / "launch.json", 4096) + host = urlsplit(spec["origin"]).hostname + # This explicit Root-protected leaf/chain is already byte-bound to selected + # TLS material; it is the local health trust anchor, not a claim that public + # browsers or the system CA store trust it. + certificate = proxy_service.ROOT / "tls/fullchain.pem" + ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + ctx.load_verify_locations(cafile=certificate) + ctx.verify_flags |= ssl.VERIFY_X509_PARTIAL_CHAIN + ctx.minimum_version = ssl.TLSVersion.TLSv1_2 + with socket.create_connection(("127.0.0.1", 443), timeout=5) as raw: + with ctx.wrap_socket(raw, server_hostname=host) as client: + client.settimeout(5) + der = client.getpeercert(binary_form=True) + pem = certificate.read_text() + leaf = pem[: pem.index("-----END CERTIFICATE-----") + len("-----END CERTIFICATE-----")] + if ( + hashlib.sha256(der).digest() + != hashlib.sha256(ssl.PEM_cert_to_DER_cert(leaf)).digest() + ): + raise DeliveryError("HTTPS served another certificate") + client.sendall( + ( + f"GET /api/v1/health/live HTTP/1.1\r\nHost: {host}\r\nConnection: close\r\n\r\n" + ).encode() + ) + response = http.client.HTTPResponse(client) + try: + response.begin() + body = response.read(4097) + if response.status != 200 or len(body) > 4096: + raise DeliveryError("HTTPS health response exceeds bound or is unavailable") + from .operator_units import decode + + value = decode(body, 4096) + if ( + not isinstance(value, dict) + or value.get("status") != "ok" + or value.get("service") != "platform" + ): + raise DeliveryError("HTTPS/API health response unavailable") + except http.client.HTTPException as error: + raise DeliveryError("HTTPS health framing unavailable") from error + finally: + response.close() + return { + "reachable": True, + "pinned_certificate_and_hostname_verified": True, + "public_browser_trust_verified": False, + } diff --git a/scripts/release/proxy_install.py b/scripts/release/proxy_install.py new file mode 100644 index 0000000..5affffa --- /dev/null +++ b/scripts/release/proxy_install.py @@ -0,0 +1,408 @@ +"""Protected HTTPS installation; no default keys, schema migration or server start.""" + +import argparse +import grp +import json +import os +import pwd +import secrets +import ssl +import stat +import subprocess +from pathlib import Path + +from . import ( + configuration, + install, + journal, + preflight, + proxy_runtime, + proxy_service, + service_install, +) +from .policy import DeliveryError, canonical, digest, fields, read_json, sha256 + + +def account(*, create: bool = False, check_lock: bool = False) -> None: + name, uid = proxy_service.USER, proxy_service.UID + matches = [user for user in pwd.getpwall() if user.pw_name == name or user.pw_uid == uid] + groups = [group for group in grp.getgrall() if group.gr_name == name or group.gr_gid == uid] + if not matches and not groups and create: + for args in ( + ["/usr/bin/groupadd", "--system", "--gid", str(uid), name], + [ + "/usr/bin/useradd", + "--system", + "--uid", + str(uid), + "--gid", + str(uid), + "--home-dir", + "/nonexistent", + "--shell", + "/usr/bin/nologin", + "--no-user-group", + name, + ], + ): + p = subprocess.run( + args, env={"PATH": "/usr/bin:/bin", "LC_ALL": "C"}, capture_output=True, timeout=10 + ) + if p.returncode: + raise DeliveryError("proxy account provisioning refused") + return account(check_lock=True) + if ( + len(matches) != 1 + or len(groups) != 1 + or matches[0].pw_name != name + or matches[0].pw_uid != uid + or matches[0].pw_gid != uid + or matches[0].pw_dir != "/nonexistent" + or matches[0].pw_shell != "/usr/bin/nologin" + or groups[0].gr_name != name + or groups[0].gr_gid != uid + or groups[0].gr_mem + ): + raise DeliveryError("conflicting ordinary proxy account") + if os.getgrouplist(name, uid) != [uid]: + raise DeliveryError("proxy supplementary authority refused") + if create or check_lock: + preflight.protected(Path("/etc/shadow")) + rows = [ + line.split(":") + for line in journal.bounded_text(Path("/etc/shadow"), 1024**2).splitlines() + ] + selected = [row for row in rows if row[0] == name] + if len(selected) != 1 or len(selected[0]) != 9 or selected[0][1] not in ("!", "!!", "*"): + raise DeliveryError("proxy OS login must remain locked") + + +def storage(*, allow_read_only: bool = False) -> dict: + path = proxy_service.DATA + preflight.protected(path.parent, directory=True) + info = path.lstat() + if ( + not stat.S_ISDIR(info.st_mode) + or info.st_uid != proxy_service.UID + or info.st_gid != proxy_service.UID + or stat.S_IMODE(info.st_mode) != 0o700 + ): + raise DeliveryError("private ordinary proxy hard volume required") + rows = [ + line.split() + for line in journal.bounded_text(Path("/proc/self/mountinfo"), 1024**2).splitlines() + ] + found = [row for row in rows if len(row) > 6 and row[4] == str(path)] + if len(found) != 1: + raise DeliveryError("dedicated proxy volume required") + row = found[0] + index = row.index("-") + vfs = os.statvfs(path) + if ( + row[3] != "/" + or row[index + 1] != "ext4" + or not {"nosuid", "nodev", "noexec"} <= set(row[5].split(",")) + or set(row[5].split(",")) & {"ro", "rw"} + not in (({"ro"}, {"rw"}) if allow_read_only else ({"rw"},)) + or not proxy_service.CAPACITY * 9 // 10 + <= vfs.f_frsize * vfs.f_blocks + <= proxy_service.CAPACITY + or any(other[2] == row[2] and other != row for other in rows) + ): + raise DeliveryError("proxy hard storage capacity/mount differs") + return { + "capacity_bytes": vfs.f_frsize * vfs.f_blocks, + "available_bytes": vfs.f_frsize * vfs.f_bavail, + } + + +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 proxy system unit") + target = Path(os.readlink(path)) + if ( + target.name != proxy_service.UNIT + or target.parent.parent != proxy_service.UNITS + or target.as_posix() != os.readlink(path) + ): + raise DeliveryError("unknown proxy unit pointer") + preflight.protected(target.parent / "binding.json") + configuration.private(target.parent / "binding.json") + selected = read_json(target.parent / "binding.json", 4096) + fields( + selected, + {"schema_version", "release_id", "configuration_id", "manifest_sha256", "receipt_sha256"}, + "proxy unit binding", + ) + if type(selected["schema_version"]) is not int or selected["schema_version"] != 1: + raise DeliveryError("proxy unit binding version differs") + sha256(selected["manifest_sha256"]) + sha256(selected["receipt_sha256"]) + if target.parent.name != proxy_service.batch(selected).name: + raise DeliveryError("proxy unit snapshot binding differs") + preflight.protected(target) + preflight.protected(target.parent / "binding.json") + if ( + target.read_bytes() != proxy_service.render(selected) + or stat.S_IMODE(target.stat().st_mode) != 0o644 + ): + raise DeliveryError("proxy unit snapshot changed") + return target.parent + + +def check_binding(release: str, batch: str) -> dict: + if os.geteuid() != 0: + raise DeliveryError("protected proxy operator required") + selected = install.selection() + if (release, batch) != (selected["release_id"], selected["configuration_id"]): + raise DeliveryError("proxy unit belongs to another selected configuration") + directory = proxy_service.batch(selected) + spec = read_json(directory / "launch.json", 4096) + for member in (directory / "launch.json", directory / "nginx.conf"): + preflight.protected(member) + if member.stat().st_gid != 0 or stat.S_IMODE(member.stat().st_mode) != 0o644: + raise DeliveryError("protected public proxy configuration required") + fields( + spec, + { + "schema_version", + "release_id", + "configuration_id", + "origin", + "configuration_sha256", + "certificate_sha256", + "key_sha256", + }, + "proxy launch spec", + ) + if ( + spec["schema_version"] != 1 + or type(spec["schema_version"]) is not int + or (spec["release_id"], spec["configuration_id"]) != (release, batch) + or digest(directory / "nginx.conf") != spec["configuration_sha256"] + ): + raise DeliveryError("proxy configuration binding changed") + from .proxy import validate_profile + + validate_profile( + { + "schema_version": 1, + "origin": spec["origin"], + "certificate_sha256": spec["certificate_sha256"], + "key_sha256": spec["key_sha256"], + } + ) + for name, key, mode, gid in [ + ("fullchain.pem", "certificate_sha256", 0o644, 0), + ("key.pem", "key_sha256", 0o640, proxy_service.UID), + ]: + p = proxy_service.ROOT / "tls" / name + preflight.protected(p) + i = p.stat() + if ( + i.st_gid != gid + or stat.S_IMODE(i.st_mode) != mode + or i.st_size > 65536 + or digest(p) != spec[key] + ): + raise DeliveryError("proxy TLS input changed") + journal.check_profile(allow_read_only=True) + storage(allow_read_only=True) + account(check_lock=True) + return {"binding_valid": True, "tls_verified": False, "execution_verified": False} + + +def install_proxy(source: Path, expected: str, certificate: Path, key: Path) -> dict: + if os.getuid() != 0 or os.geteuid() != 0 or os.getgid() != 0: + raise DeliveryError("root proxy installer required") + preflight.protected(source, directory=True) + receipt_path = source / "receipt.json" + preflight.protected(receipt_path) + configuration.private(receipt_path) + if digest(receipt_path) != sha256(expected): + raise DeliveryError("proxy receipt fingerprint differs") + receipt = proxy_service.validate_receipt(read_json(receipt_path, 8192)) + selected = install.selection() + spec = proxy_service.launch_spec(selected, receipt) + payload = install.RELEASES / selected["release_id"] / "payload" + for name in ( + "proxy.py", + "proxy_service.py", + "proxy_install.py", + "proxy_guard.py", + "proxy_runtime.py", + ): + p = payload / "delivery/scripts/release" / name + preflight.protected(p) + if digest(p) != digest(Path(__file__).parent / name): + raise DeliveryError("proxy installer differs from selected delivery") + runtime = payload / "proxy" + preflight.protected(runtime / "manifest.json") + data = proxy_runtime.verify(runtime, digest(runtime / "manifest.json")) + # The selected manifest already bound every installed byte; local catalog + # verification here is an additional runtime-tree check, not a trust origin. + inventory = read_json(payload.parent / "manifest.json")["files"] + item = next((entry for entry in inventory if entry["path"] == "proxy/manifest.json"), None) + if item is None or item["sha256"] != digest(runtime / "manifest.json"): + raise DeliveryError("proxy catalog absent from selected manifest") + env = configuration.environment_read( + configuration.installed_path( + selected["release_id"], selected["configuration_id"], "api.env" + ).read_bytes() + ) + if json.loads(env["KF_OJ_BROWSER_ORIGINS"]) != [receipt["origin"]]: + raise DeliveryError("proxy browser origin differs from API batch") + if {p.name for p in source.iterdir()} != {"receipt.json", "nginx.conf"}: + raise DeliveryError("unknown proxy source member") + preflight.protected(source / "nginx.conf") + configuration.private(source / "nginx.conf") + contents = proxy_service.configuration(receipt, selected["release_id"]) + if (source / "nginx.conf").read_bytes() != contents or digest(source / "nginx.conf") != spec[ + "configuration_sha256" + ]: + raise DeliveryError("proxy is not fixed trusted composition") + for p, fingerprint in ((certificate, spec["certificate_sha256"]), (key, spec["key_sha256"])): + preflight.protected(p) + configuration.private(p) + if not 1 <= p.stat().st_size <= 65536 or digest(p) != fingerprint: + raise DeliveryError("explicit bounded TLS material differs") + install.require_stopped() + account(create=True) + # Storage provisioning is an explicit operator step; no format/mount and no + # recursive ownership change of existing data are performed here. + storage() + journal.check_profile() + ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + + def no_password_prompt(): + raise DeliveryError("explicit unencrypted private TLS file required; no interactive prompt") + + ctx.load_cert_chain(certificate, key, password=no_password_prompt) + system = service_install.SYSTEM_UNITS / proxy_service.UNIT + known_link(system) + install.directory(proxy_service.ROOT, 0o711) + install.directory(proxy_service.ROOT / "tls", 0o711) + directory = proxy_service.batch(selected) + install.directory(directory, 0o755) + if not {p.name for p in directory.iterdir()} <= {"nginx.conf", "launch.json"}: + raise DeliveryError("unknown proxy configuration snapshot") + install.exclusive_file( + proxy_service.ROOT / "tls" / "fullchain.pem", certificate.read_bytes(), 0o644 + ) + private = proxy_service.ROOT / "tls" / "key.pem" + if private.exists() or private.is_symlink(): + preflight.protected(private) + i = private.stat() + if ( + stat.S_IMODE(i.st_mode) != 0o640 + or i.st_gid != proxy_service.UID + or digest(private) != spec["key_sha256"] + ): + raise DeliveryError("unknown existing TLS key; refused rotation") + else: + install.exclusive_file(private, key.read_bytes(), 0o640) + os.chown(private, 0, proxy_service.UID) + install.exclusive_file(directory / "nginx.conf", contents, 0o644) + install.exclusive_file(directory / "launch.json", canonical(spec), 0o644) + install.directory(proxy_service.UNITS, 0o700) + unitdir = proxy_service.UNITS / directory.name + install.directory(unitdir, 0o700) + install.exclusive_file(unitdir / "binding.json", canonical(selected), 0o600) + install.exclusive_file(unitdir / proxy_service.UNIT, proxy_service.render(selected), 0o644) + body = proxy_service.DATA / "body" + if body.exists() or body.is_symlink(): + i = body.lstat() + if ( + not stat.S_ISDIR(i.st_mode) + or i.st_uid != proxy_service.UID + or i.st_gid != proxy_service.UID + or stat.S_IMODE(i.st_mode) != 0o700 + ): + raise DeliveryError("unknown proxy temporary directory") + else: + body.mkdir(mode=0o700) + os.chown(body, proxy_service.UID, proxy_service.UID) + check_binding(selected["release_id"], selected["configuration_id"]) + install.require_stopped() + if install.selection() != selected: + raise DeliveryError("proxy selection changed before unit switch") + old = known_link(system) + if old != unitdir: + temporary = service_install.SYSTEM_UNITS / (".kf-proxy-" + secrets.token_hex(16)) + temporary.symlink_to(unitdir / proxy_service.UNIT) + try: + os.replace(temporary, system) + install.sync_directory(service_install.SYSTEM_UNITS) + finally: + if temporary.is_symlink(): + temporary.unlink() + service_install.systemctl("daemon-reload") + props = dict( + line.split("=", 1) + for line in service_install.systemctl( + "show", proxy_service.UNIT, "--property=LoadState,ActiveState,MainPID,FragmentPath" + ).splitlines() + if "=" in line + ) + if ( + props.get("LoadState") != "loaded" + or props.get("ActiveState") != "inactive" + or props.get("MainPID") != "0" + or props.get("FragmentPath") not in {str(system), str(unitdir / proxy_service.UNIT)} + ): + raise DeliveryError("proxy loaded/inactive readback unconfirmed") + return { + "installed": True, + "runtime_version": data["version"], + "source_sha": receipt["source_sha"], + "configuration_id": selected["configuration_id"], + "tls_material_parsed": True, + "tls_verified": False, + "services_started": False, + "execution_verified": False, + } + + +def main() -> None: + parser = argparse.ArgumentParser(description="Fixed protected HTTPS installation") + parser.add_argument("--check-binding", action="store_true") + parser.add_argument("--release-id") + parser.add_argument("--configuration-id") + parser.add_argument("--source", type=Path) + parser.add_argument("--receipt-sha256") + parser.add_argument("--certificate", type=Path) + parser.add_argument("--private-key", type=Path) + args = parser.parse_args() + try: + if args.check_binding: + if ( + not args.release_id + or not args.configuration_id + or any((args.source, args.receipt_sha256, args.certificate, args.private_key)) + ): + raise DeliveryError("explicit binding-only arguments required") + result = check_binding(args.release_id, args.configuration_id) + else: + if any((args.release_id, args.configuration_id)) or not all( + (args.source, args.receipt_sha256, args.certificate, args.private_key) + ): + raise DeliveryError("explicit installation inputs required") + result = install_proxy( + args.source, args.receipt_sha256, args.certificate, args.private_key + ) + print(json.dumps(result, sort_keys=True)) + except (OSError, ValueError, subprocess.SubprocessError) as error: + parser.exit( + 1, + "Proxy installation refused: " + + (str(error) if isinstance(error, DeliveryError) else type(error).__name__) + + "\n", + ) + + +if __name__ == "__main__": + main() diff --git a/scripts/release/proxy_runtime.py b/scripts/release/proxy_runtime.py new file mode 100644 index 0000000..9728148 --- /dev/null +++ b/scripts/release/proxy_runtime.py @@ -0,0 +1,111 @@ +"""Closed public NGINX runtime inventory, independent from TLS and service state.""" + +import re +import stat +from pathlib import Path + +from .policy import DeliveryError, digest, fields, integer, read_json, sha256 + +VERSION = "1.30.5" +MAX_BYTES = 64 * 1024**2 + + +def validate(value: dict) -> dict: + fields( + value, + { + "schema_version", + "format", + "version", + "architecture", + "package_sha256", + "files", + "directories", + "payload_bytes", + }, + "proxy runtime", + ) + if ( + type(value["schema_version"]) is not int + or value["schema_version"] != 1 + or value["format"] != "kf.nginx.runtime.v1" + or value["version"] != VERSION + or value["architecture"] != "x86_64" + or value["directories"] != ["bin", "lib"] + or not isinstance(value["files"], list) + or not 1 <= len(value["files"]) <= 32 + ): + raise DeliveryError("unsupported proxy runtime profile") + sha256(value["package_sha256"]) + names = set() + total = 0 + for entry in value["files"]: + fields(entry, {"path", "bytes", "sha256", "mode"}, "proxy runtime file") + name = entry["path"] + if not isinstance(name, str) or ( + name != "bin/nginx" and not re.fullmatch(r"lib/[A-Za-z0-9_.+-]+\.so(?:\.[0-9]+)*", name) + ): + raise DeliveryError("non-runtime or private proxy member") + if name in names or entry["mode"] != ("0555" if name == "bin/nginx" else "0444"): + raise DeliveryError("duplicate proxy member or unsafe mode") + names.add(name) + total += integer(entry["bytes"], 1, 32 * 1024**2, "proxy file bytes") + sha256(entry["sha256"]) + if ( + not {"bin/nginx", "lib/libc.so.6", "lib/ld-linux-x86-64.so.2"} <= names + or total > MAX_BYTES + or type(value["payload_bytes"]) is not int + or value["payload_bytes"] != total + ): + raise DeliveryError("incomplete or unbounded proxy runtime") + return value + + +def verify(root: Path, expected: str) -> dict: + if root.is_symlink() or not root.is_dir(): + raise DeliveryError("ordinary proxy runtime directory required") + catalog = root / "manifest.json" + if catalog.is_symlink() or digest(catalog) != sha256(expected): + raise DeliveryError("proxy runtime out-of-band fingerprint differs") + value = validate(read_json(catalog, 16384)) + if {p.name for p in root.iterdir()} != {"manifest.json", "bin", "lib"}: + raise DeliveryError("unexpected proxy runtime tree member") + for directory in (root / "bin", root / "lib"): + if ( + directory.is_symlink() + or not directory.is_dir() + or stat.S_IMODE(directory.stat().st_mode) != 0o555 + ): + raise DeliveryError("readonly ordinary proxy runtime directories required") + names = set() + for directory in (root / "bin", root / "lib"): + for p in directory.iterdir(): + names.add(p.relative_to(root).as_posix()) + if names != {entry["path"] for entry in value["files"]}: + raise DeliveryError("proxy runtime inventory differs") + for entry in value["files"]: + p = root / entry["path"] + info = p.lstat() + if ( + not stat.S_ISREG(info.st_mode) + or info.st_nlink != 1 + or info.st_size != entry["bytes"] + or stat.S_IMODE(info.st_mode) != int(entry["mode"], 8) + or digest(p) != entry["sha256"] + ): + raise DeliveryError("proxy runtime bytes or permission differ") + return value + + +def copy_to_payload(root: Path, expected: str, destination: Path) -> None: + from .build import copy + + value = verify(root, expected) + if destination.exists() or destination.is_symlink(): + raise DeliveryError("proxy runtime payload must be new") + destination.mkdir() + for directory in value["directories"]: + (destination / directory).mkdir() + for entry in value["files"]: + copy(root / entry["path"], destination / entry["path"]) + copy(root / "manifest.json", destination / "manifest.json") diff --git a/scripts/release/proxy_service.py b/scripts/release/proxy_service.py new file mode 100644 index 0000000..90826ef --- /dev/null +++ b/scripts/release/proxy_service.py @@ -0,0 +1,156 @@ +"""Fixed ordinary HTTPS service profile, separate from TLS/config installation.""" + +import re +from pathlib import Path + +from . import install, proxy +from .policy import DeliveryError, fields, sha256 +from .services import python_environment + +UID = 1205 +USER = "kf-proxy" +UNIT = "kf-proxy.service" +ROOT = Path("/etc/kf-proxy") +UNITS = ROOT / "units" +DATA = Path("/var/lib/kf-onlinejudge/proxy") +CAPACITY = 128 * 1024**2 +CAPABILITY = 1 << 10 # Linux CAP_NET_BIND_SERVICE, solely for the fixed TLS listener. + + +def validate_receipt(value: dict) -> dict: + fields( + value, + { + "schema_version", + "source_sha", + "manifest_sha256", + "origin", + "tls", + "configuration_sha256", + "installed", + "tls_verified", + "services_started", + "execution_verified", + }, + "proxy source 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 any( + value[k] is not False + for k in ("installed", "tls_verified", "services_started", "execution_verified") + ) + ): + raise DeliveryError("proxy source receipt authority differs") + fields(value["tls"], {"certificate_sha256", "key_sha256"}, "proxy TLS fingerprints") + proxy.validate_profile({"schema_version": 1, "origin": value["origin"], **value["tls"]}) + sha256(value["manifest_sha256"]) + sha256(value["configuration_sha256"]) + return value + + +def batch(selected: dict) -> Path: + if not re.fullmatch(r"sd-[0-9a-f]{12}", selected["release_id"]) or not re.fullmatch( + r"cfg-[0-9a-f]{24}", selected["configuration_id"] + ): + raise DeliveryError("invalid proxy service binding") + return ROOT / (selected["release_id"] + "." + selected["configuration_id"]) + + +def launch_spec(selected: dict, receipt: dict) -> dict: + validate_receipt(receipt) + if ( + receipt["source_sha"][:12] != selected["release_id"][3:] + or receipt["manifest_sha256"] != selected["manifest_sha256"] + ): + raise DeliveryError("proxy is not this selected release") + return { + "schema_version": 1, + "release_id": selected["release_id"], + "configuration_id": selected["configuration_id"], + "origin": receipt["origin"], + "configuration_sha256": receipt["configuration_sha256"], + **receipt["tls"], + } + + +def render(selected: dict) -> bytes: + batch(selected) + payload = install.RELEASES / selected["release_id"] / "payload" + arguments = ( + f" --release-id {selected['release_id']} --configuration-id {selected['configuration_id']}" + ) + clean = "/usr/bin/env -i PATH=/usr/bin:/bin LC_ALL=C.UTF-8 " + python_environment(payload) + python = str(payload / "python/bin/python3.13") + lines = [ + "[Unit]", + "Description=KF bounded ordinary HTTPS entry", + "After=local-fs.target kf-api.service", + "StartLimitIntervalSec=600", + "StartLimitBurst=3", + "[Service]", + "Type=exec", + "Slice=kfonlinejudge.slice", + "User=kf-proxy", + "Group=kf-proxy", + "UMask=0077", + "WorkingDirectory=" + str(payload), + "RuntimeDirectory=kf-proxy", + "RuntimeDirectoryMode=0700", + "ExecStartPre=+" + + clean + + " " + + python + + " -s -P -m scripts.release.proxy_install --check-binding" + + arguments, + "ExecStart=" + clean + " " + python + " -s -P -m scripts.release.proxy_guard" + arguments, + "MemoryMax=128M", + "MemorySwapMax=0", + "CPUQuota=50%", + "TasksMax=16", + "LimitCORE=0", + "LimitFSIZE=8M", + "LimitNOFILE=256", + "LimitSTACK=16M", + "NoNewPrivileges=yes", + "CapabilityBoundingSet=CAP_NET_BIND_SERVICE", + "AmbientCapabilities=CAP_NET_BIND_SERVICE", + "ProtectSystem=strict", + "ProtectHome=yes", + "ProtectControlGroups=yes", + "ProtectKernelTunables=yes", + "ProtectKernelModules=yes", + "PrivateTmp=yes", + "PrivateDevices=yes", + "PrivatePIDs=yes", + "ProtectProc=invisible", + "RestrictSUIDSGID=yes", + "LockPersonality=yes", + "RestrictAddressFamilies=AF_UNIX AF_INET", + "ReadWritePaths=" + str(DATA), + "RequiresMountsFor=" + str(DATA), + "KillMode=control-group", + "TimeoutStartSec=30", + "TimeoutStopSec=30", + "RuntimeMaxSec=24h", + "Restart=on-failure", + "RestartSec=30", + "StandardOutput=journal", + "StandardError=journal", + "LogNamespace=kf-onlinejudge", + "LogRateLimitIntervalSec=30s", + "LogRateLimitBurst=100", + "[Install]", + "WantedBy=multi-user.target", + ] + return ("\n".join(lines) + "\n").encode() + + +def configuration(receipt: dict, release_id: str) -> bytes: + validate_receipt(receipt) + return proxy.render( + release_id, {"schema_version": 1, "origin": receipt["origin"], **receipt["tls"]} + )