Django 403 behind a proxy fix

Django 403 behind a proxy fix

calendar_today ago • schedule2 min read
— Originally published at portal.uppcoder.com

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 browser's 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 controls X-Forwarded-Proto
  • CSRF_TRUSTED_ORIGINS only for cross-origin posts, and always with https://
  • 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: Uppcoder/django-postgresql-starter.

Hopefully this will save anyone facing the same or similar stack install some time and nerves.

🔥 Join developers growing publicly
Share your knowledge, build in public, and grow your developer presence with a global community.

More Posts

I Wrote a Script to Fix Audible's Unreadable PDF Filenames

snapsynapseverified - Apr 20

I’m a Senior Dev and I’ve Forgotten How to Think Without a Prompt

Karol Modelski - Mar 19

Dashboard Operasional Armada Rental Mobil dengan Python + FastAPI

Masbadar - Mar 12

How I Built a React Portfolio in 7 Days That Landed ₹1.2L in Freelance Work

Dharanidharan - Feb 9

TypeScript Complexity Has Finally Reached the Point of Total Absurdity

Karol Modelski - Apr 23
chevron_left
168 Points • 4 Badges
2Posts
1Comments

Related Jobs

Commenters (This Week)

5 comments
1 comment
1 comment

Contribute meaningful comments to climb the leaderboard and earn badges!