diff --git a/backend/tests/test_delivery.py b/backend/tests/test_delivery.py index 44ab1a4..99e9336 100644 --- a/backend/tests/test_delivery.py +++ b/backend/tests/test_delivery.py @@ -879,6 +879,64 @@ def test_proxy_is_separate_from_private_execution_control(): assert value["key_sha256"] not in contents +def test_all_enabled_nginx_temporary_paths_use_only_the_bounded_proxy_volume(): + contents = proxy.render( + "sd-123456789abc", + { + "schema_version": 1, + "origin": "https://oj.test", + "certificate_sha256": "b" * 64, + "key_sha256": "c" * 64, + }, + ).decode() + paths = dict(re.findall(r"^\s+(\w+_temp_path) (\S+) 1 2;$", contents, re.M)) + assert set(paths) == { + "client_body_temp_path", + "proxy_temp_path", + "fastcgi_temp_path", + "uwsgi_temp_path", + "scgi_temp_path", + } + assert {Path(value).parent for value in paths.values()} == {proxy_service.DATA} + assert "proxy_buffering off" in contents and "proxy_request_buffering off" in contents + + +@pytest.mark.parametrize("bad", ["wrong-mode", "link", "regular-file"]) +def test_proxy_temp_directory_conflicts_are_not_overwritten(monkeypatch, tmp_path, bad): + monkeypatch.setattr(proxy_service, "DATA", tmp_path) + monkeypatch.setattr(proxy_service, "UID", os.getuid()) + path = tmp_path / "body" + if bad == "wrong-mode": + path.mkdir(mode=0o755) + elif bad == "link": + path.symlink_to(tmp_path, target_is_directory=True) + else: + path.write_text("known conflict") + before = path.lstat() + with pytest.raises(policy.DeliveryError, match="unknown proxy temporary directory"): + proxy_install.temporary_directories() + after = path.lstat() + assert (before.st_ino, before.st_mode, before.st_uid) == ( + after.st_ino, + after.st_mode, + after.st_uid, + ) + + +def test_proxy_temp_directory_creation_is_bounded_and_repeatable(monkeypatch, tmp_path): + monkeypatch.setattr(proxy_service, "DATA", tmp_path) + monkeypatch.setattr(proxy_service, "UID", os.getuid()) + monkeypatch.setattr(proxy_install.os, "chown", lambda *args: None) + proxy_install.temporary_directories() + before = {p.name: (p.stat().st_ino, stat.S_IMODE(p.stat().st_mode)) for p in tmp_path.iterdir()} + assert set(before) == {"body", "proxy", "fastcgi", "uwsgi", "scgi"} + assert all(mode == 0o700 for _, mode in before.values()) + proxy_install.temporary_directories() + assert before == { + p.name: (p.stat().st_ino, stat.S_IMODE(p.stat().st_mode)) for p in tmp_path.iterdir() + } + + def test_ordinary_units_bind_exact_batch_without_inline_secrets(): source, checksum = "123456789abc" + "d" * 28, "f" * 64 hashes = {name: "b" * 64 for name in configuration.CONFIG_FILES} diff --git a/scripts/release/proxy.py b/scripts/release/proxy.py index 9b56d50..cacfa0d 100644 --- a/scripts/release/proxy.py +++ b/scripts/release/proxy.py @@ -96,6 +96,10 @@ http { client_body_timeout 10s; client_body_buffer_size 16k; client_body_temp_path /var/lib/kf-onlinejudge/proxy/body 1 2; + proxy_temp_path /var/lib/kf-onlinejudge/proxy/proxy 1 2; + fastcgi_temp_path /var/lib/kf-onlinejudge/proxy/fastcgi 1 2; + uwsgi_temp_path /var/lib/kf-onlinejudge/proxy/uwsgi 1 2; + scgi_temp_path /var/lib/kf-onlinejudge/proxy/scgi 1 2; large_client_header_buffers 2 8k; send_timeout 10s; keepalive_timeout 15s; diff --git a/scripts/release/proxy_install.py b/scripts/release/proxy_install.py index a3c75cb..97daf7c 100644 --- a/scripts/release/proxy_install.py +++ b/scripts/release/proxy_install.py @@ -222,6 +222,23 @@ def check_binding(release: str, batch: str) -> dict: return {"binding_valid": True, "tls_verified": False, "execution_verified": False} +def temporary_directories() -> None: + for name in ("body", "proxy", "fastcgi", "uwsgi", "scgi"): + temporary_directory = proxy_service.DATA / name + if temporary_directory.exists() or temporary_directory.is_symlink(): + i = temporary_directory.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: + temporary_directory.mkdir(mode=0o700) + os.chown(temporary_directory, proxy_service.UID, proxy_service.UID) + + 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") @@ -319,19 +336,7 @@ def install_proxy(source: Path, expected: str, certificate: Path, key: Path) -> 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) + temporary_directories() check_binding(selected["release_id"], selected["configuration_id"]) install.require_stopped() if install.selection() != selected: