generated from Dicken/dickendock
HTTPS: eigene lokale Root-CA statt externem DuckDNS/Let's Encrypt
This commit is contained in:
12
.env.example
12
.env.example
@@ -42,9 +42,11 @@ BACKEND_URL=http://localhost:3001
|
|||||||
|
|
||||||
# ─── HTTPS im lokalen Netz ──────────────────────────────────────────
|
# ─── HTTPS im lokalen Netz ──────────────────────────────────────────
|
||||||
# IP-Adresse oder Hostname, unter der du LaunchPad im LAN aufrufst
|
# 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
|
# (z. B. https://192.168.1.49:8443). Der Frontend-Container erzeugt daraus
|
||||||
# automatisch erzeugte selbstsignierte Zertifikat verwendet. Ohne eigenes
|
# automatisch eine eigene lokale Root-CA + ein davon signiertes Zertifikat
|
||||||
# Zertifikat zeigt der Browser trotzdem eine Warnung – siehe README für eine
|
# (komplett offline, keine externen Dienste). Die CA liegt danach zum
|
||||||
# Variante ohne Warnung (mkcert). Ändern erfordert einen Neustart des
|
# Download unter http://<LAUNCHPAD_HOST>:8080/ca.crt bereit – einmal pro
|
||||||
# Frontend-Containers: docker compose up -d --force-recreate frontend
|
# Gerät importiert/als vertrauenswürdig eingestuft, verschwindet die
|
||||||
|
# Browser-Warnung dauerhaft. Siehe README für die genauen Schritte.
|
||||||
|
# Ändern erfordert einen Neustart: docker compose up -d --force-recreate frontend
|
||||||
LAUNCHPAD_HOST=192.168.1.49
|
LAUNCHPAD_HOST=192.168.1.49
|
||||||
|
|||||||
80
README.md
80
README.md
@@ -20,11 +20,25 @@ docker compose up -d --build
|
|||||||
## HTTPS im lokalen Netz
|
## HTTPS im lokalen Netz
|
||||||
|
|
||||||
LaunchPad ist per HTTPS auf Port `8443` erreichbar (`https://<deine-ip>:8443`),
|
LaunchPad ist per HTTPS auf Port `8443` erreichbar (`https://<deine-ip>:8443`),
|
||||||
zusätzlich zu HTTP auf `8080` – kein erzwungener Redirect. Beim ersten Start
|
zusätzlich zu HTTP auf `8080` – kein erzwungener Redirect, kein externer
|
||||||
erzeugt der Frontend-Container automatisch ein selbstsigniertes Zertifikat für
|
Dienst beteiligt.
|
||||||
die in `.env` hinterlegte `LAUNCHPAD_HOST` (IP oder Hostname). Der Browser
|
|
||||||
zeigt dafür trotzdem eine Sicherheitswarnung – das ist bei selbstsignierten
|
Der Frontend-Container erzeugt beim ersten Start automatisch eine **eigene
|
||||||
Zertifikaten normal, nicht per se unsicher fürs eigene LAN.
|
lokale Root-CA** (`LaunchPad Local CA`) und signiert damit ein Server-
|
||||||
|
Zertifikat für die in `.env` hinterlegte `LAUNCHPAD_HOST` (IP oder Hostname).
|
||||||
|
Beides liegt in einem Docker-Volume (`launchpad-certs`) und bleibt über
|
||||||
|
Neustarts und `--build` hinweg stabil – die CA wird nur einmal erzeugt, nicht
|
||||||
|
bei jedem Deploy neu.
|
||||||
|
|
||||||
|
**Ohne weiteren Schritt** funktioniert HTTPS sofort, der Browser zeigt aber
|
||||||
|
eine Warnung (unbekannte CA). Für eine echte, warnungsfreie Verbindung – und
|
||||||
|
damit Chrome die PWA auch wirklich "installiert" statt nur eine Verknüpfung
|
||||||
|
anzulegen – reicht **ein einziger, pro Gerät einmaliger Schritt**: die
|
||||||
|
Root-CA importieren. Es gibt dafür keinen Weg drumherum, der nicht doch einen
|
||||||
|
externen Dienst braucht (siehe Kasten unten) – aber so ist es so einfach wie
|
||||||
|
möglich gehalten.
|
||||||
|
|
||||||
|
### Root-CA einmal pro Gerät importieren
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# .env: IP/Hostname eintragen, unter der du zugreifst
|
# .env: IP/Hostname eintragen, unter der du zugreifst
|
||||||
@@ -33,40 +47,40 @@ LAUNCHPAD_HOST=192.168.1.49
|
|||||||
docker compose up -d --force-recreate frontend
|
docker compose up -d --force-recreate frontend
|
||||||
```
|
```
|
||||||
|
|
||||||
Danach: `https://192.168.1.49:8443` (Warnung einmalig bestätigen/Ausnahme
|
**Am Handy (Android):** `http://192.168.1.49:8080/ca.crt` im Browser öffnen
|
||||||
hinzufügen).
|
(bewusst HTTP, dafür braucht's noch kein Vertrauen) → Android bietet direkt
|
||||||
|
"Zertifikat installieren" an → als "VPN und App" oder "WLAN"-Zertifikat
|
||||||
|
speichern (je nach Android-Version heißt die Kategorie leicht anders,
|
||||||
|
Hauptsache es landet unter "Von Nutzer installierte Zertifikate"). Danach ggf.
|
||||||
|
kurz Chrome neu starten.
|
||||||
|
|
||||||
### Ohne Browser-Warnung (empfohlen): eigenes Zertifikat per mkcert
|
**Am PC (Windows):** dieselbe URL im Browser öffnen, `ca.crt` herunterladen,
|
||||||
|
Datei doppelklicken → "Zertifikat installieren" → "Lokaler Computer" →
|
||||||
|
"Alle Zertifikate in folgendem Speicher speichern" → "Vertrauenswürdige
|
||||||
|
Stammzertifizierungsstellen" → Fertigstellen. Kein zusätzliches Tool nötig.
|
||||||
|
|
||||||
[mkcert](https://github.com/FiloSottile/mkcert) erzeugt lokal vertrauenswürdige
|
**macOS:** `ca.crt` herunterladen, doppelklicken (öffnet Schlüsselbundverwaltung),
|
||||||
Zertifikate – dein Browser vertraut ihnen automatisch, keine Warnung mehr.
|
dort öffnen, Vertrauenseinstellung auf "Immer vertrauen" setzen.
|
||||||
|
|
||||||
```bash
|
Danach zeigt `https://192.168.1.49:8443` keine Warnung mehr, und die
|
||||||
# Einmalig auf dem Rechner, von dem aus du zugreifst (nicht auf dem Server):
|
PWA-Installation sollte funktionieren.
|
||||||
mkcert -install
|
|
||||||
|
|
||||||
# Zertifikat für IP/Hostname erzeugen:
|
> **Warum es keinen Weg ganz ohne diesen Schritt gibt:** Damit ein Browser
|
||||||
mkcert -cert-file fullchain.pem -key-file privkey.pem 192.168.1.49 launchpad.home localhost 127.0.0.1
|
> einem Zertifikat *automatisch*, ohne jeden manuellen Trust-Schritt, vertraut,
|
||||||
```
|
> muss es entweder von einer öffentlich vorinstallierten CA stammen (z. B.
|
||||||
|
> Let's Encrypt) – das erfordert zwangsläufig einen externen Dienst und meist
|
||||||
|
> einen öffentlich auflösbaren Domainnamen – oder eben eine lokale CA, die du
|
||||||
|
> einmal manuell für vertrauenswürdig erklärst. Es gibt keine dritte Option.
|
||||||
|
> Diese Lösung minimiert den Aufwand auf "eine Datei öffnen, einmal pro Gerät",
|
||||||
|
> mehr geht ohne externe Dienste nicht.
|
||||||
|
|
||||||
Die beiden erzeugten Dateien nach `/opt/LaunchPad/certs/` kopieren, dann in
|
### Zertifikat wechseln / eigenes einbinden
|
||||||
`docker-compose.yml` die auskommentierten `volumes:`-Zeilen beim
|
|
||||||
`frontend`-Service aktivieren:
|
|
||||||
|
|
||||||
```yaml
|
Falls du lieber ein eigenes Zertifikat verwendest (z. B. von einer internen
|
||||||
volumes:
|
CA, die du schon hast): `fullchain.pem` + `privkey.pem` ins Volume
|
||||||
- ./certs/fullchain.pem:/etc/nginx/certs/fullchain.pem:ro
|
`launchpad-certs` legen (z. B. `docker cp fullchain.pem launchpad-frontend:/etc/nginx/certs/` bei laufendem Container, oder das Volume direkt mounten),
|
||||||
- ./certs/privkey.pem:/etc/nginx/certs/privkey.pem:ro
|
danach `docker compose restart frontend`. Liegt dabei keine `ca-cert.pem` im
|
||||||
```
|
selben Verzeichnis, generiert der Entrypoint nichts automatisch nach.
|
||||||
|
|
||||||
```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)
|
## Quickstart (lokale Entwicklung)
|
||||||
|
|
||||||
|
|||||||
@@ -2,37 +2,70 @@
|
|||||||
set -e
|
set -e
|
||||||
|
|
||||||
CERT_DIR="/etc/nginx/certs"
|
CERT_DIR="/etc/nginx/certs"
|
||||||
|
CA_KEY="$CERT_DIR/ca-key.pem"
|
||||||
|
CA_CERT="$CERT_DIR/ca-cert.pem"
|
||||||
CERT_FILE="$CERT_DIR/fullchain.pem"
|
CERT_FILE="$CERT_DIR/fullchain.pem"
|
||||||
KEY_FILE="$CERT_DIR/privkey.pem"
|
KEY_FILE="$CERT_DIR/privkey.pem"
|
||||||
|
CERT_HOST_FILE="$CERT_DIR/.cert-host"
|
||||||
|
HOST="${LAUNCHPAD_HOST:-localhost}"
|
||||||
|
|
||||||
# Falls der Nutzer eigene Zertifikate eingebunden hat (siehe README, z. B.
|
mkdir -p "$CERT_DIR"
|
||||||
# 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
|
if [ -f "$CERT_FILE" ] && [ -f "$KEY_FILE" ] && [ ! -f "$CA_CERT" ]; then
|
||||||
# IP-SAN, ein DNS-Name genügt da nicht.
|
# fullchain.pem/privkey.pem wurden vom Nutzer eingebunden (z. B. eigenes
|
||||||
case "$HOST" in
|
# Zertifikat) und es gibt keine von uns erzeugte CA dazu -> nichts anfassen.
|
||||||
*[0-9]*.*[0-9]*.*[0-9]*.*[0-9]*)
|
echo "[entrypoint] Eingebundenes Zertifikat gefunden (kein CA-Download unter /ca.crt verfügbar)."
|
||||||
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
|
else
|
||||||
echo "[entrypoint] Verwende eingebundenes Zertifikat unter $CERT_DIR."
|
# Eigene lokale Root-CA erzeugen, falls noch keine existiert. Liegt im
|
||||||
|
# Volume "launchpad-certs" und bleibt über Neustarts/Rebuilds stabil -
|
||||||
|
# einmal pro Gerät importiert, reicht dauerhaft.
|
||||||
|
if [ ! -f "$CA_KEY" ] || [ ! -f "$CA_CERT" ]; then
|
||||||
|
echo "[entrypoint] Erzeuge neue lokale Root-CA für LaunchPad..."
|
||||||
|
openssl genrsa -out "$CA_KEY" 2048 2>/dev/null
|
||||||
|
openssl req -x509 -new -nodes -key "$CA_KEY" -sha256 -days 3650 \
|
||||||
|
-subj "/CN=LaunchPad Local CA" \
|
||||||
|
-out "$CA_CERT" 2>/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Server-Zertifikat nur neu erzeugen, wenn es noch keins gibt oder sich
|
||||||
|
# LAUNCHPAD_HOST geändert hat (dann würde die alte SAN nicht mehr passen).
|
||||||
|
NEED_NEW_LEAF=false
|
||||||
|
if [ ! -f "$CERT_FILE" ] || [ ! -f "$KEY_FILE" ]; then
|
||||||
|
NEED_NEW_LEAF=true
|
||||||
|
elif [ "$(cat "$CERT_HOST_FILE" 2>/dev/null)" != "$HOST" ]; then
|
||||||
|
NEED_NEW_LEAF=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$NEED_NEW_LEAF" = "true" ]; then
|
||||||
|
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
|
||||||
|
|
||||||
|
EXTFILE=$(mktemp)
|
||||||
|
printf "subjectAltName=%s\nbasicConstraints=CA:FALSE\nkeyUsage=digitalSignature,keyEncipherment\nextendedKeyUsage=serverAuth\n" "$SAN" > "$EXTFILE"
|
||||||
|
|
||||||
|
openssl genrsa -out "$KEY_FILE" 2048 2>/dev/null
|
||||||
|
openssl req -new -key "$KEY_FILE" -subj "/CN=$HOST" -out "$CERT_DIR/server.csr" 2>/dev/null
|
||||||
|
openssl x509 -req -in "$CERT_DIR/server.csr" -CA "$CA_CERT" -CAkey "$CA_KEY" \
|
||||||
|
-CAcreateserial -days 825 -sha256 -extfile "$EXTFILE" \
|
||||||
|
-out "$CERT_FILE" 2>/dev/null
|
||||||
|
|
||||||
|
rm -f "$EXTFILE" "$CERT_DIR/server.csr"
|
||||||
|
echo "$HOST" > "$CERT_HOST_FILE"
|
||||||
|
echo "[entrypoint] Server-Zertifikat für '$HOST' erzeugt und mit lokaler CA signiert."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# CA-Zertifikat zum Download bereitstellen (Android/iOS/Windows bieten beim
|
||||||
|
# Öffnen einer .crt-Datei im Browser automatisch "Zertifikat installieren" an).
|
||||||
|
if [ -f "$CA_CERT" ]; then
|
||||||
|
mkdir -p /usr/share/nginx/html
|
||||||
|
cp "$CA_CERT" /usr/share/nginx/html/ca.crt
|
||||||
fi
|
fi
|
||||||
|
|
||||||
exec "$@"
|
exec "$@"
|
||||||
|
|||||||
@@ -26,6 +26,14 @@ location = /manifest.webmanifest {
|
|||||||
try_files $uri =404;
|
try_files $uri =404;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Root-CA zum Download – Android/iOS erkennen diesen MIME-Type und bieten
|
||||||
|
# beim Öffnen im Browser direkt "Zertifikat installieren" an.
|
||||||
|
location = /ca.crt {
|
||||||
|
add_header Cache-Control "no-cache";
|
||||||
|
default_type application/x-x509-ca-cert;
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
location ~* \.(?:css|js|svg|png|jpg|jpeg|gif|ico|woff2?)$ {
|
location ~* \.(?:css|js|svg|png|jpg|jpeg|gif|ico|woff2?)$ {
|
||||||
expires 7d;
|
expires 7d;
|
||||||
add_header Cache-Control "public, max-age=604800, immutable";
|
add_header Cache-Control "public, max-age=604800, immutable";
|
||||||
|
|||||||
@@ -33,12 +33,15 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "8080:80"
|
- "8080:80"
|
||||||
- "8443:443"
|
- "8443:443"
|
||||||
# Optional: eigenes Zertifikat statt des automatisch generierten
|
volumes:
|
||||||
# selbstsignierten einbinden (z. B. per mkcert erzeugt, siehe README):
|
# Lokale Root-CA + Server-Zertifikat, vom Container selbst erzeugt
|
||||||
# volumes:
|
# (siehe apps/frontend/docker-entrypoint.sh). Als Volume, damit die CA
|
||||||
# - ./certs/fullchain.pem:/etc/nginx/certs/fullchain.pem:ro
|
# über Neustarts/Rebuilds stabil bleibt - einmal pro Gerät importiert
|
||||||
# - ./certs/privkey.pem:/etc/nginx/certs/privkey.pem:ro
|
# (https://<host>:8080/ca.crt), reicht dauerhaft.
|
||||||
|
- launchpad-certs:/etc/nginx/certs
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
launchpad-data:
|
launchpad-data:
|
||||||
name: launchpad-data
|
name: launchpad-data
|
||||||
|
launchpad-certs:
|
||||||
|
name: launchpad-certs
|
||||||
|
|||||||
@@ -201,3 +201,34 @@ Geräte):
|
|||||||
> validiert (`nginx -t` erfolgreich), echter TLS-Handshake gegen den
|
> validiert (`nginx -t` erfolgreich), echter TLS-Handshake gegen den
|
||||||
> laufenden Server durchgeführt und geprüft, dass das ausgelieferte
|
> laufenden Server durchgeführt und geprüft, dass das ausgelieferte
|
||||||
> Zertifikat die korrekte IP als Subject Alternative Name trägt.
|
> Zertifikat die korrekte IP als Subject Alternative Name trägt.
|
||||||
|
|
||||||
|
## HTTPS-Revision: eigene lokale Root-CA statt DuckDNS
|
||||||
|
|
||||||
|
Nutzer-Feedback: "Will nix externes, soll auch nur lokal laufen" – die
|
||||||
|
DuckDNS/Let's-Encrypt-Lösung wurde daher wieder entfernt und durch eine
|
||||||
|
komplett offline funktionierende Variante ersetzt.
|
||||||
|
|
||||||
|
- `apps/frontend/docker-entrypoint.sh` erzeugt beim ersten Containerstart
|
||||||
|
eine eigene Root-CA (`LaunchPad Local CA`, `CA:TRUE`, 10 Jahre gültig) und
|
||||||
|
signiert damit ein Server-Zertifikat (`CA:FALSE`, korrekte SAN je nach
|
||||||
|
`LAUNCHPAD_HOST`) – keinerlei externe Dienste beteiligt
|
||||||
|
- CA + Server-Zertifikat liegen im neuen Docker-Volume `launchpad-certs`
|
||||||
|
(statt im Container-Dateisystem) – bleiben dadurch über Neustarts und
|
||||||
|
`--build` stabil. Ändert sich nur `LAUNCHPAD_HOST`, wird ausschließlich das
|
||||||
|
Server-Zertifikat neu ausgestellt, die CA (und damit bereits erteiltes
|
||||||
|
Gerätevertrauen) bleibt unangetastet
|
||||||
|
- Root-CA wird unter `http://<host>:8080/ca.crt` zum Download bereitgestellt
|
||||||
|
(`nginx-locations.conf`, korrekter MIME-Type `application/x-x509-ca-cert`
|
||||||
|
löst auf Android/iOS automatisch den nativen "Zertifikat installieren"-Dialog
|
||||||
|
aus) – einziger verbleibender manueller Schritt ist der einmalige Import
|
||||||
|
pro Gerät, dafür gibt es ohne externe CA keine Alternative
|
||||||
|
- DuckDNS/`acme`-Service und zugehörige `.env`-Variablen vollständig entfernt
|
||||||
|
|
||||||
|
> Verifiziert: CA- und Leaf-Zertifikat-Erzeugung lokal mit echtem `openssl`
|
||||||
|
> durchgespielt (Zertifikatskette validiert erfolgreich gegen die CA,
|
||||||
|
> `CA:FALSE`/`CA:TRUE` korrekt gesetzt, SAN korrekt), Idempotenz geprüft
|
||||||
|
> (zweiter Lauf mit gleichem Host verändert nichts, geänderter Host erzeugt
|
||||||
|
> nur ein neues Server-Zertifikat, CA bleibt stabil), kompletter End-to-End-
|
||||||
|
> Test mit echtem nginx: `/ca.crt` per HTTP heruntergeladen und **damit ohne
|
||||||
|
> `-k`-Flag** eine echte HTTPS-Verbindung erfolgreich validiert – simuliert
|
||||||
|
> exakt das Verhalten eines Geräts nach CA-Import.
|
||||||
|
|||||||
Reference in New Issue
Block a user