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 |
|---|---|
|
Adds support for the REST API using Django Ninja. |
|
Adds Celery support. |
|
Adds support for hardware security modules (HSMs). |
|
Adds support for Memcached. |
|
Adds MySQL support. |
|
Adds PostgreSQL support using Psycopg 3. |
|
Adds Redis support (usable as both cache and Celery message transport). |
|
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:
# 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:
DATABASES:
default:
ENGINE: django.db.backends.postgresql
NAME: django_ca
USER: django_ca
HOST: localhost
PORT: 5432
PASSWORD: "..."
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:
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"
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:
# 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
# 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=1parameter 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