So you put a Django app behind a reverse proxy that handles HTTPS (nginx, Traefik, Caddy, a PaaS router, a load balancer), open /admin/, type the right username and password, and get this:
Forbidden (403)
CSRF verification failed. Request aborted.
Origin checking failed - https://myapp.example does not match any trusted origins.
The username and password are fine (checked umpteen times). Django just thinks the request came in over plain HTTP.
Why it happens
The proxy normally ends HTTPS and passes the request on to Django over HTTP, adding a header that says what the original scheme was:
X-Forwarded-Proto: https
Django will ignore that header unless you tell it to trust it. So request.is_secure() is False, and Django expects the browsers Origin to be http://myapp.example. The browser sends https://myapp.example, they don't match, and since Django 4.0 that mismatch is a 403 on every POST, including the admin login.
The fix
In settings.py:
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
Now Django treats requests with X-Forwarded-Proto: https as secure, the expected origin becomes https://..., and the login works.
Caveat: only set this when your proxy always sets (or overwrites) X-Forwarded-Proto itself. If clients can reach Django directly and send that header themselves, they can make a plain HTTP request look secure. The Django docs say as much.
CSRF_TRUSTED_ORIGINS: when you need it
You don't need it for the case above. You need it when forms are posted from another origin (a separate frontend domain, for example), and since Django 4.0 the entries need the scheme:
CSRF_TRUSTED_ORIGINS = ["https://myapp.example"] # not "myapp.example"
Leaving out https:// is a common reason it "doesn't work" after an upgrade.
While you're in settings.py: the database from the environment
Most hosts hand you the database as environment variables, so no password has to sit in the code:
import os
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": os.environ["POSTGRES_DB"],
"USER": os.environ["POSTGRES_USER"],
"PASSWORD": os.environ["POSTGRES_PASSWORD"],
"HOST": os.environ["POSTGRES_HOST"],
"PORT": os.environ.get("POSTGRES_PORT", "5432"),
}
}
If you get one DATABASE_URL instead, django-environ makes it one line:
import environ
env = environ.Env()
DATABASES = {"default": env.db("DATABASE_URL")}
And list psycopg[binary] in your requirements, or the first migrate fails on the missing driver.
Summary
- 403 "Origin checking failed" behind an HTTPS proxy:
SECURE_PROXY_SSL_HEADER, as long as the proxy controlsX-Forwarded-Proto -
CSRF_TRUSTED_ORIGINSonly for cross-origin posts, and always withhttps:// - Database settings from the environment, not in the code
I hit all of this while writing a step-by-step guide for Uppcoder, the platform I work on: Deploy a Django app with PostgreSQL, free. There's also a small starter project with these settings already in place: websson/django-postgresql-starter.
Hopefully this will save anyone facing the same or similar stack install some time and nerves.
Top comments (0)