Ubuntu-Server mit Django PostgreSQL TLS/SSL einrichten

Eine ausführliche Anleitung zum Einrichten eines Ubuntu Servers für Webseiten mit Django-Backend, die über HTTPS mit sicherer SSL/TLS-Verschlüsselung erreichbar sind. Zu installierende Software ist unter anderem Nginx-Webserver, Python, Virtualenv, Gunicorn, Django-Framework, Supervisor, PostgreSQL, Memcache, Lets Encrypt.

Vorbereitung

Es wird ein Server mit einer Standard-Installation von Ubuntu Server benötigt, der über einen SSH-Zugang erreichbar ist, und auf dem der Nutzer "root"-Rechte hat. Der Server sollte keine Verwaltungssoftware wie "Plesk" o.ä. enthalten.

Außerdem sollte eine eigene Domain auf die IP-Adresse des Servers eingerichtet sein. Die Domain wird später an ein SSL/TLS-Zertifikat gebunden, so dass die Domain über HTTPS erreichbar wird.

Wichtig: Bevor es losgeht, muss der SSH-Zugang des Servers gegen Angriffe abgesichert werden! Eine Anleitung dazu findet sich hier: Ubuntu-Server sichern.

Nginx web server installieren und konfigurieren

root@server:~# apt-get remove apache2
root@server:~# apt-get install nginx

In der zentralen Konfigurationdatei /etc/nginx/nginx.conf muss zunächst nichts weiter eingestellt werden. Nur ein virtueller Host muss hinzugefügt werden, um die Domain zu bedienen. Dies passiert im Verzeichnis /etc/nginx/sites-available. Damit es übersichtlich bleibt, sollte für jede Domain jeweils eine neue Konfigurationsdatei benutzt werden, die den Namen der jeweiligen Domain hat, also zum Beispiel "example.com".

root@server:~# vim /etc/nginx/sites-available/example.com

server {
    listen 80; 
    server_name example.com *.example.com;
    root /var/www/example.com;
    index index.html;
    location / { try_files $uri $uri/ =404; }
    location ~ /\. { deny all; }
    error_page 404 /404.html;
    error_page 500 502 503 504 /500.html;
}

Es wird ein neuer virtueller Server definiert, der auf Port 80 auf eingehende Verbindungsanfragen wartet. Der Server gilt für Verbindungen, die über die Domain "example.com" und alle ihre Subdomains erfolgen. Der Server sucht nach Inhalten im Verzeichnis /var/www/example.com und sendet die Datei index.html für den Fall, das es sich bei dem angefragten Pfad um ein Verzeichniss handelt.

Eine Anfrage nach "http://example.com/irgend/wo.html" würde also die Datei "/var/www/example.com/irgend/wo.html" zurückgeben. Bei einer Anfrage nach "http://example.com/irgend/" die Datei "/var/www/example.com/irgend/index.html".

Mit location können sehr flexibel einzelne Pfade umgeleitet werden. Dabei bezieht sich location / auf alle Pfade, die mit "/" anfagen (also alle). Hierfür wird zunächst nach einer entsprechenden Datei gesucht, dann nach einem entsprechenden Unterverzeichnis, und wenn keines der beiden gefunden wurde, wird ein HTTP-Fehler 404 zurückgegeben. Mit dem Regulären Ausdruck in location ~ /\. werden alle Dateien gefunden, deren Namen mit einem "." beginnt. Dies sind "versteckte Dateien" in Linux, und mit "deny all" wird Nginx angewiesen, sie nie auszuliefern. Diese Einstellung ist sinnvoll, um z.B. Konfigurationsdateien unzugänglich zu machen.

Um den neuen virtuellen Server zu aktivieren, wird einfach ein Link im Verzeichnis /etc/nginx/sites-enabled erstellt und die Nginx-Konfiguration neu geladen.

root@server:~# ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
root@server:~# service nginx reload

Ob alles funktioniert, kann mit einer Testdatei im konfigurierten Unterverzeichnis ausprobiert werden.

root@server:~# mkdir -p /var/www/example.com
root@server:~# echo 'Hallo, Welt!' > /var/www/example.com/index.html
root@server:~# chown -R benutzer:benutzer /var/www/example.com

Der Datei-Inhalt sollte angezeigt werden, wenn die Domain im Webbrowser aufgerufen wird.

PostgreSQL Datenbank installieren

root@server:~# apt-get install postgresql postgresql-contrib libpq-dev pgtune

