{ servers { # Address-based rules and QUIC early data do not mix: a decision taken # on 0-RTT data can answer 425, and some browsers do not retry — an # intermittent lockout from the console, which is the worst kind. protocols h1 h2 } # Replace: this address receives the certificate-expiry notices for THIS # installation. Left as the copied original, they reach whoever wrote the # file instead of whoever runs the server. email you@example.com } # Reference reverse-proxy configuration. # # Copy to /etc/caddy/Caddyfile, replace the hostnames, then: # sudo bash deploy/install-agent.sh # which replaces the console's allowlist line with an import generated from the # list the owner keeps in the console itself. # # The rule this file exists to enforce: only the marketing site, the customer # portal and the status page are public. The console and the websocket endpoint # must not merely refuse — they must be indistinguishable from a hostname that # serves nothing at all. An empty 404 is itself information: it says something # terminates TLS here and chose not to answer. # ── Customer portal ────────────────────────────────────────────────────────── app.example.com { reverse_proxy 127.0.0.1:8080 } # ── Stripe webhook, and nothing else ───────────────────────────────────────── # The application answers the whole site on every hostname, so without this the # login and registration pages are served here too. Narrowed to the one request # this hostname exists for; everything else never reaches PHP. api.example.com { @webhook { method POST path /webhooks/stripe } handle @webhook { reverse_proxy 127.0.0.1:8080 } handle { respond 404 } } # ── Websockets (Reverb) ────────────────────────────────────────────────────── ws.example.com { # Only an actual websocket upgrade. A browser opening this name has no # business here, and answering it — even with a 404 — advertises that # something is listening. @websocket { header Connection *Upgrade* header Upgrade websocket } handle @websocket { reverse_proxy 127.0.0.1:8081 } handle { abort } } # ── Operator console ───────────────────────────────────────────────────────── # The network-level lock. The hostname checks inside the application are # defence in depth — a Host header is chosen by the caller, so they cannot # decide WHO gets through. This can. # # 10.66.0.0/24 is the WireGuard subnet. 62.178.50.33 is temporary, so the first # VPN access can be created at all; it comes out once that is done. admin.example.com { import /etc/caddy/clupilot-console-allow.conf handle @allowed { reverse_proxy 127.0.0.1:8080 } # Nothing at all for anyone else. An empty 404 still says "something # terminates TLS here and chose not to answer" — which is itself the # information worth hiding. Closing the connection is what a host that does # not serve this name looks like. handle { abort } } www.example.com { reverse_proxy 127.0.0.1:8080 } status.example.com { reverse_proxy 127.0.0.1:8080 } # ── Von der Konsole verwaltete Hostnamen ───────────────────────────────────── # Erzeugt von `clupilot-host-step apply-proxy-hosts` aus der Liste, die der # Betreiber in der Konsole pflegt. Damit lässt sich ein Name hinzufügen, ohne # sich auf den Server zu melden — der Handgriff, der `files.clupilot.com` # gekostet hat. # # Die Datei MUSS existieren, bevor diese Zeile sie importiert: ein Import ins # Leere macht die ganze Proxy-Konfiguration ungültig, und das nimmt die # öffentliche Website mit. `install-agent.sh` legt sie deshalb an, bevor es # hierauf hinweist. Wer die Zeile von Hand einträgt, legt vorher an: # # sudo touch /etc/caddy/clupilot-proxy-hosts.conf # import /etc/caddy/clupilot-proxy-hosts.conf # ── Dateien zum Herunterladen ──────────────────────────────────────────────── # FILES_HOST aus der .env. Zwei Dinge liegen hier, und beide müssen von überall # erreichbar sein: # # - Rechtsdokumente (AGB, AV, TOM). Ihre Adressen stehen in Verträgen und auf # Rechnungen und müssen Jahre halten. # - Das Bootstrap-Archiv für die Host-Übernahme. Es wird von einem Server im # RETTUNGSSYSTEM geholt — einer Maschine ohne Tunnel, mit einer öffentlichen # Adresse, die in keiner Freigabeliste steht. Genau deshalb steht hier KEIN # `import clupilot-console-allow.conf`: die Sperre der Konsole wäre hier die # Sperre gegen den einzigen, der es braucht. # # Der Schutz sitzt in der Anwendung, nicht im Proxy: ohne gültigen Einmal-Code # antwortet das Archiv mit 404, und die Dokumente sind ohnehin öffentlich. # # Auch KEIN `http://… { abort }` wie bei admin und ws weiter unten. Dieser Name # soll bekannt sein — und ein abgewürgtes Port 80 nimmt der ACME-Prüfung den # Weg, wenn Caddys eigener Handler einmal nicht als Erster drankommt. files.example.com { reverse_proxy 127.0.0.1:8080 } # Plain HTTP for the two hidden names. Without this Caddy answers port 80 with # an automatic redirect to https, which announces that both hostnames are # served here — the one thing this file exists to avoid. The ACME challenge is # unaffected: Caddy's own challenge handler runs ahead of these routes. http://admin.example.com, http://ws.example.com { abort }