Retain Caddy file capability at ingress boundary

This commit is contained in:
2026-08-03 20:02:29 +02:00
parent d107d94fec
commit 1f039dd39c
5 changed files with 12 additions and 0 deletions
@@ -185,6 +185,11 @@ The dispatch-only `Runtime Ingress Drill` workflow exposes the same bounded
check independently so ingress changes can be diagnosed before an immutable check independently so ingress changes can be diagnosed before an immutable
runtime publication; it accepts only digest-pinned Caddy, HAProxy, and API runtime publication; it accepts only digest-pinned Caddy, HAProxy, and API
images and has no push trigger. images and has no push trigger.
The official Caddy binary carries the `NET_BIND_SERVICE` file capability. The
managed-ingress container therefore drops every capability and adds back only
`NET_BIND_SERVICE`; otherwise Linux rejects the binary at `execve` before its
high-port configuration can start. `no-new-privileges`, a read-only root
filesystem, and non-privileged container ports remain enforced.
The bounded setup helper writes only generated public configuration as root so 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 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. non-root identity and runs read-only with all capabilities dropped.
+3
View File
@@ -551,6 +551,9 @@ class DeploymentInstallerTests(unittest.TestCase):
) )
self.assertIn("caddy-data:/data", ingress["volumes"]) self.assertIn("caddy-data:/data", ingress["volumes"])
self.assertIn("caddy-config:/config", ingress["volumes"]) self.assertIn("caddy-config:/config", ingress["volumes"])
self.assertEqual(["ALL"], ingress["cap_drop"])
self.assertEqual(["NET_BIND_SERVICE"], ingress["cap_add"])
self.assertEqual(["no-new-privileges:true"], ingress["security_opt"])
self.assertIn("reverse_proxy load-balancer:8080", render_caddy_config(spec)) self.assertIn("reverse_proxy load-balancer:8080", render_caddy_config(spec))
self.assertNotIn("operator@example.test", json.dumps(compose)) self.assertNotIn("operator@example.test", json.dumps(compose))
+1
View File
@@ -64,6 +64,7 @@ class ManagedIngressDrillTests(unittest.TestCase):
self.assertNotIn('"127.0.0.1::8080"', source) self.assertNotIn('"127.0.0.1::8080"', source)
self.assertIn("requested_http_port", source) self.assertIn("requested_http_port", source)
self.assertIn("requested_https_port", source) self.assertIn("requested_https_port", source)
self.assertIn('"--cap-add",\n "NET_BIND_SERVICE"', source)
def test_published_port_reads_the_docker_mapping(self) -> None: def test_published_port_reads_the_docker_mapping(self) -> None:
completed = subprocess.CompletedProcess( completed = subprocess.CompletedProcess(
+2
View File
@@ -388,6 +388,8 @@ def main() -> int:
"no-new-privileges", "no-new-privileges",
"--cap-drop", "--cap-drop",
"ALL", "ALL",
"--cap-add",
"NET_BIND_SERVICE",
"--publish", "--publish",
f"127.0.0.1:{requested_http_port}:8080/tcp", f"127.0.0.1:{requested_http_port}:8080/tcp",
"--publish", "--publish",
@@ -627,6 +627,7 @@ def render_compose(spec: InstallationSpec) -> dict[str, object]:
"tmpfs": ["/tmp:rw,noexec,nosuid,size=64m"], "tmpfs": ["/tmp:rw,noexec,nosuid,size=64m"],
"security_opt": ["no-new-privileges:true"], "security_opt": ["no-new-privileges:true"],
"cap_drop": ["ALL"], "cap_drop": ["ALL"],
"cap_add": ["NET_BIND_SERVICE"],
"volumes": [ "volumes": [
f"./{CADDY_CONFIG_FILENAME}:/etc/caddy/Caddyfile:ro", f"./{CADDY_CONFIG_FILENAME}:/etc/caddy/Caddyfile:ro",
"caddy-data:/data", "caddy-data:/data",