15 minute guide to secure SaaS multitenancy with Django and Let's Encrypt

Published Apr 19, 2018
15 minute guide to secure SaaS multitenancy with Django and Let's Encrypt

Software as a Service is eating the world. Many SaaS providers use subdomain-based multitenancy with an address scheme like "customername.mywebsite.com." This tutorial will get you started on how to implement a secure semi-isolated multitenant application using Django. With many cloud providers offering a free tier and free SSL certificates from Let's Encrypt, you can start your SaaS MVP with zero cash investment.

Why Django?

Let me tell you how I started using Django. Ten years ago, I left my job in order to start a small consulting business. At the time, RIA (Rich Internet Applications) like Siverlight and Flex were all the rage, so I picked an open source contender called OpenLazlo.

I made a nice proof of concept and landed my first client, but after the first month, it was clear I would not meet my deadlines using this technology stack. Boy, think of a wrong choice: I can bet most readers have never heard of this platform.

I googled "fast development web applications" and most entries were about Ruby on Rails with Django in a distant second place. I wasn't smart enough to make it through the Rails tutorial, but the Django tutorial went like a breeze.

So I started from scratch learning Python/Django along the way and it saved my business. Django has paid my rent for the last decade, so you can bet I'm biased — for me, it is really "the framework for perfectionists with deadlines."

Multi-tenancy strategies

There are typically three solutions for solving the multitenancy problem:

  1. Isolated Approach: Separate databases where each tenant has its own database.

  2. Semi Isolated Approach: Shared Database and separate schemas with one database for all tenants, but one schema per tenant.

  3. Shared Approach: Shared Database and shared schema. All tenants share the same database and schema. There is a main tenant-table, where all other tables have a foreign key pointing to.

I'm using django-tenant-schemas and it is based on the strategy number two: semi isolated sharing the database but using one namespace (schema) for each client. This approach has a good compromise between security, simplicity, and performance.

  • Simplicity: barely make any changes to your current code to support multitenancy. Plus, you only manage one database.

  • Performance: make use of shared connections, buffers, and memory.

Each solution has its upsides and downsides. For a more in-depth discussion, see Microsoft’s excellent article on Multi-Tenant Data Architecture.

Basic setup for django-tenant-schemas

I will not waste space here talking about how to bootstrap a Django project as I can't possibly do it better then the project's documentation.

In order to use django-tenant-schemas, we must make a few changes in settings.py. First, we change the database engine:

DATABASES = {
    'default': {
        'ENGINE': 'tenant_schemas.postgresql_backend',
        # ...
    }
}

Then we add a database router:

DATABASE_ROUTERS = (
    'tenant_schemas.routers.TenantSyncRouter',
)

Next, we add the middleware tenant_schemas.middleware.TenantMiddleware to the top of MIDDLEWARE_CLASSES, so that each request can be set to use the correct schema:

MIDDLEWARE_CLASSES = (
    'tenant_schemas.middleware.TenantMiddleware',
    # ...
)

There are other middlewares available. Please refer to the docs for details. We also need a template context processor:

TEMPLATES = [
    {
        'BACKEND': # ...
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                # ...
                'django.template.context_processors.request',
                # ...
            ]
        }
    }
]

We must define which applications are shared (SHARED_APPS) and which applications are tenant-specific (TENANT_APPS) — we also must set the tenant model.

# at settings.py
SHARED_APPS = (
    'tenant_schemas',  # mandatory, should always be before any django app
    'customers', # you must list the app where your tenant model resides in

    'django.contrib.contenttypes',

    # everything below here is optional
    'django.contrib.auth',
    'django.contrib.sessions',
    'django.contrib.sites',
    'django.contrib.messages',
    'django.contrib.admin',
)

TENANT_APPS = (
    'django.contrib.contenttypes',

    # your tenant-specific apps
    'myapp.hotels',
    'myapp.houses',
)

INSTALLED_APPS = (
    'tenant_schemas',  # mandatory, should always be before any django app

    'customers',
    'django.contrib.contenttypes',
    'django.contrib.auth',
    'django.contrib.sessions',
    'django.contrib.sites',
    'django.contrib.messages',
    'django.contrib.admin',
    'myapp.hotels',
    'myapp.houses',
)

TENANT_MODEL = "customers.Client"

At customers/models.py, you define a model inheriting from TenantMixin:

from django.db import models
from tenant_schemas.models import TenantMixin

class Client(TenantMixin):
    name = models.CharField(max_length=100)
    paid_until =  models.DateField()
    on_trial = models.BooleanField()
    created_on = models.DateField(auto_now_add=True)

    # default true, schema will be automatically created and synced when it is saved
    auto_create_schema = True

Now you must create your app migrations for customers:

$ python manage.py makemigrations customers

The command migrate_schemas --shared will create the shared apps on the public schema. Note: your database should be empty if this is the first time you’re running this command.

$ python manage.py migrate_schemas --shared

There are other optional steps, for example, if you want separate projects for the main website and tenants. Please check the documentation.

Getting a wildcard certificate

Let’s Encrypt is a free, automated, and open Certificate Authority. It is sponsored by a diverse group of organizations, from non-profits to Fortune 100 companies. They use a protocol called ACME (Automatic Certificate Management Environment).

With the version two of ACME, Let's Encrypt is offering wildcard SSL certificates for free — they are 500 USD/year or more at the typical commercial CA.

ACME V2 uses DNS for authentication. In the following example, I'm using DigitalOcean as my DNS provider, but there are other authentication plugins covering many popular platforms.

First, create an ini file containing your API key:

$ echo dns_digitalocean_token = 66906...your.key.here...864a > do-api.ini

The Let's Encrypt client is called certbot. Hopefully it will be just sudo apt install certbot or sudo yum install certbot in a couple weeks, but the most recent version has not hit your distro official package store, so you may have to get it from GitHub:

$ git clone https://github.com/certbot/certbot.git
$ cd certbot
$ ./certbot-auto --os-packages-only
$ ./tools/venv.sh
$ source venv/bin/activate

If there is a plugin for your DNS provider, getting a certificate is pretty easy:

$ certbot certonly --dns-digitalocean \
   --dns-digitalocean-credentials do-api.ini \
   --dns-digitalocean-propagation-seconds 60 \
   -d '*.mywebsite.com' -d mywebsite.com \
   --server https://acme-v02.api.letsencrypt.org/directory

If not, you will have to use the --manual option and update your DNS records by hand.

Webserver settings

Unfortunately, deploying Django applications is not as easy as PHP — check the docs about Django deployment.

The basic virtual host configuration for Apache looks like the following:

<VirtualHost 127.0.0.1:8080>
  ServerName mywebsite.com
  ServerAlias *.mywebsite.com mywebsite.com
  WSGIScriptAlias / "/path/to/django/scripts/mywebsite.wsgi"
  SSLEngine on
  SSLCertificateFile /etc/letsencrypt/live/mywebsite.com/cert.pem
  SSLCertificateKeyFile /etc/letsencrypt/live/mywebsite.com/privkey.pem
  SSLCertificateChainFile /etc/letsencrypt/live/mywebsite.com/fullchain.pem
</VirtualHost>

Conclusion

I hope you have enough information to get your secure multitenant web application up and running. I often answer questions at Stack Overflow so reach me there if you get stuck.

Discover and read more posts from Paulo Scardine
get started