Cover Image

Modularer Reverse Proxy mit Nginx

5. Juni 2019 - Lesezeit: 8 Minuten

Mein aktuelles Projekt: Ich habe einen virtuellen Server (VPS), auf dem ich alle meine Software-Projekte betreiben möchte. Dieser Server hat eine feste IP-Adresse, aber ich möchte ganz unterschiedliche Software darauf betreiben, und zwar in Docker-Containern. Der Reverse Proxy hat also die Aufgabe zu schauen, welche Subdomain (z.B. app, www oder chat) meiner Domain angesprochen wird und dann die Anfrage an das entsprechende Programm weiter zu leiten.

Sämtliche Software läuft auf dem gleichen Rechner (localhost), das ist aber noch nicht einmal erforderlich. Hier ein paar Beispiele, wie die Weiterleitung funktioniert:

Anfrage: app.meinedomain.de ➡️ localhost:9001
Anfrage: www.meinedomain.de ➡️ localhost:9002
Anfrage: chat.meinedomain.de ➡️ localhost:9003

Die Domain ist meinedomain.de und die verschiedenen Programme laufen in Docker-Containern und jeweils auf einem anderen Port, hier auf dem localhost.

Vorbereitung

Folgende Vorbereitungen sind nötig:

  1. Konfiguriere Deinen DNS-Server so, dass die einzelnen Sub-Domains auf die IP-Adresse Deines Webserver zeigen.
  2. Generiere anschließend die SSL-Zertifikate mittels einer API-Anfrage an Deinen DNS.
  3. Installiere vorher oder nachher die einzelnen Anwendungen zum Beispiel mit Docker, vorzugsweise auf diesem selben Host, damit Du auch sehen kannst, ob das Ganze wirklich funktioniert.

Installation von nginx

Zunächst muss die Software nginx selbst installiert werden, hier jeweils für Ubuntu/Debian;

sudo apt update; sudo apt install nginx

oder Manjaro/Arch-Linux:

sudo pacman -S nginx

Ich selbst verwende für dieses Projekt Debian als Betriebssystem.

Modularer Aufbau der Konfiguration

Zum Anfang konfigurieren wir zwei Virtuelle Server, nämlich einen für unsere Anwendung, die später mal unter https://app.meinedomain.de erreichbar sein soll und einen zweiten Default-Eintrag, der alle Aufrufe auf die Domain meinedomain.de abfängt, die nicht mit einer Anwendung hinterlegt sind.

Hier zunächst der Default-Eintrag, wir ändern dafür /etc/nginx/sites-available/default so:

server {
  listen 80 default_server;

  # Everything is a 404
  error_page 404 /404.html;
  location / {
    return 404;
  }

  access_log off;
}

server {
  listen 443 ssl http2 default_server;

  # Let's Encrypt SSL
ssl_certificate     /etc/acme.sh/certs/meinedomain.de/fullchain.cer;
ssl_certificate_key /etc/acme.sh/certs/meinedomain.de/meinedomain.de.key;

  include include.d/ssl.conf;

# Always just 404
  error_page 404 /404.html;
  location / {
    return 404;
  }

  access_log off;
}

Wir haben also einen Einsprungpunkt für das normale HTTP auf Port 80 und desweiteren ein HTTPS-Ziel auf Port 443.

Und hier die zweite Konfiguration für die eigentiche App:

server {
server_name app.meinedomain.de;
  listen 80;
  listen 443 ssl http2;

  # Proxy Settings
  set $forward_scheme http;
  set $server         "127.0.0.1";
set $port           9001;

  # Let's Encrypt SSL
ssl_certificate     /etc/acme.sh/certs/meinedomain.de/fullchain.cer;
ssl_certificate_key /etc/acme.sh/certs/meinedomain.de/meinedomain.de.key;

  if ($scheme = "http") {
    return 301 https://$host$request_uri;
  }

  include include.d/ssl.conf;
  include include.d/gzip.conf;
  include include.d/proxy.conf;
  # include include.d/acme.conf;

  access_log off;
}

