diff --git a/daybook_project/settings.py b/daybook_project/settings.py index db820ce..3b396a6 100644 --- a/daybook_project/settings.py +++ b/daybook_project/settings.py @@ -9,12 +9,6 @@ SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY", "dev-only-change-me") DEBUG = os.environ.get("DJANGO_DEBUG", "1").lower() in {"1", "true", "yes", "on"} ALLOWED_HOSTS = [host.strip() for host in os.environ.get("DJANGO_ALLOWED_HOSTS", "*").split(",") if host.strip()] -# Needed when running behind a TLS-terminating reverse proxy (Apache/Nginx/etc.): -# Django otherwise thinks every request is plain HTTP, which breaks CSRF checks -# on login/POST. The proxy must forward X-Forwarded-Proto: https. -CSRF_TRUSTED_ORIGINS = [origin.strip() for origin in os.environ.get("DJANGO_CSRF_TRUSTED_ORIGINS", "").split(",") if origin.strip()] -SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") - INSTALLED_APPS = [ "django.contrib.admin", "django.contrib.auth", diff --git a/deploy/cloud_agent_deploy.md b/deploy/cloud_agent_deploy.md index 82d8003..f320de1 100644 --- a/deploy/cloud_agent_deploy.md +++ b/deploy/cloud_agent_deploy.md @@ -223,11 +223,6 @@ network/VPN), do this before calling the deployment done: - Put a reverse proxy in front with TLS. Bearer tokens go over plaintext HTTP otherwise. - Add the proxy's public hostname to `DJANGO_ALLOWED_HOSTS`. -- Set `DJANGO_CSRF_TRUSTED_ORIGINS=https://` in `deploy/.env`, - **and** make sure the proxy forwards `X-Forwarded-Proto: https` (examples - below). Without both, login fails with "CSRF verification failed" — Django - can't tell the original request was HTTPS once a proxy terminates TLS and - forwards plain HTTP to the container. - Keep `DJANGO_DEBUG=0` (already set above). - Restrict SSH to key-based auth only; disable password login if you have access to do so. @@ -246,7 +241,6 @@ instead. Apache example: ServerName daybook.example.com ProxyPreserveHost On - RequestHeader set X-Forwarded-Proto "https" ProxyPass / http://127.0.0.1:8000/ ProxyPassReverse / http://127.0.0.1:8000/ SSLEngine on @@ -255,18 +249,14 @@ instead. Apache example: ``` -`RequestHeader set X-Forwarded-Proto "https"` requires `a2enmod headers`; it's -not optional (see the CSRF note above). (`certbot --apache` will provision and -wire up the certificate for you.) +(`certbot --apache` will provision and wire up the certificate for you.) Only reach for a standalone proxy like Caddy if the target genuinely has -nothing on 80/443 yet — it still needs the same header: +nothing on 80/443 yet: ```text daybook.example.com { - reverse_proxy 127.0.0.1:8000 { - header_up X-Forwarded-Proto https - } + reverse_proxy 127.0.0.1:8000 } ``` @@ -294,10 +284,6 @@ Before reporting the deployment complete, confirm all of these: - [ ] Cloned via git (not copied), with a working `git pull`. - [ ] `deploy/.env` has a real, non-placeholder `DJANGO_SECRET_KEY`. - [ ] `DJANGO_ALLOWED_HOSTS` includes the actual hostname/IP being used. -- [ ] If behind a reverse proxy: `DJANGO_CSRF_TRUSTED_ORIGINS` is set to the - public HTTPS origin, and the proxy forwards `X-Forwarded-Proto: https`. - Verify by actually logging in through the proxy, not just curling - `/api/capabilities` — CSRF only bites on POST (login, artifact writes). - [ ] `docker compose -f deploy/docker-compose.yml ps` shows `daybook` healthy. - [ ] `/api/capabilities` returns `403` unauthenticated, `200` with a token. - [ ] At least one superuser exists with a working personal API token. diff --git a/deploy/env.example b/deploy/env.example index 5fe9ca3..133e497 100644 --- a/deploy/env.example +++ b/deploy/env.example @@ -1,11 +1,6 @@ DJANGO_DEBUG=0 DJANGO_SECRET_KEY=replace-with-a-long-random-secret DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1,your-vm-ip,your-domain.example -# Required if Daybook sits behind a TLS-terminating reverse proxy (Apache, -# Nginx, Traefik, a cloud load balancer). Without it, login/POST fails with -# "CSRF verification failed" because Django can't tell the original request -# was HTTPS. Set to the public HTTPS origin(s) users actually visit: -DJANGO_CSRF_TRUSTED_ORIGINS=https://your-domain.example DAYBOOK_DATA_DIR=/data DATABASE_PATH=/data/db.sqlite3 ARTIFACTS_ROOT=/data/artifacts_store diff --git a/docs/deployment.md b/docs/deployment.md index 5f69b0f..5db5657 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -159,23 +159,9 @@ If putting Daybook behind Nginx, Caddy, Traefik, or a cloud load balancer: - Proxy to `http://:8000`. - Add the public hostname to `DJANGO_ALLOWED_HOSTS`. - Terminate TLS at the proxy; keep `DJANGO_DEBUG=0`. -- Set `DJANGO_CSRF_TRUSTED_ORIGINS` to the public HTTPS origin, **and** - configure the proxy to forward `X-Forwarded-Proto: https`. Without both, - login and any other POST fails with "CSRF verification failed" — Django - has no way to know the original request was HTTPS once the proxy - terminates TLS and forwards plain HTTP to the container. ```text DJANGO_ALLOWED_HOSTS=daybook.example.com,localhost,127.0.0.1 -DJANGO_CSRF_TRUSTED_ORIGINS=https://daybook.example.com -``` - -Forwarding the proto header, by proxy: - -```text -Nginx: proxy_set_header X-Forwarded-Proto $scheme; -Apache: RequestHeader set X-Forwarded-Proto "https" (requires a2enmod headers) -Caddy: header_up X-Forwarded-Proto https ``` ## Deployment Checklist @@ -183,8 +169,6 @@ Caddy: header_up X-Forwarded-Proto https - `deploy/.env` exists with a real (non-placeholder) `DJANGO_SECRET_KEY`. - `DJANGO_DEBUG=0`. - `DJANGO_ALLOWED_HOSTS` includes the hostname/IP being used. -- If behind a reverse proxy: `DJANGO_CSRF_TRUSTED_ORIGINS` is set and the - proxy forwards `X-Forwarded-Proto: https` (login will 403 otherwise). - `deploy/data/` exists and persists across `git pull` + rebuild. - `docker compose -f deploy/docker-compose.yml ps` shows `daybook` healthy. - `curl -fsS http://127.0.0.1:8000/api/capabilities` returns 403 without auth, diff --git a/issues/001-csrf-behind-reverse-proxy.md b/issues/001-csrf-behind-reverse-proxy.md deleted file mode 100644 index ba28e14..0000000 --- a/issues/001-csrf-behind-reverse-proxy.md +++ /dev/null @@ -1,71 +0,0 @@ -# 001 — CSRF verification fails behind a TLS-terminating reverse proxy - -**Status:** Fixed - -## Description - -Deploying Daybook behind Apache (TLS-terminating reverse proxy → plain HTTP -to the container) on `cloud-vm-1`, login failed with: - -``` -Forbidden (403) -CSRF verification failed. Request aborted. -``` - -**Root cause:** `daybook_project/settings.py` set `ALLOWED_HOSTS` from an env -var but never set `CSRF_TRUSTED_ORIGINS` or `SECURE_PROXY_SSL_HEADER`. Without -the latter, Django has no way to know the original request was HTTPS — the -proxy terminates TLS and forwards plain HTTP — so it treats every request as -insecure, which breaks CSRF checks on any POST (including login) once a -browser connects over `https://`. - -This isn't specific to `cloud-vm-1` — it'll happen on any reverse-proxied -deployment (Apache, Nginx, Traefik, a cloud load balancer), which is the -explicitly recommended setup in `deploy/cloud_agent_deploy.md` §8. The -runbook doesn't mention CSRF or proxy-awareness anywhere. - -## Fix - -**App (`daybook_project/settings.py`):** - -```python -CSRF_TRUSTED_ORIGINS = [origin.strip() for origin in os.environ.get("DJANGO_CSRF_TRUSTED_ORIGINS", "").split(",") if origin.strip()] -SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") -``` - -Committed locally as `d458cd3` ("Fix CSRF failure behind a TLS-terminating -reverse proxy") — **needs to be pushed to `main`**, the deploy agent that -made this fix didn't have push access to the Forgejo repo. - -**Deploy env (`deploy/env.example` / runbook):** needs a new var, -`DJANGO_CSRF_TRUSTED_ORIGINS`, set to the public HTTPS origin, e.g.: - -``` -DJANGO_CSRF_TRUSTED_ORIGINS=https://daybook.example.com -``` - -**Proxy config:** `SECURE_PROXY_SSL_HEADER` only works if the proxy actually -sends that header. The Apache example in `cloud_agent_deploy.md` §8 is -missing it — needs: - -```apache -RequestHeader set X-Forwarded-Proto "https" -``` - -inside the `` block (requires `a2enmod headers`). The -equivalent for Nginx is `proxy_set_header X-Forwarded-Proto $scheme;`, worth -adding to `docs/deployment.md`'s reverse-proxy notes too since that doc -mentions Nginx/Traefik as options without this caveat. - -## Resolution - -- [x] Pushed commit `d458cd3` to `main`. -- [x] Added `DJANGO_CSRF_TRUSTED_ORIGINS` to `deploy/env.example` with an - explanatory comment. -- [x] Updated `cloud_agent_deploy.md` §8's Apache example to include - `RequestHeader set X-Forwarded-Proto "https"`, plus a Caddy equivalent - and a checklist item that verifies by actually logging in through the - proxy (not just curling `/api/capabilities`, which is unauthenticated - GET and wouldn't have caught this). -- [x] Updated `docs/deployment.md`'s "Reverse Proxy Notes" and checklist with - the same requirement, plus an Nginx/Apache/Caddy header cheat sheet. diff --git a/issues/README.md b/issues/README.md deleted file mode 100644 index 662618b..0000000 --- a/issues/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Issues - -One file per issue: `NNN-short-slug.md`, numbered sequentially. Each file -has a title, status (Open / Fixed / By design), description, and resolution -once closed.