diff --git a/docs/INSTALLATION_AND_DEPLOYMENT_ARCHITECTURE.md b/docs/INSTALLATION_AND_DEPLOYMENT_ARCHITECTURE.md index f4e1420..5504273 100644 --- a/docs/INSTALLATION_AND_DEPLOYMENT_ARCHITECTURE.md +++ b/docs/INSTALLATION_AND_DEPLOYMENT_ARCHITECTURE.md @@ -176,6 +176,8 @@ Ingress acceptance streams generated configuration into Docker-managed volumes before starting the read-only containers. It therefore also works when an Actions job reaches a host or remote Docker daemon through a mounted socket; the drill never assumes that a job-container path is visible to that daemon. +The drill allocates explicit loopback-only host ports and verifies Docker's +published mappings, avoiding daemon-specific random-port shorthand behavior. The bounded setup helper writes only generated public configuration as root so it can initialize a new volume; the actual HAProxy process retains the image's non-root identity and runs read-only with all capabilities dropped. diff --git a/tests/test_managed_ingress_drill.py b/tests/test_managed_ingress_drill.py index 6bdc840..1b48bd1 100644 --- a/tests/test_managed_ingress_drill.py +++ b/tests/test_managed_ingress_drill.py @@ -60,6 +60,25 @@ class ManagedIngressDrillTests(unittest.TestCase): self.assertNotIn("type=bind", source) self.assertIn('"--network-alias",\n "load-balancer"', source) + self.assertNotIn('"127.0.0.1::8080"', source) + self.assertIn("requested_http_port", source) + self.assertIn("requested_https_port", source) + + def test_published_port_reads_the_docker_mapping(self) -> None: + completed = subprocess.CompletedProcess( + [], + 0, + "127.0.0.1:49152\n", + "", + ) + with patch.object(INGRESS, "_run", return_value=completed) as run: + port = INGRESS._published_port("ingress", 8443) + + self.assertEqual(49152, port) + self.assertEqual( + ["docker", "port", "ingress", "8443/tcp"], + run.call_args.args[0], + ) if __name__ == "__main__": diff --git a/tools/checks/managed-ingress-drill.py b/tools/checks/managed-ingress-drill.py index a7cc254..759164a 100644 --- a/tools/checks/managed-ingress-drill.py +++ b/tools/checks/managed-ingress-drill.py @@ -7,6 +7,7 @@ import argparse from pathlib import Path import re import shutil +import socket import subprocess import sys import tempfile @@ -86,6 +87,16 @@ def _published_port(container: str, target: int) -> int: raise RuntimeError(f"cannot determine published port from {output!r}") from exc +def _available_loopback_port(*, exclude: frozenset[int] = frozenset()) -> int: + for _attempt in range(10): + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as listener: + listener.bind(("127.0.0.1", 0)) + port = int(listener.getsockname()[1]) + if port not in exclude: + return port + raise RuntimeError("cannot allocate distinct loopback ports for ingress drill") + + def _curl(url: str, *, headers: bool = False) -> str: argv = ["curl", "--silent", "--show-error", "--insecure"] if headers: @@ -253,6 +264,10 @@ def main() -> int: "/govoplan-config/Caddyfile", ] ) + requested_http_port = _available_loopback_port() + requested_https_port = _available_loopback_port( + exclude=frozenset({requested_http_port}) + ) ingress_command = [ "docker", "run", @@ -269,9 +284,9 @@ def main() -> int: "--cap-drop", "ALL", "--publish", - "127.0.0.1::8080", + f"127.0.0.1:{requested_http_port}:8080/tcp", "--publish", - "127.0.0.1::8443", + f"127.0.0.1:{requested_https_port}:8443/tcp", "--mount", ( "type=volume," @@ -291,6 +306,11 @@ def main() -> int: _run(ingress_command) http_port = _published_port(ingress, 8080) https_port = _published_port(ingress, 8443) + if (http_port, https_port) != ( + requested_http_port, + requested_https_port, + ): + raise RuntimeError("Docker published unexpected ingress ports") body = _wait_for_https(https_port) if body.strip() != "proto=https": raise RuntimeError(f"forwarded protocol was not normalized: {body!r}")