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 settings.py
configuration file.
Once done, we create three Python files:
$ cd testsite/testsite/settings
$ touch base.py development.py production.py
The development.py
file contains settings we use during development. The production.py
contains settings for use on a production server.
We need to keep these separate since the production configuration will use settings that will not work in a development environment.
The base.py
settings file contains settings that development.py
and production.py
will inherit from. This is to reduce redundancy and to help keep the code cleaner.
These Python files will be replacing settings.py
, so we will now remove settings.py
to avoid confusing Django.
We rename settings.py
to base.py
with the following command:
$ 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 .env
file. Django looks inside a .env file in the project’s root directory to determine which settings configuration it will use.
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 dotenv
. In order to do this, we will edit, manage.py
, for development, and wsgi.py
, for production.
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 .env
file.
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 .env
to .gitignore
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 .env
with settings for that specific environment.
Our Support Techs recommend creating a .env.example
to include in the project. It will help to easily create a new .env
wherever we need one.
So, by default Django will use testsite.settings.development
, but if we change DJANGO_SETTINGS_MODULE to testsite.settings.production
, it will start using the production configuration.
Step 3: Create Development and Production Settings
Moving ahead, we will open base.py
and add the configurations to modify for each environment in separate development.py
and production.py
files.
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 base.py
– this file inherits settings from base.py
. Then we will transfer the settings we want to modify for the development environment:
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: DEBUG
, we need this True
in development, but not in production; and DATABASES
, a local database instead of a production database. We are using an SQLite database here for development.
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 development.py
, but with a different database configuration and DEBUG
set to 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 dotenv
to configure each of the given credentials, with defaults included.
Hence we have configured the project to use different settings based on DJANGO_SETTINGS_MODULE
in .env
.
Given the example settings here, when we set the project to use production settings, DEBUG
becomes False
, ALLOWED_HOSTS
is defined, and we start using a different database configured on the server.
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 production.py
configuration.
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 your_secret_key
and very_secret_url
with our own secret strings.
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