daybook/issues/001-csrf-behind-reverse-proxy.md
peter f2f2a9c96b Close out issue 001: document CSRF/reverse-proxy requirement
The app-side fix (CSRF_TRUSTED_ORIGINS, SECURE_PROXY_SSL_HEADER in
settings.py) was already committed as d458cd3 after the first Cloud-VM-1
deployment hit "CSRF verification failed" on login behind Apache. This
finishes the rest of that issue's checklist: documents
DJANGO_CSRF_TRUSTED_ORIGINS in deploy/env.example, adds the required
X-Forwarded-Proto header to the Apache/Caddy examples in
cloud_agent_deploy.md (plus a checklist item that verifies via an actual
login, not just an unauthenticated capabilities curl), and adds the same
proxy-agnostic guidance to docs/deployment.md.

Also introduces issues/, a tracked one-file-per-issue log for the project
going forward.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 12:28:41 +01:00

2.8 KiB

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):

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:

RequestHeader set X-Forwarded-Proto "https"

inside the <VirtualHost *:443> 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

  • Pushed commit d458cd3 to main.
  • Added DJANGO_CSRF_TRUSTED_ORIGINS to deploy/env.example with an explanatory comment.
  • 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).
  • Updated docs/deployment.md's "Reverse Proxy Notes" and checklist with the same requirement, plus an Nginx/Apache/Caddy header cheat sheet.