21x9.org | System Administration | Home Automation | Smart Home
19.07.2023

nginx als Reverse Proxy für Docker Container

Wer Docker Container1 nutzt stellt diesen häufig einen Reverse Proxy2 voran. Dies hat verschiedene Vorteile, da man hierbei jedoch schnelle eine Vielzahl von Containern über einen Proxy verwaltet, lohnt es, sich ein paar Gedanken über Struktur und Inhalt dieser Dateien zu machen. Dies sorgt für einen guten Überblick und Änderungen können schneller und einfacher umgesetzt werden.

Im Grunde ist der Aufbau einer Reverse Proxy Konfiguration immer gleich:

server {
    listen 80;
    listen [::]:80;

    server_name container1.example.org;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;

    server_name container1.example.org;

    ssl_certificate /var/lib/dehydrated/certs/container1.example.org/fullchain.pem;
    ssl_certificate_key /var/lib/dehydrated/certs/container1.example.org/privkey.pem;
    ssl_trusted_certificate /var/lib/dehydrated/certs/container1.example.org/fullchain.pem;
    ssl_dhparam /etc/ssl/dhparams.pem;

    location / {
        proxy_pass http://127.0.0.1:5001/;
    }
}

Der erste server Abschnitt sorgt lediglich dafür, dass alle http-Anfragen auf https weitergeleitet werden. Im zweiten server Abschnitt findet dann die Konfiguration für SSL/TLS statt (ssl_*), sowie die Weiterleitung der Anfragen an den Docker-Container (proxy_pass).

Meistens gibt man noch weitere Einstellungen für den Proxy an, z.B.:

proxy_redirect off;
proxy_intercept_errors off;
proxy_read_timeout 86400s;
proxy_ignore_client_abort on;
proxy_connect_timeout 120s;

proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;

proxy_headers_hash_max_size 512;
proxy_buffering on;
proxy_cache_bypass $http_pragma $http_authorization $cookie_nocache;

proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;

Auch für SSL/TLS werden oft noch weitere Optionen gesetzt, zum Beispiel:

gzip off;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_session_tickets off;
ssl_prefer_server_ciphers off;
ssl_session_timeout 1d;
ssl_session_cache shared:MozSSL:10m;

ssl_trusted_certificate /var/lib/dehydrated/certs/21x9.org/fullchain.pem;
ssl_stapling on;
ssl_stapling_verify on;
resolver 127.0.0.1;

add_header Strict-Transport-Security "max-age=63072000" always;

Was die einzelnen Optionen genau bewirken, kann man am Besten der nginx Dokumentation entnehmen3 4. Mir geht es hier um etwas anderes. Wenn man nur diesen einen Container hat, ist es natürlich total in Ordnung, diese Angaben so zu setzen. Aber in der Regel bleibt es ja - gerade bei einem Heimserver - nicht bei nur einem Container. Nach einer Weile würden sich hier hunderte von Zeilen redundanter Konfiguration ergeben. Und wenn z.B. irgendwann TLSv1.2 nicht mehr genutzt werden soll, müsste man dutzendfach die Zeile ssl_protocols TLSv1.2 TLSv1.3; anpassen. Klar, Suchen und Ersetzen ggf. über n-Dateien existiert. Aber schön ist anders. Und es ist auch unnötig.

Ebenso unnötig ist der erste server Abschnitt, denn auch dieser ist ja im Grunde immer gleich, er muss nur zu unterschiedlichen Zielen weiterleiten.

Man kann in nginx einen Konfigurationsabschnitt definieren, der für alle Anfragen gilt, die keinem anderen Konfigurationsabschnitt zugeordnet werden können. Verantwortlich hierfür ist die Zeile server_name in den jeweiligen Abschnitten/Dateien. Ein server-Abschnitt mit der Zeile server_name container1.example.com; beantwortet nur Anfragen für die entsprechende Domain. Ein Sonderfall ist server_name _;, die Angabe von _ als Servername ist eigentlich ungültig und wird daher niemals auf einen "echten" server-Abschnitt matchen. Er kann aber genutzt werden um alle Anfragen zu beantworten, die sich keinem server-Abschnitt zuordnen lassen.

