# Clusev reverse proxy — the panel domain is changeable FROM THE DASHBOARD. # # TLS is issued ON DEMAND: on the first HTTPS handshake for a host, Caddy asks the app # (/_caddy/ask) whether to issue a certificate, and the app approves ONLY the domain # currently configured in the dashboard (DB) / .env. So the operator changes the domain # in the UI, points DNS at this host, restarts the stack, and Caddy gets the cert for the # new domain automatically — no Caddyfile edit, no SITE_ADDRESS. # # HTTP is ALWAYS served (bare-IP access + the operator's recovery path); the app issues # the HTTPS redirect for the configured domain (PanelScheme), and never for the raw IP. # Reverb (Pusher protocol) rides the same address over /app/* (WS) and /apps/* (event # API), proxied to reverb:8080 — no extra exposed port, no mixed content. { # ACME contact for Let's Encrypt (set by install.sh; defaults to admin@localhost). email {$ACME_EMAIL} # Only mint a certificate for the host the app approves — prevents cert-issuance # abuse from arbitrary domains pointed at this IP. on_demand_tls { ask http://app:80/_caddy/ask } # Keep serving plain HTTP (still issue certs on demand): the APP decides the # HTTP->HTTPS redirect — only for the configured domain, never for the raw IP — # so bare-IP HTTP always stays reachable as the operator's recovery path. auto_https disable_redirects # Honor X-Forwarded-* from a trusted upstream proxy (external-TLS mode). Defaults to # 127.0.0.1/32 (trust nothing external); set TRUSTED_PROXY_CIDR to the upstream proxy's # EXACT address (a /32, or its tightest CIDR). SECURITY: never a broad range and never # 0.0.0.0/0 — a too-wide value lets any client forge X-Forwarded-For, which then drives # request()->ip() in the app and defeats the IP-keyed login/2FA throttles + the honeypot # brute-force ban (an attacker can spoof a victim's IP to get it banned, or a whitelisted # IP to evade the ban entirely). servers { trusted_proxies static {$TRUSTED_PROXY_CIDR:127.0.0.1/32} } } # Snippet: the shared proxy + hardening, reused by both the HTTP and HTTPS sites. (clusev_proxy) { @reverb path /app/* /apps/* reverse_proxy @reverb reverb:8080 # Terminal sidecar websocket (xterm.js ↔ PTY); Caddy handles the WS upgrade automatically. @terminal path /terminal/ws reverse_proxy @terminal terminal:3000 # The internal terminal-resolve endpoint hands out DECRYPTED SSH credentials. It is for the sidecar # only, which reaches it over the private network (app:80, bypassing Caddy). Refuse it at the public # edge so a leaked sidecar secret can't be replayed from the internet (defence in depth). @internal path /_internal/* respond @internal 404 reverse_proxy app:80 encode zstd gzip header { # conservative hardening; HSTS is asserted by the app only over HTTPS X-Content-Type-Options nosniff X-Frame-Options DENY Referrer-Policy strict-origin-when-cross-origin -Server } } # HTTPS for any host the app approves (the configured panel domain); cert obtained # on demand on the first handshake. https:// { tls { on_demand } import clusev_proxy } # Plain HTTP: always served — bare-IP access and the recovery path. Also carries the # ACME HTTP-01 challenge. The app redirects regular requests to HTTPS for the configured # domain (PanelScheme); the raw IP stays on HTTP. http:// { # Reverb (/app WS, /apps events) bypasses the app, so enforce transport here: on a # HOSTNAME force the WS/event paths onto HTTPS, so broadcasts (incl. the public metrics # channel) are never carried in cleartext and a stale/wrong host is refused by on-demand # TLS. On a bare IP there is no TLS, so plaintext there is expected and left alone. @reverb_cleartext { path /app/* /apps/* not header_regexp Host ^(\d{1,3}\.){3}\d{1,3}(:\d+)?$ # External-TLS mode: an upstream proxy already terminated TLS and forwards over HTTP with # X-Forwarded-Proto: https. The connection is secure end-to-end, so do NOT bounce the # WS/event paths to https here — this Caddy serves no cert for the domain in that mode, so # the redirect would just break the WebSocket. Only genuine cleartext is redirected. not header X-Forwarded-Proto https } handle @reverb_cleartext { redir https://{host}{uri} } handle { import clusev_proxy } }