NGINX SSL Reverse Proxy für Docker Container
Dieses HowTo soll erklären wie man mittels Docker Compose einen NGINX als Reverse Proxy online bringt, welcher mit certbot für die nötige SSL HTTPS Verschlüsselung sorgt und somit als Einfallstor für die dahinter stehenden Docker Applikationen gilt. Ziel ist es hinter dem Reverse Proxy Webapplikationen zu betreiben, welche entweder durch eigene Domain, oder aber durch Short URL nach außen via HTTPS verfügbar gemacht werden. Dafür nutze ich hier ein Zusammenspiel aus NGINX und CERTBOT. Die Konfiguration des NGINX ist schon ein wenig auf Security und Performance ausgelegt.
Im Detail passiert hier folgendes:
Es werden 2 Container, NGINX und certbot für die letsencrypt Zertifikate verwendet. Certbot selbst wird zum generieren der Zertifikate mittels Script und Domain-Konfigurationsdatei benutzt und dann im Projekt als laufender Container sich um die Erneuerung dieser alle 24h zu kümmern. Der NGINX Container bekommt die fertigen Zertifikate in sich gemountet und kann dann auf diese zugreifen und HTTP bzw. HTTPS nach außen sprechen. Nach Innen kommuniziert er via Reverse Proxy Engine mit allen möglichen Webanwendungen. Die Konfigurationsdateien des NGINX liegen ebenfalls extern und können so einfach angepasst, oder um weitere Domains erweitert werden. Sollte es zu einem Renew eines Zertifikates kommen, holt sich der NGINX dieses automatisch, da er sich selbst alle 48h neu startet.
Nun aber los... als erstes legen wir das Docker Compose Projekt mit allen Konfigs an...
Docker-Compose Projekt
Als erstes brauchen wir den Compose Projekt Ordner und springen in diesen
mkdir -p /opt/nginxproxy/data/nginx/conf/includes/
mkdir -p /opt/nginxproxy/data/letsencrypt
cd /opt/nginxproxy
Nun legen wir als Erstes das .env
Environment File an, in dem wir dem Projekt ein paar variablen mit geben:
/opt/nginxproxy/.env
# Configuration for NGINXPROXY
# Port Configuration
HTTP_PORT=80
HTTP_BIND=0.0.0.0
HTTPS_PORT=443
HTTPS_BIND=0.0.0.0
# Your timezone
TZ=Europe/Berlin
nun das docker-compose.yml
selbst, in dem wir die Container beschreiben und diese dann hochfahren. Grob zusammengefasst passiert hier folgendes:
- Container mit NGINX (Alpine) wird genutzt
- dieser Startet alle 48h Nginx neu -- damit er immer die aktuellen Zertifikate bekommt.
- dieser bekommt die im Projektordner gelegenen Konfigurationsdateien gemountet
- das Webroot für die letsencrypt acme challenge gemountet
- dann noch die SSL Zertifikate von certbot
- wir geben die Ports aus dem .env file nach draußen frei (80/443)
- und lassen den Container auf dem HOST Netzwerk mit laufen, damit haben wir ohne NAT und Sonstiges direkt Zugriff auf alle von anderen Container freigegeben Ports (kommen wir noch dazu) und die Außenwelt auf uns
- als zweiten Container installieren wir certbot, dieser fungiert nur als Zertifikats Renew Container (alle 24h)
- dieser mountet ebenfalls das certfolder und das webroot
- zusätzlich mountet er noch sein Konfigurationsfolder und die Logfiles
- Als Netzwerk geben wir ihm ein bridge Netz
das Ganze sieht dann übersetzt so aus:
/opt/nginxproxy/docker-compose.yml
version: "3.8"
services:
nginxproxy:
image: nginx:mainline-alpine
command: "/bin/sh -c 'while :; do sleep 48h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'"
environment:
- TZ=${TZ}
volumes:
- ./data/nginx/conf:/etc/nginx/conf.d/:ro
- ./data/letsencrypt/webroot:/usr/share/nginx/html:ro
- ./data/letsencrypt/conf:/etc/letsencrypt:ro
#- /opt/mailcow-dockerized/data/assets/ssl:/etc/ssl/mail/:ro
ports:
- "${HTTPS_BIND:-0.0.0.0}:${HTTPS_PORT:-443}:${HTTPS_PORT:-443}"
- "${HTTP_BIND:-0.0.0.0}:${HTTP_PORT:-80}:${HTTP_PORT:-80}"
restart: always
network_mode: "host"
certbot:
image: certbot/certbot
restart: always
depends_on:
- nginxproxy
environment:
- TZ=${TZ}
volumes:
- ./data/letsencrypt/conf:/etc/letsencrypt
- ./data/letsencrypt/lib:/var/lib/letsencrypt
- ./data/letsencrypt/webroot:/data/letsencrypt
- ./data/letsencrypt/logs:/var/log/letsencrypt
entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 24h & wait $${!}; done;'"
networks:
frontend-nw:
networks:
frontend-nw:
driver: bridge
driver_opts:
com.docker.network.bridge.name: br-nginxproxy
so nun noch die NGINX Konfiguration anlegen...
Nginx Konfiguration
nun kommen wir zur Konfiguration des NGINX. Als erstes stellen wir das Caching Verhalten der Clients ein.
/opt/nginxproxy/data/nginx/conf/global.conf
map $sent_http_content_type $expires { default 7d; # default max; # default off; text/html epoch; text/css max; text/javascript max; application/javascript max; ~image/ max; application/pdf max; "text/css; charset=utf-8" max; "text/javascript; charset=utf-8" max; }
nun die Acme Challenge, im Fall das der vhost ein Zertifikat von certbot braucht, muss diese Konfigurationsdatei mit include
eingebunden werden.
/opt/nginxproxy/data/nginx/conf/includes/cert_bot.conf
#for certbot challenges (renewal process) location ~ /.well-known/acme-challenge { allow all; root /usr/share/nginx/html; }
Nun ein paar Standards, die ebenfalls mit include
im vhost eingebunden werden sollten. Siehe die Kommentare in der Datei, was hier passiert.
/opt/nginxproxy/data/nginx/conf/includes/site-defaults.conf
# charset charset utf-8; override_charset on; # ssl cert defaults ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256'; ssl_prefer_server_ciphers on; ssl_session_cache shared:SSL:50m; ssl_session_timeout 1d; ssl_session_tickets off; client_max_body_size 0; # enable compression gzip on; gzip_disable "msie6"; gzip_vary on; gzip_proxied off; gzip_comp_level 6; gzip_buffers 16 8k; gzip_http_version 1.1; gzip_min_length 256; gzip_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript application/vnd.ms-fontobject application/x-font-ttf font/opentype image/svg+xml image/x-icon image/vnd.microsoft.icon;
so, nun kommen wir zum eigentlichen vhost. Dieser hört auf die Domains www.example.tld example.tld
, leitet dann HTTP auf HTTPS um und routet auf die interne Webapplikation unter localhost 8081
weiter.
/opt/nginxproxy/data/nginx/conf/www.example.tld.conf
server { listen 80; listen [::]:80; server_name www.example.tld example.tld; return 301 https://$host$request_uri; } server { listen 443 ssl; listen [::]:443 ssl; server_name www.example.tld example.tld; ssl_certificate /etc/letsencrypt/live/www.example.tld/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/www.example.tld/privkey.pem; include /etc/nginx/conf.d/includes/site-defaults.conf; include /etc/nginx/conf.d/includes/cert_bot.conf; expires $expires; location / { proxy_pass http://127.0.0.1:8081/; 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; client_max_body_size 0; #include /etc/nginx/conf.d/includes/proxy_cache.conf; } # show cache status and any skip cache reason #add_header Proxy-Cache $upstream_cache_status; }
Ist dies nun alles eingerichtet geht es nun um das erstellen der Zertifikate und das Hochfahren des Compose Projektes.
Absichern durch security Header
Wenn man möchte kann man seine Seite noch durch ein paar security http header absichern. Ein test dieser kann hier gemacht werden:
server { ... # security headers add_header Strict-Transport-Security "max-age=15768000;"; add_header X-Content-Type-Options nosniff; add_header X-XSS-Protection "1; mode=block"; add_header X-Download-Options noopen; add_header X-Frame-Options "sameorigin"; add_header X-Permitted-Cross-Domain-Policies none; add_header Referrer-Policy strict-origin; add_header Permissions-Policy "geolocation=(self),midi=(),sync-xhr=(),microphone=(),camera=(),magnetometer=(),gyroscope=(),fullscreen=(self),payment=()"; # hier werden crawler komplett ausgesperrt: add_header X-Robots-Tag none; }
Umschreiben auf Hauptdomain
Möchte man das zum Beispiel example.tld immer auf www.example.tld umgeschrieben wird, wie auch sämtliche andere vhost Aliase, dann nehmt diese Anleitung:
Aktivieren von HTTP2
wer möchte kann nun auch noch HTTP2 nach außen sprechen. Nach innen macht es keinen Sinn.
certbot für letsencrypt Zertifikate
Als erstes legen wir die Domain Konfigurationsdatei an. Hier einfach in eine Zeile, Komma getrennt, die Domains schreiben die in ein Zertifikat sollen. Zweite oder mehr Zeilen, bedeutet weitere Zertifikate. Jeweils die erste Domain ist auch der Zertifikatsname.
/opt/nginxproxy/domains.txt
www.example.tld,example.tld,test.example.tld,other.example.tld www.example1.tld,test.example1.tld
das hier nachstehende Skript sorgt dann dafür, dass die Zertifikate erstellt werden. Dafür wird ein certbot Container gestartet und bekommt die Domains aus der domains.txt
. E-Mail Adresse sollte angegeben werden, da man so über abgelaufene Zertifikate rechtzeitig von letsencrypt informiert wird, sollte der Automatismus versagen. Man sollte für Testzwecke STAGING
ein kommentieren. Sollte beim Starten des Skriptes das Compose Projekt bereits laufen, wird es vom Skript vorher runtergefahren und im Abschluss automatisch gestartet.
/opt/nginxproxy/generate-certs.sh
#!/bin/bash
#########################################################################
#generate-certs.sh
#This Script uses certbot/certbot docker container to generate letsencrypt
#SSL certificates.
#by A. Laub
#andreas[-at-]laub-home.de
#
#License:
#This program is free software: you can redistribute it and/or modify it
#under the terms of the GNU General Public License as published by the
#Free Software Foundation, either version 3 of the License, or (at your option)
#any later version.
#This program is distributed in the hope that it will be useful,
#but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
#or FITNESS FOR A PARTICULAR PURPOSE.
#########################################################################
#Set the language
export LANG="en_US.UTF-8"
#Load the Pathes
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
# set the variables
# Please at first define all domains in domains.txt
# comma seperated: www.example.de,test.example.de
# first domain will be the name of cert
# one certificate per line would be generated:
# www.example.de,test.example.de
# www.example.com,test.example.com
# www.example.tld,test.example.tld
# E-Mail adress for register at letsencrypt
MAILADRESS=mailadresse@example.tld
# for testing propose you can enable staging, just uncomment the line for staging
#STAGING=--staging
#########################################################################
# start the things
# first stoping the nginxproxy
docker-compose stop
# now generating certs with domainnames out of domains.txt
for domain in $(cat domains.txt); do
docker run -it --rm \
-p 80:80 \
-v $(pwd)/data/letsencrypt/conf:/etc/letsencrypt \
-v $(pwd)/data/letsencrypt/lib:/var/lib/letsencrypt \
-v $(pwd)/data/letsencrypt/webroot:/data/letsencrypt \
-v $(pwd)/data/letsencrypt/logs:/var/log/letsencrypt \
certbot/certbot \
certonly --standalone ${STAGING} \
--expand \
--webroot-path=/data/letsencrypt \
--rsa-key-size=4096 \
-d ${domain} \
--no-eff-email --agree-tos \
-m ${MAILADRESS}
done
# Change for renew to webroot
sed -i 's/'"authenticator = standalone"'/'"authenticator = webroot"'/g' $(pwd)/data/letsencrypt/conf/renewal/*
# now starting nginxproxy
docker-compose up -d
dann noch das execute Recht auf das Skript geben:
chmod +x /opt/nginxproxy/generate-certs.sh
Das nachfolgende Skript ist Optional. das Gleiche läuft im Compose Projekt alle 24h ab. Sollte man doch mal Bedarf an einem händischen Zertifikats Renew haben, dann einfach dieses Skript ausführen.
/opt/nginxproxy/renew-certs.sh
#!/bin/bash
#########################################################################
#renew-certs.sh
#This Script uses certbot/certbot docker container to renew the letsencrypt
#SSL certificates.
#by A. Laub
#andreas[-at-]laub-home.de
#
#License:
#This program is free software: you can redistribute it and/or modify it
#under the terms of the GNU General Public License as published by the
#Free Software Foundation, either version 3 of the License, or (at your option)
#any later version.
#This program is distributed in the hope that it will be useful,
#but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
#or FITNESS FOR A PARTICULAR PURPOSE.
#########################################################################
#Set the language
export LANG="en_US.UTF-8"
#Load the Pathes
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
# first stoping the nginxproxy
docker-compose stop
# renew the certificates
docker run -it --rm \
-v $(pwd)/data/letsencrypt/conf:/etc/letsencrypt \
-v $(pwd)/data/letsencrypt/lib:/var/lib/letsencrypt \
-v $(pwd)/data/letsencrypt/webroot:/data/letsencrypt \
-v $(pwd)/data/letsencrypt/logs:/var/log/letsencrypt \
certbot/certbot renew
# at least start the nginxproxy
docker-compose start
dann das execute Recht auf das Skript geben:
chmod +x /opt/nginxproxy/renew-certs.sh
SSL Zertifikate erstellen und Start des NGINX
So, nun kann der erste Start vollführt werden. Dafür nutzen wir einfach das vorhin erstellte generate-certs.sh
Skript.
cd /opt/nginxproxy/
./generate-certs.sh
Wenn alles gut läuft, werden nun die Zertifikate erstell, das Compose Projekt hochgefahren und die Webapplikation via HTTP und HTTPS erreichbar sein.
- http://www.example.tld
- https://www.example.tld
Sollte die Seite nicht verfügbar sein kann ein Blick in die Logfiles vielleicht erklären warum:
docker-compose logs -f
Deploy/Undeploy/Starten/Stoppen/Updaten/Logs
Das Compose Projekt kann durch die folgende Befehle betrieben werden:
# immer erst in den Projektfolder wechseln
cd /opt/nginxproxy/
# deploy des Projektes
docker compose up -d
# undeploy des Projektes
docker compose down
# updaten der Images
docker compose pull
# starten des Projektes
docker compose start
# stoppen des Projektes
docker compose stop
# restart des Projektes
docker compose restart
# Logfiles anzeigen
docker compose logs
# anzeigen der Logfiles, wie tail -f
docker compose logs -f
Mehr hier:
Test-Webapplikation
hat man gerade noch keine Webapplikation zur Hand um den Reverse Proxy zu testen, kann man einen einfachen Hello World NGINX Container hochfahren:
docker run -p127.0.0.1:8081:80 -d nginxdemos/hello
Testen der Konfiguration
Um auf Nummer sicher zu gehen, ob auch alles soweit geht, können wir auf einige externe Tests zurückgreifen.
- Qualys SSL Test: https://www.ssllabs.com/ssltest/
- Hier ist eine Mapping Tabelle für die Ciphers, OpenSSL to IANA Cipher Names
- Qualys SSL Server Rating Guideline
- Security Headers: https://securityheaders.com
- IPv6 Test: https://ipv6-test.com/validate.php
- Google Pagespeed: https://developers.google.com/speed/pagespeed/insights/?hl=de
Quellen
- NGINX - Rewrite auf Hauptdomain
- https://sandro-keil.de/blog/let-nginx-start-if-upstream-host-is-unavailable-or-down/
- https://www.thepolyglotdeveloper.com/2017/03/nginx-reverse-proxy-containerized-docker-applications/
- https://dev.to/danielkun/nginx-everything-about-proxypass-2ona
- https://github.com/portainer/portainer/issues/754
- https://www.humankode.com/ssl/how-to-set-up-free-ssl-certificates-from-lets-encrypt-using-docker-and-nginx
- https://medium.com/bros/enabling-https-with-lets-encrypt-over-docker-9cad06bdb82b
- https://medium.com/@pentacent/nginx-and-lets-encrypt-with-docker-in-less-than-5-minutes-b4b8a60d3a71
- https://certbot.eff.org/docs/using.html#certbot-command-line-options
- https://stackoverflow.com/questions/58366405/docker-machine-docker-compose-ssl-lets-encrypt-through-nginx-certbot