Wir können uns also einen default-Abschnitt definieren, der die http auf https Weiterleitung für alle Anfragen für uns übernimmt. Ich kombiniere diesen Abschnitt gerne auch gleich mit einem https-Part, der dann eine Default-Seite für alle Domainnamen ausliefert, die ich gar nicht vergeben habe, die aber dank eines Catch-All DNS-Eintrag5 dennoch den Weg zu meinem Server finden:

server {
    listen 80 default_server;
    listen [::]:80 default_server;

    listen 443 ssl default_server;
    listen [::]:443 ssl default_server;

    server_name _;

    root /var/www/html/default/;
    index index.html index.htm;

    ssl_certificate /var/lib/dehydrated/certs/wildcard.example.org/fullchain.pem;
    ssl_certificate_key /var/lib/dehydrated/certs/wildcard.example.org/privkey.pem;
    ssl_trusted_certificate /var/lib/dehydrated/certs/wildcard.example.org/fullchain.pem;
    ssl_dhparam /etc/ssl/dhparams.pem;

    location / {
        if ($ssl_protocol = "") {
            return 301 https://$host$request_uri;
         }
        try_files $uri $uri/ =404;
    }
}

Da der Abschnitt für http und https gleichmaßen gilt, müssen wir natürlich den redirect Part etwas anders gestalten und ermitteln über if ($ssl_protocol = ""), ob die aktuelle Anfrage SSL/TLS nutzt. Nur wenn dies nicht der Fall ist, leiten wir die Anfrage um.

Für so eine Catch-All Konfiguration eigenen sich Wildcard Zertifikate natürlich besonders, sonst gibt es - bei unbekannten Subdomains - entsprechende Zertifikatsfehler. Wie man Wildcardzertifikate bei Letscrypt bekommen kann habe ich hier genauer erläutert.

Den server Abschnitt für Port 80 aus der ursprünglichen Konfiguration für container1.example.org müssen wir natürlich entfernen, andernfalls kommt der Default-Abschnitt nie zum tragen. Der Abschnitt für Port 443 bleibt natürlich erhalten.

Damit sind wir bereits einen großen Teil an überflüssiger Konfiguration losgeworden. Doch es geht noch mehr. Schreibt man bestimmte Konfigurationseinträge außerhalb von server Abschnitten, werden sie als Defaultwert genutzt. Wir können also z.B. unsere Proxy und SSL/TLS Konfiguration ebenfalls zentralisieren. Unsere Default-Konfiguration könnte so aussehen:

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

proxy_cache_path /tmp/nginx_cache levels=1:2 keys_zone=cache1:16m max_size=1g inactive=720m use_temp_path=off;

gzip off;
ssl_certificate /var/lib/dehydrated/certs/example.org/fullchain.pem;
ssl_certificate_key /var/lib/dehydrated/certs/example.org/privkey.pem;
ssl_trusted_certificate /var/lib/dehydrated/certs/example.org/fullchain.pem;

ssl_session_timeout 1d;
ssl_session_cache shared:MozSSL:10m;
ssl_session_tickets off;
ssl_dhparam /etc/ssl/dhparams.pem;

ssl_stapling on;
ssl_stapling_verify on;
resolver 127.0.0.1;

ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;

add_header Strict-Transport-Security "max-age=63072000" always;

client_max_body_size 8192M;

proxy_redirect off;
proxy_intercept_errors off;
proxy_read_timeout 86400s;
proxy_ignore_client_abort on;
proxy_connect_timeout 120s;
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
proxy_headers_hash_max_size 512;
proxy_buffering on;
proxy_cache_bypass $http_pragma $http_authorization $cookie_nocache;

proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;

server {
    listen 80 default_server;
    listen [::]:80 default_server;

    listen 443 ssl default_server;
    listen [::]:443 ssl default_server;

    server_name _;

    root /var/www/html/default/;

    index index.html index.htm;

location / {
        if ($ssl_protocol = "") {
            return 301 https://$host$request_uri;
         }
        try_files $uri $uri/ =404;
    }
}

