# Hostnames that are reachable from the public internet. The operator console # (/admin) must NEVER be served on these — only the marketing site and the # customer portal are public; Proxmox hosts and /admin stay on the private # network / VPN. # # Deliberately a denylist, not an allowlist: a typo in an allowlist would lock # the operators out of their own console. The app-level allowlist (ADMIN_HOSTS, # see config/admin_access.php) is the strict layer — this one guarantees that a # request arriving on a known-public hostname never even reaches PHP. map $host $admin_host_is_public { default 0; www.dev.clupilot.com 1; app.dev.clupilot.com 1; api.dev.clupilot.com 1; ws.dev.clupilot.com 1; www.clupilot.com 1; app.clupilot.com 1; api.clupilot.com 1; ws.clupilot.com 1; } server { listen 80 default_server; server_name _; root /var/www/html/public; index index.php; charset utf-8; client_max_body_size 64M; # ^~ beats the regex locations, so this decides before PHP is reached. The # try_files rewrite to /index.php re-runs location matching, so the console # still executes normally on a permitted host. # # This only covers direct hits: Livewire posts component actions to # /livewire/update, which nginx cannot attribute to /admin. That case is # handled in the app (RestrictAdminHost is registered as Livewire-persistent # middleware) — do not treat this block as the only control. location ^~ /admin { if ($admin_host_is_public) { return 404; } try_files $uri $uri/ /index.php?$query_string; } # Der einzige Pfad zur Terminal-Bruecke. Bewusst eng: kein Praefix-Match # auf /terminal, sondern genau dieser eine Ort. # # AN DER WURZEL, nicht unter AdminArea::prefix(): nginx kann die Konfiguration # der Anwendung nicht lesen, dieser Ort muss also ein fester Text sein — und # er ist ohnehin keine Konsolenseite, sondern eine Steckdose, die PHP nie # sieht. resources/js/terminal.js verbindet aus demselben Grund fest hierher. # # ACHTUNG: dieser Pfad laeuft NICHT durch PHP, also greift # RestrictConsoleNetwork hier nicht. Der Riegel ist allein das Ticket — # einmalig, dreissig Sekunden, an einen Host und einen Betreiber gebunden. # So steht es auch in der Spec. Das Ticket steht deshalb NICHT in der # Adresszeile, sondern in `Sec-WebSocket-Protocol`: die Adresse eines # Upgrade-Antrags landet im Zugriffsprotokoll, der Kopf nicht. # # `terminal` ist ein Netz-Alias von queue-provisioning (siehe # docker-compose.yml): die Bruecke steht im Tunnel und teilt sich dessen # Netz-Namensraum. location = /terminal/ws { # Dieselbe Regel wie bei /admin darueber: die Bruecke ist Werkzeug der # Konsole und hat auf einem oeffentlichen Namen nichts zu suchen. if ($admin_host_is_public) { return 404; } # Aufgeloest zur Laufzeit, nicht beim Start. nginx loest einen festen # Namen in `proxy_pass` beim Laden der Konfiguration auf und WEIGERT # SICH ZU STARTEN, wenn er ihn nicht findet ("host not found in # upstream"). Damit haengte die ganze Konsole daran, dass die # Terminal-Bruecke schon laeuft — wegen eines Fensters, das gerade # niemand offen hat. Ueber eine Variable mit `resolver` (Dockers # eingebauter DNS) wird daraus ein 502 an genau dieser Stelle, waehrend # alles andere weiterlaeuft. resolver 127.0.0.11 valid=10s ipv6=off; # Der Name des Provisionierungs-Workers, nicht ein huebscherer Alias. # # Hier stand `terminal:8082`, und dieser Alias war ein Eintrag unter # `networks:` von queue-provisioning. Genau das hat den Tunnel-Container # beim ersten Ausrollen NEU BAUEN lassen — Compose baut einen Container # neu, sobald sich seine Netzkonfiguration aendert. Ein neu gebauter # Container bekommt eine neue Adresse im Compose-Netz, und damit werden # die veroeffentlichten Regeln fuer UDP 51820 neu geschrieben, auf denen # JEDER bestehende WireGuard-Fluss liegt. # # Der Preis war also: ein Wort in dieser Zeile gegen einen Abriss aller # Tunnel. Die Bruecke teilt sich ohnehin den Namensraum, `:8082` ist ihr # Port darin — der Name daneben darf ruhig der des Nachbarn sein. set $terminal_upstream queue-provisioning:8082; proxy_pass http://$terminal_upstream; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_read_timeout 900s; proxy_send_timeout 900s; } location / { try_files $uri $uri/ /index.php?$query_string; } location = /favicon.ico { access_log off; log_not_found off; } # robots.txt is generated by the app (it changes with the site's visibility), # so it must NOT be short-circuited to a file on disk. location ~ \.php$ { fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; fastcgi_param DOCUMENT_ROOT $realpath_root; } location ~ /\.(?!well-known).* { deny all; } }