Files
KF-OnlineJudge/scripts/release/proxy_guard.py
T

178 lines
5.9 KiB
Python

"""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()