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
|
||||
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.
|
||||
3. **Setup-Status prüfen**:
|
||||
```bash
|
||||
|
|
@ -189,7 +190,7 @@ npm test
|
|||
|
||||
- [ ] `ADMIN_SESSION_SECRET` sicher generieren (>= 32 Bytes random)
|
||||
- [ ] `.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
|
||||
- [ ] Rate Limiting & Audit Logs überwachen
|
||||
- [ ] Admin-Benutzerverwaltung (On-/Offboarding) dokumentieren
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ docker compose -f docker/dev/docker-compose.yml up -d
|
|||
### Zugriff
|
||||
- **Frontend**: http://localhost:3000 (Hot Module Reloading aktiv)
|
||||
- **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
|
||||
- **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 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
|
||||
|
||||
```bash
|
||||
|
|
|
|||
|
|
@ -584,7 +584,7 @@ For detailed testing instructions, see: [`tests/TESTING-CLEANUP.md`](tests/TESTI
|
|||
|
||||
| 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 |
|
||||
|
||||
### Volume Configuration
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@
|
|||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "http://localhost:5000",
|
||||
"description": "Development server"
|
||||
"url": "http://localhost:5001",
|
||||
"description": "Development server (dev compose backend)"
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ const doc = {
|
|||
version: '1.0.0',
|
||||
description: 'Auto-generated OpenAPI spec with correct mount prefixes'
|
||||
},
|
||||
host: 'localhost:5000',
|
||||
host: 'localhost:5001',
|
||||
schemes: ['http'],
|
||||
// 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)
|
||||
|
|
@ -71,7 +71,7 @@ async function generateWithPrefixes() {
|
|||
openapi: '3.0.0',
|
||||
info: doc.info,
|
||||
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 })),
|
||||
paths: allPaths
|
||||
|
|
|
|||
|
|
@ -9,6 +9,35 @@ const SESSION_DIR = process.env.ADMIN_SESSION_DIR
|
|||
: path.join(__dirname, '..', 'data');
|
||||
const SESSION_SECRET = process.env.ADMIN_SESSION_SECRET;
|
||||
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) {
|
||||
throw new Error('ADMIN_SESSION_SECRET is required for session management');
|
||||
|
|
@ -33,7 +62,7 @@ const sessionMiddleware = session({
|
|||
saveUninitialized: false,
|
||||
cookie: {
|
||||
httpOnly: true,
|
||||
secure: IS_PRODUCTION,
|
||||
secure: cookieSecure,
|
||||
sameSite: 'strict',
|
||||
maxAge: 8 * 60 * 60 * 1000 // 8 hours
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ const path = require('path');
|
|||
const initiateResources = require('./utils/initiate-resources');
|
||||
const dbManager = require('./database/DatabaseManager');
|
||||
const SchedulerService = require('./services/SchedulerService');
|
||||
const generateOpenApi = require('./generate-openapi');
|
||||
|
||||
// Dev: Swagger UI (mount only in non-production) — require lazily
|
||||
let swaggerUi = null;
|
||||
|
|
@ -29,6 +28,7 @@ class Server {
|
|||
}
|
||||
|
||||
try {
|
||||
const generateOpenApi = require('./generate-openapi');
|
||||
console.log('🔄 Generating OpenAPI specification...');
|
||||
await generateOpenApi();
|
||||
console.log('✓ OpenAPI spec generated');
|
||||
|
|
|
|||
|
|
@ -38,6 +38,8 @@ services:
|
|||
- NODE_ENV=production
|
||||
- ADMIN_SESSION_SECRET=MvFhivVIPIXvSGvWGfGOiQCkUJrmUsjWQTNGUgnSmtpsGHQlKruTBEBZgbVvOHHr
|
||||
- ADMIN_SESSION_DIR=/usr/src/app/src/data/sessions
|
||||
- ADMIN_SESSION_COOKIE_SECURE=false
|
||||
|
||||
|
||||
networks:
|
||||
npm-nw:
|
||||
|
|
|
|||
|
|
@ -50,6 +50,15 @@ http {
|
|||
# Allow large uploads for batch upload endpoints
|
||||
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)
|
||||
location ~ ^/groups/[a-zA-Z0-9_-]+(/.*)?$ {
|
||||
|
|
|
|||
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