Die PostgreSQL-Datenbank kann für eine Vielzahl von Datenarten benutzt werden. Das Programm pgtune erstellt, je nach Anwendungsprofil, automatisch eine optimierte Konfigurationsdatei für PostgreSQL. Als Datenbank für das web, wird das Anwendungsprofile "Web" konfiguriert.

root@server:~# cd /etc/postgresql/9.3/main/
root@server:~# pgtune --type=Web -i ./postgresql.conf -o ./postgresql.conf.pgtune
root@server:~# mv ./postgresql.conf ./postgresql.conf.original
root@server:~# mv ./postgresql.conf.pgtune ./postgresql.conf
root@server:~# service postgresql restart

Der Benutzer postgres wurde durch das Installationsscript erstell, und ist der Standard-Benutzer, um sich mit dem Datenbankserver zu verbinden. Zum Testen, ob allles funktioniert, wechelt man daher zunächst zum postgres-Benutzer, und dieser verbindet sich dann mit der Datenbank.

root@server:~# sudo -i -u postgres

postgres@server:~$ psql

postgres=# \conninfo
postgres=# \q

Nun wird noch ein Datenbank-Benutzer erstellt, der später von der Django-Installation genutzt wird. Der Benutzer sollte neue Datenbanken erstellen können, damit Django seine eigenen Test-Datenbank erstellen und löschen kann.

postgres@server:~$ createuser --interactive -P
Enter name of role to add: project_db_user
Enter password for new role: 
Enter it again: 
Shall the new role be a superuser? (y/n) n
Shall the new role be allowed to create databases? (y/n) y
Shall the new role be allowed to create more new roles? (y/n) n
postgres@server:~$ exit

root@server:~#

Memcache installieren

root@server:~# apt-get install memcached libmemcached-tools

Das war schon alles. Der für Memcache zur Verfügung stehende Speicher kann in der Konfigurationsdatei eingestellt werden.

root@server:~# vim /etc/memcached.conf

Der Speicher sollte ausreichend groß gewählt werden, denn bei einem vollen Speicher, werden ältere Daten überschrieben.

Virtual Environment mit Python3 und Django installieren

root@server:~# apt-get install python-virtualenv python3-setuptools python3-dev virtualenvwrapper

Nachdem Virtualenv installiert ist, sollte als normaler Benutzer weitergearbeitet werden.

root@server:~# exit

benutzer@server:~$ cd /var/www/example.com && mkdir ./venv
benutzer@server:~$ mkvirtualenv --python=python3 ./venv
benutzer@server:~$ source ./venv/bin/activate

Hat man bereits ein Django-Projekt (z.B. mit dem Namen "project_name"), das installiert werden soll, kann es nun in das Unterverzeichnis kopiert und die dazugehörigen Pakete installiert werden (-r recursive, -L follow symlinks, -t preserve file times, -P partial transfers).

ich@pc:~$ rsync -rLtvP ~/dev/project_name benutzer@example.com:/var/www/example.com/
(venv)benutzer@server:~$ pip install -r ./project_name/requirements.txt

Soll nur Django oder auch andere Pakete installiert werden, erfolgt das auch über pip.

(venv)benutzer@server:~$ pip install python-django
(venv)benutzer@server:~$ django-admin startproject project_name

Zum Testen kann Djangos eingebauter Entwicklungsserver verwendet werden.

(venv)benutzer@server:~$ cd ./project_name
(venv)benutzer@server:~$ ./manage.py runserver example.com:8000

Nun sollte die Django-Installation über http://example.com:8000 im Webbrowser erreichbar sein. Der Django-Entwicklungsserver läuft jedoch nur unter einen Thread und ist daher extrem ineffizient und nicht für den Live-Einsatz geeignet.

Der nächste Schritt ist daher, die Django-Anwendung über Gunicorn mit Nginx zu verbinden. Der Entwicklungsserver wird mit Strg+C beendet.

Django-Anwendung über Gunicorn mit Nginx verbinden

(venv)benutzer@server:~$ pip install gunicorn setproctitle
(venv)benutzer@server:~$ cd /var/www/example.com/project_name
(venv)benutzer@server:~$ gunicorn project_name.wsgi:application --bind example.com:8000