Eventuell kann es sein, dass nginx nach dieser Änderung nicht mehr startet, dies liegt dann meist daran, dass in der nginx.conf die entsprechenden Angaben (z.B. gzip on;) bereits gesetzt wurden. Diese sollten dann dort einfach entfernt werden.

Für unsere Container-Konfigurationen reichen ab nun folgende Angaben:

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;

    server_name container1.example.org;

    location / {
        proxy_pass http://127.0.0.1:5001/;
    }
}

Sofern eine Anwendung spezifische, eigene Konfigurationswerte braucht, können wir sie einfach in den server Abschnitt des Containers aufnehmen, diese überschreiben dann ggf. die Defaultwerte:

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;

    server_name container2.intern.example.org;

    ssl_certificate /var/lib/dehydrated/certs/intern.example.org/fullchain.pem;
    ssl_certificate_key /var/lib/dehydrated/certs/intern.example.org/privkey.pem;
    ssl_trusted_certificate /var/lib/dehydrated/certs/intern.example.org/fullchain.pem;

    location / {
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_set_header Host $http_host;

        proxy_pass http://127.0.0.1:5002/;
    }
}

Eine weitere Möglichkeit wiederkehrende Elemente nur in einer Datei angeben zu müssen, ist die include-Directive. Hierzu legen wir zunächst eine Datei in /etc/nginx/snippets an, ich nenne sie hier einfach einmal security.conf:

## Begin - Security
# deny all direct access for these folders
location ~* /(\.git|cache|bin|logs|backup|tests)/.*$ { return 403; }
# deny running scripts inside core system folders
location ~* /.*\.(log|txt|yaml|yml|twig|sh|bat)$ { return 403; }
# deny access to specific files in the root folder
location ~ /(LICENSE\.txt|composer\.lock|composer\.json|nginx\.conf|web\.config|htaccess\.txt|\.htaccess) { return 403; }
## End - Security

Die Datei kann dann z.B. wie folgt eingebettet werden:

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;

    server_name container1.example.org;

    location / {
        proxy_pass http://127.0.0.1:5001/;
        include /etc/nginx/snippets/security.conf;
    }
}

Dieses Verfahren bietet sich besonders dann an, wenn man wiederkehrende Elemente in location-Abschnitten einfügen möchte. Diese dürfen nämlich nicht außerhalb von server-Abschnitten vorkommen, daher funktioniert die bei den ssl_* oder proxy_* Direktiven genutzte Methode nicht.

Übersichtlicher geht es kaum.

Neben dieser sparsamen Art und Weise der Konfiguration empfehle ich sehr für jeden Container eine eigene Konfigurationsdatei anzulegen und diese in /etc/nginx/sites-available/ abzulegen. Von dort aus können sie dann als Symlink6 in /etc/nginx/sites-enabled/ angelegt werden. So kann man auch sehr leicht eine Konfiguration temporär deaktivieren.


  1. https://de.wikipedia.org/wiki/Docker_(Software)#Begriffe ↩

  2. https://de.wikipedia.org/wiki/Reverse_Proxy ↩

  3. http://nginx.org/en/docs/http/ngx_http_proxy_module.html ↩

  4. http://nginx.org/en/docs/http/ngx_http_ssl_module.html ↩

  5. https://en.wikipedia.org/wiki/Wildcard_DNS_record ↩

  6. https://de.wikipedia.org/wiki/Symbolische_Verkn%C3%BCpfung ↩

Tags: docker nginx config ssl tls proxy reverse_proxy

Mehr

  • nginx sub_filter
  • Ansible: Zusammenhängende Konfiguration über mehrere Server
  • ZFS
  • Mailversand mit msmtp oder postfix
  • Smarthome-Geräte mit ESP-Mikrokontrollern

Tags

docker nginx config ssl tls proxy reverse_proxy

Archiv

  • Mar 2025 (2)
  • May 2024 (2)
  • Oct 2023 (1)
  • Aug 2023 (5)
  • Jul 2023 (31)

  • Ältere Einträge (95)

Feeds

Atom 1.0 RSS JSON
  • Datenschutz
  • Impressum
  • Archiv