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:
2026-08-07 08:26:14 +02:00
parent 56662c0775
commit 780f8fe2b7
3 changed files with 104 additions and 12 deletions

View File

@@ -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