Gunicorn und setproctitle sind Python-Anwendungen und werden ebenfalls in die Virtuelle Umgebung installiert. Gunicorn kann zum Testen direkt gestartet werden. Die Anwendung sollte nun weiterhin über http://example.com:8000 erreichbar sein, wird nun jedoch über Gunicorn ausgeliefert.

Damit die Anwendung auch unter Last gut skaliert, wird ihr nun noch Nginx als Proxy vorgeschaltet. Dazu wird in einem Bash-Skript zunächst der Gunicorn-Start automatisiert und optimiert, und die Virtuelle Umgebung beim Start aktiviert.

(venv)benutzer@server:~$ cd /var/www/example.com
(venv)benutzer@server:~$ vim start-gunicorn.sh

#!/bin/bash

NAME="project_name_app"
DJANGODIR=/var/www/example.com/project_name
SOCKFILE=/var/www/example.com/run/gunicorn.sock
USER=benutzer
GROUP=benutzer
NUM_WORKERS=5
DJANGO_SETTINGS_MODULE=project_name.settings
DJANGO_WSGI_MODULE=project_name.wsgi

echo "Starting $NAME as `whoami`..."

cd $DJANGODIR
source ../venv/bin/activate
export DJANGO_SETTINGS_MODULE=$DJANGO_SETTINGS_MODULE
export PYTHONPATH=$DJANGODIR:$PYTHONPATH

RUNDIR=$(dirname $SOCKFILE)
test -d $RUNDIR || mkdir -p $RUNDIR

exec ../venv/bin/gunicorn ${DJANGO_WSGI_MODULE}:application \
  --name $NAME --workers $NUM_WORKERS \
  --user=$USER --group=$GROUP \
  --bind=unix:$SOCKFILE --log-level=debug --log-file=-

Der NAME wird später durch setproctitle in der Prozesslist ps aux angezeigt, und macht es einfacher, die Anwendung dort zu finden.

Der Wert für USER und GROUP sollte ein User-Account sein, der auf dem System existiert. Die Django-Anwendung wird unter diesem Benutzerkonto ausgeführt werden. Da dieser Server nicht von verschiedenen Parteien geteilt wird, kann der gleiche User-Account genutzt werden wie zuvor.

Die Punkt-Notierungen für DJANGO_SETTINGS_MODULE und DJANGO_WSGI_MODULE zeigen auf die Django-Dateien settings.py und wsgi.py, ausgehend vom Stammverzeichnis der Anwendung, DJANGODIR.

Um sicherzustellen, dass die Anwendung immer läuft, wird supervisor konfiguriert, um start-gunicorn.sh automatisch neu zu starten, falls es crasht oder das System rebootet.

benutzer@server:~$ sudo su
root@server:~# apt-get install supervisor
root@server:~# vim /etc/supervisor/conf.d/project_name.conf

[program:project_name]
command = /var/www/example.com/start-gunicorn.sh
user = benutzer
stdout_logfile = /var/log/supervisor/project_name_gunicorn.log
redirect_stderr = true
environment=LANG=de_DE.UTF-8,LC_ALL=de_DE.UTF-8

Nun wird die Konfiguration neu geladen, und Supervisor kann benutzt werden, um den Status der Anwendung zu steuern.

root@server:~# supervisorctl reread
project_name: available
root@server:~# supervisorctl update
project_name: added process group
root@server:~# supervisorctl status project_name
project_name    RUNNING
root@server:~# supervisorctl stop project_name
project_name: stopped
root@server:~# supervisorctl start project_name
project_name: started
root@server:~# supervisorctl restart project_name
project_name: stopped
project_name: started

Nun muss nur noch der Nginx-Webserver über das SOCKFILE mit unserer Gunicorn-Django-Anwendung verbunden werden. Dazu wird wieder die Nginx-Konfiguration für unsere Domain "example.com" geöffnet.

root@server:~# vim /etc/nginx/sites-available/example.com

server {
    listen 80; 
    server_name example.com *.example.com;
    root /var/www/example.com;
    index index.html;
    location / { try_files $uri $uri/ =404; }
    location ~ /\. { deny all; }
    error_page 404 /404.html;
    error_page 500 502 503 504 /500.html;
}

Und eine Einstellungen geändert (location /) und zwei hinzugefügt (upstream und location /static/).

