Add gateway.tls=acme: Let's Encrypt wildcard cert via DNS-01
internal-CA HTTPS forces every device to trust a private root, which breaks down
on some clients (Android browsers). acme mode instead obtains a real Let's Encrypt
wildcard cert (*.<domain>) via a DNS-01 challenge, so browsers trust it with zero
CA install — while services stay internal-only (no inbound exposure, no public A
records; only a transient _acme-challenge TXT touches the public zone, and LAN DNS
resolves the names to a private IP).
- Config: GatewayConfig/NodeConfig gain domain, acme_email, acme_dns_provider
(plumbed through load/save + registry + deploy, omitted-when-empty for
round-trip stability). tls stays the discriminator: off | internal | acme.
- Caddyfile generator: acme branch emits a global {email, acme_dns <provider>
{env.TOKEN}} block + one *.<domain> wildcard site with host matchers derived
from the service name (<name>.<domain>); path/static stay on :<port>. Shared
_host_matcher_block helper (reused by off-mode). CASTLE_ACME_STAGING=1 toggles
the LE staging CA; acme-without-domain falls back to HTTP with a warning.
- deploy(): _acme_preflight warns (never writes) when domain, the gateway-service
token env, or the CLOUDFLARE_API_TOKEN secret is missing.
- install.sh: opt-in --with-dns-plugin[=provider] builds a DNS-plugin Caddy via
xcaddy (pinned) to /usr/local/bin/caddy, which the gateway picks up on deploy.
- Tests: TestCaddyfileTlsAcme (global block, wildcard site, derived host matcher,
path routes on :port, staging toggle, no-domain fallback). Docs: gateway.tls
modes table + full acme/DNS-01 section (setup, wild-central LAN DNS, staging).
This commit is contained in:
@@ -21,7 +21,12 @@ class NodeConfig:
|
||||
hostname: str = ""
|
||||
castle_root: str | None = None # repo path, for dev commands
|
||||
gateway_port: int = 9000
|
||||
gateway_tls: str | None = None # None/"off" → HTTP-only; "internal" → Caddy local-CA HTTPS
|
||||
# None/"off" → HTTP-only; "internal" → Caddy local-CA HTTPS; "acme" → Let's
|
||||
# Encrypt wildcard (*.gateway_domain) via DNS-01.
|
||||
gateway_tls: str | None = None
|
||||
gateway_domain: str | None = None # acme: zone for wildcard cert + host subdomains
|
||||
acme_email: str | None = None
|
||||
acme_dns_provider: str = "cloudflare"
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not self.hostname:
|
||||
@@ -85,6 +90,9 @@ def load_registry(path: Path | None = None) -> NodeRegistry:
|
||||
castle_root=node_data.get("castle_root"),
|
||||
gateway_port=node_data.get("gateway_port", 9000),
|
||||
gateway_tls=node_data.get("gateway_tls"),
|
||||
gateway_domain=node_data.get("gateway_domain"),
|
||||
acme_email=node_data.get("acme_email"),
|
||||
acme_dns_provider=node_data.get("acme_dns_provider", "cloudflare"),
|
||||
)
|
||||
|
||||
deployed: dict[str, Deployment] = {}
|
||||
@@ -143,6 +151,12 @@ def save_registry(registry: NodeRegistry, path: Path | None = None) -> None:
|
||||
|
||||
if registry.node.gateway_tls:
|
||||
data["node"]["gateway_tls"] = registry.node.gateway_tls
|
||||
if registry.node.gateway_domain:
|
||||
data["node"]["gateway_domain"] = registry.node.gateway_domain
|
||||
if registry.node.acme_email:
|
||||
data["node"]["acme_email"] = registry.node.acme_email
|
||||
if registry.node.acme_dns_provider and registry.node.acme_dns_provider != "cloudflare":
|
||||
data["node"]["acme_dns_provider"] = registry.node.acme_dns_provider
|
||||
|
||||
for name, comp in registry.deployed.items():
|
||||
entry: dict = {
|
||||
|
||||
Reference in New Issue
Block a user