Nachdem mehrere Services mit Weboberfläche über Docker-Container laufen und sich eigentlich alle um die Ports 80 (HTTP) und 443 (HTTPS) schlagen müssten, wird es Zeit, sie alle unter ein Dach zu packen.
Zudem lasse ich alle meine Container ohne eigenes HTTPS laufen. Die Absicherung über TLS übernimmt NginX stellvertretend für alle (auch z.B. MQTTS).
Die Installation erfolgt denkbar einfach über
$ sudo apt-get install nginx-full
Im Anschluss läuft der Webserver schon auf Port 80 (HTTP).
Grundsätzlich ist NginX (wie eigentlich alle anderen Webserver auch) so aufgebaut, dass es je ein Verzeichnis für existierende (available) und aktive (enabled) Konfigurationen gibt.
/etc/nginx/
|-- dh.pem
|-- modules-available/
|-- modules-enabled/
|-- nginx.conf
|-- sites-available/
| |-- default
| |-- example.com
| |-- sub.example.com
| `-- ...
|-- sites-enabled/
| |-- example.com -> /etc/nginx/sites-available/example.com
| `-- ...
|-- snippets
| |-- ssl.conf
| |-- ssl-http.conf
| |-- ssl-stream.conf
| `-- ...
|-- streams-available/
| |-- mqtt.conf
| `-- ...
|-- streams-enabled/
| |-- mqtt.conf -> /etc/nginx/streams-available/mqtt.conf
| `-- ...
`-- ...
In der Datei /etc/nginx/nginx.conf nehmen wir kleine Anpassungen vor.
Wir erhöhen die Speichergröße für Variablen (innerhalb des http-Blocks):
variables_hash_bucket_size 64;
variables_hash_max_size 2048;
Wir erweitern die Funktionalität für WebSockets (innerhalb des http-Blocks):
##
# WebSocket
##
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
Wir fügen einen Block für die Behandlung von Streams ein (am Ende):
stream {
##
# Stream configs
##
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/streams-enabled/*;
}
Sofern diese NginX Instanz hinter einem anderen WebServer läuft, der die echte Remote-Adresse in einem Header (z.B. X-Forwarded-For) weiterreicht, so kann diese Adresse wieder "zurückgeholt" werden.
Hierbei muss natürlich auch bekannt welches die derzeitige Remote-Adresse ist, die ersetzt werden soll (hier im Beispiel 192.168.12.34).
##
# Logging configs
##
real_ip_header X-Forwarded-For;
set_real_ip_from 192.168.12.34;
Eine Seite wird immer im Ordner /etc/nginx/sites-available/ angelegt und anschließend im Ordner /etc/nginx/sites-enabled/ verlinkt (ln -s).
Wir gehen davon aus, dass die Webseite in einem Docker-Container gehostet wird. Dieser Container hat die Verbindung auf localhost gemappt mit dem Port 61100 (127.0.0.1:61100:80).
Dann schaut eine Basis-Konfiguration für NginX z.B. so aus.
/etc/nginx/sites-available/example.com:
upstream website {
server 127.0.0.1:61100;
}
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
server_tokens off;
location / {
proxy_pass http://website/;
proxy_http_version 1.1;
proxy_redirect off;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $http_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;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Nginx-Proxy true;
}
}
Die relevanten Sachen kurz erklärt:
listen-Einträge wird definiert auf welchem Port gelauscht wirdserver_name geschriebenAnschließend wird die Seite aktiviert, indem man einen Link in den Ordner sites-enabled erstellt:
$ sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/example.com
Nun kann man die Konfiguration prüfen und anschließend neu laden, damit sie aktiv wird:
$ sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
$ sudo ngnix -s reload
Die Konfiguration eines Streams unterscheidet sich nur in kleinen Teilen von einer Seite.
Im Gegensatz zu einer Seite, hat ein Stream z.B. keine Locations.
/etc/nginx/streams-available/mqtt:
upstream mqtt {
server 127.0.0.1:18830;
}
server {
listen 1883;
listen [::]:1883;
proxy_pass mqtt;
proxy_protocol on;
}
Das war es schon. Nun noch die Konfiguration aktivieren:
$ sudo ln -s /etc/nginx/streams-available/mqtt /etc/ngnix/streams-enabled/mqtt
$ sudo nginx -t
$ sudo nginx -s reload
Für eine gute Absicherung der Webseite, wird HTTPS, also HTTP mit SSL/TLS Verschlüsselung verwendet.
Beginnen wir mit den Snippets, die wir für HTTPs einfügen wollen. Die aktuell sinnvollsten Parameter kann man sich auf einer schönen Seite von Mozilla holen: Mozilla SSL Configuration Generator
/etc/nginx/snippets/ssl.conf:
ssl_protocols TLSv1.2 TLSv1.3;
ssl_dhparam /etc/nginx/dh4096.pem;
ssl_prefer_server_ciphers off;
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_timeout 1d;
ssl_session_tickets off;
resolver <own dns> 8.8.8.8 valid=300s;
resolver_timeout 10s;
Diese Einstellungen sind sowohl für die Sicherung von HTTP, als auch von Streams (z.B. MQTT) gleich. Wichtig ist, beim resolver noch die korrekte IP-Adresse einzusetzen. Folgen nun noch die Spezialisierungen für HTTP und Streams.
/etc/nginx/snippets/ssl-http.conf:
include /etc/nginx/snippets/ssl.conf;
ssl_session_cache shared:http:10m;
/etc/nginx/snippets/ssl-stream.conf:
include /etc/nginx/snippets/ssl.conf;
ssl_session_cache shared:stream:10m;
Anschließend können diese in der Konfiguration der Seite eingebunden werden.
Die Konfiguration von oben für eine Seite sieht dann so aus:
/etc/nginx/sites-available/example.com:
upstream website {
server 127.0.0.1:61100;
}
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
server_tokens off;
location / {
return 301 https://$http_host$request_uri;
}
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name example.com www.example.com;
server_tokens off;
include /etc/nginx/snippets/ssl-http.conf;
ssl_certificate /path/to/cert/example.com/fullchain.pem;
ssl_certificate_key /path/to/cert/example.com/privkey.pem;
location / {
proxy_pass http://website/;
proxy_http_version 1.1;
proxy_redirect off;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $http_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;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Nginx-Proxy true;
}
}
Für einen Stream ändert sich auch nur wenig.
/etc/nginx/streams-available/mqtt:
upstream mqtt {
server 127.0.0.1:18830;
}
server {
listen 1883;
listen [::]:1883;
listen 8883 ssl;
listen [::]:8883 ssl;
include /etc/nginx/snippets/ssl-stream.conf;
ssl_certificate /path/to/cert/example.com/fullchain.pem;
ssl_certificate_key /path/to/cert/example.com/privkey.pem;
proxy_pass mqtt;
proxy_protocol on;
}
Fehlen noch die Zertifikatsdateien, damit die Konfiguration vollständig ist.
$ sudo openssl dhparam -out /etc/nginx/dh4096.pem 4096
Generating DH parameters, 4096 bit long safe prime, generator 2
This is going to take a long time
........................+.......................................................
................................................................................
.................................+.........................................+....
...............................+................................................
.............................................................+..................
................................................................................
................................................................................
.................+..............................................................
..............................+.................................................
.......................................................................+........
.............................................................+..................
................................................................................
+...............................................................................
................................................................................
................................................................................
...............+.................................................+..............
.......................+........................................................
.......................................................+........................
................................................................................
................................................................................
..............................++*++*++*
Die Zertifikate selbst können entweder von einer Zertifikatsstelle (CA) ausgestellt werden oder man kann sie z.B. mittels Let's Encrypt selber beziehen.
Zertifikate für TLS/SSL (HTTPS) können recht einfach über die freie Authorisierungs-Instanz Let's Entrypt bezogen werden. Hierfür ist auf allen gängigen Systemen in den Paketquellen das Tool certbot zu finden.
$ sudo apt-get install certbot
Mit diesem Tool lassen sich automatisiert Zertifikate beziehen (und verlängern).
Wir beziehen hier nur einfache Domain-Zertifikate. Wildcard-Zertifikate sind grundsätzlich auch möglich, jedoch aufwändiger zu konfigurieren.
Zuerst sorgen wir für saubere Parameter, um gute Zertifikate zu erzeugen (Die E-Mail Adresse muss natürlich noch angepasst werden).
/etc/letsencrypt/cli.ini:
# Define the server to challenge normal certificates as well as wildcards
server = https://acme-v02.api.letsencrypt.org/directory
# Use a stronger key size
rsa-key-size = 4096
# Identify by this email
email = me@example.com
# Because we are using logrotate for greater flexibility, disable the
# internal certbot logrotation.
max-log-backups = 0
Um später noch weitere Services stoppen/starten zu können, erzeugen wir noch ein Verzeichnis (hooks), in dem wir dann Skripte ablegen, die erweitert werden können.
/etc/letsencrypt/hooks/pre.sh (chmod +x nicht vergessen):
#!/bin/bash
systemctl stop nginx
exit 0
/etc/letsencrypt/hooks/post.sh (chmod +x nicht vergessen):
#!/bin/bash
systemctl restart nginx
exit 0
Da wir certbot als Standalone verwenden werden, muss NgniX vor der Verwendung beendet und hinterher neu gestartet werden.
Also holen wir uns nun ein Zertifikat für die Hauptdomain ab:
$ sudo certbot certonly --standalone --pre-hook /etc/letsencrypt/hooks/pre.sh --post-hook /etc/letsencrypt/hooks/post.sh -d example.com -d www.example.com
Das Zertifikat ist im Anschluss unter /etc/letsencrypt/live/example.com/ zu finden.
cert.pem: Das öffentliche Zertifikat (ohne Zwischenzertifikate)chain.pem: Die Zwischenzertifikate (ohne End-Zertifikat)fullchain.pem: Das öffentliche Zertifikat inklusive Zwischenzertifikateprivkey.pem: Der private Teil des ZertifikatsDer Abschnitt in der NginX Konfiguration sähe demnach so aus:
include /etc/nginx/snippets/ssl-(http|stream).conf
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
Um Seiten vor ungewünschten Zugriffen zu schützen, kann die Seite entweder selber einen Login implementieren, oder man nutzt die Möglichkeit von NginX.
Die Anmeldemethode von NginX nennt sich Basic-Auth. Dabei wird die Authentifizierung mittels verschiedener HTTP-Header ermöglicht.
Um die Passwort-Dateien richtig erstellen zu können, wird noch das Tool htpasswd benötigt.
$ sudo apt-get install --no-install-recommends apache2-utils
Der Basic-Auth Block kann dabei an zwei verschiedenen Stellen stehen. Entweder direkt im server-Block, dann ist die gesamte Seite mit einem Passwortschutz versehen, oder innerhalb eines location-Blocks, sodass nur ein Teilbereich geschützt wird (oder dort der Schutz aufgehoben wird).
sudo htpasswd -c /etc/nginx/auth/example.com.htpasswd user1
New password: ****
Re-type new password: ****
Adding password for user user1
sudo htpasswd /etc/nginx/auth/example.com.htpasswd user2
Die Datei (/etc/nginx/auth/example.com.htpasswd) sieht dann hinterher z.B. so aus:
user1:$apr1$475emJWF$9gn4zQlt2PhPNQnw811co/
user2:$apr1$tYI4NZ3i$XOVD1PXgnT4OTw3BN7Y/b0
location Block[...]
location /secure {
auth_basic "Secured Area";
auth_basic_user_file /etc/nginx/auth/example.com.htpasswd;
[...]
}
[...]
server Block[...]
auth_basic "Authenticate for example.com";
auth_basic_user_file /etc/nginx/auth/example.com.htpasswd;
location / {
[...]
}
location /public {
auth_basic off;
[...]
}
[...]
Man kann die Basic-Auth Geschichte auch mittels einer internen HTTP Abfrage lösen, anstatt eine htpasswd-Datei zu verwenden.
Voraussetzung ist natürlich, dass es intern einen Service dafür gibt. Antwortet dieser mit HTTP 200, so ist die Authentifizierung OK, andernfalls (z.B. 404) wird der Weg nicht freigegeben.
Beispiel:
[...]
location /secured {
auth_request /auth;
auth_request_set $auth_cookie $upstream_http_set_cookie;
add_header Set-Cookie $auth_cookie;
[...]
}
# Only exact match of request
location = /auth {
internal;
# internal request
proxy_pass http://127.0.0.1:5000/auth/nginx;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
# information to perform the auth
proxy_set_header X-Original-URI $request_uri;
proxy_set_header X-Original-Host $http_host;
proxy_set_header X-Original-Method $request_method;
proxy_set_header X-Original-Request $http_host$request_uri;
}
Der interne Service bestimmt hierbei die Art der HTTP Authentifizierung.
Ich habe z.B. einen Service implementiert, der selbst die Basic-Auth Anforderung liefert, sodass ich filigran prüfen kann, welcher Benutzer welche Zugriffe hat.
Meine Prüfung im Service:
X-Original-URI im Service hinterlegt? NEIN Das ist natürlich nur eine kurze Version dessen, was sich im Service tut .