csrf fehler ausgebessert

This commit is contained in:
2026-05-21 17:54:38 +02:00
parent 6a103adac4
commit f129b20447
12 changed files with 413 additions and 3 deletions
+57
View File
@@ -0,0 +1,57 @@
{
"name": "Serienbrief Dev",
"dockerComposeFile": [
"../docker-compose.yml",
"../docker-compose.override.yml"
],
"service": "web",
"workspaceFolder": "/app",
"remoteUser": "app",
"shutdownAction": "stopCompose",
"customizations": {
"vscode": {
"extensions": [
"ms-python.python",
"ms-python.vscode-pylance",
"ms-python.debugpy",
"charliermarsh.ruff",
"batisteo.vscode-django",
"ms-azuretools.vscode-docker",
"redhat.vscode-yaml",
"tamasfe.even-better-toml",
"eamodio.gitlens",
"njpwerner.autodocstring"
],
"settings": {
"python.defaultInterpreterPath": "/usr/local/bin/python",
"python.terminal.activateEnvironment": false,
"python.testing.pytestEnabled": true,
"python.testing.pytestArgs": ["app"],
"[python]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "charliermarsh.ruff",
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit",
"source.fixAll": "explicit"
}
},
"files.exclude": {
"**/__pycache__": true,
"**/*.pyc": true
}
}
}
},
"forwardPorts": [8000, 5678, 5432, 6379, 8080],
"portsAttributes": {
"8000": { "label": "Django runserver" },
"5678": { "label": "debugpy" },
"8080": { "label": "App-Nginx" },
"5432": { "label": "PostgreSQL" },
"6379": { "label": "Redis" }
},
"postCreateCommand": "pip install --user -e . 2>/dev/null || true && python manage.py migrate --noinput || true"
}
+46
View File
@@ -0,0 +1,46 @@
# =============================================================================
# Serienbrief Environment
# Kopieren als .env. NICHT ins Git committen.
# =============================================================================
# --- Deployment -------------------------------------------------------------
APP_VERSION=1.0.0
APP_UID=10001
APP_GID=10001
# Bind des internen Nginx-Sockets. Wird vom äußeren Reverse-Proxy angesprochen.
# - 127.0.0.1: nur lokal (z.B. wenn der äußere Proxy auf demselben Host läuft)
# - LAN-IP: wenn der äußere Proxy auf einem anderen Host steht
APP_BIND_IP=127.0.0.1
APP_BIND_PORT=8080
# --- Django -----------------------------------------------------------------
# openssl rand -base64 64
DJANGO_SECRET_KEY=CHANGE_ME_LONG_RANDOM_STRING
DJANGO_DEBUG=False
# Hosts, unter denen die App erreichbar ist (Domain des äußeren Proxys).
DJANGO_ALLOWED_HOSTS=serienbrief.lan,127.0.0.1,localhost
# Trusted Origins für CSRF HTTPS-URL des äußeren Proxys.
CSRF_TRUSTED_ORIGINS=https://serienbrief.lan
# Wichtig hinter HTTPS-terminierendem Proxy: erlaubt Django, das Schema aus
# dem X-Forwarded-Proto-Header zu übernehmen.
USE_X_FORWARDED_HOST=True
SECURE_PROXY_SSL_HEADER=HTTP_X_FORWARDED_PROTO,https
# --- PostgreSQL -------------------------------------------------------------
POSTGRES_DB=serienbrief
POSTGRES_USER=serienbrief
# Passwort liegt in ./secrets/postgres_password.txt
DATABASE_URL=postgres://serienbrief:__FROM_SECRET__@db:5432/serienbrief
# --- Redis ------------------------------------------------------------------
# openssl rand -base64 32
REDIS_PASSWORD=CHANGE_ME_REDIS_PASSWORD
CELERY_BROKER_URL=redis://:${REDIS_PASSWORD}@redis:6379/0
CELERY_RESULT_BACKEND=redis://:${REDIS_PASSWORD}@redis:6379/1
# --- Retention --------------------------------------------------------------
JOB_RETENTION_DAYS=30
+39
View File
@@ -0,0 +1,39 @@
# --- Secrets / Env ---
.env
.env.*
!.env.example
secrets/
*.key
*.pem
# --- Python ---
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
.pytest_cache/
.ruff_cache/
.mypy_cache/
.coverage
htmlcov/
.tox/
# --- Django ---
*.sqlite3
staticfiles/
media/
# --- Backups & Data ---
backups/
postgres_data/
redis_data/
# --- IDE ---
.idea/
*.swp
*.swo
# --- OS ---
.DS_Store
Thumbs.db
+14
View File
@@ -0,0 +1,14 @@
{
"recommendations": [
"ms-python.python",
"ms-python.vscode-pylance",
"ms-python.debugpy",
"charliermarsh.ruff",
"batisteo.vscode-django",
"ms-azuretools.vscode-docker",
"ms-vscode-remote.remote-containers",
"redhat.vscode-yaml",
"tamasfe.even-better-toml",
"eamodio.gitlens"
]
}
+53
View File
@@ -0,0 +1,53 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Django: attach (debugpy in Container)",
"type": "debugpy",
"request": "attach",
"connect": { "host": "127.0.0.1", "port": 5678 },
"pathMappings": [
{
"localRoot": "${workspaceFolder}/app",
"remoteRoot": "/app"
}
],
"justMyCode": false,
"django": true
},
{
"name": "Django: runserver (lokal, ohne Docker)",
"type": "debugpy",
"request": "launch",
"program": "${workspaceFolder}/app/manage.py",
"args": ["runserver", "127.0.0.1:8000"],
"django": true,
"justMyCode": false,
"env": {
"DJANGO_SETTINGS_MODULE": "config.settings.dev"
}
},
{
"name": "Celery Worker (lokal, ohne Docker)",
"type": "debugpy",
"request": "launch",
"module": "celery",
"args": ["-A", "config", "worker", "--loglevel=debug", "--concurrency=1"],
"cwd": "${workspaceFolder}/app",
"justMyCode": false,
"env": {
"DJANGO_SETTINGS_MODULE": "config.settings.dev"
}
},
{
"name": "Pytest: aktuelle Datei",
"type": "debugpy",
"request": "launch",
"module": "pytest",
"args": ["${file}", "-v"],
"cwd": "${workspaceFolder}/app",
"console": "integratedTerminal",
"justMyCode": false
}
]
}
+38
View File
@@ -0,0 +1,38 @@
{
"python.analysis.extraPaths": ["app"],
"python.analysis.typeCheckingMode": "basic",
"python.testing.pytestEnabled": true,
"python.testing.pytestArgs": ["app"],
"python.testing.cwd": "${workspaceFolder}/app",
"[python]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "charliermarsh.ruff",
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit",
"source.fixAll": "explicit"
}
},
"ruff.lineLength": 100,
"files.associations": {
"**/templates/**/*.html": "django-html",
"**/requirements*.txt": "pip-requirements"
},
"emmet.includeLanguages": {
"django-html": "html"
},
"files.exclude": {
"**/__pycache__": true,
"**/*.pyc": true,
"**/.pytest_cache": true,
"**/.ruff_cache": true
},
"search.exclude": {
"**/staticfiles": true,
"**/media": true,
"**/backups": true
}
}
+72
View File
@@ -0,0 +1,72 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "compose: up",
"type": "shell",
"command": "docker compose up -d",
"problemMatcher": []
},
{
"label": "compose: down",
"type": "shell",
"command": "docker compose down",
"problemMatcher": []
},
{
"label": "compose: logs web",
"type": "shell",
"command": "docker compose logs -f web",
"problemMatcher": []
},
{
"label": "compose: rebuild web",
"type": "shell",
"command": "docker compose build web && docker compose up -d web",
"problemMatcher": []
},
{
"label": "django: makemigrations",
"type": "shell",
"command": "docker compose exec web python manage.py makemigrations",
"problemMatcher": []
},
{
"label": "django: migrate",
"type": "shell",
"command": "docker compose exec web python manage.py migrate",
"problemMatcher": []
},
{
"label": "django: shell",
"type": "shell",
"command": "docker compose exec web python manage.py shell",
"problemMatcher": []
},
{
"label": "django: createsuperuser",
"type": "shell",
"command": "docker compose exec web python manage.py createsuperuser",
"problemMatcher": []
},
{
"label": "django: start debugpy",
"type": "shell",
"command": "docker compose exec web python -m debugpy --listen 0.0.0.0:5678 manage.py runserver 0.0.0.0:8000 --noreload",
"problemMatcher": []
},
{
"label": "tests: pytest",
"type": "shell",
"command": "docker compose exec web pytest -v",
"group": "test",
"problemMatcher": []
},
{
"label": "lint: ruff",
"type": "shell",
"command": "docker compose exec web ruff check .",
"problemMatcher": []
}
]
}
+92 -3
View File
@@ -384,28 +384,117 @@ docker compose logs -f web
docker compose exec web python manage.py createsuperuser
```
### Externen Proxy konfigurieren (Beispiel)
### Externen Reverse-Proxy konfigurieren
Der Stack liefert intern HTTP auf `${APP_BIND_IP}:${APP_BIND_PORT}` aus (Default `127.0.0.1:8080`). Der externe Proxy übernimmt TLS-Terminierung und die nach außen sichtbare Domain.
#### Voraussetzungen für externen Zugriff
Wenn der Proxy auf einem **anderen Host** läuft (typisch bei Nginx Proxy Manager als eigener Container/VM), muss `APP_BIND_IP` von `127.0.0.1` auf eine LAN-erreichbare Adresse umgestellt werden:
```bash
# LAN-IP des Docker-Hosts ermitteln
ip -4 -br a | grep -v lo
# In .env eintragen (Beispiel)
sed -i 's/^APP_BIND_IP=.*/APP_BIND_IP=192.168.10.42/' .env
docker compose up -d
```
Alternativ `0.0.0.0` für alle Interfaces — die explizite IP ist sicherheitstechnisch sauberer.
Firewall absichern, damit nur der Proxy-Host auf 8080 zugreifen darf:
```bash
sudo ufw allow from <proxy-host-ip> to any port 8080 proto tcp comment 'reverse proxy'
sudo ufw deny 8080
```
#### Django-Seite anpassen
Die externe Domain muss in `.env` eingetragen sein, sonst lehnt Django die Requests ab (400 Bad Request bzw. 403 CSRF):
```env
DJANGO_ALLOWED_HOSTS=serienbrief.example.lan,localhost,127.0.0.1
CSRF_TRUSTED_ORIGINS=https://serienbrief.example.lan,http://localhost:8080,http://127.0.0.1:8080
```
Nach Änderung `docker compose restart web`.
#### Variante A: Nginx Proxy Manager (NPM)
Im NPM-Webinterface unter **Proxy Hosts → Add Proxy Host**:
| Feld | Wert |
|---|---|
| Domain Names | `serienbrief.example.lan` |
| Scheme | `http` |
| Forward Hostname / IP | LAN-IP des Docker-Hosts (z.B. `192.168.10.42`) |
| Forward Port | `8080` |
| Cache Assets | aus |
| Block Common Exploits | an |
| Websockets Support | an (HTMX nutzt nur AJAX, schadet aber nicht) |
Im Tab **Custom Locations** oder **Advanced** zusätzlich:
```nginx
client_max_body_size 50M;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
```
NPM setzt `Host`, `X-Forwarded-For` und Standard-Header bereits selbst. **Pflicht** ist nur `X-Forwarded-Proto $scheme`, weil Djangos `SECURE_PROXY_SSL_HEADER` darauf basiert.
TLS-Zertifikat (Let's Encrypt für interne Domains via DNS-Challenge, oder eigenes CA-Cert) im Tab **SSL** zuweisen.
#### Variante B: Generisches Nginx / OpenResty
```nginx
server {
listen 443 ssl http2;
server_name serienbrief.lan;
server_name serienbrief.example.lan;
ssl_certificate /etc/ssl/lan/serienbrief.crt;
ssl_certificate_key /etc/ssl/lan/serienbrief.key;
client_max_body_size 50M; # für CSV/DOCX-Uploads
location / {
proxy_pass http://<docker-host-ip>:8080;
proxy_http_version 1.1;
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 https;
proxy_set_header X-Forwarded-Host $host;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
proxy_redirect off;
client_max_body_size 50M; # für CSV/DOCXUploads
}
}
```
#### Diagnose bei 502 Bad Gateway
Vom Proxy-Host aus testen:
```bash
# TCP-Connect — "succeeded" erwartet
nc -zv <docker-host-ip> 8080
# HTTP-Request — 200/302 erwartet
curl -v -H "Host: serienbrief.example.lan" http://<docker-host-ip>:8080/
```
Typische Ursachen:
- `Connection refused``APP_BIND_IP` steht noch auf `127.0.0.1`
- `Timeout` → Firewall blockt
- 502 ohne Connect-Fehler → falsche Upstream-IP im Proxy-Eintrag
---
## Entwicklung in VS Code
@@ -0,0 +1 @@
,hans,jacboy,21.05.2026 15:40,file:///home/hans/.config/libreoffice/4;
@@ -0,0 +1 @@
,hans,jacboy,21.05.2026 15:32,file:///home/hans/.config/libreoffice/4;
Binary file not shown.
Binary file not shown.