From 962cba7c04cd548e39b109f9454caee32ce10a4a Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 25 Sep 2026 20:36:41 +0200 Subject: [PATCH 1/2] route-proxy: internal names are certified by the mesh's own authority MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two name spaces, two authorities (08-connectivity §2): a public name is certified by a public CA, an internal one by the mesh's own. step-ca now offers that second seat as internal-acme-ca beside its existing acme-ca, and route-proxy requires both — the server dispatches by which authority may certify the name at all, so an .internal alias stops being plain-HTTP only without ever asking a public CA for a name it cannot validate. --- modules/route-proxy/module.json | 45 +++++++++++++++++++++++++++++---- modules/step-ca/module.json | 8 ++++++ 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/modules/route-proxy/module.json b/modules/route-proxy/module.json index 3e24339..94c0d2e 100644 --- a/modules/route-proxy/module.json +++ b/modules/route-proxy/module.json @@ -18,10 +18,12 @@ "route": "/var/lib/route-proxy/routes/mesh.json" }, "requires": [ - "acme-ca" + "acme-ca", + "internal-acme-ca" ], "binds": { - "acme-ca": "/var/lib/route-proxy/acme-ca.json" + "acme-ca": "/var/lib/route-proxy/acme-ca.json", + "internal-acme-ca": "/var/lib/route-proxy/internal-acme-ca.json" }, "listens": [ { @@ -69,6 +71,13 @@ "mode": "0600", "content": "ACME_DIRECTORY=https://${bound:acme-ca:at}:${bound:acme-ca:port}${bound:acme-ca:path}\nACME_ROOTS=https://${bound:acme-ca:at}:${bound:acme-ca:port}${bound:acme-ca:roots}\nACME_ROOTS_PATH=${bound:acme-ca:roots}\n" }, + { + "id": "internal-acme-env", + "type": "file", + "path": "/var/lib/route-proxy/internal-acme.env", + "mode": "0600", + "content": "INTERNAL_ACME_DIRECTORY=https://${bound:internal-acme-ca:at}:${bound:internal-acme-ca:port}${bound:internal-acme-ca:path}\nINTERNAL_ACME_ROOTS=https://${bound:internal-acme-ca:at}:${bound:internal-acme-ca:port}${bound:internal-acme-ca:roots}\nINTERNAL_ACME_ROOTS_PATH=${bound:internal-acme-ca:roots}\n" + }, { "id": "trust", "type": "container", @@ -91,6 +100,28 @@ "acme-env" ] }, + { + "id": "internal-trust", + "type": "container", + "name": "route-proxy-internal-trust", + "artifact": "trust", + "run-once": true, + "network": "host", + "env-file": [ + "/var/lib/route-proxy/internal-acme.env" + ], + "volumes": [ + "/var/lib/route-proxy/ca:/ca" + ], + "args": [ + "sh", + "-c", + "if [ -z \"$INTERNAL_ACME_ROOTS_PATH\" ]; then cp /etc/ssl/certs/ca-certificates.crt /ca/internal-root.crt; exit 0; fi; for i in $(seq 1 60); do wget -q -T 10 --no-check-certificate -O /ca/internal-root.crt \"$INTERNAL_ACME_ROOTS\" && grep -q 'BEGIN CERTIFICATE' /ca/internal-root.crt && exit 0; sleep 2; done; echo \"the authority at $INTERNAL_ACME_ROOTS did not serve its roots within two minutes\" >&2; exit 1" + ], + "restart-on": [ + "internal-acme-env" + ] + }, { "id": "server", "type": "container", @@ -98,7 +129,8 @@ "artifact": "server", "network": "host", "env-file": [ - "/var/lib/route-proxy/acme.env" + "/var/lib/route-proxy/acme.env", + "/var/lib/route-proxy/internal-acme.env" ], "volumes": [ "/var/lib/route-proxy/routes:/routes:ro", @@ -110,11 +142,14 @@ "LISTEN": ":80", "TLS_LISTEN": ":443", "ACME_CACHE": "/acme", - "ACME_CA_BUNDLE": "/ca/root.crt" + "ACME_CA_BUNDLE": "/ca/root.crt", + "INTERNAL_ACME_CA_BUNDLE": "/ca/internal-root.crt" }, "restart-on": [ "trust", - "acme-env" + "acme-env", + "internal-trust", + "internal-acme-env" ] } ], diff --git a/modules/step-ca/module.json b/modules/step-ca/module.json index 31cc53a..d73f612 100644 --- a/modules/step-ca/module.json +++ b/modules/step-ca/module.json @@ -8,12 +8,20 @@ { "name": "acme-ca", "scope": "mesh" + }, + { + "name": "internal-acme-ca", + "scope": "mesh" } ], "serves": { "acme-ca": { "path": "/acme/acme/directory", "roots": "/roots.pem" + }, + "internal-acme-ca": { + "path": "/acme/acme/directory", + "roots": "/roots.pem" } }, "listens": [ From c2353fc0a635235d1ae1aeba9cea195715ce9d69 Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 25 Sep 2026 20:51:44 +0200 Subject: [PATCH 2/2] gitea: the internal-API refusal is part of the route, not a file beside the proxy The 2026-09-12 incident response blocked /api/internal by hand in the predecessor's dynamic directory, with a note that its durable home is the mesh's routing. A route carries the policy applied to a request (ADR 0108), so the refusal now travels with the grant: route-proxy enforces it on both the public name and the internal alias the moment it serves this route, and the adapter skips it aloud (no port, nothing to write) while the predecessor's own file still stands. The hand-authored file retires with the proxy it configures. --- modules/gitea/module.json | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/modules/gitea/module.json b/modules/gitea/module.json index a6ef32f..3b1b705 100644 --- a/modules/gitea/module.json +++ b/modules/gitea/module.json @@ -11,8 +11,16 @@ "name": "gitea" }, "route": { - "label": "git", - "port": 3000 + "web": { + "label": "git", + "port": 3000 + }, + "internal-api-refused": { + "label": "git", + "path": "/api/internal", + "deny": true, + "priority": 100000 + } } }, "binds": {