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.pyDEBUG = FalseALLOWED_HOSTS = ["quanlychitieu.vn", "www.quanlychitieu.vn"]SECURE_SSL_REDIRECT = TrueSECURE_HSTS_SECONDS = 31536000SECURE_HSTS_INCLUDE_SUBDOMAINS = TrueSECURE_HSTS_PRELOAD = TrueSESSION_COOKIE_SECURE = TrueCSRF_COOKIE_SECURE = TrueSESSION_COOKIE_HTTPONLY = TrueSESSION_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.pyCSRF_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.pyINSTALLED_APPS += ["axes"]AUTHENTICATION_BACKENDS = [ "axes.backends.AxesStandaloneBackend", "django.contrib.auth.backends.ModelBackend",]AXES_FAILURE_LIMIT = 5AXES_COOLOFF_TIME = 1 # giờAXES_LOCKOUT_TEMPLATE = "lockout.html"
# config/urls.pyfrom axes.decorators import axes_dispatchurlpatterns = [ 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
- Django Running in Production #10: Celery in Production — Workers, Beat, Flower, systemd, and Retries — the previous episode: background tasks running reliably under systemd.
- Django Running in Production #9: Caching with Redis — Cache Framework, Invalidation Patterns, and Avoiding Stale Data — Redis setup reused by Celery as its broker.
- Django Production Operations #3: Gunicorn + Nginx — Running Django Behind a Reverse Proxy — the reverse proxy in front of Django, including forwarding the headers HTTPS checks rely on.
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
- Django Documentation — Deployment checklist: https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/
- django-axes Documentation: https://django-axes.readthedocs.io/
- OWASP Cheat Sheet — Django Security: https://cheatsheetseries.owasp.org/cheatsheets/Django_Cheat_Sheet.html