Die Datei acme.conf ist hier auskommentiert. Sie wird nur benötigt, wenn die Zertifikate von Letsencrypt mittels eines HTTP-Challenge-Response-Verfahrens geholt werden soll. Das erkläre ich, obwohl ich es eleganter anstelle, weiter unten weiter unten. Wie man vorbereitend ganz einfach diese eleganten Wildcard-Zertifikate mit acme.sh erstellt, erfährst Du demnächst in diesem Artikel. Das access.log habe ich abgeschaltet, ich liebe Datenschutz.

Die modularen Include-Dateien

Vorbereitend legen wir für unsere einzubindenden Dateien das Verzeichnis /etc/nginx/include.d an. Dahinein kommen nun folgende vier Dateien, die wir bei Bedarf wie oben in unsere Konfigurationen einbinden:

Die Datei ssl.conf. In dieser sind wichtig allgemeine Einstellungen gesetzt, wie zum Beispiel das SSL-Session-Timeout oder HSTS:

ssl_session_timeout 5m;
ssl_session_cache shared:SSL:50m;

# intermediate configuration. tweak to your needs.
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-
ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AE
S128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA';
ssl_prefer_server_ciphers on;

# HSTS (ngx_http_headers_module is required) (15768000 seconds = 6 months)
add_header Strict-Transport-Security max-age=15768000;

Die Datei gzip.conf. Hierin ist gesetzt, dass alle unsere Antworten auf Webanfragen gezippt über den Kanal gehen, damit nicht so viele Daten transportiert werden. Das ist vor allem für Mobilgeräte gut:

gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript application/activity+json application/atom+xml;

Die Datei proxy.conf. Diese passt für alle Weiterleitungen, weil die darin benutzten Variablen für das Weiterleitungsschema http, den localhost und den jeweiligen Port der Anwendung gaz zentral in derServer-Konfiguration gesetzt werden (s.o.):

 location / {
  add_header       X-Served-By $host;
  proxy_set_header Host $host;
  proxy_set_header X-Forwarded-Scheme $scheme;
  proxy_set_header X-Forwarded-Proto  $scheme;
  proxy_set_header X-Forwarded-For    $remote_addr;
  proxy_pass       $forward_scheme://$server:$port;
}

Und zuletzt die Datei acme.conf. Diese wir nur dann benötigt, wenn die für SSL benötigten Zertifikate  mit einer Verifizierung nicht über eine Kommunikation mit dem DNS beantragt werden, sondern direkt über einen Abruf über den Port 80 auf der jeweiligen Domain.

Dies kann besonders dann sinnvoll sein, wenn man keinen API-Zugriff auf den DNS hat oder wenn man viele einzelne kleine Domains hat. Bindet man diese Datei ein, dann werden alle Anfragen bei der Erzeugung der Letsencrypt-Zertifikate auf Port 80 in ein eigenes extra dafür bereit zu haltendes Verzeichnis /var/www/letsencrypt weitergeleitet:

location ^~ /.well-known/acme-challenge/ {
    auth_basic off;
    default_type "text/plain";
    root /var/www/letsencrypt;
}

location = /.well-known/acme-challenge/ {
    return 404;
}

Ergebnis

Mit diesen 6 Dateien kannst Du nun fast jede Anforderung abdecken. Die Weiterleitung vom zentralen Reverse Proxy auf die jeweiligen Zielanwendungen mit ihren einzelen Ports ist so ruckzuck erledigt. 👍🏻

Vielen Dank auch besonders an jc21 aus Brisbane, bei dem ich etliches abgekupfert habe. Er hat für Freunde des grafischen Benutzerinterfaces eine Anwendung programmiert, die dieses ganze Konfigurieren in einem Webinterface ermöglicht.