Write down what an internal deployment actually needs
The target is a VM inside the company network, reachable only from there. Two consequences decide whether this works at all, and both are easy to discover too late — after the firewall rules are already written. The server needs outbound access even though nothing comes in. Auth.js exchanges the authorisation code for a token server-side and fetches the issuer's configuration, so login.microsoftonline.com must be reachable from the VM; the database likewise. That the person signs in through their own browser is not enough, which is the assumption worth naming before someone builds a closed network around it. HTTPS is not optional either: Entra accepts http only for localhost. The practical route without public reachability is a public DNS name pointing at a private address and a certificate obtained through the DNS challenge — allowed, common, and it yields a normally trusted certificate while the server stays unreachable from outside. deploy/Caddyfile does that, and the alternative (self-signed, trusted on every workstation) is written down with its cost. docker-compose now publishes port 3000 on 127.0.0.1 only. It was on every interface, so the same service also stood there unencrypted, and one gap in the firewall was enough. The proxy is the only way in. AUTH_URL is documented for the same reason a comment sits in the Caddyfile: behind a proxy the container does not see the name the browser used, and the callback would point somewhere nobody can reach. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
45
deploy/Caddyfile
Normal file
45
deploy/Caddyfile
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user