feat(delivery): add guarded offline installation and node configuration

This commit is contained in:
2026-10-12 20:44:40 +08:00
parent 5d127ad4db
commit 3cde198358
11 changed files with 1674 additions and 11 deletions
+126
View File
@@ -2,6 +2,7 @@
import importlib
import json
import secrets
import sys
from pathlib import Path
@@ -12,6 +13,7 @@ sys.path.insert(0, str(ROOT))
policy = importlib.import_module("scripts.release.policy")
manifest = importlib.import_module("scripts.release.manifest")
builder = importlib.import_module("scripts.release.build")
configuration = importlib.import_module("scripts.release.configuration")
architecture = importlib.import_module("scripts.check_architecture")
@@ -111,3 +113,127 @@ def test_signed_guest_library_alias_uses_guest_root(tmp_path):
builder.curated(root, records, "services/usr/lib/library.so", target)
assert target.read_bytes() == library.read_bytes()
assert not target.is_symlink()
@pytest.fixture
def delivery_configuration(tmp_path, monkeypatch):
"""Synthetic operator files only; no services, DB or user program are run."""
import hashlib
images = {}
for name, toolchain, kind, version in (
("gnu", "gnu-cxx-v1", "linux_gnu_cxx_x86_64_v1", "16.2.1+r23+gd564253eb6c8"),
("python313", "cpython313-v1", "linux_cpython313_x86_64_v1", "3.13.16"),
("java21", "openjdk21-v1", "linux_openjdk21_x86_64_v1", "21.0.12.1+1-LTS"),
):
images[name] = {
"toolchain_id": toolchain,
"image_kind": kind,
"image_revision": 1,
"runtime_version": version,
"image_digest": hashlib.sha256(name.encode()).hexdigest(),
}
data = {
"images": images,
"release_id": "sd-123456789abc",
"source_sha": "123456789abc" + "d" * 28,
}
node = {
"schema_version": 1,
"origin": "https://oj.test",
"worker_id": "a28adc33-8fbd-4a33-8b21-7a46431e506c",
"measurement_nonce": secrets.token_hex(32),
}
urls = {}
for name, role in configuration.PROFILES.items():
password = secrets.token_urlsafe(32)
authority = "" if name == "import" else "127.0.0.1:5432"
urls[name] = f"postgresql://{role}:{password}@{authority}/kf_onlinejudge"
if name == "import":
urls[name] += "?host=/run/postgresql"
endpoints = {
"schema_version": 1,
"database": urls,
"amqp": {
name: f"amqp://kf_{name}:{secrets.token_urlsafe(32)}@127.0.0.1:5672/kf"
for name in ("worker", "outbox")
},
}
configuration.validate_profile(node)
configuration.validate_connections(endpoints)
observed = {
"kernel": "6.18-fixture",
"architecture": "x86_64",
"online_cpus": 2,
"processor": {"model name": "fixture"},
}
monkeypatch.setattr(configuration, "measured_hardware", lambda: dict(observed))
raw = configuration.compose(ROOT, data, node, endpoints, attest=True)
paths = {}
for name, content in raw.items():
path = tmp_path / name
path.write_bytes(content)
path.chmod(0o600)
paths[name] = path
return paths, raw, data, node, endpoints, observed
def test_generated_configuration_preserves_wire_policies_and_backend_settings(
delivery_configuration, monkeypatch
):
from kf_oj.core.settings import Settings
paths, raw, data, _, _, _ = delivery_configuration
configuration.validate_contents(paths, ROOT, data, attest=True)
env = configuration.environment_read(raw["api.env"])
for key, value in env.items():
monkeypatch.setenv(key, value)
settings = Settings(_env_file=None)
assert settings.database_url.get_secret_value().startswith("postgresql+asyncpg:")
admission = json.loads(raw["admission.json"])
coordinator, worker, root = [
json.loads(raw[name]) for name in ("coordinator.json", "worker.json", "runtime.json")
]
assert settings.judge_admission.model_dump(mode="json") == admission
assert coordinator["admission"] == worker["admission"] == admission
assert "database_url" not in worker and "database_url" not in root
assert root["identities"]["count"] == 1
assert root["image_root"] not in [image["image_root"] for image in root["runtime_images"]]
assert admission["legacy"]["toolchain"] == {
"toolchain_id": "gnu-cxx-v1",
"image_kind": "linux_gnu_cxx_x86_64_v1",
"image_revision": 1,
}
def test_epoch_rebinds_when_kernel_changes(delivery_configuration):
paths, raw, data, node, endpoints, observed = delivery_configuration
before = json.loads(raw["measurement.json"])["epoch"]
observed["kernel"] = "6.19-fixture"
with pytest.raises(policy.DeliveryError, match="measurement policy mismatch"):
configuration.validate_contents(paths, ROOT, data, attest=True)
after = configuration.compose(ROOT, data, node, endpoints, attest=True)
assert json.loads(after["measurement.json"])["epoch"] != before
def test_unattested_configuration_has_no_comparable_epoch(delivery_configuration):
_, _, data, node, endpoints, _ = delivery_configuration
raw = configuration.compose(ROOT, data, node, endpoints, attest=False)
assert json.loads(raw["coordinator.json"])["workers"][0]["measurement_epoch"] is None
def test_configuration_receipt_is_bound_before_path_use():
receipt = {
"schema_version": 1,
"source_sha": "123456789abc" + "d" * 28,
"release_id": "sd-123456789abc",
"manifest_sha256": "f" * 64,
"configuration_files": {name: "b" * 64 for name in configuration.CONFIG_FILES},
"operator_attested_measurement": False,
"installed": False,
"execution_verified": False,
}
configuration.validate_receipt(receipt, installed=False)
receipt["installed"] = 0
with pytest.raises(policy.DeliveryError, match="configuration binding"):
configuration.validate_receipt(receipt, installed=False)
+38
View File
@@ -44,3 +44,41 @@ data-loss target; restoring from already prepared release/backup materials has a
verification. These are acceptance targets, not results inferred from this file.
Installation/service/restore evidence is required before this pre-release can be
treated as an accepted delivery; the contract itself does not deploy production.
The node input for `generate-configuration` has exactly `schema_version: 1`,
`origin` (one HTTPS origin), `worker_id` (canonical nonzero UUID), and
`measurement_nonce` (null or a fresh operator-generated SHA-256-sized nonce).
It is a 0600 single-link file. Endpoint input is a separate 0600 JSON object
with exactly `schema_version: 1`, `database` and `amqp`: database keys are
`api`, `coordinator`, `import`, `migration`, `identity`; broker keys are `outbox`
and `worker`. Login names and numeric loopback/socket locations are validated by
the tool. Every endpoint requires its own operator-supplied private credential;
there is no working example password or shared default. Import uses the reachable
`/run/postgresql` Unix socket; all other control connections use numeric loopback.
Generated admission uses the actual verified catalogs for C17/C++20, CPython
3.13.16 and OpenJDK 21.0.12.1+1-LTS and the unchanged public per-language budgets.
The protected GNU legacy image is a separate directory: it retains the original
v1 header and identical verified GNU executable/library bytes, while the three
v2 catalog identities remain untouched. Shared Worker/task UID/GID pool and
execution/prefetch/import slots remain one. No canonical judging report or
historical runtime identity is converted by the installer.
Comparability defaults to unavailable (`measurement_epoch=null`). A trusted
operator may explicitly use `--attest-measurement` only after actual node/service
limits and the intended execution environment have been verified. The attestation
binds a fresh nonce, worker, source, exact images, budgets, CPU/microcode, kernel
and node scheduling snapshot. `check-configuration` refuses an old binding when
those values change; it does not manufacture replacement metrics for old reports.
Before any service resume after such a change, drain execution, generate/install
a new complete private group with a new nonce and independently revalidate the
environment. Raw configuration checks are not performance or isolation evidence.
The installation entry points are available with `python3.13 -m scripts.release
--help`. Use them in order: verify/preflight, install-files, generate-configuration,
install-configuration, explicit DB identity provision, controlled Backend Alembic
and account CLI operations, then activate and the separately verified bounded
service lifecycle. These operations never contact a package index and do not
install onto a business host on their own. OS dependencies (systemd 262, matching
glibc ABI bytes, PostgreSQL 18.6 and the fixed broker runtime) must already be
present from separately verified offline materials.
+41
View File
@@ -28,3 +28,44 @@ contains the source and manifest fingerprints, file count and byte total, with
`installed=false` and `execution_verified=false`. Installation and service
acceptance remain separate operations. See `deploy/release/README.md` for the
versioned installation contract and authority split.
`preflight` reads installation eligibility only. `install-files` requires root,
a completely root-controlled verified input tree and the fixed target profile;
it installs protected versions/images and bounded OS identities, without table
migrations, application accounts, services or the active-version switch.
Repeated identical files converge; unknown modes/owners, changed bytes or an
incomplete staging directory require operator inspection and are never removed.
`generate-configuration` composes exact public contracts with separately supplied
0600 node and endpoint inputs. It emits a new private directory and a hash-bound
receipt, never prints tokens, never changes a supplied password and never silently
overwrites an earlier identity. API, publisher, import, migration and identity
operations have distinct required database authority; Worker holds only its own
MQ/control identity, Root receives no business credentials. Backend URLs use the
existing asyncpg driver. No uvx/pip or online runtime installation is used.
`install-configuration` validates the entire group before writing any member;
`check-configuration` checks installed bytes, same-source admission, identity and
hardware/kernel/node scheduler bindings. `activate` requires that check and
stopped control ports, no factory socket and no retained task/identity/capture
resources. It atomically selects an installed directory and starts no process;
draining, migrations and actual health remain separate operations. It does not
erase retained journals or release resources lacking cleanup proof.
`provision-database-identities` is an explicit local database operator step.
It uses the locked asyncpg dependency, bounded connections/statements and a
120-second overall deadline. It installs only fixed independent logins/NOLOGIN
groups and the application database, verifies every supplied credential and
refuses unexpected existing authority. There is no application table DDL or
automatic account bootstrap: the migration login owns the database without
global superuser/create-role/create-db/bypass-RLS rights; Backend Alembic remains
the only schema/grant migration entry. Runtime logins cannot grant/set group
authority. See [PostgreSQL role membership](https://www.postgresql.org/docs/18/role-membership.html)
and [function ownership](https://www.postgresql.org/docs/18/sql-alterfunction.html).
Always bootstrap validation from a separately trusted tool copy and an out-of-band
manifest SHA. Do not execute a checker supplied inside an unverified bundle to
establish trust in that same bundle. A verified delivery includes `bin/kf-delivery`
and its source for later offline operations. Command success/receipt generation
does not claim Linux execution, working service roles or installation acceptance;
these require the recorded independent VM evidence.
+56 -3
View File
@@ -5,7 +5,7 @@ import json
import subprocess
from pathlib import Path
from . import build, manifest, policy
from . import build, configuration, database, install, manifest, policy, preflight
def main() -> None:
@@ -21,6 +21,28 @@ def main() -> None:
check.add_argument("--inputs", type=Path, required=True)
check.add_argument("--output", type=Path, required=True)
check.add_argument("--work", type=Path, required=True)
for name in ("preflight", "install-files"):
check = sub.add_parser(name)
check.add_argument("bundle", type=Path)
check.add_argument("--manifest-sha256", required=True)
check = sub.add_parser("generate-configuration")
check.add_argument("bundle", type=Path)
check.add_argument("--manifest-sha256", required=True)
check.add_argument("--profile", type=Path, required=True)
check.add_argument("--connections", type=Path, required=True)
check.add_argument("--output", type=Path, required=True)
check.add_argument("--attest-measurement", action="store_true")
check = sub.add_parser("install-configuration")
check.add_argument("directory", type=Path)
check.add_argument("--receipt-sha256", required=True)
check = sub.add_parser("check-configuration")
check.add_argument("receipt", type=Path)
check = sub.add_parser("provision-database-identities")
check.add_argument("--operator", type=Path, required=True)
check.add_argument("--connections", type=Path, required=True)
check = sub.add_parser("activate")
check.add_argument("release_id")
check.add_argument("--manifest-sha256", required=True)
args = parser.parse_args()
try:
if args.command == "contract":
@@ -36,10 +58,41 @@ def main() -> None:
"installed": False,
"execution_verified": False,
}
else:
elif args.command == "build":
result = build.build(args.repo, args.inputs, args.output, args.work)
elif args.command == "preflight":
manifest.verify(args.bundle, args.manifest_sha256)
result = preflight.observe(args.bundle / "payload")
print(json.dumps(result, sort_keys=True))
if not result["eligible"]:
parser.exit(1)
return
elif args.command == "install-files":
result = install.files(args.bundle, args.manifest_sha256)
elif args.command == "generate-configuration":
result = configuration.generate(
args.bundle,
args.manifest_sha256,
args.profile,
args.connections,
args.output,
attest=args.attest_measurement,
)
elif args.command == "install-configuration":
result = configuration.install_configuration(args.directory, args.receipt_sha256)
elif args.command == "check-configuration":
receipt = configuration.check_installed(args.receipt)
result = {
"configuration_valid": True,
"source_sha": receipt["source_sha"],
"execution_verified": False,
}
elif args.command == "provision-database-identities":
result = database.provision(args.operator, args.connections)
else:
result = install.activate(args.release_id, args.manifest_sha256)
print(json.dumps(result, sort_keys=True))
except (policy.DeliveryError, OSError, subprocess.SubprocessError) as error:
except (ValueError, OSError, subprocess.SubprocessError) as error:
# OSError filenames can include private operator paths; do not echo them.
parser.exit(
1,
+9 -1
View File
@@ -13,6 +13,7 @@ from . import manifest
from .policy import DeliveryError, contract, digest, fields, read_json, relative_name, sha256
ENTRYPOINTS = {
"kf-delivery": "scripts.release",
"kf-api": (
"uvicorn kf_oj.api.app:create_app --factory --host 127.0.0.1 --port 8000 "
"--no-proxy-headers --limit-concurrency 32 --backlog 64"
@@ -322,6 +323,13 @@ def build(repo: Path, inputs: Path, output: Path, work: Path) -> dict:
dependency_records(payload / "site")
tree(repo / "frontend/dist", payload / "frontend")
tracked(repo, "deploy", payload)
for raw in subprocess.check_output(
["git", "ls-files", "-z", "scripts/release"], cwd=repo
).split(b"\0"):
if raw:
name = raw.decode()
relative_name("delivery/" + name)
copy(repo / name, payload / "delivery" / name)
for name in ("kf-sandbox", "kf-worker", "kf-coordinator"):
copy(repo / "judge/target/release" / name, payload / "bin" / name)
for name in sorted(records):
@@ -374,7 +382,7 @@ def build(repo: Path, inputs: Path, output: Path, work: Path) -> dict:
"#!/bin/sh\nset -eu\n"
'APP_ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd -P)\n'
'export PYTHONHOME="$APP_ROOT/python"\n'
'export PYTHONPATH="$APP_ROOT/backend/src:$APP_ROOT/site"\n'
'export PYTHONPATH="$APP_ROOT/backend/src:$APP_ROOT/site:$APP_ROOT/delivery"\n'
"export PYTHONDONTWRITEBYTECODE=1\n"
'export LD_LIBRARY_PATH="$APP_ROOT/lib:$APP_ROOT/python/lib"\n'
'cd "$APP_ROOT"\n'
+597
View File
@@ -0,0 +1,597 @@
"""Compose public contracts and private identities without application state writes."""
import hashlib
import json
import os
import re
import secrets
import stat
import uuid
from pathlib import Path
from urllib.parse import parse_qs, urlsplit
from . import manifest, preflight
from .policy import DeliveryError, canonical, digest, fields, read_json, sha256
PROFILES = {
"api": "kf_platform",
"coordinator": "kf_coordinator",
"import": "kf_problem_import",
"identity": "kf_identity_operator",
"migration": "kf_migrator",
}
IMPORT_CGROUP = "/sys/fs/cgroup/system.slice/kf-problem-import.service"
CONFIG_FILES = {
"api.env",
"outbox.env",
"import.env",
"identity.env",
"migration.env",
"coordinator.json",
"worker.json",
"runtime.json",
"admission.json",
"measurement.json",
}
def private(path: Path) -> None:
info = path.lstat()
if (
not stat.S_ISREG(info.st_mode)
or info.st_nlink != 1
or stat.S_IMODE(info.st_mode) != 0o600
or info.st_uid != os.geteuid()
):
raise DeliveryError("private single-link operator input required")
def profile(path: Path) -> dict:
private(path)
return validate_profile(read_json(path, 16384))
def validate_profile(value: dict) -> dict:
value = fields(
value,
{"schema_version", "origin", "worker_id", "measurement_nonce"},
"node profile",
)
if type(value["schema_version"]) is not int or value["schema_version"] != 1:
raise DeliveryError("unsupported node profile")
origin = value["origin"]
if not isinstance(origin, str) or len(origin) > 253:
raise DeliveryError("invalid browser origin")
parts = urlsplit(origin)
if (
parts.scheme != "https"
or not parts.hostname
or parts.path
or parts.query
or parts.fragment
or parts.username
or parts.password
or not re.fullmatch(r"[a-z0-9][a-z0-9.-]*[a-z0-9]", parts.hostname)
or parts.port not in (None, 443)
):
raise DeliveryError("explicit HTTPS origin required")
try:
worker = uuid.UUID(value["worker_id"])
except (ValueError, TypeError, AttributeError) as error:
raise DeliveryError("invalid worker identity") from error
if str(worker) != value["worker_id"] or worker.int == 0:
raise DeliveryError("canonical nonzero worker UUID required")
if value["measurement_nonce"] is not None:
sha256(value["measurement_nonce"])
return value
def connections(path: Path) -> dict:
private(path)
return validate_connections(read_json(path, 16384))
def validate_connections(value: dict) -> dict:
value = fields(value, {"schema_version", "database", "amqp"}, "service connections")
if type(value["schema_version"]) is not int or value["schema_version"] != 1:
raise DeliveryError("unsupported service connections")
database = fields(value["database"], set(PROFILES), "database connections")
passwords = set()
for name, raw in database.items():
if not isinstance(raw, str) or len(raw) > 1024:
raise DeliveryError("invalid database connection")
parts = urlsplit(raw)
unix = (
name == "import"
and parts.hostname is None
and parse_qs(parts.query, strict_parsing=True) == {"host": ["/run/postgresql"]}
)
network = (
name != "import"
and parts.hostname == "127.0.0.1"
and parts.port == 5432
and not parts.query
)
if (
parts.scheme != "postgresql"
or parts.username != PROFILES[name]
or not re.fullmatch(r"[A-Za-z0-9_-]{32,128}", parts.password or "")
or parts.path != "/kf_onlinejudge"
or parts.fragment
or not (unix or network)
):
raise DeliveryError("database authority or loopback/socket policy mismatch")
if parts.password in passwords:
raise DeliveryError("service credentials must be independent")
passwords.add(parts.password)
broker = fields(value["amqp"], {"outbox", "worker"}, "broker connections")
for name, raw in broker.items():
if not isinstance(raw, str) or len(raw) > 1024:
raise DeliveryError("invalid broker connection")
parts = urlsplit(raw)
if (
parts.scheme != "amqp"
or parts.hostname != "127.0.0.1"
or parts.port != 5672
or parts.username != "kf_" + name
or not re.fullmatch(r"[A-Za-z0-9_-]{32,128}", parts.password or "")
or parts.path != "/kf"
or parts.query
or parts.fragment
):
raise DeliveryError("broker authority or loopback policy mismatch")
if parts.password in passwords:
raise DeliveryError("service credentials must be independent")
passwords.add(parts.password)
return value
def environmental(values: dict) -> bytes:
"""JSON strings are valid systemd EnvironmentFile double-quoted values here.
Input URLs/passwords cannot contain dollar/backtick/newline expansions;
only trusted JSON admission data and known fixed paths appear otherwise.
"""
return "".join(
key + "=" + json.dumps(value, separators=(",", ":")) + "\n"
for key, value in sorted(values.items())
).encode()
def measured_hardware() -> dict:
value = preflight.hardware()
# Operator command cgroup does not define execution scheduling. The fixed
# execution/supervisor budgets below are bound separately and read back by
# Sandbox; no transient installer PID/unit name is part of the epoch.
value.pop("parent_limits")
# Bind stable node-wide scheduling and ancestor constraints too. Per-service
# ceilings are in the policy; Sandbox separately verifies their actual
# values on every execution. This is not a transient operator unit snapshot.
value["node_scheduler"] = {}
for label, group in (
("root", Path("/sys/fs/cgroup")),
("system", Path("/sys/fs/cgroup/system.slice")),
):
value["node_scheduler"][label] = {
name: (group / name).read_text().strip() if (group / name).exists() else None
for name in ("cpu.max", "memory.max", "cpuset.cpus.effective")
}
return value
def admission_runtime(payload: Path, data: dict) -> tuple[dict, dict]:
from kf_oj.contracts.judging_v2 import AdmissionRegistry
admission = read_json(payload / "deploy/judge.admission.example.json", 16384)
root = read_json(payload / "deploy/sandbox.managed.runtime.example.json", 16384)
for cap in admission["multi_runtime"]["capabilities"]:
image = {"c17": "gnu", "cpp20": "gnu", "python313": "python313", "java21": "java21"}[
cap["language"]
]
cap["toolchain"] = data["images"][image]
admission = AdmissionRegistry.model_validate(admission).model_dump(mode="json")
state = "/var/lib/kf-judge/images/" + data["release_id"]
root["image_root"] = state + "/legacy-gnu"
root["delegated_cgroup_root"] = "/sys/fs/cgroup/kf-judge-tasks"
root["identities"] = {
"worker_uid": preflight.ACCOUNTS["kf-worker"],
"worker_gid": preflight.ACCOUNTS["kf-worker"],
"first_task_uid": preflight.TASK_UID,
"first_task_gid": preflight.TASK_GID,
"count": 1,
}
for image, entry in zip(("gnu", "python313", "java21"), root["runtime_images"], strict=True):
entry["image_root"] = state + "/" + image
entry["runtime_identity"] = data["images"][image]
if root["max_concurrent_runs"] != 1:
raise DeliveryError("single-slot configuration required")
return admission, root
def compose(
payload: Path,
data: dict,
node: dict,
endpoints: dict,
*,
attest: bool,
service_tokens: tuple[str, str] | None = None,
) -> dict:
"""Caller has verified the full delivery inventory before this composition."""
admission, root = admission_runtime(payload, data)
measurement = {
"schema_version": 1,
"hardware": measured_hardware(),
"source_sha": data["source_sha"],
"images": data["images"],
"admission": admission,
"sandbox_budget": root["aggregate"],
"measurement_nonce": node["measurement_nonce"],
"worker_id": node["worker_id"],
}
epoch = None
if attest:
if node["measurement_nonce"] is None:
raise DeliveryError("explicit fresh measurement nonce required for attestation")
epoch = hashlib.sha256(canonical(measurement)).hexdigest()
worker_token, platform_token = service_tokens or (secrets.token_hex(32), secrets.token_hex(32))
coordinator = {
"listen": "127.0.0.1:8100",
"database_url": endpoints["database"]["coordinator"],
"workers": [{"id": node["worker_id"], "token": worker_token, "measurement_epoch": epoch}],
"platform_token": platform_token,
"admission": admission,
"lease_seconds": 120,
"recovery_seconds": 5,
}
worker = {
"worker_id": node["worker_id"],
"service_token": worker_token,
"coordinator_url": "http://127.0.0.1:8100",
"platform_url": "http://127.0.0.1:8000",
"amqp_url": endpoints["amqp"]["worker"],
"socket": "/var/lib/kf-judge/factory/control.sock",
"staging": "/var/lib/kf-onlinejudge/worker",
"admission": admission,
"control_timeout_ms": 5000,
}
common = {
"KF_OJ_ENVIRONMENT": "production",
"KF_OJ_LOG_LEVEL": "INFO",
"KF_OJ_BROWSER_ORIGINS": canonical([node["origin"]]).decode().strip(),
"KF_OJ_ARTIFACT_ROOT": "/var/lib/kf-onlinejudge/artifacts",
"KF_OJ_IMPORT_STAGING_ROOT": "/var/lib/kf-onlinejudge/imports",
"KF_OJ_ARTIFACT_MAX_BYTES": "536870912",
"KF_OJ_ARTIFACT_MAX_FILES": "4096",
}
def backend_url(name):
return endpoints["database"][name].replace("postgresql:", "postgresql+asyncpg:", 1)
api = {
**common,
"KF_OJ_DATABASE_URL": backend_url("api"),
"KF_OJ_JUDGE_ADMISSION": canonical(admission).decode().strip(),
"KF_OJ_COORDINATOR_URL": "http://127.0.0.1:8100",
"KF_OJ_COORDINATOR_TOKEN": platform_token,
}
files = {
"api.env": environmental(api),
"outbox.env": environmental(
{
**common,
"KF_OJ_DATABASE_URL": backend_url("api"),
"KF_OJ_AMQP_URL": endpoints["amqp"]["outbox"],
}
),
"import.env": environmental(
{
**common,
"KF_OJ_DATABASE_URL": backend_url("import"),
"KF_OJ_IMPORT_WORKER_CGROUP": IMPORT_CGROUP,
}
),
"identity.env": environmental(
{
**common,
"KF_OJ_DATABASE_URL": backend_url("api"),
"KF_OJ_IDENTITY_OPERATOR_DATABASE_URL": backend_url("identity"),
}
),
"migration.env": environmental({**common, "KF_OJ_DATABASE_URL": backend_url("migration")}),
"admission.json": canonical(admission),
"runtime.json": canonical(root),
"coordinator.json": canonical(coordinator),
"worker.json": canonical(worker),
"measurement.json": canonical(
{"binding": measurement, "epoch": epoch, "operator_attested": attest}
),
}
return files
def validate_receipt(value: dict, *, installed: bool) -> dict:
value = fields(
value,
{
"schema_version",
"source_sha",
"release_id",
"manifest_sha256",
"configuration_files",
"operator_attested_measurement",
"installed",
"execution_verified",
},
"configuration 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 value["release_id"] != "sd-" + value["source_sha"][:12]
or type(value["operator_attested_measurement"]) is not bool
or value["installed"] is not installed
or value["execution_verified"] is not False
):
raise DeliveryError("invalid configuration binding")
sha256(value["manifest_sha256"])
names = CONFIG_FILES
if installed:
names = {installed_path(value["release_id"], name).as_posix() for name in CONFIG_FILES}
fields(value["configuration_files"], names, "configuration inventory")
for checksum in value["configuration_files"].values():
sha256(checksum)
return value
def installed_path(release_id: str, name: str) -> Path:
base = "/etc/kf-judge" if name == "runtime.json" else "/etc/kf-onlinejudge"
return Path(base) / (release_id + "." + name)
def environment_read(contents: bytes) -> dict:
if len(contents) > 32768:
raise DeliveryError("environment file exceeds limit")
values = {}
for line in contents.decode().splitlines():
name, sep, raw = line.partition("=")
if not sep or not re.fullmatch(r"KF_OJ_[A-Z_]+", name) or name in values:
raise DeliveryError("invalid service environment")
value = json.loads(raw)
if not isinstance(value, str) or "\n" in value or "\r" in value or "\x00" in value:
raise DeliveryError("invalid service environment value")
values[name] = value
return values
def validate_contents(paths: dict[str, Path], payload: Path, data: dict, *, attest: bool) -> None:
"""Validate public policy, endpoint roles and epoch binding, without health claims."""
if set(paths) != CONFIG_FILES:
raise DeliveryError("incomplete service configuration")
raw = {}
for name, path in paths.items():
private(path)
if path.stat().st_size > 32768:
raise DeliveryError("service configuration exceeds limit")
raw[name] = path.read_bytes()
envs = {name: environment_read(raw[name]) for name in CONFIG_FILES if name.endswith(".env")}
measurement = fields(
read_json(paths["measurement.json"], 32768),
{"binding", "epoch", "operator_attested"},
"measurement record",
)
binding = fields(
measurement["binding"],
{
"schema_version",
"hardware",
"source_sha",
"images",
"admission",
"sandbox_budget",
"measurement_nonce",
"worker_id",
},
"measurement binding",
)
origins = json.loads(envs["api.env"].get("KF_OJ_BROWSER_ORIGINS", "null"))
if not isinstance(origins, list) or len(origins) != 1:
raise DeliveryError("one explicit browser origin required")
node = validate_profile(
{
"schema_version": 1,
"origin": origins[0],
"worker_id": binding["worker_id"],
"measurement_nonce": binding["measurement_nonce"],
}
)
coordinator = fields(
read_json(paths["coordinator.json"], 32768),
{
"listen",
"database_url",
"workers",
"platform_token",
"admission",
"lease_seconds",
"recovery_seconds",
},
"coordinator configuration",
)
worker = fields(
read_json(paths["worker.json"], 32768),
{
"worker_id",
"service_token",
"coordinator_url",
"platform_url",
"amqp_url",
"socket",
"staging",
"admission",
"control_timeout_ms",
},
"worker configuration",
)
def database_url(name: str, key="KF_OJ_DATABASE_URL"):
raw = envs[name].get(key)
if not isinstance(raw, str) or not raw.startswith("postgresql+asyncpg:"):
raise DeliveryError("backend database driver mismatch")
return raw.replace("postgresql+asyncpg:", "postgresql:", 1)
endpoints = validate_connections(
{
"schema_version": 1,
"database": {
"api": database_url("api.env"),
"coordinator": coordinator.get("database_url"),
"import": database_url("import.env"),
"migration": database_url("migration.env"),
"identity": database_url("identity.env", "KF_OJ_IDENTITY_OPERATOR_DATABASE_URL"),
},
"amqp": {
"outbox": envs["outbox.env"].get("KF_OJ_AMQP_URL"),
"worker": worker.get("amqp_url"),
},
}
)
worker_token, platform_token = (
sha256(worker.get("service_token")),
sha256(coordinator.get("platform_token")),
)
if worker_token == platform_token:
raise DeliveryError("service identities overlap")
expected = compose(
payload, data, node, endpoints, attest=attest, service_tokens=(worker_token, platform_token)
)
for name in CONFIG_FILES:
# JSON comparisons via canonical bytes distinguish true/1 and null/[];
# environmental files must have precisely the generated keys/encoding.
if name.endswith(".json"):
match = canonical(read_json(paths[name], 32768)) == expected[name]
else:
match = raw[name] == expected[name]
if not match:
raise DeliveryError("configuration/identity/measurement policy mismatch")
def check_installed(receipt_path: Path) -> dict:
from .install import RELEASES
preflight.protected(receipt_path)
private(receipt_path)
receipt = validate_receipt(read_json(receipt_path, 16384), installed=True)
release = RELEASES / receipt["release_id"]
preflight.protected(release, directory=True)
data = manifest.verify(release, receipt["manifest_sha256"])
if receipt["source_sha"] != data["source_sha"]:
raise DeliveryError("configuration source differs from release")
paths = {}
for name in CONFIG_FILES:
path = installed_path(receipt["release_id"], name)
preflight.protected(path)
if digest(path) != receipt["configuration_files"][path.as_posix()]:
raise DeliveryError("installed configuration fingerprint differs")
paths[name] = path
validate_contents(
paths, release / "payload", data, attest=receipt["operator_attested_measurement"]
)
return receipt
def generate(
bundle: Path,
expected: str,
node_path: Path,
connections_path: Path,
output: Path,
*,
attest: bool = False,
) -> dict:
data = manifest.verify(bundle, expected)
node, endpoints = profile(node_path), connections(connections_path)
if output.exists() or output.is_symlink():
raise DeliveryError("configuration output must be new; secrets are never overwritten")
files = compose(bundle / "payload", data, node, endpoints, attest=attest)
output.mkdir(mode=0o700)
hashes = {}
for name, contents in files.items():
with (output / name).open("xb") as stream:
os.chmod(output / name, 0o600)
stream.write(contents)
hashes[name] = digest(output / name)
receipt = {
"schema_version": 1,
"source_sha": data["source_sha"],
"release_id": data["release_id"],
"manifest_sha256": expected,
"configuration_files": hashes,
"operator_attested_measurement": attest,
"installed": False,
"execution_verified": False,
}
(output / "receipt.json").write_bytes(canonical(receipt))
(output / "receipt.json").chmod(0o600)
return {
"configuration_generated": True,
"source_sha": data["source_sha"],
"receipt_sha256": digest(output / "receipt.json"),
"operator_attested_measurement": attest,
"installed": False,
"execution_verified": False,
}
def install_configuration(source: Path, expected_receipt: str) -> dict:
from .install import RELEASES, exclusive_file
if os.geteuid() != 0:
raise DeliveryError("configuration installation requires root")
preflight.protected(source, directory=True)
receipt_path = source / "receipt.json"
preflight.protected(receipt_path)
if digest(receipt_path) != sha256(expected_receipt):
raise DeliveryError("configuration receipt fingerprint differs")
receipt = validate_receipt(read_json(receipt_path, 16384), installed=False)
data = manifest.verify(RELEASES / receipt["release_id"], receipt["manifest_sha256"])
if (
receipt["source_sha"] != data["source_sha"]
or type(receipt["schema_version"]) is not int
or receipt["schema_version"] != 1
):
raise DeliveryError("configuration source mismatch")
if {p.name for p in source.iterdir()} != CONFIG_FILES | {"receipt.json"}:
raise DeliveryError("unexpected configuration member")
# Validate the entire group before publishing any configuration member.
for name, checksum in receipt["configuration_files"].items():
preflight.protected(source / name)
if digest(source / name) != checksum:
raise DeliveryError("configuration bytes differ")
validate_contents(
{name: source / name for name in CONFIG_FILES},
RELEASES / data["release_id"] / "payload",
data,
attest=receipt["operator_attested_measurement"],
)
installed = {}
for name, checksum in receipt["configuration_files"].items():
member = source / name
preflight.protected(member)
private(member)
if digest(member) != sha256(checksum):
raise DeliveryError("configuration bytes differ")
target = installed_path(data["release_id"], name)
exclusive_file(target, member.read_bytes(), 0o600)
installed[str(target)] = checksum
receipt = {**receipt, "configuration_files": installed, "installed": True}
target = Path("/etc/kf-onlinejudge") / (data["release_id"] + ".configuration.json")
exclusive_file(target, canonical(receipt), 0o600)
return {
"configuration_installed": True,
"source_sha": data["source_sha"],
"receipt_sha256": digest(target),
"execution_verified": False,
}
+177
View File
@@ -0,0 +1,177 @@
"""Explicit DB identities only; all application schema changes belong to Alembic."""
import asyncio
import os
import re
from pathlib import Path
from urllib.parse import urlsplit
from .configuration import PROFILES, connections, private
from .policy import DeliveryError, fields, read_json
GROUPS = (
"kf_oj_platform",
"kf_oj_coordinator",
"kf_oj_problem_import",
"kf_oj_identity_operator",
"kf_oj_identity_authority_owner",
)
MEMBERSHIP = {
"kf_platform": "kf_oj_platform",
"kf_coordinator": "kf_oj_coordinator",
"kf_problem_import": "kf_oj_problem_import",
"kf_identity_operator": "kf_oj_identity_operator",
"kf_migrator": "kf_oj_identity_authority_owner",
}
DATABASE = "kf_onlinejudge"
SETTINGS = {
"statement_timeout": "5000",
"lock_timeout": "1000",
"idle_in_transaction_session_timeout": "5000",
"application_name": "kf-delivery-identities",
}
def operator_url(path: Path) -> str:
private(path)
value = fields(read_json(path, 4096), {"schema_version", "url"}, "DB operator connection")
if type(value["schema_version"]) is not int or value["schema_version"] != 1:
raise DeliveryError("unsupported DB operator input")
raw = value["url"]
if not isinstance(raw, str) or len(raw) > 1024:
raise DeliveryError("invalid DB operator connection")
parts = urlsplit(raw)
if (
parts.scheme != "postgresql"
or parts.hostname != "127.0.0.1"
or parts.port != 5432
or parts.username != "postgres"
or parts.path != "/postgres"
or parts.query
or parts.fragment
or not re.fullmatch(r"[A-Za-z0-9_-]{32,128}", parts.password or "")
):
raise DeliveryError("explicit local DB operator identity required")
return raw
async def _provision(operator: Path, endpoint_path: Path) -> dict:
import asyncpg
endpoints = connections(endpoint_path)
db = await asyncpg.connect(
operator_url(operator),
timeout=3,
command_timeout=5,
server_settings={**SETTINGS, "log_min_error_statement": "panic"},
)
try:
if not await db.fetchval("SELECT rolsuper FROM pg_roles WHERE rolname=current_user"):
raise DeliveryError("explicit local DB identity operator required")
for name, expected in (
("log_statement", "none"),
("log_min_duration_statement", "-1"),
("log_min_duration_sample", "-1"),
("log_transaction_sample_rate", "0"),
):
if await db.fetchval("SHOW " + name) != expected:
raise DeliveryError("DB statement logging incompatible with secret provisioning")
async with db.transaction():
for name in (*GROUPS, *PROFILES.values()):
login = name in PROFILES.values()
row = await db.fetchrow(
"SELECT rolcanlogin,rolsuper,rolcreatedb,rolcreaterole,"
"rolreplication,rolbypassrls,"
"rolconnlimit FROM pg_roles WHERE rolname=$1",
name,
)
wanted = (login, False, False, False, False, False, 8 if login else -1)
if row is None:
# Names are fixed public constants. The password alphabet
# is validated before any utility SQL is constructed.
statement = (
"CREATE ROLE "
+ name
+ (" LOGIN" if login else " NOLOGIN")
+ " NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION "
"NOBYPASSRLS INHERIT"
)
if login:
statement += " CONNECTION LIMIT 8"
key = next(key for key, role in PROFILES.items() if role == name)
password = urlsplit(endpoints["database"][key]).password
statement += " PASSWORD '" + password + "'"
await db.execute(statement)
elif tuple(row) != wanted:
raise DeliveryError("existing DB authority differs; refused overwrite")
memberships = [
tuple(row)
for row in await db.fetch(
"SELECT r.rolname,m.admin_option,m.inherit_option,m.set_option "
"FROM pg_auth_members m JOIN pg_roles r ON r.oid=m.roleid "
"JOIN pg_roles u ON u.oid=m.member WHERE u.rolname=$1",
name,
)
]
expected = (
[]
if not login
else [(MEMBERSHIP[name], False, name != "kf_migrator", name == "kf_migrator")]
)
if memberships and memberships != expected:
raise DeliveryError("existing DB role membership differs")
for login, group in MEMBERSHIP.items():
inherit = "FALSE" if login == "kf_migrator" else "TRUE"
set_role = "TRUE" if login == "kf_migrator" else "FALSE"
await db.execute(
f"GRANT {group} TO {login} WITH ADMIN FALSE, INHERIT {inherit}, SET {set_role}"
)
existing = await db.fetchval(
"SELECT r.rolname FROM pg_database d JOIN pg_roles r ON r.oid=d.datdba "
"WHERE d.datname=$1",
DATABASE,
)
if existing is None:
await db.execute(
"CREATE DATABASE kf_onlinejudge OWNER kf_migrator "
"TEMPLATE template0 ENCODING 'UTF8'"
)
elif existing != "kf_migrator":
raise DeliveryError("existing application database owner differs")
finally:
await db.close(timeout=3)
verified = []
for name, raw in endpoints["database"].items():
session = await asyncpg.connect(raw, timeout=3, command_timeout=5, server_settings=SETTINGS)
try:
row = await session.fetchrow("SELECT current_user,current_database()")
if tuple(row) != (PROFILES[name], DATABASE):
raise DeliveryError("DB identity readback differs")
verified.append(name)
finally:
await session.close(timeout=3)
return {
"database_identities_provisioned": sorted(verified),
"application_schema_migrated": False,
"application_accounts_created": False,
"secrets_reset": False,
}
def provision(operator: Path, endpoint_path: Path) -> dict:
"""No table DDL, no secret reset and no automatic application initialization.
Pre-created NOLOGIN groups allow the unchanged Backend Alembic migrations
to run under a separate non-superuser database owner. Only this migration
login can SET the authority-function owner; ordinary logins inherit their
own one group and cannot grant it. Unexpected existing authority is refused.
"""
if os.geteuid() != 0:
raise DeliveryError("DB provisioning requires the root operator")
import asyncpg
try:
return asyncio.run(asyncio.wait_for(_provision(operator, endpoint_path), timeout=120))
except (asyncpg.PostgresError, asyncpg.InterfaceError, OSError, TimeoutError) as error:
# A server error may include utility SQL; never format it or its query.
raise DeliveryError("DB identity provisioning/readback refused") from error
+335
View File
@@ -0,0 +1,335 @@
"""Root operator file installation, independent of migrations and service start."""
import os
import re
import secrets
import shutil
import stat
import subprocess
from pathlib import Path
from . import manifest, preflight
from .policy import DeliveryError, canonical, digest, read_json, sha256
RELEASES = Path("/opt/kf-onlinejudge/releases")
CURRENT = Path("/opt/kf-onlinejudge/current")
IMAGES = Path("/var/lib/kf-judge/images")
LEGACY_HEADER = {"image_kind": "linux_gnu_cxx_x86_64_v1", "architecture": "x86_64", "revision": 1}
def directory(path: Path, mode: int) -> None:
if not path.exists() and not path.is_symlink():
preflight.protected(path.parent, directory=True)
path.mkdir(mode=mode)
preflight.protected(path, directory=True)
if stat.S_IMODE(path.stat().st_mode) != mode:
raise DeliveryError("existing directory mode differs; operator review required")
def exclusive_file(path: Path, data: bytes, mode: int) -> None:
preflight.protected(path.parent, directory=True)
if path.exists() or path.is_symlink():
preflight.protected(path)
if stat.S_IMODE(path.stat().st_mode) != mode or path.read_bytes() != data:
raise DeliveryError("existing installed file differs; refused overwrite")
return
fd = os.open(path, os.O_CREAT | os.O_EXCL | os.O_WRONLY | os.O_NOFOLLOW, mode)
try:
with os.fdopen(fd, "wb", closefd=False) as output:
output.write(data)
output.flush()
os.fsync(fd)
finally:
os.close(fd)
sync_directory(path.parent)
def sync_directory(path: Path) -> None:
fd = os.open(path, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
try:
os.fsync(fd)
finally:
os.close(fd)
def copy_verified(source: Path, target: Path, expected: str, mode: int) -> None:
preflight.protected(source)
if digest(source) != sha256(expected):
raise DeliveryError("source changed after bundle verification")
if target.exists() or target.is_symlink():
preflight.protected(target)
if digest(target) != expected or stat.S_IMODE(target.stat().st_mode) != mode:
raise DeliveryError("existing release differs; refused overwrite")
return
preflight.protected(target.parent, directory=True)
incoming = os.open(source, os.O_RDONLY | os.O_NOFOLLOW)
outgoing = os.open(target, os.O_CREAT | os.O_EXCL | os.O_WRONLY | os.O_NOFOLLOW, 0o600)
try:
with (
os.fdopen(incoming, "rb", closefd=False) as src,
os.fdopen(outgoing, "wb", closefd=False) as dst,
):
shutil.copyfileobj(src, dst, 1024 * 1024)
dst.flush()
os.fsync(outgoing)
if digest(target) != expected:
raise DeliveryError("installed bytes differ; incomplete release retained")
os.fchmod(outgoing, mode)
finally:
os.close(incoming)
os.close(outgoing)
def ensure_accounts() -> None:
if preflight.identity_conflicts():
raise DeliveryError("OS identity conflicts")
import grp
import pwd
for name, identity in preflight.ACCOUNTS.items():
try:
grp.getgrnam(name)
except KeyError:
subprocess.run(
["/usr/bin/groupadd", "--gid", str(identity), name],
check=True,
timeout=10,
capture_output=True,
)
try:
pwd.getpwnam(name)
except KeyError:
subprocess.run(
[
"/usr/bin/useradd",
"--uid",
str(identity),
"--gid",
str(identity),
"--no-create-home",
"--no-user-group",
"--key",
"SUB_UID_COUNT=0",
"--key",
"SUB_GID_COUNT=0",
"--home-dir",
"/nonexistent",
"--shell",
"/usr/bin/nologin",
name,
],
check=True,
timeout=10,
capture_output=True,
)
if preflight.identity_conflicts():
raise DeliveryError("OS identity readback differs")
def data_directory(path: Path, owner: int) -> None:
preflight.protected(path.parent, directory=True)
if not path.exists() and not path.is_symlink():
path.mkdir(mode=0o700)
os.chown(path, owner, owner)
info = path.lstat()
if (
not stat.S_ISDIR(info.st_mode)
or info.st_uid != owner
or info.st_gid != owner
or stat.S_IMODE(info.st_mode) != 0o700
):
raise DeliveryError("existing private data directory identity/mode differs")
def frozen_copy(source: Path, target: Path, *, path_prefix: str = "") -> None:
"""For already verified release images, never accept external archive members."""
records = manifest.inventory(source, path_prefix=path_prefix)
directories = manifest.directories(source, path_prefix=path_prefix)
if target.exists() or target.is_symlink():
preflight.protected(target, directory=True)
if (
manifest.inventory(target, path_prefix=path_prefix) != records
or manifest.directories(target, path_prefix=path_prefix) != directories
):
raise DeliveryError("existing protected image differs")
for record in records:
preflight.protected(target / record["path"])
return
target.mkdir(mode=0o700)
for name in sorted(directories, key=lambda name: (name.count("/"), name)):
(target / name).mkdir(mode=0o700)
for record in records:
copy_verified(
source / record["path"],
target / record["path"],
record["sha256"],
int(record["mode"], 8),
)
for name in sorted(directories, key=lambda name: name.count("/"), reverse=True):
(target / name).chmod(0o555)
sync_directory(target / name)
target.chmod(0o555)
sync_directory(target)
if (
manifest.inventory(target, path_prefix=path_prefix) != records
or manifest.directories(target, path_prefix=path_prefix) != directories
):
raise DeliveryError("installed image readback differs")
def files(bundle: Path, expected: str) -> dict:
if os.geteuid() != 0:
raise DeliveryError("installer requires root")
preflight.protected(bundle, directory=True)
data = manifest.verify(bundle, expected)
observation = preflight.observe(bundle / "payload")
if not observation["eligible"]:
raise DeliveryError("installation preflight refused: " + "; ".join(observation["errors"]))
# Per-entry ownership matters even when the inventory is identical.
for record in data["files"]:
preflight.protected(bundle / "payload" / record["path"])
for name in data["directories"]:
preflight.protected(bundle / "payload" / name, directory=True)
ensure_accounts()
for name, mode in (
("/opt/kf-onlinejudge", 0o755),
(str(RELEASES), 0o755),
("/etc/kf-onlinejudge", 0o700),
("/etc/kf-judge", 0o700),
("/var/lib/kf-onlinejudge", 0o755),
("/var/lib/kf-judge", 0o755),
("/var/lib/kf-judge/tasks", 0o755),
(str(IMAGES), 0o755),
):
directory(Path(name), mode)
for name in ("artifacts", "imports"):
data_directory(Path("/var/lib/kf-onlinejudge") / name, preflight.ACCOUNTS["kf-oj"])
data_directory(Path("/var/lib/kf-onlinejudge/worker"), preflight.ACCOUNTS["kf-worker"])
release = RELEASES / data["release_id"]
if release.exists():
preflight.protected(release, directory=True)
manifest.verify(release, expected)
for record in data["files"]:
preflight.protected(release / "payload" / record["path"])
else:
stage = RELEASES / ("." + data["release_id"] + ".installing")
if stage.exists() or stage.is_symlink():
raise DeliveryError("incomplete release requires explicit operator inspection")
stage.mkdir(mode=0o700)
# `frozen_copy` accepts names through the same bounded allowlist.
frozen_copy(bundle / "payload", stage / "payload")
copy_verified(bundle / "manifest.json", stage / "manifest.json", expected, 0o444)
manifest.verify(stage, expected)
stage.chmod(0o555)
os.rename(stage, release)
sync_directory(RELEASES)
image_base = IMAGES / data["release_id"]
directory(image_base, 0o755)
for name in data["images"]:
frozen_copy(
release / "payload/images" / name, image_base / name, path_prefix="images/catalog/"
)
legacy = image_base / "legacy-gnu"
if legacy.exists():
preflight.protected(legacy, directory=True)
if read_json(legacy / "manifest.json") != LEGACY_HEADER:
raise DeliveryError("legacy compatibility image header differs")
wanted = {
r["path"]: r
for r in manifest.inventory(image_base / "gnu", path_prefix="images/catalog/")
if r["path"] != "manifest.json"
}
actual = {
r["path"]: r
for r in manifest.inventory(legacy, path_prefix="images/catalog/")
if r["path"] != "manifest.json"
}
if wanted != actual or (
manifest.directories(legacy, path_prefix="images/catalog/")
!= manifest.directories(image_base / "gnu", path_prefix="images/catalog/")
):
raise DeliveryError("legacy compatibility image bytes differ")
else:
# This is an explicit v1 compatibility image, not a catalog rewrite.
# All executable/library bytes are identical to the verified GNU image.
frozen_copy(image_base / "gnu", legacy, path_prefix="images/catalog/")
legacy.chmod(0o700)
old = legacy / "manifest.json"
old.chmod(0o600)
old.write_bytes(canonical(LEGACY_HEADER))
old.chmod(0o444)
legacy.chmod(0o555)
receipt = {
"schema_version": 1,
"release_id": data["release_id"],
"source_sha": data["source_sha"],
"manifest_sha256": expected,
"images": data["images"],
"legacy_header_sha256": digest(legacy / "manifest.json"),
"files_installed": True,
"activated": False,
"migrated": False,
"execution_verified": False,
}
exclusive_file(
Path("/etc/kf-onlinejudge") / (data["release_id"] + ".files.json"),
canonical(receipt),
0o600,
)
return receipt
def activate(release_id: str, expected: str) -> dict:
"""Only files + separately installed exact configuration may be selected.
This command starts no service, migrates no table, and removes no old version.
Runtime admission/isolation/health remains a separate bounded operation.
"""
if os.geteuid() != 0 or not re.fullmatch(r"sd-[0-9a-f]{12}", release_id):
raise DeliveryError("invalid activation identity or privilege")
release = RELEASES / release_id
preflight.protected(release, directory=True)
data = manifest.verify(release, expected)
config_receipt = Path("/etc/kf-onlinejudge") / (release_id + ".configuration.json")
from .configuration import check_installed
binding = check_installed(config_receipt)
if (
binding.get("manifest_sha256") != expected
or binding.get("source_sha") != data["source_sha"]
):
raise DeliveryError("configuration not bound to selected release")
for port in (8000, 8100):
import socket
with socket.socket() as check:
try:
check.bind(("127.0.0.1", port))
except OSError as error:
raise DeliveryError("stop and drain services before activation") from error
if Path("/var/lib/kf-judge/factory/control.sock").exists():
raise DeliveryError("factory socket present; explicit stop/cleanup required")
for name in ("tasks", "identities", "captures"):
path = Path("/var/lib/kf-judge") / name
if path.exists():
preflight.protected(path, directory=True)
if next(path.iterdir(), None) is not None:
raise DeliveryError("execution leases or task resources remain; activation refused")
if CURRENT.exists() or CURRENT.is_symlink():
info = CURRENT.lstat()
if not stat.S_ISLNK(info.st_mode) or info.st_uid != 0:
raise DeliveryError("unknown current entry refused")
old = Path(os.readlink(CURRENT))
if old.parent.parent != RELEASES or old.name != "payload":
raise DeliveryError("current link outside release layout")
preflight.protected(old, directory=True)
temporary = CURRENT.parent / (".current-" + secrets.token_hex(16))
temporary.symlink_to(release / "payload")
os.replace(temporary, CURRENT)
sync_directory(CURRENT.parent)
return {
"selected_source_sha": data["source_sha"],
"activated": True,
"services_started": False,
"execution_verified": False,
}
+5 -4
View File
@@ -24,7 +24,7 @@ from .policy import (
)
def inventory(root: Path) -> list[dict]:
def inventory(root: Path, *, path_prefix: str = "") -> list[dict]:
"""No links/devices or inferred inclusion; bounds apply before hashing."""
if not root.is_dir() or root.is_symlink():
raise DeliveryError("payload root must be an ordinary directory")
@@ -50,7 +50,8 @@ def inventory(root: Path) -> list[dict]:
total += size
if total > MAX_BUNDLE_BYTES or len(entries) >= MAX_FILES:
raise DeliveryError("payload aggregate limit")
relative = relative_name(path.relative_to(root).as_posix())
relative = path.relative_to(root).as_posix()
relative_name(path_prefix + relative)
entries.append(
{
"path": relative,
@@ -62,13 +63,13 @@ def inventory(root: Path) -> list[dict]:
return sorted(entries, key=lambda x: x["path"])
def directories(root: Path) -> list[str]:
def directories(root: Path, *, path_prefix: str = "") -> list[str]:
result = []
for directory, names, _ in os.walk(root, followlinks=False):
for name in names:
path = Path(directory) / name
value = path.relative_to(root).as_posix()
relative_name(value + "/.__directory__")
relative_name(path_prefix + value + "/.__directory__")
if path.is_symlink() or stat.S_IMODE(path.stat().st_mode) != 0o555:
raise DeliveryError("payload directories must be read-only ordinary directories")
result.append(value)
+29 -3
View File
@@ -13,7 +13,17 @@ MAX_BUNDLE_BYTES = 4 * 1024**3
FORMAT = "kf.single-node.directory.v1"
PLATFORM = "linux_x86_64_v3_glibc244_systemd"
TOOLCHAINS = {"python": "3.13.16", "rust": "1.99.0", "node": "26.10.0", "pnpm": "11.28.4"}
ROOTS = ("backend/", "python/", "site/", "lib/", "bin/", "frontend/", "images/", "deploy/")
ROOTS = (
"backend/",
"python/",
"site/",
"lib/",
"bin/",
"frontend/",
"images/",
"deploy/",
"delivery/",
)
FORBIDDEN = {".git", ".localplan", "docs", "__pycache__", ".env", ".ssh"}
@@ -73,11 +83,27 @@ def read_json(path: Path, maximum: int = MAX_METADATA_BYTES) -> dict:
if len(data) > maximum:
raise DeliveryError("metadata size limit")
try:
return json.loads(data, object_pairs_hook=unique_object)
except (UnicodeError, json.JSONDecodeError) as error:
value = json.loads(data, object_pairs_hook=unique_object, parse_constant=invalid_constant)
pending = [(value, 0)]
count = 0
while pending:
item, depth = pending.pop()
count += 1
if depth > 32 or count > 400000:
raise DeliveryError("metadata structure limit")
if isinstance(item, dict):
pending.extend((member, depth + 1) for member in item.values())
elif isinstance(item, list):
pending.extend((member, depth + 1) for member in item)
return value
except (UnicodeError, json.JSONDecodeError, RecursionError) as error:
raise DeliveryError("invalid JSON metadata") from error
def invalid_constant(_value: str):
raise DeliveryError("nonfinite JSON value refused")
def canonical(value: Any) -> bytes:
return (json.dumps(value, sort_keys=True, separators=(",", ":")) + "\n").encode()
+261
View File
@@ -0,0 +1,261 @@
"""Read-only installation eligibility; never an execution isolation permit."""
import grp
import os
import platform
import pwd
import re
import shutil
import socket
import stat
import subprocess
from pathlib import Path
from .policy import DeliveryError, digest
GIB = 1024**3
ACCOUNTS = {"kf-oj": 1200, "kf-coordinator": 1201, "kf-worker": 1202}
TASK_UID = 100000
TASK_GID = 100000
def protected(path: Path, *, directory: bool = False, ancestors: bool = True) -> None:
"""Only the deliberate `current` link is handled by the activation module."""
paths = [path, *path.parents] if ancestors else [path]
for item in paths:
info = item.lstat()
if info.st_uid != 0 or info.st_mode & 0o022 or stat.S_ISLNK(info.st_mode):
raise DeliveryError("root-owned non-writable ordinary path required")
if item != path or directory:
if not stat.S_ISDIR(info.st_mode):
raise DeliveryError("ordinary protected directory required")
elif not stat.S_ISREG(info.st_mode) or info.st_nlink != 1:
raise DeliveryError("ordinary protected single-link file required")
def hardware() -> dict:
"""Conservative measurement binding; no performance claim is inferred."""
cpu = Path("/proc/cpuinfo").read_text()[: 1024 * 1024]
keys = {"vendor_id", "cpu family", "model", "model name", "stepping", "microcode", "flags"}
processor = {}
for line in cpu.split("\n\n", 1)[0].splitlines():
if ":" in line:
key, value = line.split(":", 1)
if key.strip() in keys:
processor[key.strip()] = value.strip()
group = current_cgroup()
limits = {
name: (group / name).read_text().strip() if (group / name).exists() else "max"
for name in ("cpu.max", "memory.max")
}
cpuset = group / "cpuset.cpus.effective"
limits["cpuset.cpus.effective"] = cpuset.read_text().strip() if cpuset.exists() else ""
return {
"kernel": platform.release(),
"architecture": platform.machine(),
"online_cpus": os.cpu_count(),
"processor": processor,
"parent_limits": limits,
}
def current_cgroup() -> Path:
value = Path("/proc/self/cgroup").read_text().strip()
if not value.startswith("0::/") or "\n" in value:
raise DeliveryError("unified cgroup v2 required")
return Path("/sys/fs/cgroup") / value.removeprefix("0::/")
def identity_conflicts() -> list[str]:
errors = []
for name, identity in ACCOUNTS.items():
for lookup, kind in ((pwd.getpwuid, "uid"), (grp.getgrgid, "gid")):
try:
row = lookup(identity)
except KeyError:
continue
if row[0] != name:
errors.append("reserved " + kind + " conflicts")
try:
account = pwd.getpwnam(name)
if (account.pw_uid, account.pw_gid, account.pw_dir, account.pw_shell) != (
identity,
identity,
"/nonexistent",
"/usr/bin/nologin",
):
errors.append("existing service identity differs")
except KeyError:
pass
try:
group = grp.getgrnam(name)
if group.gr_gid != identity or group.gr_mem:
errors.append("existing service group differs")
except KeyError:
pass
if any(name in group.gr_mem for group in grp.getgrall() if group.gr_gid != identity):
errors.append("service identity has supplementary authority")
for rows, field, reserved in (
(pwd.getpwall(), "pw_uid", TASK_UID),
(grp.getgrall(), "gr_gid", TASK_GID),
):
if any(getattr(row, field) == reserved for row in rows):
errors.append("task identity already assigned")
for name, reserved in (("subuid", TASK_UID), ("subgid", TASK_GID)):
source = Path("/etc") / name
if not source.exists():
continue
raw = source.read_text()
if len(raw) > 1024 * 1024:
errors.append("subordinate identity file exceeds bound")
continue
for line in raw.splitlines():
if not line or line.startswith("#"):
continue
fields = line.split(":")
if len(fields) != 3 or not fields[1].isdigit() or not fields[2].isdigit():
errors.append("subordinate identity allocation invalid")
elif int(fields[1]) <= reserved < int(fields[1]) + int(fields[2]):
errors.append("task identity overlaps subordinate allocation")
return errors
def observe(payload: Path, *, require_root: bool = True) -> dict:
errors = []
observations = {}
if platform.system() != "Linux" or platform.machine() != "x86_64":
errors.append("unsupported architecture")
version = re.match(r"(\d+)\.(\d+)", platform.release())
if not version or tuple(map(int, version.groups())) < (6, 6):
errors.append("kernel below supported policy")
if require_root and os.geteuid() != 0:
errors.append("installer requires root")
os_release = Path("/etc/os-release").read_text()
if not re.search(r'^ID=(?:"?)(arch|cachyos)(?:"?)$', os_release, re.MULTILINE):
errors.append("unsupported distribution profile")
if Path("/proc/1/comm").read_text().strip() != "systemd":
errors.append("systemd PID1 required")
try:
observations["hardware"] = hardware()
flags = set(observations["hardware"]["processor"].get("flags", "").split())
required = {
"avx",
"avx2",
"bmi1",
"bmi2",
"f16c",
"fma",
"movbe",
"xsave",
"ssse3",
"sse4_1",
"sse4_2",
"cx16",
"popcnt",
}
if not required <= flags or not flags & {"abm", "lzcnt"}:
errors.append("x86_64-v3 features missing")
if (os.cpu_count() or 0) < 2:
errors.append("two online CPUs required")
controllers = set(Path("/sys/fs/cgroup/cgroup.controllers").read_text().split())
if not {"cpu", "memory", "pids"} <= controllers:
errors.append("required cgroup controllers missing")
group = current_cgroup()
while True:
if group == Path("/sys/fs/cgroup"):
break
memory_max = (group / "memory.max").read_text().strip()
if memory_max != "max" and int(memory_max) < 7 * GIB // 2:
errors.append("effective memory limit below node capacity")
quota, period = (group / "cpu.max").read_text().split()
if quota != "max" and int(quota) < 2 * int(period):
errors.append("effective CPU quota below two CPUs")
group = group.parent
except (OSError, ValueError):
errors.append("cgroup/CPU observations unavailable")
memory = {
key: int(value.split()[0]) * 1024
for key, value in (
line.split(":", 1) for line in Path("/proc/meminfo").read_text().splitlines()
)
}
observations["memory"] = {key: memory[key] for key in ("MemTotal", "MemAvailable")}
# A 4-GiB physical guest exposes slightly less MemTotal after kernel reserve.
if memory["MemTotal"] < 15 * GIB // 4 or memory["MemAvailable"] < 5 * GIB // 2:
errors.append("physical/available memory below installation capacity")
observations["disk_free_bytes"] = shutil.disk_usage("/var/lib").free
if observations["disk_free_bytes"] < 12 * GIB:
errors.append("installation/data disk reserve below 12 GiB")
for name in ("libc.so.6", "ld-linux-x86-64.so.2"):
system = Path("/usr/lib") / name
try:
protected(system)
if digest(system) != digest(payload / "lib" / name):
errors.append("system GNU ABI bytes differ from verified release")
except (DeliveryError, OSError):
errors.append("system GNU ABI unavailable/unprotected")
for executable in (
"/usr/bin/systemctl",
"/usr/bin/useradd",
"/usr/bin/groupadd",
"/usr/bin/nologin",
"/usr/bin/postgres",
"/usr/bin/psql",
):
try:
protected(Path(executable))
except (DeliveryError, OSError):
errors.append("offline system dependency absent/unprotected")
for path in (
"/usr/lib/rabbitmq/sbin/rabbitmq-server",
"/usr/lib/rabbitmq/plugins/rabbit-4.3.6/ebin/rabbit.app",
"/usr/lib/erlang/erts-15.2.7/bin/beam.smp",
):
try:
protected(Path(path))
except (DeliveryError, OSError):
errors.append("offline broker/OTP dependency absent/unprotected")
broker_version = Path("/usr/lib/rabbitmq/plugins/rabbit-4.3.6/ebin/rabbit.app")
if broker_version.is_file():
with broker_version.open("rb") as stream:
header = stream.read(65537)
if len(header) > 65536 or not re.search(rb'\{vsn,\s*"4\.3\.6"\}', header):
errors.append("offline broker version differs")
for executable, expected in (
("/usr/bin/systemctl", "systemd 262"),
("/usr/bin/postgres", "(PostgreSQL) 18.6"),
):
try:
result = subprocess.run(
[executable, "--version"],
capture_output=True,
text=True,
timeout=5,
env={"PATH": "/usr/bin", "LC_ALL": "C"},
)
if result.returncode or expected not in result.stdout[:4096]:
errors.append("offline system dependency version differs")
except (OSError, subprocess.SubprocessError):
errors.append("offline system dependency unavailable")
errors += identity_conflicts()
for directory in ("/opt", "/etc", "/var/lib"):
try:
protected(Path(directory), directory=True)
except (OSError, DeliveryError):
errors.append("installation ancestor unprotected")
# No socket is opened for a running service and no peer is contacted.
for port in (8000, 8100):
with socket.socket() as check:
try:
check.bind(("127.0.0.1", port))
except OSError:
errors.append("application control port occupied")
if Path("/var/lib/kf-judge/factory/control.sock").exists():
errors.append("factory socket exists; drain and stop before installation")
return {
"eligible": not errors,
"errors": sorted(set(errors)),
"observations": observations,
"installed": False,
"execution_verified": False,
}