NGINX SSL Reverse Proxy für Docker Container

Aus Laub-Home Wiki

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.

Quellen