© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2021
V. GagliardiDecoupled Django https://doi.org/10.1007/978-1-4842-7144-5_7

7. API Security and Deployment

Valentino Gagliardi1  
Colle di Val D’Elsa, Italy
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.


In the first part of this chapter, we assume you are in the repo root decoupled-dj, with the Python virtual environment active, and with DJANGO_SETTINGS_MODULE configured as decoupled_dj.settings.development.

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:
  • decoupled_dj/settings/base.py

  • decoupled_dj/settings/development.py

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
Listing 7-1

decoupled_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.
DATABASE_URL=psql://decoupleddjango:[email protected]/decoupleddjango
Listing 7-2

The Development .env File


How does DEBUG work here if we pass yes instead of a Boolean? The conversion is handled by django-environ for us.

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.
DATABASE_URL=psql://decoupleddjango:[email protected]/decoupleddjango
Listing 7-3

decoupled_dj/settings/.env.production.example - The Production .env File


The database settings shown here assume we are using Postgres as the database for the project. To use SQLite instead, change the database configuration to DATABASE_URL=sqlite:/decoupleddjango.sqlite3.

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:
  • STATIC_URL is now configured to read static assets from a static.decoupled-django.com subdomain

  • STATIC_ROOT in production will read files from the static folder

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-4

billing/views.py - A TemplateView Serves the Vue.js SPA

Locally, we can access the view at 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-5

billing/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 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:
npm i vue-cookies
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-6

billing/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 => {
       .catch(err => console.log(err));
Listing 7-7

billing/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 should now work as expected.


A popular alternative to Fetch, axios can help with an interceptor feature. It’s convenient for attaching cookies or other headers globally, on each request.

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.
Listing 7-8

decoupled_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 = [
       path("[email protected]/", admin.site.urls),
   ] + urlpatterns
Listing 7-9

decoupled_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.
Listing 7-10

decoupled_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.
Listing 7-11

decoupled_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.
Listing 7-12

Decoupled_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.
Listing 7-13

decoupled_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.
Listing 7-14

decoupled_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.
Listing 7-15

decoupled_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-16

billing/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.
Listing 7-17

decoupled_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.


We could also enforce these checks only in decoupled_dj/settings/production.py. This means we won’t be bothered by authentication in development. However, I prefer to apply authentication and authorization globally to ensure a more realistic scenario, particularly in testing.

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.
   "DEFAULT_RENDERER_CLASSES": ["rest_framework.renderers.JSONRenderer"]
Listing 7-18

decoupled_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.


The source code for the Ansible playbook is at https://github.com/valentinogagliardi/decoupled-dj/blob/chapter_07_security_deployment/deployment/site.yml. Instructions on how to launch the playbook can be found in the README.

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.


The Ansible playbook expects Ubuntu as the operating system used for the deployment; a version not older than Ubuntu 20.04 LTS is enough.

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 {
Listing 7-19

deployment/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-20

deployment/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
Listing 7-21

requirements/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.
Listing 7-22

decoupled_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-23

deployment/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.
Listing 7-24

billing/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.


In real-world projects, production JavaScript bundles are not directly pushed to source control. Instead, a continuous integration/deployment system takes care of building production assets, or Docker images, after all the test suites pass.

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.
DATABASE_URL=psql://decoupleddjango:[email protected]/decoupleddjango
Listing 7-25

decoupled_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 --log-file -
The Ansible playbook also includes a Systemd service for setting up Gunicorn at boot. If everything goes well, we can access https://decoupled-django.com/[email protected]/, log in to the website with the superuser credentials, and visit https://decoupled-django.com/billing/, where our Vue.js app lives. Figure 7-1 shows the result of our work.
Figure 7-1

Django and the Vue.js app deployed in production

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.


It is a good moment to commit the changes you made so far, and to push the work to your Git repo. You can find the source code for this chapter at https://github.com/valentinogagliardi/decoupled-dj/tree/chapter_07_security_deployment.


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.

Additional Resource

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.