Fix redirect-lus achter TLS-proxy (v0.2.02 beta)
Inloggen mislukte met DJANGO_SECURE=1 achter een reverse proxy die TLS afhandelt, terwijl het inlogscherm zelf wel laadde. - nginx.conf gaf `X-Forwarded-Proto $scheme` door. Achter een TLS-proxy komt het verzoek als http binnen, dus overschreef nginx de `https` van de proxy met `http`. Django zag de verbinding daardoor als onveilig en stuurde elk verzoek naar /api en /admin door naar https, waarna de proxy het weer als http aanleverde: een oneindige redirect-lus. - nginx neemt nu de X-Forwarded-Proto van de proxy over via een map, en valt alleen terug op $scheme als die header ontbreekt. Werkt daardoor zowel achter een externe proxy als bij TLS op nginx zelf. - .env.example: voorbeelden voor CSRF-origin en frontend-URL op https gezet, DJANGO_SECURE standaard op 1, met uitleg waarom een http-origin het inloggen met een CSRF-fout laat stranden. - DEPLOY.md: checklist "Draaien achter TLS" toegevoegd met de drie vereisten (proxy-header, CSRF-origin, allowed hosts), commando's om te controleren of de header aankomt, en het advies poort 8080 aan de loopback te binden zodat de app niet buiten TLS om bereikbaar is. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FAQ4Np13v8fwbTtKHKnwDz
This commit is contained in:
parent
4f0bc009bb
commit
1865de948e
6 changed files with 66 additions and 9 deletions
13
.env.example
13
.env.example
|
|
@ -15,11 +15,17 @@ DJANGO_DEBUG=0
|
||||||
DJANGO_ALLOWED_HOSTS=rooster-test.example.nl,localhost,127.0.0.1
|
DJANGO_ALLOWED_HOSTS=rooster-test.example.nl,localhost,127.0.0.1
|
||||||
|
|
||||||
# Volledige origin(s) inclusief http(s):// voor CSRF achter nginx.
|
# Volledige origin(s) inclusief http(s):// voor CSRF achter nginx.
|
||||||
DJANGO_CSRF_TRUSTED_ORIGINS=http://rooster-test.example.nl:8080
|
# LET OP: dit moet exact de origin zijn die de browser gebruikt. Draai je met
|
||||||
|
# TLS, dan hoort hier https:// te staan (en zonder poort als je op 443 zit).
|
||||||
|
# Staat hier http:// terwijl de browser https:// gebruikt, dan mislukt elke
|
||||||
|
# POST — dus ook het inloggen — met een CSRF-fout.
|
||||||
|
DJANGO_CSRF_TRUSTED_ORIGINS=https://rooster.example.nl
|
||||||
|
|
||||||
# Zet op 1 zodra de server achter HTTPS draait (TLS-certificaat aanwezig).
|
# Zet op 1 zodra de server achter HTTPS draait (TLS-certificaat aanwezig).
|
||||||
# Activeert o.a. veilige cookies, HSTS en een redirect naar https.
|
# Activeert o.a. veilige cookies, HSTS en een redirect naar https.
|
||||||
DJANGO_SECURE=0
|
# Vereist dat de reverse proxy X-Forwarded-Proto: https meestuurt; nginx.conf
|
||||||
|
# in dit project geeft die header door.
|
||||||
|
DJANGO_SECURE=1
|
||||||
|
|
||||||
# --- PostgreSQL -----------------------------------------------------------
|
# --- PostgreSQL -----------------------------------------------------------
|
||||||
POSTGRES_DB=rooster
|
POSTGRES_DB=rooster
|
||||||
|
|
@ -37,5 +43,6 @@ EMAIL_HOST_PASSWORD=
|
||||||
EMAIL_USE_TLS=1
|
EMAIL_USE_TLS=1
|
||||||
DEFAULT_FROM_EMAIL=Roosterwijs <noreply@jouwschool.nl>
|
DEFAULT_FROM_EMAIL=Roosterwijs <noreply@jouwschool.nl>
|
||||||
# Basis-URL van de frontend voor resetlinks in e-mails (zonder slash op het eind),
|
# Basis-URL van de frontend voor resetlinks in e-mails (zonder slash op het eind),
|
||||||
# bv. http://rooster-test.example.nl:8080 (leeg = afleiden uit het verzoek).
|
# bv. https://rooster.example.nl (leeg = afleiden uit het verzoek).
|
||||||
|
# Met TLS: gebruik https://, anders wijzen resetlinks naar een http-adres.
|
||||||
FRONTEND_BASE_URL=
|
FRONTEND_BASE_URL=
|
||||||
|
|
|
||||||
38
DEPLOY.md
38
DEPLOY.md
|
|
@ -146,6 +146,44 @@ Migraties draaien automatisch mee bij het opstarten van de backend.
|
||||||
je breidt de nginx-config uit met certificaten. De nginx-container is daar al
|
je breidt de nginx-config uit met certificaten. De nginx-container is daar al
|
||||||
het logische punt voor. **Zet daarna `DJANGO_SECURE=1` in `.env`** — dat
|
het logische punt voor. **Zet daarna `DJANGO_SECURE=1` in `.env`** — dat
|
||||||
activeert veilige cookies, HSTS en een https-redirect in Django.
|
activeert veilige cookies, HSTS en een https-redirect in Django.
|
||||||
|
|
||||||
|
Zie hieronder wat er dan verder moet kloppen.
|
||||||
|
|
||||||
|
### Draaien achter TLS: checklist
|
||||||
|
|
||||||
|
Met `DJANGO_SECURE=1` moeten drie dingen kloppen, anders lukt **inloggen niet**
|
||||||
|
terwijl het inlogscherm zelf gewoon laadt (dat wordt door nginx geserveerd,
|
||||||
|
zonder tussenkomst van Django).
|
||||||
|
|
||||||
|
1. **De proxy moet `X-Forwarded-Proto: https` meesturen.** Django leest die
|
||||||
|
header (`SECURE_PROXY_SSL_HEADER`) om te bepalen of de verbinding veilig is.
|
||||||
|
Ontbreekt hij, dan denkt Django dat het http is en stuurt het elk verzoek
|
||||||
|
naar `/api` en `/admin` door naar https — waarna de proxy het weer als http
|
||||||
|
aanlevert: een oneindige redirect-lus (`ERR_TOO_MANY_REDIRECTS`) en dus een
|
||||||
|
mislukte login. Caddy en Traefik zetten deze header standaard; controleer het
|
||||||
|
als je een eigen nginx/Apache ervoor hebt.
|
||||||
|
2. **`DJANGO_CSRF_TRUSTED_ORIGINS` moet de https-origin zijn**, exact zoals de
|
||||||
|
browser hem gebruikt (bv. `https://rooster.example.nl`, zonder poort op 443).
|
||||||
|
Staat hier nog een `http://`-adres, dan geeft elke POST — inclusief het
|
||||||
|
inloggen — een CSRF-fout (HTTP 403).
|
||||||
|
3. **`DJANGO_ALLOWED_HOSTS` moet de publieke hostnaam bevatten**, anders volgt
|
||||||
|
een 400 Bad Request.
|
||||||
|
|
||||||
|
Controleren of de header goed aankomt:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose logs backend | grep -i "Mislukte inlogpoging" # bereikt de login de backend?
|
||||||
|
curl -sI https://<jouw-domein>/api/auth/me/ | head -n 1 # 200/401 = goed, 301 = lus
|
||||||
|
```
|
||||||
|
|
||||||
|
Krijg je bij die `curl` een `301`, dan komt `X-Forwarded-Proto` niet goed door.
|
||||||
|
|
||||||
|
- **Beperk poort 8080 tot de proxy:** in `docker-compose.yml` staat
|
||||||
|
`"8080:80"`, wat op álle netwerkinterfaces luistert. Daardoor is de app ook
|
||||||
|
rechtstreeks via `http://<server>:8080` te bereiken — buiten TLS om. Draait je
|
||||||
|
reverse proxy op dezelfde machine, maak er dan `"127.0.0.1:8080:80"` van.
|
||||||
|
Draait de proxy op een andere host, laat het dan staan en zet er een firewall
|
||||||
|
op: nginx vertrouwt de `X-Forwarded-Proto` van de aanroeper.
|
||||||
- **Inloggen verplicht:** de hele API vereist een ingelogde gebruiker; de
|
- **Inloggen verplicht:** de hele API vereist een ingelogde gebruiker; de
|
||||||
frontend toont eerst een inlogscherm. Maak gebruikers aan via
|
frontend toont eerst een inlogscherm. Maak gebruikers aan via
|
||||||
`createsuperuser` (zie boven) of in de Django-admin. Modulebeheer is
|
`createsuperuser` (zie boven) of in de Django-admin. Modulebeheer is
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,16 @@
|
||||||
# Nginx: serveert de React-app en stuurt API/admin door naar gunicorn.
|
# Nginx: serveert de React-app en stuurt API/admin door naar gunicorn.
|
||||||
|
|
||||||
|
# Draait er een reverse proxy vóór ons die TLS afhandelt (Caddy/Traefik), dan
|
||||||
|
# komt het verzoek hier als gewoon http binnen en is $scheme dus "http". Zouden
|
||||||
|
# we dat doorgeven, dan denkt Django dat de verbinding onveilig is en stuurt het
|
||||||
|
# met DJANGO_SECURE=1 elk verzoek door naar https — een oneindige redirect-lus.
|
||||||
|
# Daarom: neem de X-Forwarded-Proto van de proxy over, en val alleen terug op
|
||||||
|
# $scheme als die header ontbreekt (nginx zelf aan de buitenkant, of geen TLS).
|
||||||
|
map $http_x_forwarded_proto $rw_forwarded_proto {
|
||||||
|
default $http_x_forwarded_proto;
|
||||||
|
"" $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name _;
|
server_name _;
|
||||||
|
|
@ -33,7 +45,7 @@ server {
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $rw_forwarded_proto;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Django-admin loopt ook via de backend.
|
# Django-admin loopt ook via de backend.
|
||||||
|
|
@ -42,7 +54,7 @@ server {
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $rw_forwarded_proto;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Alle overige paden: de single-page React-app.
|
# Alle overige paden: de single-page React-app.
|
||||||
|
|
|
||||||
4
frontend/package-lock.json
generated
4
frontend/package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "roosterwijs-frontend",
|
"name": "roosterwijs-frontend",
|
||||||
"version": "0.2.1-beta",
|
"version": "0.2.2-beta",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "roosterwijs-frontend",
|
"name": "roosterwijs-frontend",
|
||||||
"version": "0.2.1-beta",
|
"version": "0.2.2-beta",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1"
|
"react-dom": "^18.3.1"
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "roosterwijs-frontend",
|
"name": "roosterwijs-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.2.1-beta",
|
"version": "0.2.2-beta",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ button,a,input,select{transition:border-color .15s,background-color .15s,color .
|
||||||
.cog-btn{width:100%;min-height:38px;justify-content:flex-start;font-size:15px;padding:0}
|
.cog-btn{width:100%;min-height:38px;justify-content:flex-start;font-size:15px;padding:0}
|
||||||
.cog-btn::after{content:"Beheer en instellingen";margin-left:8px;font-size:13px}
|
.cog-btn::after{content:"Beheer en instellingen";margin-left:8px;font-size:13px}
|
||||||
.cog-dropdown{position:static;min-width:0;margin:4px 0 0;box-shadow:none}
|
.cog-dropdown{position:static;min-width:0;margin:4px 0 0;box-shadow:none}
|
||||||
.topbar::after{content:"v0.2.01 beta";order:4;margin:10px 8px 0;color:#98a2b3;font-size:10px}
|
.topbar::after{content:"v0.2.02 beta";order:4;margin:10px 8px 0;color:#98a2b3;font-size:10px}
|
||||||
.content{margin-left:var(--sidebar);width:calc(100% - var(--sidebar));max-width:1680px;padding:32px 38px 48px}
|
.content{margin-left:var(--sidebar);width:calc(100% - var(--sidebar));max-width:1680px;padding:32px 38px 48px}
|
||||||
.content-wide{padding:24px 28px 42px}
|
.content-wide{padding:24px 28px 42px}
|
||||||
h1{font-size:clamp(24px,2.4vw,30px);line-height:1.2;letter-spacing:-.025em;font-weight:750}
|
h1{font-size:clamp(24px,2.4vw,30px);line-height:1.2;letter-spacing:-.025em;font-weight:750}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue