This episode locks down the expense-tracking app before opening it to real users: enable HTTPS and HSTS, tighten ALLOWED_HOSTS, CSRF, CORS and the SECURE_* flags, then block password guessing with django-axes. Following episode 10, where Celery now runs reliably under systemd, this is the final defensive layer before real traffic arrives.

1. Turn Off DEBUG and Tighten ALLOWED_HOSTS

DEBUG = True in production is the most serious vulnerability: Django’s error pages expose settings, environment variables and the code around the error. Turn DEBUG off, then list only the real domain names in ALLOWED_HOSTS so Django rejects any request carrying an unfamiliar Host header.

# config/settings/prod.py
DEBUG = False
ALLOWED_HOSTS = ["quanlychitieu.vn", "www.quanlychitieu.vn"]
SECURE_SSL_REDIRECT = True
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = "Lax"
CSRF_COOKIE_SAMESITE = "Lax"

2. Enforce HTTPS and Enable HSTS

SECURE_SSL_REDIRECT forces every HTTP request over to HTTPS, so there is no longer any plain channel for passwords or tokens to travel on. HSTS goes further: the browser remembers to use only HTTPS with this domain for a year. Only enable HSTS_PRELOAD once every subdomain already has HTTPS, because getting removed from the preload list is very difficult.

3. Lock Down Session and CSRF Cookies

Session and CSRF cookies must carry the Secure flag so the browser only sends them over HTTPS, and HttpOnly so JavaScript cannot read the sessionid — an XSS hole then cannot escalate into a hijacked account. SameSite=Lax stops the browser from attaching cookies to requests from another site, reducing CSRF risk without breaking the login flow.

4. CSRF_TRUSTED_ORIGINS and CORS for the API

Since Django 4.0, a POST form over HTTPS is rejected with a 403 if its origin is not in CSRF_TRUSTED_ORIGINS — this error often appears right after HTTPS is switched on, and the correct fix is to declare the right origin, not to turn CSRF off. For the DRF API, django-cors-headers opens CORS only for your own frontend.

# config/settings/prod.py
CSRF_TRUSTED_ORIGINS = ["https://quanlychitieu.vn"]
CORS_ALLOWED_ORIGINS = ["https://quanlychitieu.vn"]
CORS_ALLOW_CREDENTIALS = True

5. Block Brute-Force Attacks with django-axes

Even strong passwords can be guessed if an attacker gets unlimited attempts. django-axes locks an account after a few failed logins, records the IP of each attempt, and unlocks automatically after a cool-off period. Attach axes to the admin and the login page, where the financial data lives.

# config/settings/prod.py
INSTALLED_APPS += ["axes"]
AUTHENTICATION_BACKENDS = [
"axes.backends.AxesStandaloneBackend",
"django.contrib.auth.backends.ModelBackend",
]
AXES_FAILURE_LIMIT = 5
AXES_COOLOFF_TIME = 1 # giờ
AXES_LOCKOUT_TEMPLATE = "lockout.html"
# config/urls.py
from axes.decorators import axes_dispatch
urlpatterns = [
path("admin/", axes_dispatch(admin.site.urls)),
]

6. Run check –deploy Before Every Release

Django ships with a security check for production: python manage.py check –deploy lists every setting that falls short, from SECURE_HSTS_SECONDS to X_FRAME_OPTIONS. Add this command to the CI pipeline in episode 17, and run it by hand right now — an automated checklist is always more reliable than memory.

python manage.py check --deploy

Apply It to Your Project

Do it tonight: turn DEBUG off, put the correct domain in ALLOWED_HOSTS, switch on all the SECURE_* flags in prod settings, declare CSRF_TRUSTED_ORIGINS, then run check –deploy and fix warnings until none remain. After that, install django-axes and try logging in wrong five times yourself to see the lockout page.

Related Posts

Frequently Asked Questions

Once HSTS is on, can I go back to HTTP?

Not easily: browsers that have recorded the policy will force HTTPS for the whole HSTS duration, and the preload list is almost impossible to leave. So only enable HSTS once HTTPS has run stably for a few weeks.

How do I fix a 403 CSRF error after enabling HTTPS?

Add the correct HTTPS origin to CSRF_TRUSTED_ORIGINS — for example https://quanlychitieu.vn — then check that Nginx forwards the X-Forwarded-Proto header correctly. Never disable CsrfViewMiddleware just to “make it work”.

What if django-axes locks out a real user by mistake?

The user waits out AXES_COOLOFF_TIME and logs in again, or an admin unlocks them manually in the axes admin interface. Set the failure limit to 5 attempts and enable notifications so you can spot abnormal mass lockouts.

References