Retire gateway.tls=internal; host routes use a bare subdomain label
acme mode fully replaced the internal-CA path (which required installing a private CA on every device — the exact pain, esp. on Android, that acme avoids). Remove `internal` entirely and simplify how services declare host routes. - Generator (caddyfile.py): drop the `tls internal` branch — modes are now `off` | `acme`. In acme mode the published subdomain is the **first DNS label** of `proxy.caddy.host` (a bare `claw`, or a legacy `claw.civil.lan`, both → `claw.<domain>`), so services stay domain-agnostic and the declared value is authoritative again (no more silent service-name override). Shared `_host_matcher_block` reused by off-mode and the acme wildcard site. - castle-api: delete `GET /gateway/ca.crt`, `_gateway_ca_pem`, `_ca_fingerprint` and the now-unused imports; drop `ca_fingerprint` from `GatewayInfo` (keep `tls`). - Dashboard: remove the CA-cert download button + unused imports; drop `ca_fingerprint` from the `GatewayInfo` type. - Tests: replace TestCaddyfileTlsInternal with an off-mode class (keeps the runner-agnostic host-route coverage); acme tests assert first-label derivation incl. label-wins-over-service-name; drop the castle-api CA-endpoint test. - Docs: registry.md + dns-and-tls.md — two-mode tables (off|acme), remove the internal sections/CA-download, document the bare-label host convention; note a domain-less node stays on `off`.
This commit is contained in:
@@ -186,27 +186,23 @@ def generate_caddyfile_from_registry(
|
||||
) -> str:
|
||||
"""Render the route list to a Caddyfile.
|
||||
|
||||
Three modes, set by `gateway.tls`:
|
||||
Two modes, set by `gateway.tls`:
|
||||
|
||||
- **off (default)** — HTTP-only. Everything (host matchers + path prefixes +
|
||||
static) lives in one `:<port>` site with `auto_https off`, so a named host
|
||||
can't pull the listener into TLS or try to bind :80/:443.
|
||||
- **internal** — each host route becomes its own `<host> { tls internal … }`
|
||||
site, served over HTTPS by Caddy's local CA (Caddy listens :443 and
|
||||
redirects :80). This makes those hosts a browser "secure context". Path
|
||||
prefixes, static frontends, and the dashboard stay on the HTTP `:<port>`
|
||||
site — give a service a `proxy.caddy.host` to put it on HTTPS.
|
||||
- **acme** — host routes are served under a single `*.<domain>` site with a
|
||||
real Let's Encrypt **wildcard** cert obtained via a DNS-01 challenge (one
|
||||
cert for all of them). Publicly trusted → no CA install on clients. Each
|
||||
host route maps to `<service-name>.<domain>`. Requires `gateway.domain`;
|
||||
host route is published at `<label>.<domain>`, where `<label>` is the first
|
||||
DNS label of the service's `proxy.caddy.host` (so a bare `claw`, or a legacy
|
||||
`claw.civil.lan`, both yield `claw.<domain>`). Requires `gateway.domain`;
|
||||
the DNS provider token reaches Caddy via `{env.<TOKEN>}`.
|
||||
"""
|
||||
routes = compute_routes(registry, None, remote_registries)
|
||||
node = registry.node
|
||||
gw_port = node.gateway_port
|
||||
mode = (node.gateway_tls or "").lower()
|
||||
tls_internal = mode == "internal"
|
||||
domain = node.gateway_domain
|
||||
tls_acme = mode == "acme" and bool(domain) # acme without a domain → off-mode
|
||||
|
||||
@@ -225,26 +221,16 @@ def generate_caddyfile_from_registry(
|
||||
lines.append(f" acme_ca {_ACME_STAGING_CA}")
|
||||
lines += ["}", ""]
|
||||
# One wildcard site → a single DNS-01 cert covers every host route, so a
|
||||
# new host-routed service needs no new cert or challenge.
|
||||
# new host-routed service needs no new cert or challenge. The published
|
||||
# subdomain is the host's first label (a bare `claw` or `claw.civil.lan`
|
||||
# both → `claw.<domain>`), keeping services domain-agnostic.
|
||||
if host_routes:
|
||||
lines.append(f"*.{domain} {{")
|
||||
for r in host_routes:
|
||||
sub = r.name or r.address.split(".")[0]
|
||||
sub = (r.address.split(".")[0] if r.address else "") or r.name or ""
|
||||
lines += _host_matcher_block(r.name or r.address, f"{sub}.{domain}", r.target)
|
||||
lines.append("}")
|
||||
lines.append("")
|
||||
elif tls_internal:
|
||||
# Per-host HTTPS sites via Caddy's internal CA. `tls internal` overrides
|
||||
# ACME for that host, so no public cert is attempted; clients must trust
|
||||
# the Caddy root CA (`caddy trust`, then distribute root.crt).
|
||||
for r in host_routes:
|
||||
lines += [
|
||||
f"{r.address} {{",
|
||||
" tls internal",
|
||||
f" reverse_proxy {r.target}",
|
||||
"}",
|
||||
"",
|
||||
]
|
||||
else:
|
||||
# HTTP-only: keep auto-HTTPS off so the bare-port site stays plain HTTP
|
||||
# and named hosts don't trigger cert provisioning.
|
||||
@@ -280,8 +266,8 @@ def generate_caddyfile_from_registry(
|
||||
"",
|
||||
]
|
||||
elif r.is_host: # host-based proxy
|
||||
if tls_internal or tls_acme:
|
||||
continue # emitted as its own HTTPS / wildcard site above
|
||||
if tls_acme:
|
||||
continue # emitted in the *.<domain> wildcard site above
|
||||
lines += _host_matcher_block(r.name or r.address, r.address, r.target)
|
||||
else: # path-prefix proxy (local or remote)
|
||||
if r.kind == "remote":
|
||||
|
||||
@@ -234,12 +234,11 @@ class TestLocalRoutesFromConfig:
|
||||
assert "reverse_proxy localhost:8001" in caddyfile
|
||||
|
||||
|
||||
class TestCaddyfileTlsInternal:
|
||||
"""gateway.tls=internal → host routes become their own HTTPS sites."""
|
||||
class TestCaddyfileOffMode:
|
||||
"""gateway.tls unset/off → HTTP-only: host matchers live on the :<port> site."""
|
||||
|
||||
def _host_registry(self, tls: str | None) -> NodeRegistry:
|
||||
def _host_registry(self) -> NodeRegistry:
|
||||
return _make_registry(
|
||||
gateway_tls=tls,
|
||||
deployed={
|
||||
"claw": Deployment(
|
||||
runner="node", run_cmd=["claw"], port=18789, proxy_host="claw.civil.lan"
|
||||
@@ -250,17 +249,21 @@ class TestCaddyfileTlsInternal:
|
||||
},
|
||||
)
|
||||
|
||||
def test_host_route_becomes_tls_site(self) -> None:
|
||||
caddyfile = generate_caddyfile_from_registry(self._host_registry("internal"))
|
||||
assert "claw.civil.lan {" in caddyfile
|
||||
assert "tls internal" in caddyfile
|
||||
assert "reverse_proxy localhost:18789" in caddyfile
|
||||
def test_host_matcher_and_auto_https_off(self) -> None:
|
||||
caddyfile = generate_caddyfile_from_registry(self._host_registry())
|
||||
assert "auto_https off" in caddyfile
|
||||
assert "@host_claw host claw.civil.lan" in caddyfile
|
||||
assert "tls internal" not in caddyfile # internal mode is gone
|
||||
assert "claw.civil.lan {" not in caddyfile # not a standalone TLS site
|
||||
|
||||
def test_compose_substrate_host_route_is_tls_site(self) -> None:
|
||||
"""The Supabase substrate (compose runner + host route) becomes its own
|
||||
HTTPS site under tls:internal — routing is runner-agnostic."""
|
||||
def test_path_routes_on_http_port(self) -> None:
|
||||
caddyfile = generate_caddyfile_from_registry(self._host_registry())
|
||||
assert ":9000 {" in caddyfile
|
||||
assert "handle_path /api/*" in caddyfile
|
||||
|
||||
def test_routing_is_runner_agnostic(self) -> None:
|
||||
"""A compose-runner service with a host route is matched like any other."""
|
||||
registry = _make_registry(
|
||||
gateway_tls="internal",
|
||||
deployed={
|
||||
"supabase": Deployment(
|
||||
runner="compose",
|
||||
@@ -271,29 +274,9 @@ class TestCaddyfileTlsInternal:
|
||||
},
|
||||
)
|
||||
caddyfile = generate_caddyfile_from_registry(registry)
|
||||
assert "supabase.lan {" in caddyfile
|
||||
assert "tls internal" in caddyfile
|
||||
assert "@host_supabase host supabase.lan" in caddyfile
|
||||
assert "reverse_proxy localhost:8000" in caddyfile
|
||||
|
||||
def test_no_auto_https_off_in_tls_mode(self) -> None:
|
||||
# auto_https off would suppress the internal-CA certs we now want.
|
||||
caddyfile = generate_caddyfile_from_registry(self._host_registry("internal"))
|
||||
assert "auto_https off" not in caddyfile
|
||||
# Host matcher form must NOT be used when the host is its own TLS site.
|
||||
assert "@host_claw" not in caddyfile
|
||||
|
||||
def test_path_routes_stay_on_http_port(self) -> None:
|
||||
caddyfile = generate_caddyfile_from_registry(self._host_registry("internal"))
|
||||
assert ":9000 {" in caddyfile
|
||||
assert "handle_path /api/*" in caddyfile
|
||||
|
||||
def test_off_mode_keeps_host_matcher_and_auto_https_off(self) -> None:
|
||||
caddyfile = generate_caddyfile_from_registry(self._host_registry(None))
|
||||
assert "auto_https off" in caddyfile
|
||||
assert "@host_claw host claw.civil.lan" in caddyfile
|
||||
assert "tls internal" not in caddyfile
|
||||
assert "claw.civil.lan {" not in caddyfile
|
||||
|
||||
|
||||
class TestCaddyfileTlsAcme:
|
||||
"""gateway.tls=acme → host routes served under one *.domain wildcard site
|
||||
@@ -322,10 +305,27 @@ class TestCaddyfileTlsAcme:
|
||||
def test_wildcard_site_with_derived_host_matcher(self) -> None:
|
||||
caddyfile = generate_caddyfile_from_registry(self._acme_registry())
|
||||
assert "*.civil.payne.io {" in caddyfile
|
||||
# Subdomain derived from the SERVICE NAME (claw), not the declared .lan host.
|
||||
# Published name = the host's first label under the gateway domain.
|
||||
assert "@host_claw host claw.civil.payne.io" in caddyfile
|
||||
assert "reverse_proxy localhost:18789" in caddyfile
|
||||
|
||||
def test_subdomain_from_host_label_not_service_name(self) -> None:
|
||||
# Service is named "openclaw" but declares host label "claw" → the label
|
||||
# wins (published as claw.<domain>), so the declared name is authoritative.
|
||||
registry = _make_registry(
|
||||
gateway_tls="acme",
|
||||
gateway_domain="civil.payne.io",
|
||||
acme_email="paul@example.com",
|
||||
deployed={
|
||||
"openclaw": Deployment(
|
||||
runner="node", run_cmd=["c"], port=18789, proxy_host="claw"
|
||||
),
|
||||
},
|
||||
)
|
||||
caddyfile = generate_caddyfile_from_registry(registry)
|
||||
assert "host claw.civil.payne.io" in caddyfile
|
||||
assert "openclaw.civil.payne.io" not in caddyfile
|
||||
|
||||
def test_path_routes_stay_on_http_port(self) -> None:
|
||||
caddyfile = generate_caddyfile_from_registry(self._acme_registry())
|
||||
assert ":9000 {" in caddyfile
|
||||
|
||||
Reference in New Issue
Block a user