Noch ein paar Hardening-Sachen
This commit is contained in:
10
.env.example
10
.env.example
@@ -7,10 +7,18 @@ ACME_EMAIL=admin@example.com
|
|||||||
# --- Database ---
|
# --- Database ---
|
||||||
MYSQL_PASSWORD=change_this_password
|
MYSQL_PASSWORD=change_this_password
|
||||||
MYSQL_ROOT_PASSWORD=change_this_root_password
|
MYSQL_ROOT_PASSWORD=change_this_root_password
|
||||||
|
MYSQL_DATABASE=rezepte
|
||||||
|
MYSQL_USER=rezepte_user
|
||||||
|
MYSQL_PORT=3307
|
||||||
|
|
||||||
# --- Auth / Security ---
|
# --- Auth / Security ---
|
||||||
JWT_SECRET=please_change_to_secure_32_char_min
|
JWT_SECRET=please_change_to_secure_32_char_min
|
||||||
CORS_ORIGIN=https://rezepte.${DOMAIN}
|
CORS_ORIGIN=https://rezepte.${DOMAIN}
|
||||||
|
ALLOW_INSECURE_CORS=0
|
||||||
|
BACKEND_PORT=3001
|
||||||
|
FRONTEND_PORT=3000
|
||||||
|
MAX_FILE_SIZE=5242880
|
||||||
|
UPLOAD_PATH=/app/uploads
|
||||||
|
|
||||||
# --- Images from Registry (override if pushing to GHCR or custom registry) ---
|
# --- Images from Registry (override if pushing to GHCR or custom registry) ---
|
||||||
BACKEND_IMAGE=docker.citysensor.de/rezepte-backend:latest
|
BACKEND_IMAGE=docker.citysensor.de/rezepte-backend:latest
|
||||||
@@ -18,6 +26,8 @@ FRONTEND_IMAGE=docker.citysensor.de/rezepte-frontend:latest
|
|||||||
|
|
||||||
# --- Frontend API URL (if building locally outside compose) ---
|
# --- Frontend API URL (if building locally outside compose) ---
|
||||||
PRODUCTION_API_URL=https://rezepte.${DOMAIN}/api
|
PRODUCTION_API_URL=https://rezepte.${DOMAIN}/api
|
||||||
|
DATABASE_URL=mysql://${MYSQL_USER}:${MYSQL_PASSWORD}@mysql:3306/${MYSQL_DATABASE}
|
||||||
|
VITE_API_URL=https://rezepte.${DOMAIN}/api
|
||||||
|
|
||||||
# --- Optional HOST_IP for local network override builds ---
|
# --- Optional HOST_IP for local network override builds ---
|
||||||
HOST_IP=192.168.1.100
|
HOST_IP=192.168.1.100
|
||||||
|
|||||||
33
README.md
33
README.md
@@ -9,7 +9,6 @@ Dieses Repository enthält zwei Welten: den modernen Node.js / React Stack sowie
|
|||||||
| mysql | default | 3307 -> 3306 | MySQL 8 Datenbank |
|
| mysql | default | 3307 -> 3306 | MySQL 8 Datenbank |
|
||||||
| backend | default | 3001 -> 3001 | Node.js API (Express + Prisma) |
|
| backend | default | 3001 -> 3001 | Node.js API (Express + Prisma) |
|
||||||
| frontend | default | 3000 -> 80 | React Build via nginx |
|
| frontend | default | 3000 -> 80 | React Build via nginx |
|
||||||
| php-app | legacy | 8082 -> 80 | Altes PHP Frontend |
|
|
||||||
| phpmyadmin | admin | 8083 -> 80 | DB Verwaltung |
|
| phpmyadmin | admin | 8083 -> 80 | DB Verwaltung |
|
||||||
|
|
||||||
## Node / Modern Stack
|
## Node / Modern Stack
|
||||||
@@ -30,23 +29,12 @@ docker compose up -d
|
|||||||
```
|
```
|
||||||
Öffnen: http://localhost:3000
|
Öffnen: http://localhost:3000
|
||||||
|
|
||||||
### Mit Legacy PHP zusätzlich
|
### Mit phpMyAdmin
|
||||||
```bash
|
|
||||||
docker compose --profile legacy up -d
|
|
||||||
```
|
|
||||||
Öffnen: http://localhost:8082
|
|
||||||
|
|
||||||
### Mit phpMyAdmin zusätzlich
|
|
||||||
```bash
|
```bash
|
||||||
docker compose --profile admin up -d
|
docker compose --profile admin up -d
|
||||||
```
|
```
|
||||||
Öffnen: http://localhost:8083
|
Öffnen: http://localhost:8083
|
||||||
|
|
||||||
### Beide Zusatz-Profile
|
|
||||||
```bash
|
|
||||||
docker compose --profile legacy --profile admin up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
### Alles stoppen
|
### Alles stoppen
|
||||||
```bash
|
```bash
|
||||||
docker compose down
|
docker compose down
|
||||||
@@ -79,6 +67,25 @@ Danach Backend neu bauen.
|
|||||||
|
|
||||||
## Datenbankzugang
|
## Datenbankzugang
|
||||||
|
|
||||||
|
## Environment (.env)
|
||||||
|
Beispiel siehe `.env.example`. Wichtigste Variablen:
|
||||||
|
```
|
||||||
|
DOMAIN=example.com
|
||||||
|
MYSQL_DATABASE=rezepte
|
||||||
|
MYSQL_USER=rezepte_user
|
||||||
|
MYSQL_PASSWORD=change_this_password
|
||||||
|
MYSQL_ROOT_PASSWORD=change_this_root_password
|
||||||
|
BACKEND_PORT=3001
|
||||||
|
FRONTEND_PORT=3000
|
||||||
|
DATABASE_URL=mysql://${MYSQL_USER}:${MYSQL_PASSWORD}@mysql:3306/${MYSQL_DATABASE}
|
||||||
|
JWT_SECRET=please_change_to_secure_32_char_min
|
||||||
|
CORS_ORIGIN=http://localhost:3000
|
||||||
|
VITE_API_URL=http://localhost:3001/api
|
||||||
|
UPLOAD_PATH=/app/uploads
|
||||||
|
MAX_FILE_SIZE=5242880
|
||||||
|
```
|
||||||
|
Nicht gesetzte Variablen fallen auf sichere Defaults (oder Dummy Werte) zurück; vor Produktion immer mit echten Secrets überschreiben.
|
||||||
|
|
||||||
## Datenbankzugang
|
## Datenbankzugang
|
||||||
|
|
||||||
### Für die Anwendung:
|
### Für die Anwendung:
|
||||||
|
|||||||
@@ -30,28 +30,44 @@ const limiter = rateLimit({
|
|||||||
});
|
});
|
||||||
app.use(limiter);
|
app.use(limiter);
|
||||||
|
|
||||||
// CORS configuration - support comma separated origins in CORS_ORIGIN
|
// CORS Hardening
|
||||||
// NOTE: In docker-compose we temporarily set CORS_ORIGIN="*" for troubleshooting.
|
// Supports comma separated origins. Wildcard '*' only allowed if ALLOW_INSECURE_CORS=1 and not in production.
|
||||||
// Narrow this down for production: e.g. CORS_ORIGIN="http://esprimo:3000,http://localhost:3000".
|
const insecureOverride = process.env.ALLOW_INSECURE_CORS === '1';
|
||||||
|
const isProd = process.env.NODE_ENV === 'production';
|
||||||
|
|
||||||
let allowedOrigins: string[] = [];
|
let allowedOrigins: string[] = [];
|
||||||
if (config.cors.origin === '*') {
|
if (config.cors.origin.includes(',')) {
|
||||||
allowedOrigins = ['*'];
|
|
||||||
} else if (config.cors.origin.includes(',')) {
|
|
||||||
allowedOrigins = config.cors.origin.split(',').map(o => o.trim()).filter(Boolean);
|
allowedOrigins = config.cors.origin.split(',').map(o => o.trim()).filter(Boolean);
|
||||||
|
} else if (config.cors.origin === '*' && (!isProd || insecureOverride)) {
|
||||||
|
allowedOrigins = ['*'];
|
||||||
} else {
|
} else {
|
||||||
allowedOrigins = [config.cors.origin];
|
allowedOrigins = [config.cors.origin];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always add defaults if not already present
|
// De-dupe & normalize trailing slashes
|
||||||
|
allowedOrigins = Array.from(new Set(allowedOrigins.map(o => o.replace(/\/$/, ''))));
|
||||||
|
|
||||||
|
// Auto-add common localhost dev origins if not prod and not wildcard
|
||||||
|
if (!isProd && !allowedOrigins.includes('*')) {
|
||||||
['http://localhost:5173','http://localhost:3000'].forEach(def => {
|
['http://localhost:5173','http://localhost:3000'].forEach(def => {
|
||||||
if (!allowedOrigins.includes(def) && !allowedOrigins.includes('*')) allowedOrigins.push(def);
|
if (!allowedOrigins.includes(def)) allowedOrigins.push(def);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// If in production and wildcard attempted without override, remove it
|
||||||
|
if (isProd && allowedOrigins.includes('*') && !insecureOverride) {
|
||||||
|
console.warn('[CORS] Wildcard removed in production. Set CORS_ORIGIN explicitly or ALLOW_INSECURE_CORS=1 (NOT RECOMMENDED).');
|
||||||
|
allowedOrigins = allowedOrigins.filter(o => o !== '*');
|
||||||
|
}
|
||||||
|
|
||||||
app.use(cors({
|
app.use(cors({
|
||||||
origin: (origin, callback) => {
|
origin: (origin, callback) => {
|
||||||
if (!origin) return callback(null, true); // non-browser (curl, server-side)
|
if (!origin) return callback(null, true); // Non-browser / same-origin
|
||||||
if (allowedOrigins.includes('*') || allowedOrigins.includes(origin)) return callback(null, true);
|
if (allowedOrigins.includes('*') || allowedOrigins.includes(origin.replace(/\/$/, ''))) {
|
||||||
return callback(new Error(`CORS blocked for origin ${origin}`));
|
return callback(null, true);
|
||||||
|
}
|
||||||
|
console.warn(`[CORS] Blocked origin: ${origin}`);
|
||||||
|
return callback(new Error('CORS not allowed for this origin'));
|
||||||
},
|
},
|
||||||
credentials: true,
|
credentials: true,
|
||||||
}));
|
}));
|
||||||
@@ -60,10 +76,11 @@ app.use(cors({
|
|||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
const origin = req.headers.origin;
|
const origin = req.headers.origin;
|
||||||
|
|
||||||
|
const normalized = origin?.replace(/\/$/, '');
|
||||||
if (allowedOrigins.includes('*')) {
|
if (allowedOrigins.includes('*')) {
|
||||||
res.header('Access-Control-Allow-Origin', origin || '*');
|
res.header('Access-Control-Allow-Origin', origin || '*');
|
||||||
} else if (origin && allowedOrigins.includes(origin)) {
|
} else if (normalized && allowedOrigins.includes(normalized)) {
|
||||||
res.header('Access-Control-Allow-Origin', origin);
|
res.header('Access-Control-Allow-Origin', normalized);
|
||||||
}
|
}
|
||||||
res.header('Access-Control-Allow-Credentials', 'true');
|
res.header('Access-Control-Allow-Credentials', 'true');
|
||||||
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
||||||
|
|||||||
@@ -7,38 +7,18 @@ services:
|
|||||||
profiles:
|
profiles:
|
||||||
- default
|
- default
|
||||||
environment:
|
environment:
|
||||||
MYSQL_ROOT_PASSWORD: rezepte123
|
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-change_this_root_password}
|
||||||
MYSQL_DATABASE: rezepte
|
MYSQL_DATABASE: ${MYSQL_DATABASE:-rezepte}
|
||||||
MYSQL_USER: rezepte_user
|
MYSQL_USER: ${MYSQL_USER:-rezepte_user}
|
||||||
MYSQL_PASSWORD: rezepte_pass
|
MYSQL_PASSWORD: ${MYSQL_PASSWORD:-change_this_password}
|
||||||
ports:
|
ports:
|
||||||
- "3307:3306"
|
- "${MYSQL_PORT:-3307}:3306"
|
||||||
volumes:
|
volumes:
|
||||||
- mysql_data:/var/lib/mysql
|
- mysql_data:/var/lib/mysql
|
||||||
- ./sql-init:/docker-entrypoint-initdb.d
|
- ./sql-init:/docker-entrypoint-initdb.d
|
||||||
networks:
|
networks:
|
||||||
- rezepte_network
|
- rezepte_network
|
||||||
|
|
||||||
# PHP Application with Apache
|
|
||||||
php-app:
|
|
||||||
build: .
|
|
||||||
container_name: rezepte_app
|
|
||||||
restart: always
|
|
||||||
profiles:
|
|
||||||
- legacy
|
|
||||||
ports:
|
|
||||||
- "8082:80"
|
|
||||||
volumes:
|
|
||||||
- .:/var/www/html
|
|
||||||
depends_on:
|
|
||||||
- mysql
|
|
||||||
networks:
|
|
||||||
- rezepte_network
|
|
||||||
environment:
|
|
||||||
DB_HOST: mysql
|
|
||||||
DB_NAME: rezepte
|
|
||||||
DB_USER: rezepte_user
|
|
||||||
DB_PASS: rezepte_pass
|
|
||||||
|
|
||||||
# phpMyAdmin
|
# phpMyAdmin
|
||||||
phpmyadmin:
|
phpmyadmin:
|
||||||
@@ -48,12 +28,12 @@ services:
|
|||||||
profiles:
|
profiles:
|
||||||
- admin
|
- admin
|
||||||
ports:
|
ports:
|
||||||
- "8083:80"
|
- "${PHPMYADMIN_PORT:-8083}:80"
|
||||||
environment:
|
environment:
|
||||||
PMA_HOST: mysql
|
PMA_HOST: mysql
|
||||||
PMA_USER: rezepte_user
|
PMA_USER: ${MYSQL_USER:-rezepte_user}
|
||||||
PMA_PASSWORD: rezepte_pass
|
PMA_PASSWORD: ${MYSQL_PASSWORD:-change_this_password}
|
||||||
MYSQL_ROOT_PASSWORD: rezepte123
|
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-change_this_root_password}
|
||||||
depends_on:
|
depends_on:
|
||||||
- mysql
|
- mysql
|
||||||
networks:
|
networks:
|
||||||
@@ -68,16 +48,15 @@ services:
|
|||||||
profiles:
|
profiles:
|
||||||
- default
|
- default
|
||||||
environment:
|
environment:
|
||||||
NODE_ENV: production
|
NODE_ENV: ${NODE_ENV:-production}
|
||||||
PORT: 3001
|
PORT: ${BACKEND_PORT:-3001}
|
||||||
DATABASE_URL: mysql://rezepte_user:rezepte_pass@mysql:3306/rezepte
|
DATABASE_URL: ${DATABASE_URL:-mysql://${MYSQL_USER:-rezepte_user}:${MYSQL_PASSWORD:-change_this_password}@mysql:3306/${MYSQL_DATABASE:-rezepte}}
|
||||||
JWT_SECRET: your-super-secret-jwt-key-change-in-production
|
JWT_SECRET: ${JWT_SECRET:-please_change_to_secure_32_char_min}
|
||||||
UPLOAD_PATH: /app/uploads
|
UPLOAD_PATH: ${UPLOAD_PATH:-/app/uploads}
|
||||||
MAX_FILE_SIZE: 5242880
|
MAX_FILE_SIZE: ${MAX_FILE_SIZE:-5242880}
|
||||||
# CORS_ORIGIN: Restrict in production (example: http://esprimo:3000,http://localhost:3000)
|
CORS_ORIGIN: ${CORS_ORIGIN:-http://localhost:3000}
|
||||||
CORS_ORIGIN: "*"
|
|
||||||
ports:
|
ports:
|
||||||
- "3001:3001"
|
- "${BACKEND_PORT:-3001}:${BACKEND_PORT:-3001}"
|
||||||
volumes:
|
volumes:
|
||||||
- uploads_data:/app/uploads
|
- uploads_data:/app/uploads
|
||||||
- ./uploads:/app/legacy-uploads:ro
|
- ./uploads:/app/legacy-uploads:ro
|
||||||
@@ -98,7 +77,7 @@ services:
|
|||||||
profiles:
|
profiles:
|
||||||
- default
|
- default
|
||||||
ports:
|
ports:
|
||||||
- "3000:80"
|
- "${FRONTEND_PORT:-3000}:80"
|
||||||
networks:
|
networks:
|
||||||
- rezepte_network
|
- rezepte_network
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|||||||
Reference in New Issue
Block a user