This chapter covers:
Django hardening
REST API hardening
Deployment to production
In the previous chapter, we assembled a pseudo-decoupled Django project with the Django REST Framework and Vue.js.
It’s now time to explore the security implications of such a setup, which are not so dissimilar from running a monolith, but do require some extra steps due to the presence of the REST API. After a focus on security, in the second part of the chapter we cover deployment to production with Gunicorn and NGINX.
Django Hardening
Django is one of the most secure web frameworks out there.
However, it’s easy to let things slip out, especially when we are in a hurry to see our project up and running in production. Before exposing our website or our API to the world, we need to take care of some extra details to avoid surprises. It’s important to keep in mind that the suggestions provided in this chapter are far from exhaustive. Security is a huge topic, not counting that each project and each team might have different needs when it comes to security, due to regional regulations or governmental requirements.
Django Settings for Production
In Chapter 5, in the “Splitting the Settings File” section, we configured our Django project to use different settings for each environment.
As of now, we have the following settings:
To prepare the project for
production, we create another settings file in
decoupled_dj/settings/production.py, which will hold all the production-related settings. What should go in this file? Some of the most important settings for production in Django are:
SECURE_SSL_REDIRECT: Ensures that every request via HTTP gets redirected to HTTPS
ALLOWED_HOSTS: Drives what hostnames Django will serve
STATIC_ROOT: Is where Django will look for static files
In addition to these settings, there are also DRF-related configurations, which we touch on in the next sections. There are also a lot more authentication-related settings that we cover in Chapter
10. To start off, create
decoupled_dj/settings/production.py and configure it as shown in Listing
7-1.
from .base import * # noqa
SECURE_SSL_REDIRECT = True
ALLOWED_HOSTS = env.list("ALLOWED_HOSTS")
STATIC_ROOT = env("STATIC_ROOT")
Listing 7-1decoupled_dj/settings/production.py – The First Settings for Production
These settings will be read from an
.env file, depending on the environment. In development, we have the settings shown in Listing
7-2.
DEBUG=yes
SECRET_KEY=!changethis!
STATIC_URL=/static/
Listing 7-2The Development .env File
In production, we need to tweak this file according to the requirements we describe in
decoupled_dj/settings/production.py. This means we must deploy the
.env file shown in Listing
7-3.
ALLOWED_HOSTS=decoupled-django.com,static.decoupled-django.com
DEBUG=no
SECRET_KEY=!changethis!
STATIC_URL=https://static.decoupled-django.com
STATIC_ROOT=static/
Listing 7-3decoupled_dj/settings/.env.production.example - The Production .env File
It is of utmost importance in production to disable
DEBUG to avoid error leaking. In the previous file, note how the static related settings are slightly different from development:
With this basic configuration for production, we can move to harden our Django project a little bit more, with authentication.
Authentication and Cookies in Django
In the previous chapter, we configured a Vue.js single-page app, served from a Django view. Let’s review the code in
billing/views.py, which is summarized in Listing
7-4.
from django.views.generic import TemplateView
class Index(TemplateView):
template_name = "billing/index.html"
Listing 7-4billing/views.py - A TemplateView Serves the Vue.js SPA
Locally, we can access the view at
http://127.0.0.1:8000/billing/ after running the Django development server, which is fine. However, once the project goes live, nothing stops anonymous users from freely reaching the view and making unauthenticated requests. To harden our project, we can, first of all, require
authentication on the view with the
LoginRequiredMixin for class-based views. Open
billing/views.py and change the view, as shown in Listing
7-5.
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import TemplateView
class Index(LoginRequiredMixin, TemplateView):
template_name = "billing/index.html"
Listing 7-5billing/views.py - Adding Authentication to the Billing View
From now on, any user who wants to access this view must authenticate. For us at this stage, it’s enough to create a superuser in development with the following command:
python manage.py createsuperuser
Once this is done, we can authenticate through the admin view, and then visit
http://127.0.0.1:8000/billing/ to create new invoices. But as soon as we fill the form and click on Create Invoice, Django will return an error. In the Network tab of the browser’s console, after trying to submit the form, we should see the following error in the response from the server:
"CSRF Failed: CSRF token missing or incorrect."
Django has a protection against CSRF attacks, and it won’t let us submit AJAX requests without a valid CSRF token. In traditional Django forms, this token is usually included as a template tag, and it’s sent to the backend by the browser as a cookie. However, when the frontend is built entirely with JavaScript, the CSRF token must be retrieved from the cookie storage and sent alongside the request as a header. To fix this problem in our Vue.js app, we can use
vue-cookies, a convenient library for handling cookies. In a terminal, move to the Vue project folder called
billing/vue_spa and run the following command:
Next up, load the library in
billing/vue_spa/src/main.js, as shown in Listing
7-6.
...
import VueCookies from "vue-cookies";
Listing 7-6billing/vue_spa/src/main.js - Enabling Vue-Cookies
Finally, in
billing/vue_spa/src/components/InvoiceCreate.vue, grab the cookie and include it as a header, as outlined in Listing
7-7.
...
const csrfToken = this.$cookies.get("csrftoken");
fetch("/billing/api/invoices/", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRFToken": csrfToken
},
body: JSON.stringify(data)
})
.then(response => {
if (!response.ok) throw Error(response.statusText);
return response.json();
})
.then(json => {
console.log(json);
})
.catch(err => console.log(err));
...
Listing 7-7billing/vue_spa/src/components/InvoiceCreate.vue - Including the CSRF Token in the AJAX Request
To test things out, we can rebuild the Vue app with the following command:
npm run build -- --mode staging
After running Django, the creation of a new invoice at http://127.0.0.1:8000/billing/ should now work as expected.
Back to the authentication front. At this stage, we enabled the most straightforward authentication method in Django: session-based authentication. This is one of the most traditional and most robust authentication mechanisms in Django. It relies on sessions, saved in the Django database. When the user logs in with credentials, Django stores a session in the database and sends back two cookies to the user’s browser:
csrftoken and
sessionid. When the user makes requests to the website, the browser sends back these cookies, which Django validates against what has been stored in the database. Since HTTPS encryption is a mandatory requirement for websites these days, it makes sense to disable the transmission of
csrftoken and
sessionid over plain HTTP. To do so, we can add two configuration directives in
decoupled_dj/settings/production.py, as shown in Listing
7-8.
...
CSRF_COOKIE_SECURE = True
SESSION_COOKIE_SECURE = True
...
Listing 7-8decoupled_dj/settings/production.py - Securing Authentication Cookies
With CSRF_COOKIE_SECURE and SESSION_COOKIE_SECURE set to True, we ensure that session authentication related cookies are transmitted only over HTTPS.
Randomize the Admin URL
The built-in admin
panel is probably one of the most beloved Django features. However, the URL for this panel, which by default is
admin/, can be targeted by automated brute force attacks when the website is exposed online. To mitigate the issue, we can introduce a bit of randomness in the URL, by changing it to something not easily guessable. This change needs to happen in the project root
decoupled_dj/urls.py, as shown in Listing
7-9.
from django.urls import path, include
from django.contrib import admin
from django.conf import settings
urlpatterns = [
path("billing/", include("billing.urls", namespace="billing")),
]
if settings.DEBUG:
urlpatterns = [
path("admin/", admin.site.urls),
] + urlpatterns
if not settings.DEBUG:
urlpatterns = [
] + urlpatterns
Listing 7-9decoupled_dj/urls.py - Hiding the Real Admin URL in Production
This code tells Django to change the admin URL from admin/ to [email protected]/ when DEBUG is False. With this little change, we add a bit more protection to the admin panel. Let’s now see what we can do to improve the security of our REST API.
REST API Hardening
What is better than a REST API? A secure REST API, of course.
In the following sections, we will cover a set of strategies for improving the security posture of our REST API. To do so, we borrow some guidance from the REST Security Cheat Sheet by the OWASP foundation.
HTTPS Encryption and HSTS
HTTPS
is a must for every website these days.
By configuring SECURE_SSL_REDIRECT in our Django project, we ensure that our REST API is secured as well. When we cover deployment in the next sections, we will see that in our setup, NGINX provides SSL termination for our Django project. In addition to HTTPS, we can also configure Django to attach an HTTP header named Strict-Transport-Security to the response. By doing so, we ensure that browsers will connect to our websites only through HTTPS. This feature is called HSTS, and while Django has HSTS-related settings, it is common practice to add these headers at the webserver/proxy level. The website https://securityheaders.com offers a free scanner that can help in identifying what security headers can be added to the NGINX configuration.
Audit Logging
Audit logging refers to the practice of writing logs for each action carried in a system—be it a web application, a REST API, or a database—as a way to record “who did what” at a particular point in time.
Paired with a log aggregation system, audit logging is a great way to improve data security. The OWASP REST Security Cheat Sheet prescribes audit logging for REST APIs. Out of the box, Django already provides some minimal form of audit logging in the admin. Also, the user table in Django records the last login of each user in the system. But these two trails are far from being a full-fledged audit logging solution and do not cover the REST API. There are a couple of packages for Django to add audit logging capabilities:
django-simple-history
django-auditlog
django-simple-history can track changes on models. This capability, paired with access logging, can provide effective audit logging for Django projects. django-simple-history is a mature package, actively supported. On the other hand, django-auditlog provides the same functionalities, but it is still in development at the time of this writing.
Cross-Origin Resource Sharing
In a decoupled setup, JavaScript is the main consumer for REST and GraphQL APIs.
By default, JavaScript can request resources with
XMLHttpRequest or
fetch, as long as the server and the frontend live in the same origin. An origin in HTTP is the combination of the scheme or protocol, the domain, and the port. This means that the origin
http://localhost:8000 is not equal to
http://localhost:3000. When JavaScript attempts to fetch a resource from a different origin than its own, a mechanism known as
Cross-Origin Resource Sharing (CORS) kicks in the browser. In any REST or GraphQL project, CORS is necessary to control what origins can connect to the API. To enable
CORS in Django, we can install
django-cors-headers in our project with the following command:
pip install django-cors-headers
To enable the package, include
corsheaders in
decoupled_dj/settings/base.py, as shown in Listing
7-10.
INSTALLED_APPS = [
...
'corsheaders',
...
]
Listing 7-10decoupled_dj/settings/base.py - Enabling django-cors-headers in Django
Next up, enable the CORS middleware as much higher in the list of middleware, as shown in Listing
7-11.
MIDDLEWARE = [
...
'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.CommonMiddleware',
...
]
Listing 7-11decoupled_dj/settings/base.py - Enabling CORS Middleware
With this change in place, we can configure
django-cors-headers. In development, we may want to allow all origins to bypass CORS altogether. To
decoupled_dj/settings/development.py, add the configuration shown in Listing
7-12.
CORS_ALLOW_ALL_ORIGINS = True
Listing 7-12Decoupled_dj/settings/development.py - Relaxing CORS in Development
In production, we have to be more restrictive.
django-cors-headers allows us to define a list of allowed origins, which can be configured in
decoupled_dj/settings/production.py, as shown in Listing
7-13.
CORS_ALLOWED_ORIGINS = [
"https://example.com",
"http://another1.io",
"http://another2.io",
]
Listing 7-13decoupled_dj/settings/production.py - Hardening CORS in Production
Since we are using variables per environment, we can make this configuration directive a list, as shown in Listing
7-14.
CORS_ALLOWED_ORIGINS = env.list(
"CORS_ALLOWED_ORIGINS",
default=[]
)
Listing 7-14decoupled_dj/settings/production.py - Hardening CORS in Production
This way we can define allowed origins as a comma-separated list in .env for production. CORS is a basic form of protection for users, since without this mechanism in place, any website would be able to fetch and inject malicious code in the page, and a protection for REST APIs, which can explicitly allow a list of predefined origins instead of being open to the world. Of course, CORS does not absolutely replace authentication, which is covered briefly in the next section.
Authentication and Authorization in the DRF
Authentication
in the DRF integrates seamlessly with what Django already provides out of the box. By default, the DRF
authenticates the user with two classes,
SessionAuthentication and
BasicAuthentication, aptly named after the two most common authentication methods for websites. Basic authentication is a highly insecure authentication method, even under HTTPS, and it makes sense to disable it altogether to leave enabled at least only session-based authentication. To configure this aspect of the DRF, open
decoupled_dj/settings/base.py, add the
REST_FRAMEWORK dictionary, and configure the desired authentication classes, as shown in Listing
7-15.
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework.authentication.SessionAuthentication",
],
}
Listing 7-15decoupled_dj/settings/base.py - Tweaking Authentication for the Django REST Framework
In web applications, authentication refers to the “who you are?” part of the identification flow. Authorization instead looks at the “what can you do with your credentials” part. In fact, authentication alone is not enough to protect resources in a website or in a REST API. As of now, the REST API for our billing app is open to any user. Specifically, we need to secure two DRF views in
billing/api/views.py, summarized in Listing
7-16.
from .serializers import InvoiceSerializer
from .serializers import UserSerializer, User
from rest_framework.generics import CreateAPIView, ListAPIView
class ClientList(ListAPIView):
serializer_class = UserSerializer
queryset = User.objects.all()
class InvoiceCreate(CreateAPIView):
serializer_class = InvoiceSerializer
Listing 7-16billing/api/views.py – The DRF View for the Billing App
These two views handle the logic for the following endpoints:
/billing/api/clients/
/billing/api/invoices/
Right now, both are accessible by anyone. By default, the DRF does not enforce any form of permission on views. The default permission class is
AllowAny. To fix the security of all DRF views in the project, we can apply the
IsAdminUser permission globally. To do so, in
decoupled_dj/settings/base.py, we augment the
REST_FRAMEWORK dictionary with a permission class, as shown in Listing
7-17.
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework.authentication.SessionAuthentication",
],
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.IsAdminUser"
],
}
Listing 7-17decoupled_dj/setting/base.py - Adding Permissions Globally in the DRF
Permission classes can be set not only globally, but also on a single view, depending on the specific use case.
Disable the Browsable API
The DRF eases most of the mundane work of building REST APIs. When we create an endpoint, the DRF gives us a free web interface for interacting with the API. For example, for creation views, we can access an HTML form to create new objects through the interface. In this regard, the
browsable API is a huge boon for developers because it offers a convenient UI for interacting with the API. However, the interface can potentially leak data and expose too many details if we forget to protect the API. By default, the DRF uses
BrowsableAPIRenderer to render the browsable API. We can change this behavior by exposing only
JSONRenderer. This configuration can be placed in
decoupled_dj/settings/production.py, as shown in Listing
7-18.
...
REST_FRAMEWORK = {**REST_FRAMEWORK,
"DEFAULT_RENDERER_CLASSES": ["rest_framework.renderers.JSONRenderer"]
}
...
Listing 7-18decoupled_dj/setting/production.py - Disabling the Browsable API in Production
This disables the browsable API only in production.
Deploying a Decoupled Django Project
The modern cloud landscape offers endless possibilities to deploy Django.
It would be impossible to cover every single deployment style, not counting Docker, Kubernetes, and serverless setups. Instead, in this section, we employ one of the most traditional setups for Django in production. With the help of Ansible, a popular automation tool, we deploy Django, NGINX, and Gunicorn. Included in the source code for this chapter there is an Ansible playbook, which is helpful to replicate the setup on your own servers. From the preparation of the target machine to the configuration of NGINX, the following sections cover the deployment theory for the project we have built so far.
Preparing the Target Machine
To deploy Django, we need all the required packages in place: NGINX, Git, a newer version of Python, and Certbot for requesting SSL certificates.
The Ansible playbook covers the installation of these packages. In this chapter, we skip the installation of Postgres to keep things simple. The reader is encouraged to check the PostgreSQL download page to see the installation instructions. On the target system, there should also be an unprivileged user for the Django project. Once you’re done with these prerequisites, you can move to configure NGINX, the reverse proxy.
Configuring NGINX
In a typical production arrangement, NGINX works at the edge of the system.
It receives requests from the users, deals with SSL, and forwards these requests to a WSGI or ASGI server. Django lives behind this curtain. To configure NGINX, in this example, we use the domain name
decoupled-django.com and the subdomain
static.decoupled-django.com. The NGINX configuration for a typical Django project is composed of three sections at least:
One or more upstream declarations
A server declaration for the main Django entry point
A server declaration for serving static files
The
deployment/templates/decoupled-django.com.j2 file includes the whole configuration; here we outline just some details of the setup. The
upstream directive instructs NGINX about the location of the WSGI/ASGI server. Listing
7-19 shows the relevant configuration.
upstream gunicorn {
server 127.0.0.1:8000;
}
Listing 7-19deployment/templates/decoupled-django.com.j2 - Upstream Configuration for NGINX
In the first
server block, we tell NGINX to forward all the requests for the main domain to the
upstream, as shown in Listing
7-20.
server {
server_name {{ domain }};
location / {
proxy_pass http://gunicorn;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
## SSL configuration is managed by Certbot
}
Listing 7-20deployment/templates/decoupled-django.com.j2 - Server Configuration for NGINX
Here
{{ domain }} is an Ansible variable declared in the playbook. What’s important here is the
proxy_pass directive, which forwards requests to Gunicorn. In addition, in this section we also set headers for the proxy, which are handed down to Django on each request. In particular, we have:
X-Real-IP and X-Forwarded-For, which ensure that Django gets the IP address of the real visitor, not the address of the proxy
X-Forwarded-Proto, which tells Django which protocol the client is connecting with (HTTP or HTTPS)
Gunicorn and Django Production Requirements
In Chapter
3, we introduced asynchronous Django, and we used Uvicorn to run Django locally under ASGI. In production, we may want to run Uvicorn with
Gunicorn. To do so, we need to configure our dependencies for production. In the
requirements folder, create a new file named
production.txt. In this file, we declare all the dependencies for the ASGI part, as shown in Listing
7-21.
-r ./base.txt
gunicorn==20.0.4
uvicorn==0.13.1
httptools==0.1.1
uvloop==0.15.2
Listing 7-21requirements/production.txt - Production Requirements
This file should land in the Git repo, as it will be used in the deployment phase. Let’s now see how to prepare our Vue.js app for production.
Preparing Vue.js in Production with Django
In Chapter
6, we saw how to serve
Vue.js under Django in development. We configured
vue.config.js, and a file named
.env.staging inside the root folder of the Vue.js app. This time, we are going to ship things in production. This means we need a production Vue.js bundle which should be served by NGINX, not from Django anymore. In regard to static files, in production Django wants to know where it can find JavaScript and CSS. This is configured in
STATIC_URL, as in Listing
7-22, extracted from the beginning of this chapter.
...
STATIC_URL=https://static.decoupled-django.com/
STATIC_ROOT=static/
...
Listing 7-22decoupled_dj/settings/.env.production.example - Static Configuration for Production
Notice that we use
https://static.decoupled-django.com, and this subdomain must be configured in NGINX. Listing
7-23 shows the
subdomain configuration.
...
server {
server_name static.{{ domain }};
location / {
alias /home/{{ user }}/code/static/;
}
}
...
Listing 7-23deployment/templates/decoupled-django.com.j2 - Ansible Template for NGINX
Here,
{{ user }} is another variable defined in the Ansible playbook. After setting up Django and NGINX, to configure Vue.js so that it “knows” that it will be served from the above subdomain, we need to create another environment file in
billing/vue_spa, named
.env.production with the content shown in Listing
7-24.
VUE_APP_STATIC_URL=https://static.decoupled-django.com/billing/
Listing 7-24billing/vue_spa/.env.production - Production Configuration for Vue.js
This tells Vue.js that its bundle will be served from a specific subdomain/path. With the file in place, if we move to the
billing/vue_spa folder, we can run the following command:
npm run build -- --mode production
This will build the optimized Vue.js bundle in static/billing. We now need to push these files to the Git repo. After doing so, in the next section we finally see how to deploy the project starting right from this repo.
The Deployment
After building Vue for production locally and committing the files to the repo, we need to deploy the actual code to the target machine.
To do so, we log in as the unprivileged user created in the previous steps (the Ansible playbook defines a user called
decoupled-django) or with SSH. Once done, we clone the repo to a folder, which can be called
code for convenience:
git clone --branch chapter_07_security_deployment https://github.com/valentinogagliardi/decoupled-dj.git code
This command clones the repo for the project from the specified branch
chapter_07_security_deployment. When the code is in place, we move to the newly created folder, and we activate a Python virtual environment:
cd code
python3.8 -m venv venv
source venv/bin/activate
Next up, we install
production dependencies with the following command:
pip install -r requirements/production.txt
Before running Django, we need to configure the environment file for production. This file must be placed in
decoupled_dj/settings/.env. Extra care must be taken when managing this file, as it contains sensitive credentials and the Django secret key. In particular, .
env files should never land in source control. Listing
7-25 recaps the configuration directive for the production environment.
ALLOWED_HOSTS=decoupled-django.com,static.decoupled-django.com
DEBUG=no
SECRET_KEY=!changethis!
STATIC_URL=https://static.decoupled-django.com/
STATIC_ROOT=static/
Listing 7-25decoupled_dj/settings/.env.production.example - Environment Variables for Production
An example of this file is available in the source repo in
decoupled_dj/settings/.env.production.example. With this file in place, we can switch Django to production with the following command:
export DJANGO_SETTINGS_MODULE=decoupled_dj.settings.production
Finally, we can collect static assets with
collectstatic and apply migrations:
python manage.py collectstatic --noinput
python manage.py migrate
The first command will copy static files to
/home/decoupled-django/code/static, which are picked up by NGINX. In the Ansible playbook there is a series of tasks to automate all the steps presented here. Before running the project, we can create a superuser to access protected routes:
python manage.py createsuperuser
To test things out, still in
/home/decoupled-django/code, we can run Gunicorn with the following command:
gunicorn decoupled_dj.asgi:application -w 2 -k uvicorn.workers.UvicornWorker -b 127.0.0.1:8000 --log-file -
Again, the Ansible playbook covers the deployment from the Git repo as well. For most projects, Ansible is a good starting point to set up and deploy your Django projects. Other alternatives these days are Docker and Kubernetes, which more and more teams have fully internalized into their deployment toolchains.
Summary
We covered a lot in this chapter. We went over security and deployment. In the process, you learned that:
Django is quite secure by default, but extra measures must be taken when exposing a REST API
Django doesn’t work alone; a reverse proxy like NGINX is a must for production setups
There are many ways to deploy Django; a configuration tool like Ansible can work well in most cases
In the next chapter, we cover how Next.js, the React Framework, can be used as a frontend for Django.