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.