Quickstart from source

This guide provides instructions for running your own certificate authority by installing django-ca from source. This method requires a lot of manual configuration and a lot of expert knowledge, but is a good choice if you use an exotic system or other options do not work for you for some reason. If you’re looking for a faster and easier option, you might consider using Docker Compose.

Note

This tutorial uses structured-tutorials.

This means that the documentation you see here is rendered from a configuration file and can also be run locally to verify correctness and completeness.

Note

All commands below assume that you have a shell with superuser privileges.

This tutorial will give you a CA with

  • A root and intermediate CA.

  • A browsable admin interface, protected by TLS (using certificates signed by your CA).

  • Certificate revocation using CRLs and OCSP.

  • (Optional) ACMEv2 support (= get certificates using certbot).

Requirements

This guide assumes that you have moderate knowledge of running servers, installing software and how TLS certificates work.

The guide assumes you use a dedicated server to set up your certificate authority and does not account for potential conflicts with other software like ports, directories or container names.

A certificate authority needs plain HTTP for CRL and OCSP access and HTTPS for ACMEv2. Using standard ports are strongly recommended (clients might fail otherwise). If you do not need any of the mentioned features, you can use all other features from the command line and do not need a web server.

Setup DNS

Before you set up your certificate authority, you need a domain name that points to the server where you install django-ca. Since the domain is encoded in CA certificates, it cannot be easily changed later.

This guide assumes that ca.example.com is the name that points to the server where you are setting up your certificate authority.

Required software

To run django-ca, you need Python, a database and a web server (like NGINX or Apache).

Whenever this tutorial installs software, it assumes a Debian/Ubuntu based system. If you use a different distribution, refer to their manuals for instructions. We assume that the APT cache is up to date and you have some basics installed:

root@host:~# apt update
root@host:~# apt dist-upgrade
root@host:~# apt install python3 curl

Install database

django-ca will not run without a supported database. This tutorial will show you how to use PostgreSQL or MariaDB. To install either:

root@host:~# apt install build-essential postgresql postgresql-client libpq-dev
root@host:~# apt install build-essential pkg-config mariadb-server mariadb-client \
>    libmariadb-dev

Install cache

Using a distributed cache is highly recommended. This tutorial will show you how to use Memcached or Redis. To install either:

root@host:~# apt install redis-server
root@host:~# apt install memcached

Install broker for Celery

Using Celery is optional but also highly recommended for performance and security reasons. If you use Celery, the web server does not need to use the private key (or the HSM storing the private key). If you want to split the setup across multiple hosts, only those running Celery need to be able to sign data (certificates, CRLs, …).

For Celery, you’ll need a broker. Redis can double as a broker, but you can use a variety of other systems. This tutorial will show you how to use Redis or RabbitMQ.

Redis was already installed above, as it can double as a cache. If you want to use RabbitMQ instead:

root@host:~# apt install redis-server
root@host:~# apt install rabbitmq-server

Install uv

Additionally, this guide uses uv to set up a Python Virtual Environment. Please refer to the installation instructions for how to install it. On most Linux systems you can simply run:

root@host:~# curl -LsSf https://astral.sh/uv/install.sh | sh
downloading uv ...

Later in the tutorial, we will let uv manage python versions. This allows you to use the newest Python version regardless of what your distribution (version) offers. uv will install Python versions in /root/ by default, which then can not be accessed by Gunicorn and Celery (which run with a lower-privileged user):

root@host:~# export UV_PYTHON_INSTALL_DIR=/opt/django-ca/python
root@host:~# echo "export UV_PYTHON_INSTALL_DIR=/opt/django-ca/python" | tee -a ~/.profile
root@host:~# uv python install

Environment

To make the guide less error-prone, we export the domain name for your certificate authority to $HOSTNAME. In all commands below assume that you have set the environment variable like this:

root@host:~# export HOSTNAME=ca.example.com

Installation

With this guide, you will install django-ca to /opt/django-ca/, with your local configuration residing in /etc/django-ca/.

Start by creating a system user and some essential directories:

