Django CSRF issues with reverse proxy

1. Output of caddy version:


2. How I run Caddy:

I run Caddy on a Digital Ocean droplet as a reverse proxy pointing at a Django app on Heroku.

a. System environment:

Caddy is run using systemd

d. My complete Caddy config:

    on_demand_tls {
        interval 1m
        burst 5
https:// {
    tls {

    reverse_proxy {
        header_up Host {upstream_hostport}
        header_up X-Forwarded-Host {host}
        header_up X-Forwarded-Proto https

    log {
        output file /var/log/caddy/access.log

3. The problem I’m having:

Everything is running well for displaying content, but all form requests on Django are failing with CSRF errors. This is due to Caddy forwarding the request to Django unencrypted (http between Django and Caddy) which Django considers non-secure.
The CSRF protection expects the Origin header sent by the browser to be http://because the request is not secure. In my case, it is https:// because my browser is talking to Caddy over https
Because the Origin header does not match what the CSRF middleware expects, the request is rejected

4. Error messages and/or full log output:

All forms fail with CSRF invalid errors.

5. What I already tried:

I’ve been struggling on this for a while, but the best solution I could find which isn’t working unfortunately is setting SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') in Django.

I also tried forwarding the X-Forwarded-Proto using Caddy, but I believe that is stripped to prevent spoofing.

6. Links to relevant resources:

This link is a good approximation of my problem, but the solution didn’t work for me as I feel like I’m missing something on Caddy’s side.

Any and all help is appreciated :slight_smile:

Please upgrade to v2.5.2

You don’t need these, Caddy sets them automatically:

I think your issue is with your Heroku app. Are you sure that it doesn’t strip any headers before it passes the request to your Django app?

This doesn’t look like a problem with Caddy to me.

You can turn on the debug global option (add it above your on_demand_tls config) and check your logs. You’ll see the headers Caddy is sending upstream.

1 Like

It looks like Caddy is passing the Origin and Referrer well, unfortunately no luck in getting this to work on Django. This should be solved using SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') on Django side, but it’s still saying the domain is insecure.

It may be that the Cookie header isn’t passing the cookie along, here are the headers in the log for the failing request. I think it’s supposed to be passing along a cookie in the headers, but I’m unsure if this is something Caddy-side.

"headers": {
            "Accept-Encoding": [
                "gzip, deflate, br"
            "Origin": [
            "Content-Length": [
            "Accept-Language": [
            "User-Agent": [
                "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Safari/605.1.15"
            "Referer": [
            "Accept": [
            "Content-Type": [
            "Cookie": []

Are you certain there isn’t some other webserver between Caddy and Django intercepting it? Are you actually running Django configured with HTTPS? Or is Heroku doing some HTTPS stuff for you itself? It might be filtering some of the headers.

The log you showed here are the request headers as Caddy receives them. This all looks fine.

After turning on the debug global option, check the logs for the reverse_proxy upstream request, which will show you the X-Forwarded-* headers Caddy adds to request.

So after a bunch of scrounging around, I’ve finally figured it out. For Django CSRF verification, the Host and Referrer headers need to match or the request is considered insecure. However when using a reverse proxy in-front of Heroku the Host is set as 'Host': '' and the Origin and Referrer is set to the actual domain. So there is a mismatch.

Since X-Forwarded-host is set with Caddy, Django has an option in settings for USE_X_FORWARDED_HOST = True to handle forwarded hosts. This works perfectly.

Thanks for all the help @francislavoie!


This topic was automatically closed after 30 days. New replies are no longer allowed.