Need help to harden the security of Django? We can help you.
Developing a Django application can be quick and clean because its approach is flexible and scalable.
However, when it comes to production deployment, there are several ways to further secure the project.
As part of our Server Management Services, we assist our customers with several Django queries.
Today, let us see how to harden the security of Django.
Security of Django
Even though developing a Django application is a quick and clean experience, the production deployment requires several ways to further secure the project.
In order to secure the project, we can restructure it by breaking up the settings. This will allow us to easily set up different configurations based on the environment.
Similarly, leveraging
dotenv
for hiding environment variables or confidential settings will ensure that we do not release any details that may compromise the project. Implementation of these strategies and features might seem time-consuming. However, developing a practical workflow will allow us to deploy releases of the project without compromising on security or productivity.
In this article, we will leverage a security-oriented workflow for Django development by implementing and configuring environment-based settings,
dotenv
, and Django’s built-in security settings. All these features complement each other and will result in a version of the Django project that is ready for different approaches we may take to deployment.
In order to begin, our Support Techs suggest having a pre-existing Django project.
Harden the security of Django
In this article, we will use the
testsite
project as an example. An existing Django project may have different requirements. Let us discuss in detail how to harden the security of Django.
Step 1: Restructure Django’s Settings
Initially, let us rearrange the
settings.py
file into environment-specific configurations. This arrangement will mean less reconfiguration for different environments; instead, we will use an environment variable to switch between configurations.
We need to create a new directory called settings in the project’s sub-directory:
$ mkdir testsite/testsite/settings
This directory will replace the current
configuration file.settings.py
Once done, we create three Python files:
$ cd testsite/testsite/settings $ touch base.py development.py production.py
The
file contains settings we use during development. Thedevelopment.py
contains settings for use on a production server.production.py
We need to keep these separate since the production configuration will use settings that will not work in a development environment.
The
settings file contains settings thatbase.py
anddevelopment.py
will inherit from. This is to reduce redundancy and to help keep the code cleaner.production.py
These Python files will be replacing
, so we will now removesettings.py
to avoid confusing Django.settings.py
We rename
tosettings.py
with the following command:base.py
$ mv ../settings.py base.py
We have now come to the end of creating the outline of the new environment-based settings directory.
Step 2: Use python-dotenv
At the moment, Django will fail to recognize the new settings directory or its internal files. So, before we continue, we need to make Django work with
.python-dotenv
This is a dependency that loads environment variables from a
file. Django looks inside a .env file in the project’s root directory to determine which settings configuration it will use..env
Initially, we go to the project’s root directory:
$ cd ../../
Then we install
:python-dotenv
$ pip install python-dotenv
Now we need to configure Django to use
. In order to do this, we will edit,dotenv
, for development, andmanage.py
, for production.wsgi.py
Let us start by editing manage.py:
$ nano manage.py Add the following code: import os import sys import dotenv def main(): os.environ.setdefault(‘DJANGO_SETTINGS_MODULE’, ‘testsite.settings.development’) if os.getenv(‘DJANGO_SETTINGS_MODULE’): os.environ[‘DJANGO_SETTINGS_MODULE’] = os.getenv(‘DJANGO_SETTINGS_MODULE’) try: from django.core.management import execute_from_command_line except ImportError as exc: raise ImportError( “Couldn’t import Django. Are you sure it’s installed and ” “available on your PYTHONPATH environment variable? Did you ” “forget to activate a virtual environment?” ) from exc execute_from_command_line(sys.argv) if __name__ == ‘__main__’: main() dotenv.load_dotenv( os.path.join(os.path.dirname(__file__), ‘.env’) )
Save and close manage.py and then open wsgi.py for editing:
$ nano testsite/wsgi.py
Add the following highlighted lines:
import os import dotenv from django.core.wsgi import get_wsgi_application dotenv.load_dotenv( os.path.join(os.path.dirname(os.path.dirname(__file__)), ‘.env’) ) os.environ.setdefault(‘DJANGO_SETTINGS_MODULE’, ‘testsite.settings.development’) if os.getenv(‘DJANGO_SETTINGS_MODULE’): os.environ[‘DJANGO_SETTINGS_MODULE’] = os.getenv(‘DJANGO_SETTINGS_MODULE’) application = get_wsgi_application()
The code we have added looks for
file..env
If the file exists, we instruct Django to use the settings file that .env recommends, otherwise, we use the development configuration by default.
Save and close the file.
Finally, let us create a .env in the project’s root directory:
$ nano .env
Now add in the following line to set the environment to development:
DJANGO_SETTINGS_MODULE=”testsite.settings.development”
Add
to.env
file, to use this file to contain data such as passwords and API keys that we do not want visible publicly. Every environment the project is running on will have its own.gitignore
with settings for that specific environment..env
Our Support Techs recommend creating a
to include in the project. It will help to easily create a new.env.example
wherever we need one..env
So, by default Django will use
, but if we change DJANGO_SETTINGS_MODULE totestsite.settings.development
, it will start using the production configuration.testsite.settings.production
Step 3: Create Development and Production Settings
Moving ahead, we will open
and add the configurations to modify for each environment in separatebase.py
anddevelopment.py
files.production.py
Ensure that the production.py has the production database credentials.
We can determine the settings to configure, based on the environment. Here, we will only cover a common example for production and development settings.
Initially, we will move settings from base.py to development.py. To do that, open development.py:
$ nano testsite/settings/development.py
First, we will import from
– this file inherits settings frombase.py
. Then we will transfer the settings we want to modify for the development environment:base.py
from .base import * DEBUG = True DATABASES = { ‘default’: { ‘ENGINE’: ‘django.db.backends.sqlite3’, ‘NAME’: os.path.join(BASE_DIR, ‘db.sqlite3’), } }
In this case, the settings specific to development are:
, we need thisDEBUG
in development, but not in production; andTrue
, a local database instead of a production database. We are using an SQLite database here for development.DATABASES
For security purposes, Django’s DEBUG output will never display any settings that may contain the strings: API, KEY, PASS, SECRET, SIGNATURE, or TOKEN.
Next, let us add to production.py:
$ nano testsite/settings/production.py
Production will be similar to
, but with a different database configuration anddevelopment.py
set toDEBUG
:False
from .base import * DEBUG = False ALLOWED_HOSTS = [] DATABASES = { ‘default’: { ‘ENGINE’: os.environ.get(‘SQL_ENGINE’, ‘django.db.backends.sqlite3’), ‘NAME’: os.environ.get(‘SQL_DATABASE’, os.path.join(BASE_DIR, ‘db.sqlite3’)), ‘USER’: os.environ.get(‘SQL_USER’, ‘user’), ‘PASSWORD’: os.environ.get(‘SQL_PASSWORD’, ‘password’), ‘HOST’: os.environ.get(‘SQL_HOST’, ‘localhost’), ‘PORT’: os.environ.get(‘SQL_PORT’, ”), } }
For the example database configuration given, we can use
to configure each of the given credentials, with defaults included.dotenv
Hence we have configured the project to use different settings based on
inDJANGO_SETTINGS_MODULE
..env
Given the example settings here, when we set the project to use production settings,
becomesDEBUG
,False
is defined, and we start using a different database configured on the server.ALLOWED_HOSTS
Step 4: Work with Django’s Security Settings
Django includes security settings ready for us to add to the project. These settings are intended for use when the project is available to the public.
Our Support Engineers does not recommend using any of these settings in the development environment. Hence, we limit these settings to the
configuration.production.py
These settings are going to enforce the use of HTTPS for various web features, such as session cookies, upgrading HTTP to HTTPS, and so on. Therefore, if the server is not set up with a domain pointing to it, hold off on this section for now.
First, we open production.py:
$ nano production.py
In the file, add the highlighted settings that work for the project, according to the explanations following the code:
from .base import * DEBUG = False ALLOWED_HOSTS = [‘your_domain’, ‘www.your_domain’] DATABASES = { ‘default’: { ‘ENGINE’: os.environ.get(‘SQL_ENGINE’, ‘django.db.backends.sqlite3’), ‘NAME’: os.environ.get(‘SQL_DATABASE’, os.path.join(BASE_DIR, ‘db.sqlite3’)), ‘USER’: os.environ.get(‘SQL_USER’, ‘user’), ‘PASSWORD’: os.environ.get(‘SQL_PASSWORD’, ‘password’), ‘HOST’: os.environ.get(‘SQL_HOST’, ‘localhost’), ‘PORT’: os.environ.get(‘SQL_PORT’, ”), } } SECURE_SSL_REDIRECT = True SESSION_COOKIE_SECURE = True CSRF_COOKIE_SECURE = True SECURE_BROWSER_XSS_FILTER = True
-
SECURE_SSL_REDIRECT
This redirects all HTTP requests to HTTPS. Hence, the project will always try to use an encrypted connection. We need SSL configured on the server for this to work. However, if we have Nginx or Apache configured to do this already, this setting will be redundant.
-
SESSION_COOKIE_SECURE
This tells the browser that cookies can only be handled over HTTPS. Hence, cookies that the project produces for activities, such as logins, will only work over an encrypted connection.
-
CSRF_COOKIE_SECURE
It is the same as SESSION_COOKIE_SECURE but applies to the CSRF token. Django CSRF protection protects against Cross-Site Request Forgery by ensuring that the forms submitted to the project were created by the project and not a third party.
-
SECURE_BROWSER_XSS_FILTER
This sets the X-XSS-Protection: 1; mode=block header on all responses that do not already have it. This ensures third parties cannot inject scripts into the project.
Step 5: Use python-dotenv for Secrets
The final section will help us leverage
python-dotenv
. This allows us to hide certain information such as the project’s SECRET_KEY or the admin’s login URL. This is a great idea if we intend to publish the code on platforms like GitHub or GitLab since these secrets will not be published.
Instead, whenever we initially set up the project on a local environment or a server, we can create a new .env file and define those secret variables.
We must hide SECRET_KEY so we will start working on that in this section.
Initially, open .env file:
$ nano .env
And add the following line:
DJANGO_SETTINGS_MODULE=”django_hardening.settings.development” SECRET_KEY=”your_secret_key”
Then in base.py:
$ nano testsite/settings/base.py
Let us update the SECRET_KEY variable:
. . . SECRET_KEY = os.getenv(‘SECRET_KEY’) . . .
The project will now use the SECRET_KEY located in .env.
Finally, we will hide the admin URL by adding a long string of random characters to it. This will ensure bots cannot brute force the login fields and strangers cannot try guessing the login.
Open .env again:
$ nano .env
Then add a SECRET_ADMIN_URL variable:
DJANGO_SETTINGS_MODULE=”django_hardening.settings.development” SECRET_KEY=”your_secret_key” SECRET_ADMIN_URL=”very_secret_url”
Now we will tell Django to hide the admin URL with SECRET_ADMIN_URL:
$ nano /testsite/urls.py
Do not forget to replace
andyour_secret_key
with our own secret strings.very_secret_url
Python provides a fantastic secrets.py library for generating random strings for these variables.
Edit the admin URL like so:
import os from django.contrib import admin from django.urls import path urlpatterns = [ path(os.getenv(‘SECRET_ADMIN_URL’) + ‘/admin/’, admin.site.urls), ]
We can now find the admin login page at
.very_secret_url/admin/ instead of just /admin/
[Stuck with the procedures? We’d be happy to assist you]
Conclusion
To conclude, the project now leverages python-dotenv for handling secrets and settings. Today, we saw how our Support Techs go about to harden the security of Django
0 Comments