feat(release): install and supervise the bounded HTTPS entry

This commit is contained in:
2026-10-13 06:36:14 +08:00
parent 83e235567f
commit 8e6133a8d0
12 changed files with 1120 additions and 13 deletions
+32 -1
View File
@@ -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.
+18 -3
View File
@@ -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)
+8 -2
View File
@@ -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):
+33 -3
View File
@@ -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):
+1
View File
@@ -23,6 +23,7 @@ ROOTS = (
"images/",
"deploy/",
"delivery/",
"proxy/",
)
FORBIDDEN = {".git", ".localplan", "docs", "__pycache__", ".env", ".ssh"}
+3 -4
View File
@@ -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;
+177
View File
@@ -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()
+64
View File
@@ -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,
}
+408
View File
@@ -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()
+111
View File
@@ -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")
+156
View File
@@ -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"]}
)