upstream project_name_app_server {
    server unix:/var/www/example.com/run/gunicorn.sock fail_timeout=0;
}
server {
    listen 80; 
    server_name example.com *.example.com;
    root /var/www/example.com;
    index index.html;
    location / {
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_redirect off;
        if (!-f $request_filename) {
            proxy_pass http://project_name_app_server;
            break;
        }
    }
    location ~ /\. { deny all; }
    location /static/ {
        alias /var/www/example.com/project_name/static/;
    }
    error_page 404 /404.html;
    error_page 500 502 503 504 /500.html;
}

Mit upstream wird die vorher erstellte Socket-Datei unter der Variablen project_name_app_server als Weiterleitungsziel definiert, und vom Proxy darauf umgeleitet für alle Pfade (location /).

Außerdem gibt es jetzt noch die location /static/ für Djangos /static/-Dateien. Bei Bedarf kann auch noch ein Verzeichnis für /media/ erstellt werden oder andere Verzeichnisse, die statische Dateien enthalten.

root@server:~# service nginx reload

Nachdem Nginx die Änderungen neu geladen hat, sollte die Django-Anwendung unter der Domain "http://example.com" im Webbrowser erreichbar sein, und auch bei Server-Reboot oder Crash automatisch neu starten.

SSL/TLS-Zertifikat installieren

Webseiten mit Benutzer-Login sollten immer über eine verschlüsselte SSL/TLS-Verbindung ausgeliefert werden. Und seit Ende 2015 gibt es eine gemeinnützige Certificate Authority, die SSL-Zertifikate kostenlos und mit einer einfachen Installation anbietet.

LetsEncrypt wird von der Linux Foundation getragen und unterstützt von zahlreichen bekannten Vereinen und Unternehmen (EFF, Mozilla, Cisco, Internet Society, usw.)

1. Die letsencrypt-Anwendung installieren

Viele Linux-Distributionen haben die letsencrypt-Anwendung noch nicht in ihren Software-Repositories (Stand Januar 2016). Sie kann aber direkt aus dem letsencrypt Githubkonto installiert werden (manuell installierte Software liegt üblicherweise im /opt-Verzeichnis).

root@server:~# cd /opt
root@server:~# git clone https://github.com/letsencrypt/letsencrypt
root@server:~# cd letsencrypt
root@server:~# ./letsencrypt-auto --help

Durch den Aufruf von letsencrypt-auto werden alle notwendigen Einstellungen vorgenommen.

2. Neues SSL-Zertifikat erstellen und einrichten

Letsencrypt bietet zwei Möglichkeiten, um ein Zertifikat zu installieren. Hier wird die einfachere genutzt, bei der der Nginx-Webserver kurz angehalten werden muss.

root@server:~# service nginx stop
root@server:~# /opt/letsencrypt/letsencrypt-auto certonly --standalone --standalone-supported-challenges tls-sni-01 -d example.com -d www.example.com -d mail.example.com
root@server:~# service nginx start

Bisher unterstützt Letsencrypt noch keine Wildcard-Domains, also *.example.com geht nicht. Das Zertifikat sollte daher für "example.com" und übliche Subdomains wie "www" und "mail" (damit auch POP3-Email über eine verschlüsselte Verbindung abgerufen werden kann) ausgestellt werden.

Das SSL-Zertifikat liegt nun unter /etc/letsencrypt/live/example.de in vier *.pem Dateien.

Mögliche Probleme mit LetsEncrypt

Wenn ein SSL-Zertifikat für die Hauptdomain eingerichtet wird, also zum Beispiel für "example.com", dann gehen viele Webbrowser automatisch davon aus, dass auch alle Subdomains nur über HTTPS erreichbar sind.

Verbindungen auf Subdomains wie "http://foo.example.com" werden vom Browser dann automatisch auf "https://" umgeleitet, indem ein HTTP header Upgrade-Insecure-Requests: 1 zum Server gesendet wird. Der Server wird auf den Header reagieren, und den Browser umleiten auf die entsprechende Adresse mit TLS.

Ist für diese Subdomain jedoch kein TLS-Zertifikat installiert, weil sie nicht explizit im LetsEncrypt-Zertifikat erwähnt ist, dann benutzt der Server ein selbst signiertes Zertifikat. Diese sind jedoch nicht wirklich sicher, und der Browser wird den Nutzer warnen: "Your connection is not private! Attackers might be trying to steal your information from up.yaix.de (for example, passwords, messages, or credit cards). " Das sollte natürlich vermieden werden.

Nginx mit SSL/TLS-Zertifikat einrichten

