fix: enforce session cookie behavior in prod

This commit is contained in:
Matthias Lotz 2025-11-24 20:00:52 +01:00
parent 7a14c239d4
commit b912670cab
10 changed files with 126 additions and 9 deletions

View File

@ -24,6 +24,7 @@ Die API verwendet **zwei verschiedene Authentifizierungs-Mechanismen** für unte
```env ```env
ADMIN_SESSION_SECRET=$(openssl rand -hex 32) ADMIN_SESSION_SECRET=$(openssl rand -hex 32)
``` ```
> Standardmäßig setzt der Server in Production HTTPS-Only Cookies (`Secure`). Falls deine Installation **ohne HTTPS** hinter einem internen Netzwerk läuft, kannst du das Verhalten über `ADMIN_SESSION_COOKIE_SECURE=false` explizit deaktivieren. Verwende dies nur in vertrauenswürdigen Umgebungen!
2. **Backend starten** Migration legt Tabelle `admin_users` an. 2. **Backend starten** Migration legt Tabelle `admin_users` an.
3. **Setup-Status prüfen**: 3. **Setup-Status prüfen**:
```bash ```bash
@ -189,7 +190,7 @@ npm test
- [ ] `ADMIN_SESSION_SECRET` sicher generieren (>= 32 Bytes random) - [ ] `ADMIN_SESSION_SECRET` sicher generieren (>= 32 Bytes random)
- [ ] `.env` nicht in Git committen (bereits in `.gitignore`) - [ ] `.env` nicht in Git committen (bereits in `.gitignore`)
- [ ] HTTPS verwenden (TLS/SSL) damit Cookies `Secure` gesetzt werden können - [ ] HTTPS verwenden (TLS/SSL) damit Cookies `Secure` gesetzt werden können (falls nicht möglich: `ADMIN_SESSION_COOKIE_SECURE=false` setzen nur in vertrauenswürdigen Netzen)
- [ ] Session-Store auf persistentem Volume ablegen - [ ] Session-Store auf persistentem Volume ablegen
- [ ] Rate Limiting & Audit Logs überwachen - [ ] Rate Limiting & Audit Logs überwachen
- [ ] Admin-Benutzerverwaltung (On-/Offboarding) dokumentieren - [ ] Admin-Benutzerverwaltung (On-/Offboarding) dokumentieren

View File

@ -33,7 +33,7 @@ docker compose -f docker/dev/docker-compose.yml up -d
### Zugriff ### Zugriff
- **Frontend**: http://localhost:3000 (Hot Module Reloading aktiv) - **Frontend**: http://localhost:3000 (Hot Module Reloading aktiv)
- **Backend**: http://localhost:5001 (API) - **Backend**: http://localhost:5001 (API)
- **API Documentation**: http://localhost:5001/api/docs/ (Swagger UI) - **API Documentation**: http://localhost:5001/api/docs/ (Swagger UI, nur in Development verfügbar)
- **Slideshow**: http://localhost:3000/slideshow - **Slideshow**: http://localhost:3000/slideshow
- **Moderation**: http://localhost:3000/moderation (Login über Admin Session) - **Moderation**: http://localhost:3000/moderation (Login über Admin Session)
@ -430,6 +430,17 @@ git commit -m "feat: Add new feature"
git push origin feature/my-feature git push origin feature/my-feature
``` ```
### Git Hook (optional Absicherung)
Für Deployments ohne HTTPS muss `docker/prod/docker-compose.yml` die Zeile `- ADMIN_SESSION_COOKIE_SECURE=false` enthalten.
Ein vorgefertigtes Pre-Commit-Hook stellt sicher, dass diese Zeile vorhanden ist bzw. automatisch korrigiert wird:
```bash
ln -s ../../scripts/git-hooks/pre-commit .git/hooks/pre-commit
```
Nach der Installation aktualisiert der Hook die Datei bei Bedarf und staged sie direkt. Entfernen kannst du ihn jederzeit über `rm .git/hooks/pre-commit`.
## Nützliche Befehle ## Nützliche Befehle
```bash ```bash

View File