root@host:~# adduser --system --group --disabled-login --home=/opt/django-ca/home django-ca
Adding system user `django-ca' ...
root@host:~# mkdir -p /opt/django-ca/src/django-ca /etc/django-ca /opt/django-ca/home/files
root@host:~# chown django-ca:django-ca /opt/django-ca/home/files
root@host:~# chmod go-rwx /opt/django-ca/home/files

Create database

Create a database and make sure to use a randomly generated password. You will need to again when configuring django-ca:

root@host:~# openssl rand -base64 32
...
root@host:~# sudo -u postgres psql
postgres=# CREATE DATABASE django_ca;
CREATE DATABASE
postgres=# CREATE USER django_ca WITH ENCRYPTED PASSWORD '...';
CREATE ROLE
postgres=# GRANT ALL PRIVILEGES ON DATABASE django_ca TO django_ca;
GRANT
root@host:~# openssl rand -base64 32
...
root@host:~# mariadb
....
MariaDB [(none)]> CREATE DATABASE django_ca CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
Query OK, 1 row affected (0.000 sec)

MariaDB [(none)]> CREATE USER 'django_ca'@'localhost' IDENTIFIED BY '...';
Query OK, 0 rows affected (0.001 sec)

MariaDB [(none)]>
MariaDB [(none)]> GRANT ALL PRIVILEGES ON django_ca.* TO 'django_ca'@'localhost';
Query OK, 0 rows affected (0.001 sec)

MariaDB [(none)]> FLUSH PRIVILEGES;
Query OK, 0 rows affected (0.000 sec)

Setup broker

If you use Redis as a broker, you can use it out of the box. For RabbitMQ, a little configuration is necessary:

root@host:~# python3 -c "import secrets, string; print(secrets.token_urlsafe(32))"
...
root@host:~# rabbitmqctl delete_user guest
Deleting user "guest" ...
root@host:~# rabbitmqctl add_vhost django_ca
Adding vhost "django_ca" ...
root@host:~# rabbitmqctl add_user django_ca
Adding user "django_ca" ...
root@host:~# rabbitmqctl set_permissions -p django_ca django_ca ".*" ".*" ".*"
Setting permissions for user "django_ca" in vhost "django_ca" ...

Get the source

You can clone django-ca from git or download an archive from GitHub. In the example below, we extract the source to /opt/django-ca/src/ and create a symlink without a version so that you can roll back to old versions during an update:

root@host:~# cd /opt/django-ca/src
root@host:/opt/django-ca/src# curl -Lo django-ca-3.0.0.tar.gz \
>    https://github.com/mathiasertl/django-ca/releases/download/3.0.0/django_ca-3.0.0.tar.gz
root@host:/opt/django-ca/src# export SETUPTOOLS_SCM_PRETEND_VERSION_FOR_django_ca=3.0.0
root@host:/opt/django-ca/src# tar xf django-ca-3.0.0.tar.gz
root@host:/opt/django-ca/src# ln -s django-ca-3.0.0 django-ca
root@host:~# apt install git
root@host:~# cd /opt/django-ca/src
root@host:/opt/django-ca/src# git clone https://github.com/mathiasertl/django-ca.git \
>    -v 3.0.0

Create a virtualenv

We use uv to create and manage the Python environment. By default, uv will manage both a local Python installation and virtualenv for you, but you can instruct it to use the system Python installation (try uv sync --help).

root@host:/opt/django-ca/src# cd django-ca
root@host:/opt/django-ca/src/django-ca# uv sync -p `uv python find` --no-default-groups \
>    --group gunicorn --all-extras --no-extra mysql
root@host:/opt/django-ca/src# cd django-ca
root@host:/opt/django-ca/src/django-ca# uv sync -p `uv python find` --no-default-groups \
>    --group gunicorn --all-extras --no-extra postgres

Depending on your needs you might also want to disable other extras as well. This is a list of all currently available extras:

Extra

Description

api

Adds support for the REST API using Django Ninja.

celery

Adds Celery support.

hsm

Adds support for hardware security modules (HSMs).

memcached

Adds support for Memcached.

mysql

Adds MySQL support.

postgres

Adds PostgreSQL support using Psycopg 3.

redis

Adds Redis support (usable as both cache and Celery message transport).

yaml

Adds support for YAML configuration files (only used when installing as a full project).

You can of course use a regular virtualenv and pip to manage your environment as well. For example:

root@host:~# cd /opt/django-ca/src/django-ca/
root@host:/opt/django-ca/src/django-ca/# python3 -m venv .venv/
root@host:/opt/django-ca/src/django-ca/# .venv/bin/pip install -U \
>     pip setuptools wheel
root@host:/opt/django-ca/src/django-ca/# .venv/bin/pip install -U \
>     -e /opt/django-ca/src/django-ca[api,hsm,postgres,celery,redis,yaml]

Add SystemD services

SystemD services are included with django-ca. You need to add three services, one for the Gunicorn application server (django-ca), one for the Celery task worker (django-ca-celery) and one for the Celery task scheduler (django-ca-celerybeat):

root@host:/opt/django-ca/src/django-ca# ln -s \
>    /opt/django-ca/src/django-ca/systemd/systemd.conf /etc/django-ca/
root@host:/opt/django-ca/src/django-ca# ln -s /opt/django-ca/src/django-ca/systemd/*.socket \
>    /etc/systemd/system/
root@host:/opt/django-ca/src/django-ca# ln -s /opt/django-ca/src/django-ca/systemd/*.service \
>    /etc/systemd/system/
root@host:/opt/django-ca/src/django-ca# systemctl daemon-reload
root@host:/opt/django-ca/src/django-ca# systemctl enable django-ca django-ca-celery \
>    django-ca-celerybeat
Created symlink ...

Note that the services will not yet start due to missing configuration.

If you use an installation directory other then /opt/django-ca, set INSTALL_BASE in /etc/systemd/systemd-local.conf (see Secure configuration) and add a SystemD override for WorkingDirectory=.

Configuration

django-ca will load configuration from all *.yaml files in /etc/django-ca/ in alphabetical order. These files can contain any Django setting, Celery setting or django-ca setting.

If you (mostly) followed the above examples, you can symlink conf/source/00-settings.yaml to /etc/django-ca and just override a few settings in /etc/django-ca/10-localsettings.yaml. To create the symlink:

root@host:/opt/django-ca/src/django-ca# ln -s \
>    /opt/django-ca/src/django-ca/conf/source/00-settings.yaml /etc/django-ca/

And then simply create a minimal /etc/django-ca/10-localsettings.yaml - but you can override any other setting here as well:

/etc/django-ca/10-localsettings.yaml
# django-ca local settings file. For more information:
#   https://django-ca.readthedocs.io/en/latest/settings.html for more information.

# The hostname for your CA.
# WARNING: Changing this requires new CAs (because the hostname goes into the certificates).
CA_DEFAULT_HOSTNAME: "ca.example.com"

# The URL base path used for ACMEv2/OCSP/CRL URLs. If given, the path **must** end with a slash.
#
# If you're upgrading from a previous version and have existing CAs, uncomment or set to "django_ca/".
#
# WARNING: Changing this requires new CAs (because the path goes into the certificates).
DJANGO_CA_CA_URL_PATH: ""

# Secret key used by this installation. Generate e.g. with "openssl rand -base64 32".
SECRET_KEY: "..."

Please see Custom settings for a list of available settings.

Configure the database

Configure database access in a dedicated configuration file. Use the PASSWORD you used when you created a database:

/etc/django-ca/20-database.yaml
DATABASES:
    default:
        ENGINE: django.db.backends.postgresql
        NAME: django_ca
        USER: django_ca
        HOST: localhost
        PORT: 5432
        PASSWORD: "..."
/etc/django-ca/20-database.yaml
DATABASES:
    default:
        ENGINE: django.db.backends.mysql
        NAME: django_ca
        USER: django_ca
        PASSWORD: "..."
        HOST: localhost
        PORT: 3306
        OPTIONS:
          charset: utf8mb4

Configure the cache

Configure the cache in a dedicated configuration file:

/etc/django-ca/20-cache.yaml
CACHES:
    default:
        BACKEND: django.core.cache.backends.redis.RedisCache
        LOCATION: redis://localhost:6379
        OPTIONS:
            # WARNING: When using Redis both as cache and message broker for
            #   Celery, ensure that the Redis db is different between the two.
            db: "1"
/etc/django-ca/20-cache.yaml
CACHES:
    default:
        BACKEND: django.core.cache.backends.memcached.PyMemcacheCache
        LOCATION: "127.0.0.1:11211"

Configure broker

Configure the Broker used by Celery in a dedicated configuration file:

/etc/django-ca/30-broker.yaml
# Use Redis as a broker.
# WARNING: When using Redis both as cache and message broker for Celery, ensure
#   that the Redis db is different between the two.
CELERY_BROKER_URL: redis://localhost:6379/0
/etc/django-ca/30-broker.yaml
# Use RabbitMQ as a broker.
CELERY_BROKER_URL: "amqp://django_ca:@127.0.0.1:5672/django_ca"

Configure Gunicorn

Gunicorn requires a dedicated configuration file. Minimal default settings are included in django-ca:

root@host:/opt/django-ca/src/django-ca# ln -s \
>    /opt/django-ca/src/django-ca/gunicorn/gunicorn.conf.py /etc/django-ca/

If you need different Gunicorn settings, you’ll have to copy and modify the file instead.

Secure configuration

Since the configuration contains sensitive information (database password, etc), make sure it is not world-readable:

root@host:/opt/django-ca/src/django-ca# chown -R root:django-ca /etc/django-ca
root@host:/opt/django-ca/src/django-ca# chmod -R u+rwX,g+rX,o-rwx /etc/django-ca

SystemD configuration

When you added SystemD services you also created a symlink for /etc/django-ca/systemd.conf. If settings there do not suit you, you can override them in /etc/django-ca/systemd-local.conf.

Add manage.py shortcut

As optional convenience, you can create a symlink to a small wrapper script that allows you to easily run manage.py commands. In the examples below the guide assumes you created this symlink at /usr/local/bin/django-ca, but of course you can name the symlink anything you like:

root@host:/opt/django-ca/src/django-ca# ln -s \
>    /opt/django-ca/src/django-ca/conf/source/manage /usr/local/bin/django-ca
root@host:/opt/django-ca/src/django-ca# chmod a+rx /usr/local/bin/django-ca
root@host:/opt/django-ca/src/django-ca# django-ca check
System check identified no issues (0 silenced).

Setup Database and static files

Populate the database and setup the static files directory:

root@host:/opt/django-ca/src/django-ca# django-ca migrate
Operations to perform:
...
root@host:/opt/django-ca/src/django-ca# FORCE_USER=root django-ca collectstatic
... static files copied to '/opt/django-ca/html/static'.

The collectstatic command needs to run as root.

Start

You can now finally start the Gunicorn application server and the Celery worker (omit django-ca service if you do not intend to run a web server):

root@host:/opt/django-ca/src/django-ca# systemctl start django-ca django-ca-celery \
>    django-ca-celerybeat

Create admin user and set up CAs

Because we created a shortcut above above, we can use django-ca to use django-ca from the command line.

Custom management commands are documented in Command-line interface. You need to create a user (that can log into the admin interface) and create a root and intermediate CA:

root@host:/opt/django-ca/src/django-ca# django-ca createsuperuser
...
root@host:/opt/django-ca/src/django-ca# django-ca init_ca --path-length=1 Root CN=Root
<root serial>
root@host:/opt/django-ca/src/django-ca# django-ca init_ca  --acme-enable --parent=Root \
>    Intermediate CN=Intermediate
<child serial>

There are a few things to break down in the above commands:

  • The subject (CN=...) in the CA is only used by browsers to display the name of a CA. It can be any human readable value and does not have to be a domain name.

  • The first positional argument to init_ca, (“Root”, “Intermediate”) is just a human readable name used to identify the CA within the command-line interface and web interface. Unlike the CommonName, it must be unique.

  • The --pathlen=1 parameter for the root CA means that there is at most one level of intermediate CAs.

Setup NGINX

A web server is required for the admin interface, certificate revocation status via OCSP or CRLs and ACMEv2 (the protocol used by Let’s Encrypt/certbot integration).

Warning

While theoretically possible, do not use a local CAs ACMEv2 interface to get certificates. Any misconfiguration might make it impossible to retrieve a certificate!

In this setup, we’ll create certificates using the CA we created above. If you want to use Let’s Encrypt certificates instead, you can have a look at our Quickstart with Docker Compose for an example.

First, you need to install NGINX:

root@host:/opt/django-ca/src/django-ca# apt install nginx

Delete the default hostname to minimize the setup:

root@host:/opt/django-ca/src/django-ca# rm -f /etc/nginx/sites-enabled/default

Create a private/public key pair for NGINX to use - you could also sign the certificate using a 3rd-party certificate authority of course:

root@host:/opt/django-ca/src/django-ca# openssl genrsa -out /etc/ssl/$HOSTNAME.key 4096
root@host:/opt/django-ca/src/django-ca# openssl req -new -key /etc/ssl/$HOSTNAME.key -out \
>    /tmp/ca.csr -utf8 -batch
root@host:/opt/django-ca/src/django-ca# django-ca sign_cert --ca=Intermediate \
>    --csr=/tmp/ca.csr --bundle --webserver --subject CN=$HOSTNAME > /etc/ssl/$HOSTNAME.pem

Create DH parameters:

root@host:/opt/django-ca/src/django-ca# mkdir -p /etc/nginx/dhparams/
root@host:/opt/django-ca/src/django-ca# openssl dhparam -dsaparam -out \
>    /etc/nginx/dhparams/dhparam.pem 4096
Generating DSA parameters, 4096 bit long prime
...

django-ca includes a template for envsubst(1) that you can use. The template assumes that you have set $HOSTNAME:

root@host:/opt/django-ca/src/django-ca# envsubst < \
>    /opt/django-ca/src/django-ca/nginx/source.template > \
>    /etc/nginx/sites-available/django-ca.conf
root@host:/opt/django-ca/src/django-ca# ln -fs /etc/nginx/sites-available/django-ca.conf \
>    /etc/nginx/sites-enabled/
root@host:/opt/django-ca/src/django-ca# nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
root@host:/opt/django-ca/src/django-ca# systemctl restart nginx

Use your CAs

After adding your Root CA to your system, you can use the admin interface at https://ca.example.com/admin/ with the credentials you created above to create new certificates or revoke certificates.

CRL and OCSP services are provided by default, so there is nothing you need to do to enable them.

You can use the Command-line interface for creating new CAs as well as issuing, renewing and revoking certificates. The manage.py script is available via the django-ca symlink you created above, for example:

root@host:~# django-ca list_cas

Add CA to your system

To get the certificate for your Root CA, you can use the admin interface or the view_ca command:

root@host:~#django-ca view_ca --output-format=PEM Root > root.pem

You can add this file directly to the list of known CAs in your browser.

Distributions usually provide instructions for how to add a CA to the whole system, see for example these instructions for Debian/Ubuntu.

Use ACME with certbot

If you enabled ACMEv2 support, all you need to do is enable ACMEv2 for the intermediate CA using the admin interface (or using django-ca edit_ca). After that, you can retrieve a certificate using a simple certbot command:

$ sudo certbot register --server https://ca.example.com/acme/directory/
$ sudo certbot certonly --server https://ca.example.com/acme/directory/ ...

Update

When updating, first check the ChangeLog for any breaking changes. Under Update you’ll also find notes on any manual steps you might need to take.

Downloading the new release works the same as before, but you have to remove the old symlink before creating the new one:

root@host:~# cd /opt/django-ca/src/
root@host:/opt/django-ca/src/# ls -l
lrwxrwxrwx  ...   django-ca -> /opt/django-ca/src/django-ca-1.19.1
drwxrwxr-x  ...   django-ca-1.19.1
root@host:/opt/django-ca/src/# wget -O django-ca-3.0.0.tar.gz \
>    https://github.com/mathiasertl/django-ca/archive/refs/tags/3.0.0.tar.gz
root@host:/opt/django-ca/src/# tar xf django-ca-3.0.0.tar.gz
root@host:/opt/django-ca/src/# rm django-ca
root@host:/opt/django-ca/src/# ln -s /opt/django-ca/src/django-ca-3.0.0 django-ca
root@host:/opt/django-ca/src/# ls -l
lrwxrwxrwx  ...   django-ca -> /opt/django-ca/src/django-ca-1.19.2
drwxrwxr-x  ...   django-ca-1.19.1
drwxrwxr-x  ...   django-ca-1.19.2

Create a new virtual environment (with updated dependencies) using uv:

root@host:~# cd /opt/django-ca/src/django-ca/
root@host:/opt/django-ca/src/django-ca/# uv sync --no-default-groups \
>     --all-extras --no-extra postgres

Update the database schema and static files:

root@host:~# django-ca migrate
root@host:~# FORCE_USER=root django-ca collectstatic

Restart services:

root@host:~# systemctl restart django-ca django-ca-celery django-ca-celerybeat

Update the NGINX configuration:

root@host:~# envsubst < /opt/django-ca/src/django-ca/nginx/source.template \
>     < /opt/django-ca/src/django-ca/nginx/source.template \
>     > /etc/nginx/sites-available/django-ca.conf
root@host:~# nginx -t
root@host:~# systemctl restart nginx

Uninstall

To completely uninstall django-ca, stop related services and remove files that where created:

root@host:~# systemctl stop django-ca django-ca-celery django-ca-celerybeat
root@host:~# systemctl disable django-ca django-ca-celery django-ca-celerybeat
root@host:~# rm -f /etc/nginx/sites-*/django-ca.conf
root@host:~# rm -f /var/log/nginx/$HOSTNAME*.log
root@host:~# rm -f /usr/local/bin/django-ca
root@host:~# rm -rf /etc/django-ca/ /opt/django-ca/ /var/log/django-ca
root@host:~# rm -f /etc/ssl/$HOSTNAME.{key,pem}

Restart NGINX so that it no longer knows about the configurations:

root@host:~# systemctl restart nginx

Remove the system user:

root@host:~# deluser django-ca

Drop the PostgreSQL database:

root@host:~# sudo -u postgres psql
postgres=# DROP DATABASE django_ca;
DROP DATABASE
postgres=# DROP USER django_ca;
DROP ROLE