From d0e48f33859a2f10d8c49b3ae12e583eae8d2aeb Mon Sep 17 00:00:00 2001 From: thomas Date: Sun, 30 Aug 2026 20:47:02 +0200 Subject: [PATCH] =?UTF-8?q?fix(deploy):=20Backend=20startet=20wieder=20?= =?UTF-8?q?=E2=80=94=20JWT-Secret=20kam=20leer=20im=20Container=20an?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Startblocker aus d9fecb6: die Compose-Dateien lesen seither ${NACHSUCHE_JWT_SECRET} (bzw. DROHNENFUEHRER_/STOEBERHUNDE_), die .env-Dateien definieren aber weiterhin JWT_SECRET. Compose ersetzte die unbekannte Variable still durch einen Leerstring, config/env.js beendete den Prozess daraufhin mit Exit-Code 1 — mit restart: unless-stopped ein Crash-Loop. Nachgewiesen mit `docker compose config` (JWT_SECRET: "") und einem Startversuch. - Compose nutzt jetzt ${..._JWT_SECRET:?...}: fehlt der Wert, bricht compose mit einer Meldung ab, statt ihn stillschweigend zu leeren. - Die .env-Dateien (nicht versioniert) tragen die korrekten Namen und je App ein EIGENES 32-Byte-Secret. Vorher stand in allen drei derselbe 16-Zeichen- Wert, wodurch die App-Trennung aus d9fecb6 wirkungslos war. FOLGE: alle laufenden Admin-Sitzungen und Fuehrer-Tokens sind ungueltig, alle muessen sich einmal neu anmelden. - Fuehrer-Tokens tragen jetzt app: config.appName, und handlerAuth.js sowie die umbenannten Varianten pruefen den Claim. Vorher trugen sie nur { id, role } — die app-Pruefung in middleware/auth.js lief bei ihnen ins Leere, weil sie an `decoded.app &&` haengt. Geprueft: Fuehrer-Token aus App A gegen /me von App B -> 403, gegen die eigene -> 200. - APP_URL steht in Compose wieder auf ${APP_URL:-}. Der Default aus der letzten Runde hat die Warnung aus config/env.js unterdrueckt, die auf eine fehlende Konfiguration hinweisen sollte. - .env.example dokumentiert die neuen Namen, den openssl-Aufruf und dass CORS_ORIGIN/APP_URL produktiv nicht auf localhost stehen duerfen. Co-Authored-By: Claude Opus 5 --- backend/.gitignore | 1 - backend/logs/combined.log | 0 backend/logs/error.log | 0 drohnenfuehrer/backend/.env.example | 10 ++++++++-- .../backend/controllers/drohnenfuehrerController.js | 2 +- .../backend/middleware/drohnenfuehrerAuth.js | 5 +++++ drohnenfuehrer/docker-compose.yml | 8 ++++++-- nachsuche/backend/.env.example | 10 ++++++++-- nachsuche/backend/controllers/handlerController.js | 2 +- nachsuche/backend/middleware/handlerAuth.js | 5 +++++ nachsuche/docker-compose.yml | 8 ++++++-- stoeberhunde/backend/.env.example | 10 ++++++++-- .../controllers/stoeberhundefuehrerController.js | 2 +- .../backend/middleware/stoeberhundefuehrerAuth.js | 5 +++++ stoeberhunde/docker-compose.yml | 8 ++++++-- 15 files changed, 60 insertions(+), 16 deletions(-) delete mode 100644 backend/.gitignore delete mode 100644 backend/logs/combined.log delete mode 100644 backend/logs/error.log diff --git a/backend/.gitignore b/backend/.gitignore deleted file mode 100644 index 8b13789..0000000 --- a/backend/.gitignore +++ /dev/null @@ -1 +0,0 @@ - diff --git a/backend/logs/combined.log b/backend/logs/combined.log deleted file mode 100644 index e69de29..0000000 diff --git a/backend/logs/error.log b/backend/logs/error.log deleted file mode 100644 index e69de29..0000000 diff --git a/drohnenfuehrer/backend/.env.example b/drohnenfuehrer/backend/.env.example index feaf9bf..edda797 100644 --- a/drohnenfuehrer/backend/.env.example +++ b/drohnenfuehrer/backend/.env.example @@ -6,13 +6,17 @@ NODE_ENV=development MONGO_URI=mongodb://127.0.0.1:27017/drohnenfuehrer # JWT Configuration -JWT_SECRET=your-super-secret-jwt-key-change-this-in-production +# Der Name MUSS zu docker-compose.yml passen. Je App ein EIGENES Secret: +# openssl rand -hex 32 +DROHNENFUEHRER_JWT_SECRET= JWT_EXPIRES_IN=24h # Admin Initial Password (used by seed.js if admin doesn't exist) # ADMIN_INITIAL_PASSWORD=secure-password-here # CORS Configuration (comma-separated for multiple origins) +# Produktiv die echte oeffentliche Herkunft eintragen, nicht localhost — +# APP_URL wird daraus abgeleitet, wenn es nicht gesetzt ist. CORS_ORIGIN=http://localhost:3000 # Geocoding Configuration (OpenStreetMap Nominatim) @@ -23,7 +27,9 @@ GEOCODE_MIN_DELAY_MS=1100 # Basis-URL der App fuer Links in E-Mails (Passwort-Reset). # MUSS den Unterpfad enthalten, unter dem die App ausgeliefert wird. # Ohne diesen Wert wird er aus CORS_ORIGIN + "/drohnenfuehrer" zusammengesetzt. -APP_URL=http://localhost:8081/drohnenfuehrer +# Produktiv die echte oeffentliche URL inkl. Unterpfad eintragen. +# Leer lassen -> wird aus CORS_ORIGIN + "/drohnenfuehrer" gebildet (mit Warnung). +APP_URL= # SMTP fuer Passwort-Reset-Mails (optional). # Fehlt die Konfiguration, wird der Reset-Link nur ins Log geschrieben. diff --git a/drohnenfuehrer/backend/controllers/drohnenfuehrerController.js b/drohnenfuehrer/backend/controllers/drohnenfuehrerController.js index 51d1af5..611c604 100644 --- a/drohnenfuehrer/backend/controllers/drohnenfuehrerController.js +++ b/drohnenfuehrer/backend/controllers/drohnenfuehrerController.js @@ -25,7 +25,7 @@ const drohnenfuehrerLogin = async (req, res) => { } const token = jwt.sign( - { id: user._id.toString(), role: 'drohnenfuehrer' }, + { id: user._id.toString(), role: 'drohnenfuehrer', app: config.appName }, config.jwtSecret, { expiresIn: '12h' } ); diff --git a/drohnenfuehrer/backend/middleware/drohnenfuehrerAuth.js b/drohnenfuehrer/backend/middleware/drohnenfuehrerAuth.js index 9def758..60446f1 100644 --- a/drohnenfuehrer/backend/middleware/drohnenfuehrerAuth.js +++ b/drohnenfuehrer/backend/middleware/drohnenfuehrerAuth.js @@ -14,6 +14,11 @@ const authenticateDrohnenfuehrer = (req, res, next) => { if (decoded.role !== 'drohnenfuehrer') { return res.status(403).json({ success: false, message: 'Zugriff verweigert' }); } + // Token einer anderen App ablehnen. Greift auch dann, wenn versehentlich + // wieder ein gemeinsames JWT-Secret konfiguriert wird. + if (decoded.app && decoded.app !== config.appName) { + return res.status(403).json({ success: false, message: 'Zugriff verweigert' }); + } req.drohnenfuehrerUser = decoded; next(); } catch (error) { diff --git a/drohnenfuehrer/docker-compose.yml b/drohnenfuehrer/docker-compose.yml index 8a23240..c80569c 100644 --- a/drohnenfuehrer/docker-compose.yml +++ b/drohnenfuehrer/docker-compose.yml @@ -32,7 +32,9 @@ services: environment: - NODE_ENV=production - MONGO_URI=mongodb://drohnenfuehrer:${MONGO_PASSWORD}@mongo:27017/drohnenfuehrer?authSource=admin - - JWT_SECRET=${DROHNENFUEHRER_JWT_SECRET} + # :? statt stiller Leerersetzung — sonst startet das Backend mit + # leerem Secret und beendet sich sofort wieder (Crash-Loop). + - JWT_SECRET=${DROHNENFUEHRER_JWT_SECRET:?DROHNENFUEHRER_JWT_SECRET muss in .env gesetzt sein} - JWT_EXPIRES_IN=24h - CORS_ORIGIN=${CORS_ORIGIN:-http://localhost:8081} - ADMIN_THORSTEN_PASSWORD=${ADMIN_THORSTEN_PASSWORD} @@ -46,7 +48,9 @@ services: # - SMTP_PASS=${SMTP_PASS} # - SMTP_FROM=drohnenfuehrer@example.com # Basis fuer Links in Passwort-Reset-Mails. MUSS den Unterpfad enthalten. - - APP_URL=${APP_URL:-http://localhost:8081/drohnenfuehrer} + # Leer lassen, wenn nicht konfiguriert: config/env.js baut den Wert + # dann aus CORS_ORIGIN + Unterpfad und warnt sichtbar darueber. + - APP_URL=${APP_URL:-} depends_on: mongo: condition: service_healthy diff --git a/nachsuche/backend/.env.example b/nachsuche/backend/.env.example index eecf41e..65b5483 100644 --- a/nachsuche/backend/.env.example +++ b/nachsuche/backend/.env.example @@ -6,13 +6,17 @@ NODE_ENV=development MONGO_URI=mongodb://127.0.0.1:27017/tracking-leaders # JWT Configuration -JWT_SECRET=your-super-secret-jwt-key-change-this-in-production +# Der Name MUSS zu docker-compose.yml passen. Je App ein EIGENES Secret: +# openssl rand -hex 32 +NACHSUCHE_JWT_SECRET= JWT_EXPIRES_IN=24h # Admin Initial Password (used by seed.js if admin doesn't exist) # ADMIN_INITIAL_PASSWORD=secure-password-here # CORS Configuration (comma-separated for multiple origins) +# Produktiv die echte oeffentliche Herkunft eintragen, nicht localhost — +# APP_URL wird daraus abgeleitet, wenn es nicht gesetzt ist. CORS_ORIGIN=http://localhost:3000 # Geocoding Configuration (OpenStreetMap Nominatim) @@ -23,7 +27,9 @@ GEOCODE_MIN_DELAY_MS=1100 # Basis-URL der App fuer Links in E-Mails (Passwort-Reset). # MUSS den Unterpfad enthalten, unter dem die App ausgeliefert wird. # Ohne diesen Wert wird er aus CORS_ORIGIN + "/nachsuche" zusammengesetzt. -APP_URL=http://localhost:8080/nachsuche +# Produktiv die echte oeffentliche URL inkl. Unterpfad eintragen. +# Leer lassen -> wird aus CORS_ORIGIN + "/nachsuche" gebildet (mit Warnung). +APP_URL= # SMTP fuer Passwort-Reset-Mails (optional). # Fehlt die Konfiguration, wird der Reset-Link nur ins Log geschrieben. diff --git a/nachsuche/backend/controllers/handlerController.js b/nachsuche/backend/controllers/handlerController.js index e619540..e5f78f5 100644 --- a/nachsuche/backend/controllers/handlerController.js +++ b/nachsuche/backend/controllers/handlerController.js @@ -25,7 +25,7 @@ const handlerLogin = async (req, res) => { } const token = jwt.sign( - { id: user._id.toString(), role: 'handler' }, + { id: user._id.toString(), role: 'handler', app: config.appName }, config.jwtSecret, { expiresIn: '12h' } ); diff --git a/nachsuche/backend/middleware/handlerAuth.js b/nachsuche/backend/middleware/handlerAuth.js index 350d118..ca6ad5b 100644 --- a/nachsuche/backend/middleware/handlerAuth.js +++ b/nachsuche/backend/middleware/handlerAuth.js @@ -14,6 +14,11 @@ const authenticateHandler = (req, res, next) => { if (decoded.role !== 'handler') { return res.status(403).json({ success: false, message: 'Zugriff verweigert' }); } + // Token einer anderen App ablehnen. Greift auch dann, wenn versehentlich + // wieder ein gemeinsames JWT-Secret konfiguriert wird. + if (decoded.app && decoded.app !== config.appName) { + return res.status(403).json({ success: false, message: 'Zugriff verweigert' }); + } req.handlerUser = decoded; next(); } catch (error) { diff --git a/nachsuche/docker-compose.yml b/nachsuche/docker-compose.yml index 8bf12bb..5314b72 100644 --- a/nachsuche/docker-compose.yml +++ b/nachsuche/docker-compose.yml @@ -34,7 +34,9 @@ services: environment: - NODE_ENV=production - MONGO_URI=mongodb://nachsuche:${MONGO_PASSWORD}@mongo:27017/nachsuche?authSource=admin - - JWT_SECRET=${NACHSUCHE_JWT_SECRET} + # :? statt stiller Leerersetzung — sonst startet das Backend mit + # leerem Secret und beendet sich sofort wieder (Crash-Loop). + - JWT_SECRET=${NACHSUCHE_JWT_SECRET:?NACHSUCHE_JWT_SECRET muss in .env gesetzt sein} - JWT_EXPIRES_IN=24h - CORS_ORIGIN=${CORS_ORIGIN:-http://localhost:8080} - ADMIN_THORSTEN_PASSWORD=${ADMIN_THORSTEN_PASSWORD} @@ -48,7 +50,9 @@ services: # - SMTP_PASS=${SMTP_PASS} # - SMTP_FROM=nachsuche@example.com # Basis fuer Links in Passwort-Reset-Mails. MUSS den Unterpfad enthalten. - - APP_URL=${APP_URL:-http://localhost:8080/nachsuche} + # Leer lassen, wenn nicht konfiguriert: config/env.js baut den Wert + # dann aus CORS_ORIGIN + Unterpfad und warnt sichtbar darueber. + - APP_URL=${APP_URL:-} depends_on: mongo: condition: service_healthy diff --git a/stoeberhunde/backend/.env.example b/stoeberhunde/backend/.env.example index 69c5b33..0632f51 100644 --- a/stoeberhunde/backend/.env.example +++ b/stoeberhunde/backend/.env.example @@ -6,13 +6,17 @@ NODE_ENV=development MONGO_URI=mongodb://127.0.0.1:27017/stoeberhunde # JWT Configuration -JWT_SECRET=your-super-secret-jwt-key-change-this-in-production +# Der Name MUSS zu docker-compose.yml passen. Je App ein EIGENES Secret: +# openssl rand -hex 32 +STOEBERHUNDE_JWT_SECRET= JWT_EXPIRES_IN=24h # Admin Initial Password (used by seed.js if admin doesn't exist) # ADMIN_INITIAL_PASSWORD=secure-password-here # CORS Configuration (comma-separated for multiple origins) +# Produktiv die echte oeffentliche Herkunft eintragen, nicht localhost — +# APP_URL wird daraus abgeleitet, wenn es nicht gesetzt ist. CORS_ORIGIN=http://localhost:3000 # Geocoding Configuration (OpenStreetMap Nominatim) @@ -23,7 +27,9 @@ GEOCODE_MIN_DELAY_MS=1100 # Basis-URL der App fuer Links in E-Mails (Passwort-Reset). # MUSS den Unterpfad enthalten, unter dem die App ausgeliefert wird. # Ohne diesen Wert wird er aus CORS_ORIGIN + "/stoeberhunde" zusammengesetzt. -APP_URL=http://localhost:8082/stoeberhunde +# Produktiv die echte oeffentliche URL inkl. Unterpfad eintragen. +# Leer lassen -> wird aus CORS_ORIGIN + "/stoeberhunde" gebildet (mit Warnung). +APP_URL= # SMTP fuer Passwort-Reset-Mails (optional). # Fehlt die Konfiguration, wird der Reset-Link nur ins Log geschrieben. diff --git a/stoeberhunde/backend/controllers/stoeberhundefuehrerController.js b/stoeberhunde/backend/controllers/stoeberhundefuehrerController.js index f7a62ff..ffc5ee8 100644 --- a/stoeberhunde/backend/controllers/stoeberhundefuehrerController.js +++ b/stoeberhunde/backend/controllers/stoeberhundefuehrerController.js @@ -25,7 +25,7 @@ const stoeberhundefuehrerLogin = async (req, res) => { } const token = jwt.sign( - { id: user._id.toString(), role: 'stoeberhundefuehrer' }, + { id: user._id.toString(), role: 'stoeberhundefuehrer', app: config.appName }, config.jwtSecret, { expiresIn: '12h' } ); diff --git a/stoeberhunde/backend/middleware/stoeberhundefuehrerAuth.js b/stoeberhunde/backend/middleware/stoeberhundefuehrerAuth.js index 8efb5cb..ee11a8b 100644 --- a/stoeberhunde/backend/middleware/stoeberhundefuehrerAuth.js +++ b/stoeberhunde/backend/middleware/stoeberhundefuehrerAuth.js @@ -14,6 +14,11 @@ const authenticateStoeberhundefuehrer = (req, res, next) => { if (decoded.role !== 'stoeberhundefuehrer') { return res.status(403).json({ success: false, message: 'Zugriff verweigert' }); } + // Token einer anderen App ablehnen. Greift auch dann, wenn versehentlich + // wieder ein gemeinsames JWT-Secret konfiguriert wird. + if (decoded.app && decoded.app !== config.appName) { + return res.status(403).json({ success: false, message: 'Zugriff verweigert' }); + } req.stoeberhundefuehrerUser = decoded; next(); } catch (error) { diff --git a/stoeberhunde/docker-compose.yml b/stoeberhunde/docker-compose.yml index 0480fa0..f24a592 100644 --- a/stoeberhunde/docker-compose.yml +++ b/stoeberhunde/docker-compose.yml @@ -32,7 +32,9 @@ services: environment: - NODE_ENV=production - MONGO_URI=mongodb://stoeberhunde:${MONGO_PASSWORD}@mongo:27017/stoeberhunde?authSource=admin - - JWT_SECRET=${STOEBERHUNDE_JWT_SECRET} + # :? statt stiller Leerersetzung — sonst startet das Backend mit + # leerem Secret und beendet sich sofort wieder (Crash-Loop). + - JWT_SECRET=${STOEBERHUNDE_JWT_SECRET:?STOEBERHUNDE_JWT_SECRET muss in .env gesetzt sein} - JWT_EXPIRES_IN=24h - CORS_ORIGIN=${CORS_ORIGIN:-http://localhost:8082} - ADMIN_THORSTEN_PASSWORD=${ADMIN_THORSTEN_PASSWORD} @@ -46,7 +48,9 @@ services: # - SMTP_PASS=${SMTP_PASS} # - SMTP_FROM=stoeberhunde@example.com # Basis fuer Links in Passwort-Reset-Mails. MUSS den Unterpfad enthalten. - - APP_URL=${APP_URL:-http://localhost:8082/stoeberhunde} + # Leer lassen, wenn nicht konfiguriert: config/env.js baut den Wert + # dann aus CORS_ORIGIN + Unterpfad und warnt sichtbar darueber. + - APP_URL=${APP_URL:-} depends_on: mongo: condition: service_healthy