fix: enforce session cookie behavior in prod
This commit is contained in:
parent
7a14c239d4
commit
b912670cab
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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": [
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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
65
scripts/git-hooks/pre-commit
Executable 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
|
||||||
Loading…
Reference in New Issue
Block a user