diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index c43b519..e417a2a 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -21,6 +21,39 @@ Linux-Server läuft. enthaltene `cron`-Sidecar-Container diese Aufgabe mit demselben Schema und demselben Bearer-Secret, das die Route bereits erwartet. +## Betrieb im Firmennetz — zwei Dinge vorab + +Die App läuft intern, aber sie ist **nicht** von der Aussenwelt unabhängig. +Beides vor der Installation klären, sonst scheitert es am Ende an der +Firewall: + +**1. Der Server braucht ausgehenden Zugang.** Eingehend aus dem Internet +nichts, ausgehend zwingend: + +| Ziel | Wofür | Ohne das | +|---|---|---| +| `login.microsoftonline.com` (443) | Auth.js tauscht den Anmeldecode **serverseitig** gegen ein Token und lädt die Konfiguration des Ausstellers | keine Anmeldung möglich | +| Die Datenbank (Supabase: `*.pooler.supabase.com`, 6543) | jede Abfrage | die App startet, zeigt aber nichts | + +Dass die Anmeldung im Browser der Person stattfindet, genügt **nicht** — der +Tausch von Code gegen Token läuft vom Server aus. Liegt die VM in einem +abgeschotteten Netz, ist entweder ein Proxy nötig oder eine PostgreSQL- +Instanz im selben Netz statt Supabase. + +**2. HTTPS ist Pflicht, auch intern.** Entra ID akzeptiert `http` nur für +`localhost`. Der praktikable Weg ohne öffentliche Erreichbarkeit: ein +**öffentlicher DNS-Name, der auf die private Adresse zeigt** (z. B. +`hr.elycon.solutions` → `10.x.x.x`) und ein Zertifikat über die +DNS-Challenge. Das ist zulässig, verbreitet, und liefert ein regulär +vertrauenswürdiges Zertifikat, ohne dass der Server je aus dem Internet +erreichbar ist. + +Beispielkonfiguration: [`deploy/Caddyfile`](deploy/Caddyfile). + +Die Alternative — selbst signiertes Zertifikat — bedeutet, es auf jedem +Arbeitsplatz als vertrauenswürdig zu hinterlegen. Bei zwei Personen machbar, +bei zwanzig nicht. + ## Voraussetzungen - Docker + Docker Compose (v2, das im Docker Desktop/Docker Engine @@ -97,20 +130,30 @@ Dasselbe Abbild läuft damit in Test und Produktion. Next.js selbst sollte laut den offiziellen Docs **nicht** direkt exponiert werden – ein Reverse Proxy übernimmt TLS, Rate-Limiting und Request- -Validierung. Beispiel mit [Caddy](https://caddyserver.com/) (automatisches -HTTPS via Let's Encrypt): +Validierung. -```caddyfile -# /etc/caddy/Caddyfile -hr.example.com { - reverse_proxy localhost:3000 -} +Fertige Konfiguration: [`deploy/Caddyfile`](deploy/Caddyfile) — mit +DNS-Challenge, weil der Server aus dem Internet nicht erreichbar ist (siehe +[oben](#betrieb-im-firmennetz--zwei-dinge-vorab)). + +```bash +sudo cp deploy/Caddyfile /etc/caddy/Caddyfile +sudo systemctl reload caddy ``` -`docker-compose.yml` published Port 3000 aktuell auf den Host – bei -Verwendung eines Reverse Proxys auf demselben Host kann das Publishing auf -`127.0.0.1:3000:3000` eingeschränkt werden, damit der Container-Port nicht -direkt von außen erreichbar ist. +`docker-compose.yml` veröffentlicht Port 3000 bewusst nur auf +`127.0.0.1` — die App ist also ausschliesslich über den Proxy erreichbar, +nicht daneben unverschlüsselt. + +Zusätzlich in die `.env`: + +``` +AUTH_URL=https://hr.elycon.solutions +``` + +Ohne diesen Wert baut Auth.js seine Rückruf-Adresse aus dem, was der +Container sieht — und das ist hinter dem Proxy nicht der Name, den der +Browser benutzt hat. ## 5. Updates ausrollen diff --git a/deploy/Caddyfile b/deploy/Caddyfile new file mode 100644 index 0000000..ca0febf --- /dev/null +++ b/deploy/Caddyfile @@ -0,0 +1,45 @@ +# Reverse Proxy für den Betrieb im Firmennetz. +# +# Der Name ist öffentlich (hr.elycon.solutions), die Adresse dahinter privat +# (10.x). Das ist erlaubt und der übliche Weg: der DNS-Eintrag ist von aussen +# auflösbar, der Server nicht erreichbar. +# +# Warum das der Aufwand wert ist: Entra ID akzeptiert `http` nur für +# localhost. Ohne HTTPS gibt es keine Anmeldung — und ein selbst signiertes +# Zertifikat müsste auf jedem Arbeitsplatz als vertrauenswürdig hinterlegt +# werden. Mit der DNS-Challenge kommt ein regulär vertrauenswürdiges +# Zertifikat zustande, ohne dass der Server je aus dem Internet erreichbar +# sein muss. +# +# Voraussetzung: ein Caddy-Build mit dem DNS-Modul des eigenen Anbieters, +# etwa +# xcaddy build --with github.com/caddy-dns/cloudflare +# Andere Anbieter siehe https://github.com/caddy-dns +# +# Der API-Schlüssel gehört in eine Umgebungsvariable des Dienstes, nicht in +# diese Datei. + +hr.elycon.solutions { + tls { + dns cloudflare {env.CLOUDFLARE_API_TOKEN} + } + + # Die Anwendung lauscht nur auf der Loopback-Adresse (siehe + # docker-compose.yml), erreichbar ist sie also ausschliesslich über + # diesen Proxy. + reverse_proxy 127.0.0.1:3000 + + # Ohne diese Weitergabe baut Auth.js seine Rückruf-Adresse aus dem + # Container-Hostnamen statt aus dem echten Namen — die Anmeldung landet + # dann auf einer Adresse, die niemand kennt. `trustHost` in + # lib/auth/config.ts wertet genau diese Header aus. + header_up X-Forwarded-Proto {scheme} + header_up X-Forwarded-Host {host} + + encode gzip zstd + + log { + output file /var/log/caddy/hr.log + format json + } +} diff --git a/docker-compose.yml b/docker-compose.yml index be90fcc..24c2775 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,8 +6,12 @@ services: build: context: . restart: unless-stopped + # Nur auf der Loopback-Adresse, nicht auf allen Schnittstellen. Erreichbar + # ist die App damit ausschliesslich über den Reverse Proxy, der TLS + # beendet — sonst stünde daneben derselbe Dienst unverschlüsselt offen, + # und ein Fehler in der Firewall genügte. ports: - - "3000:3000" + - "127.0.0.1:3000:3000" env_file: - .env