@ -584,7 +584,7 @@ For detailed testing instructions, see: [`tests/TESTING-CLEANUP.md`](tests/TESTI
| Variable | Default | Description | | Variable | Default | Description |
|----------|---------|-------------| |----------|---------|-------------|
| `API_URL` | `http://localhost:5000` | Backend API endpoint | | `API_URL` | `http://localhost:5001` | Backend API endpoint |
| `CLIENT_URL` | `http://localhost` | Frontend application URL | | `CLIENT_URL` | `http://localhost` | Frontend application URL |
### Volume Configuration ### Volume Configuration

View File

@ -7,8 +7,8 @@
}, },
"servers": [ "servers": [
{ {
"url": "http://localhost:5000", "url": "http://localhost:5001",
"description": "Development server" "description": "Development server (dev compose backend)"
} }
], ],
"tags": [ "tags": [

View File

@ -19,7 +19,7 @@ const doc = {
version: '1.0.0', version: '1.0.0',
description: 'Auto-generated OpenAPI spec with correct mount prefixes' description: 'Auto-generated OpenAPI spec with correct mount prefixes'
}, },
host: 'localhost:5000', host: 'localhost:5001',
schemes: ['http'], schemes: ['http'],
// Add base path hints per router (swagger-autogen doesn't natively support per-file prefixes, // Add base path hints per router (swagger-autogen doesn't natively support per-file prefixes,
// so we'll post-process or use @swagger annotations in route files) // so we'll post-process or use @swagger annotations in route files)
@ -71,7 +71,7 @@ async function generateWithPrefixes() {
openapi: '3.0.0', openapi: '3.0.0',
info: doc.info, info: doc.info,
servers: [ servers: [
{ url: 'http://localhost:5000', description: 'Development server' } { url: 'http://localhost:5001', description: 'Development server (dev compose backend)' }
], ],
tags: Array.from(allTags).map(name => ({ name })), tags: Array.from(allTags).map(name => ({ name })),
paths: allPaths paths: allPaths

View File

@ -9,6 +9,35 @@ const SESSION_DIR = process.env.ADMIN_SESSION_DIR
: path.join(__dirname, '..', 'data'); : path.join(__dirname, '..', 'data');
const SESSION_SECRET = process.env.ADMIN_SESSION_SECRET; const SESSION_SECRET = process.env.ADMIN_SESSION_SECRET;
const IS_PRODUCTION = process.env.NODE_ENV === 'production'; const IS_PRODUCTION = process.env.NODE_ENV === 'production';
const ADMIN_SESSION_COOKIE_SECURE = process.env.ADMIN_SESSION_COOKIE_SECURE;
const parseBooleanEnv = (value) => {
if (typeof value !== 'string') {
return undefined;
}
switch (value.toLowerCase().trim()) {
case 'true':
case '1':
case 'yes':
case 'on':
return true;
case 'false':
case '0':
case 'no':
case 'off':
return false;
default:
return undefined;
}
};
const secureOverride = parseBooleanEnv(ADMIN_SESSION_COOKIE_SECURE);
const cookieSecure = secureOverride ?? IS_PRODUCTION;
if (IS_PRODUCTION && secureOverride === false) {
console.warn('[Session] ADMIN_SESSION_COOKIE_SECURE=false detected secure cookies disabled in production. Only do this on trusted HTTP deployments.');
}
if (!SESSION_SECRET) { if (!SESSION_SECRET) {
throw new Error('ADMIN_SESSION_SECRET is required for session management'); throw new Error('ADMIN_SESSION_SECRET is required for session management');
@ -33,7 +62,7 @@ const sessionMiddleware = session({
saveUninitialized: false, saveUninitialized: false,
cookie: { cookie: {
httpOnly: true, httpOnly: true,
secure: IS_PRODUCTION, secure: cookieSecure,
sameSite: 'strict', sameSite: 'strict',
maxAge: 8 * 60 * 60 * 1000 // 8 hours maxAge: 8 * 60 * 60 * 1000 // 8 hours
} }

View File

@ -4,7 +4,6 @@ const path = require('path');
const initiateResources = require('./utils/initiate-resources'); const initiateResources = require('./utils/initiate-resources');
const dbManager = require('./database/DatabaseManager'); const dbManager = require('./database/DatabaseManager');
const SchedulerService = require('./services/SchedulerService'); const SchedulerService = require('./services/SchedulerService');
const generateOpenApi = require('./generate-openapi');
// Dev: Swagger UI (mount only in non-production) — require lazily // Dev: Swagger UI (mount only in non-production) — require lazily
let swaggerUi = null; let swaggerUi = null;
@ -29,6 +28,7 @@ class Server {
} }
try { try {
const generateOpenApi = require('./generate-openapi');
console.log('🔄 Generating OpenAPI specification...'); console.log('🔄 Generating OpenAPI specification...');
await generateOpenApi(); await generateOpenApi();
console.log('✓ OpenAPI spec generated'); console.log('✓ OpenAPI spec generated');

View File

@ -38,6 +38,8 @@ services:
- NODE_ENV=production - NODE_ENV=production
- ADMIN_SESSION_SECRET=MvFhivVIPIXvSGvWGfGOiQCkUJrmUsjWQTNGUgnSmtpsGHQlKruTBEBZgbVvOHHr - ADMIN_SESSION_SECRET=MvFhivVIPIXvSGvWGfGOiQCkUJrmUsjWQTNGUgnSmtpsGHQlKruTBEBZgbVvOHHr
- ADMIN_SESSION_DIR=/usr/src/app/src/data/sessions - ADMIN_SESSION_DIR=/usr/src/app/src/data/sessions
- ADMIN_SESSION_COOKIE_SECURE=false
networks: networks:
npm-nw: npm-nw:

View File

@ -51,6 +51,15 @@ http {
client_max_body_size 100M; client_max_body_size 100M;
} }
# Admin auth/session endpoints (login/logout/setup/csrf)
location /auth/ {
proxy_pass http://image-uploader-backend:5000;
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;
}
# API - Groups API routes (NO PASSWORD PROTECTION) # API - Groups API routes (NO PASSWORD PROTECTION)
location ~ ^/groups/[a-zA-Z0-9_-]+(/.*)?$ { location ~ ^/groups/[a-zA-Z0-9_-]+(/.*)?$ {
proxy_pass http://image-uploader-backend:5000; proxy_pass http://image-uploader-backend:5000;

65
scripts/git-hooks/pre-commit Executable file
View File

@ -0,0 +1,65 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
TARGET_FILE="$ROOT_DIR/docker/prod/docker-compose.yml"
ANCHOR_LINE=" - ADMIN_SESSION_DIR=/usr/src/app/src/data/sessions"
EXPECTED_LINE=" - ADMIN_SESSION_COOKIE_SECURE=false"
if [[ ! -f "$TARGET_FILE" ]]; then
exit 0
fi
export TARGET_FILE
export ANCHOR_LINE
export EXPECTED_LINE
result=$(python3 <<'PY'
import os
import pathlib
import re
import sys
path = pathlib.Path(os.environ['TARGET_FILE'])
anchor = os.environ['ANCHOR_LINE']
expected = os.environ['EXPECTED_LINE']
text = path.read_text()
changed = False
if 'ADMIN_SESSION_COOKIE_SECURE' in text:
pattern = re.compile(r'(\-\s*ADMIN_SESSION_COOKIE_SECURE\s*=\s*)([^\n\r]+)')
new_text, count = pattern.subn(r'\1false', text, count=1)
if count:
changed = new_text != text
else:
if anchor not in text:
print('ERROR: Anchor line not found for ADMIN_SESSION_COOKIE_SECURE insertion', file=sys.stderr)
sys.exit(2)
new_text = text.replace(anchor, anchor + '\n' + expected, 1)
changed = True
if expected not in new_text:
print('ERROR: Failed to ensure ADMIN_SESSION_COOKIE_SECURE=false in docker-compose.yml', file=sys.stderr)
sys.exit(3)
if changed:
path.write_text(new_text)
print('UPDATED')
else:
print('UNCHANGED')
PY
)
status=$?
if [[ $status -ne 0 ]]; then
echo "$result"
echo "[pre-commit] Failed to normalize ADMIN_SESSION_COOKIE_SECURE" >&2
exit $status
fi
if [[ $result == "UPDATED" ]]; then
echo "[pre-commit] Normalized ADMIN_SESSION_COOKIE_SECURE in docker/prod/docker-compose.yml"
git -C "$ROOT_DIR" add "$TARGET_FILE"
fi
exit 0