From a695159b1db9228695b19debc7ff2f2e49855fc3 Mon Sep 17 00:00:00 2001 From: Dicken Date: Sun, 19 Jul 2026 16:03:29 +0200 Subject: [PATCH] HTTPS im lokalen Netz (Port 8443, selbstsigniertes Zertifikat, optional mkcert) --- .env.example | 9 +++++ README.md | 53 +++++++++++++++++++++++++++++- apps/frontend/Dockerfile | 7 +++- apps/frontend/docker-entrypoint.sh | 38 +++++++++++++++++++++ apps/frontend/nginx-locations.conf | 32 ++++++++++++++++++ apps/frontend/nginx.conf | 51 +++++++++++----------------- docker-compose.yml | 9 +++++ docs/ROADMAP.md | 20 +++++++++++ 8 files changed, 185 insertions(+), 34 deletions(-) create mode 100644 apps/frontend/docker-entrypoint.sh create mode 100644 apps/frontend/nginx-locations.conf diff --git a/.env.example b/.env.example index f459bee..a956390 100644 --- a/.env.example +++ b/.env.example @@ -39,3 +39,12 @@ FRITZBOX_PASSWORD= # ─── Frontend (nur für lokale Entwicklung, `pnpm dev:frontend`) ───── # Ziel für den Vite-Dev-Proxy von /api -> Backend BACKEND_URL=http://localhost:3001 + +# ─── HTTPS im lokalen Netz ────────────────────────────────────────── +# IP-Adresse oder Hostname, unter der du LaunchPad im LAN aufrufst +# (z. B. https://192.168.1.49:8443). Wird für das beim Containerstart +# automatisch erzeugte selbstsignierte Zertifikat verwendet. Ohne eigenes +# Zertifikat zeigt der Browser trotzdem eine Warnung – siehe README für eine +# Variante ohne Warnung (mkcert). Ändern erfordert einen Neustart des +# Frontend-Containers: docker compose up -d --force-recreate frontend +LAUNCHPAD_HOST=192.168.1.49 diff --git a/README.md b/README.md index dee152e..d3f8a61 100644 --- a/README.md +++ b/README.md @@ -14,9 +14,60 @@ cp .env.example .env # optional anpassen, siehe unten docker compose up -d --build ``` -- Frontend: http://localhost:8080 +- Frontend: http://localhost:8080 (HTTPS: https://localhost:8443, siehe unten) - Backend / Health-API: http://localhost:3001/api/health +## HTTPS im lokalen Netz + +LaunchPad ist per HTTPS auf Port `8443` erreichbar (`https://:8443`), +zusätzlich zu HTTP auf `8080` – kein erzwungener Redirect. Beim ersten Start +erzeugt der Frontend-Container automatisch ein selbstsigniertes Zertifikat für +die in `.env` hinterlegte `LAUNCHPAD_HOST` (IP oder Hostname). Der Browser +zeigt dafür trotzdem eine Sicherheitswarnung – das ist bei selbstsignierten +Zertifikaten normal, nicht per se unsicher fürs eigene LAN. + +```bash +# .env: IP/Hostname eintragen, unter der du zugreifst +LAUNCHPAD_HOST=192.168.1.49 + +docker compose up -d --force-recreate frontend +``` + +Danach: `https://192.168.1.49:8443` (Warnung einmalig bestätigen/Ausnahme +hinzufügen). + +### Ohne Browser-Warnung (empfohlen): eigenes Zertifikat per mkcert + +[mkcert](https://github.com/FiloSottile/mkcert) erzeugt lokal vertrauenswürdige +Zertifikate – dein Browser vertraut ihnen automatisch, keine Warnung mehr. + +```bash +# Einmalig auf dem Rechner, von dem aus du zugreifst (nicht auf dem Server): +mkcert -install + +# Zertifikat für IP/Hostname erzeugen: +mkcert -cert-file fullchain.pem -key-file privkey.pem 192.168.1.49 launchpad.home localhost 127.0.0.1 +``` + +Die beiden erzeugten Dateien nach `/opt/LaunchPad/certs/` kopieren, dann in +`docker-compose.yml` die auskommentierten `volumes:`-Zeilen beim +`frontend`-Service aktivieren: + +```yaml +volumes: + - ./certs/fullchain.pem:/etc/nginx/certs/fullchain.pem:ro + - ./certs/privkey.pem:/etc/nginx/certs/privkey.pem:ro +``` + +```bash +docker compose up -d --force-recreate frontend +``` + +`mkcert -install` legt eine lokale Root-CA an, der nur *dieser* Rechner +vertraut. Für weitere Geräte (Handy etc.) muss die Root-CA +(`mkcert -CAROOT` zeigt den Pfad) dort zusätzlich installiert werden, oder es +bleibt bei der Browser-Warnung des automatisch generierten Zertifikats. + ## Quickstart (lokale Entwicklung) Voraussetzungen: Node.js ≥ 20, pnpm ≥ 9. diff --git a/apps/frontend/Dockerfile b/apps/frontend/Dockerfile index f0c34fb..ff53dfb 100644 --- a/apps/frontend/Dockerfile +++ b/apps/frontend/Dockerfile @@ -24,7 +24,12 @@ RUN pnpm --filter @launchpad/frontend build # ---- runtime: nginx -------------------------------------------------------- FROM nginx:1.27-alpine AS runtime +RUN apk add --no-cache openssl COPY apps/frontend/nginx.conf /etc/nginx/conf.d/default.conf +COPY apps/frontend/nginx-locations.conf /etc/nginx/launchpad-locations.conf +COPY apps/frontend/docker-entrypoint.sh /docker-entrypoint.sh +RUN chmod +x /docker-entrypoint.sh COPY --from=build /app/apps/frontend/dist /usr/share/nginx/html -EXPOSE 80 +EXPOSE 80 443 +ENTRYPOINT ["/docker-entrypoint.sh"] CMD ["nginx", "-g", "daemon off;"] diff --git a/apps/frontend/docker-entrypoint.sh b/apps/frontend/docker-entrypoint.sh new file mode 100644 index 0000000..9a70dc3 --- /dev/null +++ b/apps/frontend/docker-entrypoint.sh @@ -0,0 +1,38 @@ +#!/bin/sh +set -e + +CERT_DIR="/etc/nginx/certs" +CERT_FILE="$CERT_DIR/fullchain.pem" +KEY_FILE="$CERT_DIR/privkey.pem" + +# Falls der Nutzer eigene Zertifikate eingebunden hat (siehe README, z. B. +# per mkcert erzeugt), diese nicht überschreiben. +if [ ! -f "$CERT_FILE" ] || [ ! -f "$KEY_FILE" ]; then + HOST="${LAUNCHPAD_HOST:-localhost}" + mkdir -p "$CERT_DIR" + + # IP-Adresse oder Hostname? Browser verlangen bei IP-Zugriff eine + # IP-SAN, ein DNS-Name genügt da nicht. + case "$HOST" in + *[0-9]*.*[0-9]*.*[0-9]*.*[0-9]*) + SAN="IP:$HOST,DNS:localhost,IP:127.0.0.1" + ;; + *) + SAN="DNS:$HOST,DNS:localhost,IP:127.0.0.1" + ;; + esac + + openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \ + -keyout "$KEY_FILE" \ + -out "$CERT_FILE" \ + -subj "/CN=$HOST" \ + -addext "subjectAltName=$SAN" \ + 2>/dev/null + + echo "[entrypoint] Selbstsigniertes HTTPS-Zertifikat für '$HOST' erzeugt." + echo "[entrypoint] Der Browser zeigt dafür eine Sicherheitswarnung - siehe README fuer eine Variante ohne Warnung (mkcert)." +else + echo "[entrypoint] Verwende eingebundenes Zertifikat unter $CERT_DIR." +fi + +exec "$@" diff --git a/apps/frontend/nginx-locations.conf b/apps/frontend/nginx-locations.conf new file mode 100644 index 0000000..7a20d49 --- /dev/null +++ b/apps/frontend/nginx-locations.conf @@ -0,0 +1,32 @@ +# PWA / SPA: alle unbekannten Routen auf index.html zurückführen +location / { + try_files $uri $uri/ /index.html; +} + +# API-Anfragen an den Backend-Container weiterleiten +location /api/ { + proxy_pass http://backend:3001/api/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; +} + +# Service Worker und Manifest dürfen nicht langfristig gecacht werden, +# sonst bekommen installierte PWA-Nutzer nie ein Update mit. +location = /sw.js { + add_header Cache-Control "no-cache"; + try_files $uri =404; +} + +location = /manifest.webmanifest { + add_header Cache-Control "no-cache"; + default_type application/manifest+json; + try_files $uri =404; +} + +location ~* \.(?:css|js|svg|png|jpg|jpeg|gif|ico|woff2?)$ { + expires 7d; + add_header Cache-Control "public, max-age=604800, immutable"; +} diff --git a/apps/frontend/nginx.conf b/apps/frontend/nginx.conf index b14530d..9fe9b8f 100644 --- a/apps/frontend/nginx.conf +++ b/apps/frontend/nginx.conf @@ -1,39 +1,26 @@ +# HTTP – bleibt zusätzlich zu HTTPS verfügbar (kein erzwungener Redirect, +# damit bestehende http://-Zugriffe nicht plötzlich brechen). server { listen 80; server_name _; root /usr/share/nginx/html; index index.html; - # PWA / SPA: alle unbekannten Routen auf index.html zurückführen - location / { - try_files $uri $uri/ /index.html; - } - - # API-Anfragen an den Backend-Container weiterleiten - location /api/ { - proxy_pass http://backend:3001/api/; - proxy_http_version 1.1; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } - - # Service Worker und Manifest dürfen nicht langfristig gecacht werden, - # sonst bekommen installierte PWA-Nutzer nie ein Update mit. - location = /sw.js { - add_header Cache-Control "no-cache"; - try_files $uri =404; - } - - location = /manifest.webmanifest { - add_header Cache-Control "no-cache"; - default_type application/manifest+json; - try_files $uri =404; - } - - location ~* \.(?:css|js|svg|png|jpg|jpeg|gif|ico|woff2?)$ { - expires 7d; - add_header Cache-Control "public, max-age=604800, immutable"; - } + include /etc/nginx/launchpad-locations.conf; +} + +# HTTPS – Zertifikat wird vom Entrypoint-Skript automatisch erzeugt +# (selbstsigniert) oder ist vom Nutzer eingebunden, siehe README. +server { + listen 443 ssl; + http2 on; + server_name _; + root /usr/share/nginx/html; + index index.html; + + ssl_certificate /etc/nginx/certs/fullchain.pem; + ssl_certificate_key /etc/nginx/certs/privkey.pem; + ssl_protocols TLSv1.2 TLSv1.3; + + include /etc/nginx/launchpad-locations.conf; } diff --git a/docker-compose.yml b/docker-compose.yml index 60eeca4..3d9675d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -25,10 +25,19 @@ services: dockerfile: apps/frontend/Dockerfile container_name: launchpad-frontend restart: unless-stopped + env_file: + - path: ./.env + required: false depends_on: - backend ports: - "8080:80" + - "8443:443" + # Optional: eigenes Zertifikat statt des automatisch generierten + # selbstsignierten einbinden (z. B. per mkcert erzeugt, siehe README): + # volumes: + # - ./certs/fullchain.pem:/etc/nginx/certs/fullchain.pem:ro + # - ./certs/privkey.pem:/etc/nginx/certs/privkey.pem:ro volumes: launchpad-data: diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 93adf46..d216a6d 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -181,3 +181,23 @@ Geräte): (3) Umbenennen einer Kategorie im Adminbereich aktualisiert automatisch alle Dienste mit dem alten Namen. Einschränkung: manuell per API angelegte Dienste (nicht über einen Scan) lösen diesen Sync aktuell nicht aus. + +## HTTPS im lokalen Netz + +- Frontend-Container liefert jetzt zusätzlich zu HTTP (Port 8080) auch HTTPS + auf Port 8443 aus, kein erzwungener Redirect. +- Entrypoint-Skript (`apps/frontend/docker-entrypoint.sh`) erzeugt beim + Containerstart automatisch ein selbstsigniertes Zertifikat für die in + `.env` hinterlegte `LAUNCHPAD_HOST` (IP-SAN bei IP-Adressen, DNS-SAN bei + Hostnamen) – übernimmt nichts, falls unter `/etc/nginx/certs/` bereits + eigene Zertifikate eingebunden sind (siehe README, z. B. per mkcert für + eine Variante ohne Browser-Warnung). +- `nginx.conf` in HTTP- und HTTPS-Serverblock aufgeteilt, gemeinsame + Location-Regeln in `nginx-locations.conf` ausgelagert (kein doppelt + gepflegter Proxy-/Cache-Code). + +> Verifiziert: Zertifikatserzeugung für IP-, Hostname- und Fallback-Fall +> getestet, komplette nginx-Konfiguration mit einem echten nginx-Binary +> validiert (`nginx -t` erfolgreich), echter TLS-Handshake gegen den +> laufenden Server durchgeführt und geprüft, dass das ausgelieferte +> Zertifikat die korrekte IP als Subject Alternative Name trägt.