CSRF verification failed. Request aborted. after putting behind SSL proxy

I have site hosted being served by Nginx, behind a Nginx reverse proxy server. Everything is working fine until I enable SSL on the reverse proxy server. Once that is enabled, I am able to access my site, but when I attempt to login, I get:

Forbidden (403)

CSRF verification failed. Request aborted.

I’m sure this is a settings issue, but I have no idea where to start.

"""
Django settings for studio_management project.

Generated by 'django-admin startproject' using Django 4.2.8.

For more information on this file, see
https://docs.djangoproject.com/en/4.2/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.2/ref/settings/
"""

import os
from pathlib import Path

# LOGGING = {
#     "version": 1,
#     "disable_existing_loggers": False,
#     "handlers": {
#         "file": {
#             "level": "DEBUG",
#             "class": "logging.FileHandler",
#             "filename": "/home/django/logs/debug.log",
#         },
#     },
#     "loggers": {
#         "django": {
#             "handlers": ["file"],
#             "level": "WARNING",
#             "propagate": True,
#         },
#     },
# }

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 
#print(SECRET_KEY)

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = str(os.environ.get('DEBUG')) == "1"


ALLOWED_HOSTS = [myallowed hosts]
#ALLOWED_HOSTS += os.environ.get('ALLOWED_HOST').split(',')
# if not DEBUG:
#     ALLOWED_HOSTS += os.environ.get('ALLOWED_HOST').split(',')
# else:
#     ALLOWED_HOSTS += []



#print(ALLOWED_HOSTS)


# Application definition

DEFAULT_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

THIRD_PARTY_APPS = [
    'localflavor',
    'django_htmx',
    'widget_tweaks',
    'crispy_forms',
    'crispy_bootstrap5',
    'import_export',
]

LOCAL_APPS = [
    
    'accounts',
    'controls',
    'customers',
    'dashboard',
    'inventory',
    'finance',
    'pdf',
    'pricesheet',
    #'vendors',
    'workorders',
    
]

INSTALLED_APPS = DEFAULT_APPS + LOCAL_APPS + THIRD_PARTY_APPS

CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"

CRISPY_TEMPLATE_PACK = "bootstrap5"

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'whitenoise.middleware.WhiteNoiseMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    'django_htmx.middleware.HtmxMiddleware',
]

ROOT_URLCONF = 'studio_management.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [BASE_DIR / "templates",],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

WSGI_APPLICATION = 'studio_management.wsgi.application'


# Database
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases

TESTING = str(os.environ.get('TESTING')) == "1"
if not TESTING:
    #Production database
    DATABASES = {
        'default': {
            'ENGINE': 'django.db.backends.mysql',
            'OPTIONS': {
        	        'read_default_file': '/etc/mysql/my.cnf',
    	},
        }
    }
else:
    #Development database
    DATABASES = {
        'default': {
            'ENGINE': os.environ.get('ENGINE'),
            'NAME': BASE_DIR / os.environ.get('NAME'),
        }
    }


# DATABASES = {
#     'default': {
#         'ENGINE': 'django.db.backends.sqlite3',
#         'NAME': BASE_DIR / 'db.sqlite3',
#     }
# }


# DATABASES = {
#     'default': {
#         'ENGINE': os.environ.get('ENGINE'),
#         'NAME': BASE_DIR / os.environ.get('NAME'),
#         'USER': os.environ.get('USER'),
#         'PASSWORD': os.environ.get('HOST'),
#         'HOST': os.environ.get('HOST'),
#         'PORT': os.environ.get('PORT'),
#     }
# }

# DATABASES = {
#     'default' : os.environ.get('DB')
# }


# Password validation
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]


# Internationalization
# https://docs.djangoproject.com/en/4.2/topics/i18n/

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'America/Chicago'

USE_I18N = True

USE_L10N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.2/howto/static-files/

STATIC_URL = '/static/'

STATIC_ROOT = BASE_DIR / "staticfiles"
#STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles/') 


# STATICFILES_DIRS = [
#     #BASE_DIR / "static",
#     BASE_DIR / "templates/static",
# ]

STATICFILES_DIRS = (
    os.path.join(BASE_DIR, 'static'),
)

FIXTURE_DIRS = (
    os.path.join(BASE_DIR, 'fixtures'),
)

# Media Files
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')

MEDIA_URL = '/media/'

# Default primary key field type
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field

DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

Are you using a standard web-front end, or a JavaScript-based front end (Vue, React, etc)?

How are you running Django? (gunicorn, uwsgi, Daphne, something else?)

What does your nginx configuration look like for this?

It is a standard frontend. I am running Django with gunicorn.

nginx:

server {
   listen 80 default;
    #server_name 192.168.110.230;
    #server_name 10.6.83.230;

   # listen 443 ssl;

    server_name mysite.com;

    location = /favicon.ico { access_log off; log_not_found off; }
    location /staticfiles/ {
        root /home/django/studio_management;
    }


    location /media/ {
        alias /home/django/studio_management/media/;
    }

    location / {
        include proxy_params;
        proxy_pass http://unix:/run/gunicorn.sock;
    }
}

Are you using one nginx server or two?

If two, then I also need to see the nginx configuration for the reverse proxy.

If only one, I don’t see the configuration for ssl.

I am using two.

Django project → Nginx → nginxproxymanager

# ------------------------------------------------------------
# mysite.com
# ------------------------------------------------------------


server {
  set $forward_scheme http;
  set $server         "192.168.110.230";
  set $port           80;

  listen 80;
listen [::]:80;

listen 443 ssl http2;
listen [::]:443 ssl http2;


  server_name mysite.com;


  # Let's Encrypt SSL
  include conf.d/include/letsencrypt-acme-challenge.conf;
  include conf.d/include/ssl-ciphers.conf;
  ssl_certificate /etc/letsencrypt/live/npm-14/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/npm-14/privkey.pem;


    # Force SSL
    include conf.d/include/force-ssl.conf;


  access_log /data/logs/proxy-host-24_access.log proxy;
  error_log /data/logs/proxy-host-24_error.log warn;

  location / {

    # Proxy!
    include conf.d/include/proxy.conf;
  }


  # Custom
  include /data/nginx/custom/server_proxy[.]conf;

'''

Also, after doing some more exploring, everything behind the login works fine.

I disabled SSL on the proxymanager, logged into the site, and am able to access all of my data, the only issue comes with posting forms.

I would suggest either removing, or tracing data through some of the layers here. I’d suggest issuing the GET of the login page and the POST that attempts to submit the credentials. You’ll want to look at the CSRF_TOKEN that is in the form data and the csrf cookie sent from the server to the browser. Then, on the way back, you’ll want to verify that the cookie and the token continue to be passed all the way back into Django.

It was a simple fix. I didn’t have the CSRF_TRUSTED_ORIGINS set

CSRF_TRUSTED_ORIGINS = ["https://my.site.com", "http://my.site.com"]