Modules: internal/proxy, internal/trust, internal/dns, internal/tunnel · Milestone: M5 · Effort: ~7w
Give every project service a stable local URL with working HTTPS at https://<service>.<project>.localhost, routed by a shared reverse proxy that needs no central config as projects come and go — and an optional public URL via Cloudflare Tunnel. Local HTTPS is opt-in so a broken trust path never blocks up.
- Proxy = Caddy 2 via
lucaslorentz/caddy-docker-proxy(pin image, e.g.:2.12-alpine), run as one shared container in the shared stack ondevstack_shared. Routing is label-driven: project service templates emitcaddy=<svc>.<proj>.localhost,caddy.reverse_proxy={{upstreams <port>}},caddy.tls=internal. Adding/removing a project never touches central proxy config — caddy-docker-proxy reloads on the Docker event. Traefik/nginx stay pluggable behind aProxyinterface. - Single source of truth = a
[]Route{host, upstream, port, tls}that renders both the Caddy labels and the cloudflared ingress block, so local and public routing can never drift. - Local CA via
mkcert(Q-CA): shell out to the maintainedmkcertbinary to install the root CA into host + Firefox/NSS stores — notsmallstep/truststore(unmaintained).trust install|uninstall|statuscommands. (If full pure-Go self-ownership is later required, generate the CA incrypto/x509, mount it as Caddy's internal-CA root, and vendor a trust-store installer — higher maintenance.) - Default domain
<service>.<project>.localhost;.testis opt-in behind a privilegeddns setup(resolver/dnsmasq drop-in). For OS-resolver clients, generate idempotent marker-fenced/etc/hostsentries (sudo) — owned byinternal/dns, removed on uninstall. - Tunnel = cloudflared as an optional managed container, locally-managed named tunnel (credentials-file, config-as-code), ingress → the Caddy container (so public reuses local routing).
tunnel login|create|route|up|down. Default DOWN, loud confirm, and refuse to tunnel a service whose env carries non-localsecret://values without an override. - Orchestrate Caddy + cloudflared as containers via the Docker SDK / shared compose — do not vendor them as Go libraries.
*.localhostis NOT uniformly zero-config:- Linux: works only if systemd-resolved stub is active (not guaranteed on WSL2/minimal Ubuntu).
- macOS: the OS resolver resolves
*.localhostonly on macOS 26+; on ≤15, non-browser clients (Go's net stack, curl, Docker healthchecks, the Caddy/tunnel origin lookups) and Safari fail without/etc/hosts. - Chrome/Firefox bypass the OS resolver (work everywhere) — but Firefox ignores
/etc/hostsfor*.localhost. - →
/etc/hostsis the only consistently reliable mechanism for OS-resolver clients; treat macOS version + systemd-resolved presence as runtime-detected branches.
- Caddy trust is NOT automatic in a container: Caddy generates the cert fine, but installing its root into the host store fails from inside a container/unprivileged. Hence the explicit host-side
mkcert/trust step. Persist Caddy's/datavolume or the CA regenerates and trust breaks on every recreate. Internal-CA leaf certs are short-lived (fine for browsers; can surprise strict non-browser clients). - WSL2 dual trust store: the CA must be imported into the Windows store (
certutil.exe -addstore -user Root) via interop because browsers run on Windows, not in the distro. Windows-side Firefox has its own NSS. Corporate-managed Windows may block user-root CAs — document the manual fallback. Detect WSL viauname -rcontainsmicrosoft/ WSLInterop. mkcertneeds platform tools at runtime (securityon macOS + sudo;update-ca-certificates/truston Linux + sudo;certutilfromlibnss3-toolsfor Firefox — absent on clean Ubuntu/WSL2).trust statusmust diagnose and give the exact remediation.- Cloudflare Tunnel: the wildcard CNAME (
*.project) must be created manually in DNS (dashboard/API) —cloudflared tunnel route dnsand the dashboard reject*. Only a single leading label is wildcardable. Free tier ≈ 1000 tunnels. Generate creds viacloudflared tunnel create(writes<UUID>.json) to avoid the 2025.2–2025.4tunnel loginregression. WSL2: runs as a normal Linux binary but QUIC/clock-skew can bite — offer the http2 protocol fallback.
-
upwithhttpsLocal: true→https://api.shop.localhostloads in Chrome with a trusted cert aftertrust install. - Adding a new project service → it gets a working URL with no edit to any central proxy config (label-driven reload).
-
trust statuson a clean Ubuntu correctly reports missingcertutiland printsapt install libnss3-tools. - On WSL2,
trust installimports the CA into the Windows store viacertutil.exeand Chrome-on-Windows trusts it. -
tunnel upis refused (with override hint) for a service whose env contains a non-localsecret://; default state is DOWN. - Local routing and tunnel ingress are generated from the same
[]Route(no drift). -
uninstallremoves the CA from all stores and the/etc/hostsmarker block.
The proxy/cloudflared containers live in the shared stack (spec 03); routes are emitted by project templates (spec 02); tunnel creds stored via secrets (spec 04). State (CA key/cert, tunnel JSON, cert.pem) lives 0600 under the XDG state dir, gitignored.
Q-PROXY (Caddy default — confirm), Q-CA (mkcert dependency acceptable?).