Nun wird die Nginx-Konfiguration der Django-Anwendung angepasst, um Webseiten nur noch über HTTPS und Port 443 auszuliefern. Außerdem sollen alle unsicheren Aufrufe mit HTTP auf Port 80 automatisch auf HTTPS umgeleitet werden.

All das wird in der bereits bekannten Nginx-Konfigurationsdatei für die Domain eingestellt.

root@server:~# vim /etc/nginx/sites-available/example.com

upstream project_name_app_server {
    server unix:/var/www/example.com/run/gunicorn.sock fail_timeout=0;
}
server {
    listen 80;
    server_name example.com *.example.com;
    return 301 https://example.com$request_uri;
}
server {
    listen *:443 ssl;
    listen [::]:443 ssl;

    server_name example.com *.example.com;
    root /var/www/example.com;
    index index.html;

    ssl on;
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    ssl_session_cache shared:SSL:20m;
    ssl_session_timeout 10m;
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers 'ECDH+AESGCM:ECDH+AES256:ECDH+AES128:DH+3DES:!ADH:!AECDH:!MD5';
    ssl_prefer_server_ciphers on;

    add_header Strict-Transport-Security "max-age=31536000; includeSubdomains";
    add_header X-Frame-Options DENY;
    add_header X-Content-Type-Options nosniff;

    location / {
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_redirect off;
        if (!-f $request_filename) {
            proxy_pass http://project_name_app_server;
            break;
        }
    }
    location ~ /\. { deny all; }
    location /static/ {
            alias /var/www/example.com/project_name/static/;
    }
    error_page 404 /404.html;
    error_page 500 502 503 504 /500.html;
}

Mit dem server, der weiterhin auf Post 80 lauscht, werden unverschlüsselte HTTP-Verbindungen umgeleitet auf HTTPS.

Der zweite server lauscht auf listen *:443 ssl; (IPv4) und listen [::]:443 ssl; (für IPv6), also auf den sicheren HTTPS-Ports, auch mit Notierung für das Internet-Protokol version 6.

Weiter hinzugekommen sind zahlreiche ssl- und add_header-Einträge, wobei nur ssl_certificate und ssl_certificate_key angepasst werden müssen für die eigene Domain. Diese Einträge zeigen auf den öffentlichen und privaten Schlüssel des SSL-Zertifikats.

root@server:~# service nginx reload

Nun sollte die Domain im Browser nur noch unter https://example.com erreichbar sein. Falls es Probleme gibt, ist ein erster Schritt, den Browser-Cache zu leeren, denn insbesondere Chrome/Chromium cachen viel und gerne und kommen dann mit plötzlich neuem HTTPS durcheinander.

Letsentrypt-SSL-Zertifikate automatisch erneuern

Zwar funktioniert nun alles, jedoch besteht noch ein Problem: zurzeit haben Letsencrypt-Zertifikate nur eine sehr kurze Lebensdauer von 3 Monaten, und müssen spätestens dann verlängert werden.

Das kann jedoch recht einfach mit einem Bash-Script und einem Crontab-Eintrag erledigt werden.

root@server:~# vim /opt/letsencrypt_updater.sh

#!/bin/bash

service nginx stop
/opt/letsencrypt/letsencrypt-auto --renew certonly --standalone --agree-tos --text --standalone-supported-challenges tls-sni-01 -d example.com -d www.example.com -d mail.example.com
service nginx start

Und über den Crontab des root-Benutzers wird das Skript einmal pro Woche ausgeführt.

root@server:~# crontab -e

# Jeden Donnerstag um 5:30 Uhr morgens
30 5 * * 3 /opt/letsencrypt_updater.sh

Das Zertifikat wird nur wirklich aktualisiert, wenn weniger als ein Monat Restlaufzeit verbleiben. Mit dem wöchentlichen Check geht man hier auf Nummer sicher.

Extra: Log-Dateien-Analyse in der Kommandozeile mit GoAccess

GoAccess liest Standard-Webserver Log-Dateien ("NCSA Combined format") und erstellt daraus einen übersichtlichen Bericht in der Shell.

root@server:~# echo "deb http://deb.goaccess.io $(lsb_release -cs) main" | tee -a /etc/apt/sources.list
root@server:~# wget -O - http://deb.goaccess.io/gnugpg.key | apt-key add -
root@server:~# apt-get update && apt-get install goaccess

benutzer@server:~$ goaccess --std-geoip --real-os --log-file=/var/log/nginx/access.log

Mehr zum Thema