Domain-Routing mit HTTPS per Traefik Reverse Proxy
Veröffentlicht am 16. September 2023 • 10 Min. Lesezeit • 2.025 WörterWie sagt man seinem Server, welchen Dienst er unter welcher (Sub-) Domain zur Verfügung stellen soll?
Viele Besitzer oder Mieter eines Root-Servers, stellen recht bald fest, dass der eine gehostete Dienst den Server nicht annähernd auslastet. Diese Erkenntnis lässt bald den Wunsch aufkommen, auch andere Dienste selbst hosten zu wollen. Doch das bringt sofort einige Fragen auf:
So komplex die Problematik auch klingen mag, so einfach ist die Lösung: Durch die Installation eines Reverse Proxies wie dem hier verwendeten Traefik proxy oder auch dem etwas weniger komplexen Projekts Nginx Proxy Manager , das ich auch gerne verwendet habe.
Im Folgenden wird am Beispiel des http-Webserver Images, das im Beitrag Hugo Webseite & Webserver im Docker-Image erstellt wurde, gezeigt, wie Traefik als Docker Container installiert wird und ein http-Dienst per https betrieben werden kann.
htpasswd
Alle Tools sind komplett frei und OpenSource.
Für die die verstehen möchten woher Traefik weiß, welche Anfrage wohin zu senden ist (Domain-Routing), habe ich im Folgenden einen schnellen Abriss der Vorgänge zusammengefasst. Wem das bekannt ist kann den Absatz natürlich überspringen.
Nach Eingabe der Anfrage an www.example.com/index.html
passiert eine Menge im Hintergrund bis letztendlich die gewünschte Seite dargestellt wird.
Im Internet ist alles IP-basiert, weshalb zunächst eine DNS-Anfrage gestellt wird, die aus www.example.com
-> 93.184.216.34
macht (Überraschung, die Domain gibt es wiklich)
Der Browser sendet einen HTTP-Request für die gewünschte Seite an diese IP Adresse, ähnlich:
GET /index.html HTTP/1.1
Host: www.example.com
User-Agent: Mozilla/5.0
Accept: image/gif, image/jpeg, */*
Connection: close
Diese Anfrage wird stellvertretend für alle Webserver, die unter 93.184.216.34
erreichbar sind, von einer Proxy-Software entgegengenommen. Diese stellt fest, dass sie zwar für Anfragen an www.example.com
zuständig ist, aber nur per verschlüsselter Kommunikation (HTTPS). Deshalb antwortet der Proxy dem Browser mit einer Weiterleitung (Statuscode 301) auf https://www.example.com/index.html
zur verschlüsselten Kommunikation.
Der Browser empfängt diese Antwort und erstellt eine neue Anfrage, diesmal an https://www.example.com/index.html
.
(Die IP-Adresse bleibt die selbe, weshalb keine erneute DNS-Anfrage erfolgt)
Der Proxy unter 93.184.216.34
interpretiert die neue Anfrage und stellt fest, dass er sowohl für www.example.com
zuständig ist als auch weiß, wie/wo der zuständige Webserver zu erreichen ist.
Der Proxy sendet daraufhin das Zertifikat für www.example.com
an den Browser und legt gleich eine Liste von (aus seiner Sicht) vertrauenswürdigen Zertifikatsfirmen bei.
Der Browser prüft nun ob diese Firmen wirklich vertrauenswürdig sind und anschließend die Gültigkeit des Zertifikats bei einer dieser.
Der Proxy hat inzwischen die Anfrage an www.example.com/index.html
an den internen Webserver weitergereicht. Dafür besitzt er eine Zuordnung von Domains auf IP-Adresse:Port, unter welcher diese erreichbar sind. (Die Webserver-Adresse kann die des Proxys sein oder eine virtuelle Adresse auf diesem oder eine nicht öffentliche Adresse in einem privaten Sub-Netz). Das entscheidende ist, dass dieses Stück der Kommunikation innerhalb sicherer Mauern stattfindet und damit per HTTP erfolgen kann.
Der Webserver interpretiert die Anfrage, erzeugt die entsprechende Web-Seite und schickt sie zum Proxy, der sie nun verschlüsselt an den Browser zurück sendet.
Traefik
als Reverse-Proxy steht also in vorderster Front und regelt den aus den Tiefen des Internets eintreffenden Verkehr:
Let's Encrypt
Gerade der letzte Punkt ist sehr attraktiv, da damit unser Hugo-Nginx-Container, der nur einen HTTP-Endpunkt anbietet, sicher im Internet veröffentlicht werden kann.
Aufgrund der schreibtechnisch umfangreicheren Konfiguration wird diese für Docker Compose formuliert. Docker-Compose ist eine alternative Art, um mit Docker zu kommunizieren, erleichtert wesentlich das Arbeiten mit Containern und verbessert die Lesbarkeit der Konfigurations-Files.
Erstellen wir zunächst ein Docker-Compose-File, um Traefik so zu konfigurieren, dass es später den Hugo-Nginx Container per HTTPS veröffentlichen kann.
version: '3.3'
services:
traefik:
# Verwende das aktuellste v3.x Traefik image
image: traefik:v3.0
restart: unless-stopped
ports:
# Öffne Port 80 (HTTP), notwendig um nach HTTPS weiterzuleiten
- target: 80
published: 80
mode: host
# Öffne Port 443 (HTTPS)
- target: 443
published: 443
mode: host
labels:
# Ermögliche den Starts des Traefik Services
- traefik.enable=true
# Name des Docker-Netzwerks in dem sich alle Endpunkte befinden
- traefik.docker.network=traefik-public
# Alle Services müssen dieses Label verwenden
- traefik.constraint-label=traefik-public
# Admin-auth Middleware mit HTTP Basic auth
# Verwende Umgebungs-Variablen USERNAME und HASHED_PASSWORD
- traefik.http.middlewares.admin-auth.basicauth.users=${USERNAME?Variable not set}:${HASHED_PASSWORD?Variable not set}
# Https-redirect Middleware, um HTTP nach HTTPS weiterzuleiten
- traefik.http.middlewares.https-redirect.redirectscheme.scheme=https
- traefik.http.middlewares.https-redirect.redirectscheme.permanent=true
# Traefik-http Konfiguration, nur für die Weiterleitung nach https
# Verwende Umgebungs-Variable DOMAIN
- traefik.http.routers.traefik-public-http.rule=Host(`${DOMAIN?Variable not set}`)
- traefik.http.routers.traefik-public-http.entrypoints=http
- traefik.http.routers.traefik-public-http.middlewares=https-redirect
# Traefik-https Konfiguration für den HTTPS Router
# Verwende Umgebungs-Variable DOMAIN
- traefik.http.routers.traefik-public-https.rule=Host(`${DOMAIN?Variable not set}`)
- traefik.http.routers.traefik-public-https.entrypoints=https
- traefik.http.routers.traefik-public-https.tls=true
# Verwenden des speziellen Traefik Service api@internal für das Web UI/Dashboard
- traefik.http.routers.traefik-public-https.service=api@internal
# Verwende "le" (Let's Encrypt), wird weiter unten erstellt
- traefik.http.routers.traefik-public-https.tls.certresolver=le
# Ermögliche HTTP Basic auth unter Verwendung der oben erstellten Middleware
- traefik.http.routers.traefik-public-https.middlewares=admin-auth
# Definiere und benutze dafür den folgenden internen Port des Docker-Services
- traefik.http.services.traefik-public.loadbalancer.server.port=8080
volumes:
# Füge Docker hinzu, damit Traefik die Labels anderer Services lesen kann
- /var/run/docker.sock:/var/run/docker.sock:ro
# Füge ein Volume hinzu, um die Zertifikate speichern zu können
- /production/traefik/:/certificates
command:
# Ermögliche Zugriff auf Docker, um Labels anderer Services lesen zu können
- --providers.docker
# Verwende ausschließlich Services mit den Labels
# "traefik.constraint-label=traefik-public"
- --providers.docker.constraints=Label(`traefik.constraint-label`, `traefik-public`)
# Veröffentliche nicht alle Docker Services nach außen, sondern nur solche,
# die explizit angegeben werden
- --providers.docker.exposedbydefault=false
# Erstelle einen Endpunkt "http" an Port 80
- --entrypoints.http.address=:80
# Erstelle einen Endpunkt "https" an Port 443
- --entrypoints.https.address=:443
# Erstelle einen Zertifikat-Dienst "le" für Let's Encrypt, verwende Umgebungs-Variable EMAIL
- --certificatesresolvers.le.acme.email=${EMAIL?Variable not set}
# Speichere die Let's Encrypt Zertifikate im erstellen Volume
- --certificatesresolvers.le.acme.storage=/certificates/acme.json
# Verwende die TLS Challenge für Let's Encrypt
- --certificatesresolvers.le.acme.tlschallenge=true
# Ermögliche das Erstellen eines Access-logs für HTTP Anfragen
- --accesslog
# Ermögliche das Erstellen eines Traefik-logs für Konfigurationsänderungen und Fehler
- --log
# Ermögliche die Verwendung des Dashboards und der API
- --api
networks:
# Gemeinsames Netzwerk von Traefik und aller angeschlossenen Endpunkt
- traefik-public
networks:
# Definition des gemeinsamen Netzwerke "traefik-public"
traefik-public:
external: true
Für den server-internen Datenverkehr zwischen Traefik und den angeschlossenen Diensten wird ein eigenes Docker-Netzwerk erstellt (traefik-public
). Dieses sorgt für eine Isolation der Datenströme und damit für eine Erhöhung der Sicherheit. Per --providers.docker.constraints
wird Traefik so konfiguriert, dass es lediglich Containern zur Verfügung steht, die auch mit diesem virtuellen Netz kommunizieren dürfen.
Das Netzwerk wird erstellt via:
docker network create traefik-public
Die referenzierten Umgebungs-Variablen werden in einem File .env.prod
angegeben, das erhöht die Wiederverwendbarkeit des docker-compose Files.
# Benutzername für Traefik
export USERNAME="<user>"
# Gehashtes Passwort für Traefik (escape "$" -> "\$")
export HASHED_PASSWORD="<hashed super password>"
# Domain unter der das Traefik Dashboard erreicht werden kann
export DOMAIN="<domain>"
# EMail, die für die Zertifikat-Erstellung mit Let's Encrypt benötigt wird
export EMAIL="admin@<domain>"
Die Traefik Dokumentation weist darauf hin, dass das HASHED_PASSWORD mit htpasswd
erstellt werden kann, welches Teil der Apache-Utils ist
sudo apt install apache2-utils
htpasswd -nb <user> "<super password>"
<user>:<hashed super password>
Aus der Ausgabe kopiert man sich das gehashte Passwort nach dem Doppelpunkt (<hashed super password>
) und kopiert es in .env.prod
File als HASHED_PASSWORD
.
Gestartet wird Traefik dann auf dem Server indem zunächst die Umgebungs-Variablen gesetzt werden und anschließend der Dienst gestartet wird. (Vermutlich wird beim ersten Start noch das Image von Docker-Hub geladen und entpackt.)
source .env.prod
docker-compose -f ./docker-compose.traefik.yml up
Traefik wird daraufhin in der Console gestartet und zeigt die aktuellen Log-Ausgaben. Das ist nützlich um beim ersten Start Tipp-Fehler zu finden.
Wenn alles geklappt hat erreicht man das Traefik-Dashboard unter der in .env.prod
angegebenen Domain-Adresse mit Username und Passwort, das so oder so ähnlich aussieht:
Verläuft alles zur Zufriedenheit, stoppt man die Ausführung von Traefik via CTRL+C und startet erneut, diesmal aber unter Verwendung des Parameters -d
, wodurch der Dienst in den Hintergrund geschickt wird.
docker-compose -f ./docker-compose.traefik.yml up -d
Gestoppt wird der Dienst über
docker-compose ./docker-compose.traefik.yml down
Bisher ist noch nichts passiert, außer dass wir einen Türsteher installiert haben, der sich die ankommenden Anfragen anschaut und (freundlich) ablehnt.
Bleiben wir beim Beipiel www.example.com und erstellen ein Docker-Compose-File, um einen Hugo-Nginx Container so zu konfigurieren, dass er von Traefik per HTTPS veröffentlicht wird.
version: "3.9"
services:
hugo_nginx:
# Name des Containers
hostname: hugo-nginx
# Verweis auf das Image des Dienstes, der via Traefik veröffentlicht werden soll
image: hugo-nginx:latest
restart: unless-stopped
labels:
# Ermögliche die Verwendung von Traefik
- traefik.enable=true
# Name des Docker-Netzwerks in dem sich alle Endpunkte befinden müssen
- traefik.docker.network=traefik-public
- traefik.constraint-label=traefik-public
# Definition des Routers mit beliebigem Namen für die Webseite, z.B. "example_site"
# HTTP für die Weiterleitung auf HTTPS
- traefik.http.routers.example_site-http.service=example_site
- traefik.http.routers.example_site-http.rule=Host("example.com") || Host("www.example.com") # << Domain anpassen
- traefik.http.routers.example_site-http.entrypoints=http
- traefik.http.routers.example_site-http.middlewares=https-redirect
# HTTPS
- traefik.http.routers.example_site-https.rule=Host("example.com") || Host("www.example.com") # << Domain anpassen
- traefik.http.routers.example_site-https.entrypoints=https
- traefik.http.routers.example_site-https.tls=true
# Verwende Let's Encrypt
- traefik.http.routers.example_site-https.tls.certresolver=le
# Port unter der der Webserver für die Hugo-Nginx Webseite erreichbar ist
# (dies ist der Port der virtuellen IP unter der der Hugo-NginX-Container läuft,
# nicht der Host-Port)
- traefik.http.services.example_site.loadbalancer.server.port=80
networks:
- traefik-public
networks:
traefik-public:
external: true
Diese Konfiguration erstellt zunächst einen Container unseres hugo-nginx Images und erlaubt diesem, Teilnehmer des Netzwerkes traefik-public
zu sein. Zugleich weist die Konfiguration über die angegebenen Labels (die von Traefik gelesen werden können) Traefik an:
https://www.example.com
intern an den http-Port 80 des Containers weiterzuleiten, in dem die Hugo-Nginx Webseite läuftDomain-Eintrag
In diesem Beispiel sind wir Besitzer der Domain example.com
und konfigurieren einen DNS-Eintrag für die Sub-Domain www.example.com
derart, dass sie auf die IP-Adresse unseres Servers zeigt.
Außerhalb des Beispiels benötigen wir eine eigene Domain-Adresse. Die DNS-Konfiguration erfolgt dann zumeist in einem Menu des Dienstleisters, bei dem die Domain registriert wurde.
Nach der Änderung eines DNS-Eintrages vergehen ein paar (oder ein paar mehr) Minuten, bis diese weltweit publiziert und somit verfügbar ist. Ungeduldige können mit einem ping auf die Domain prüfen, ob sie bereits auf den Server zeigt
ping www.example.com
ping: www.example.com: Der Name oder der Dienst ist nicht bekannt
[... wenig später ...]
ping www.example.com
PING www.example.com (93.184.216.34) 56(84) bytes of data.
64 bytes from 93.184.216.34 (93.184.216.34): icmp_seq=1 ttl=54 time=112 ms
64 bytes from 93.184.216.34 (93.184.216.34): icmp_seq=2 ttl=54 time=111 ms
[...]
Jetzt haben wir alles beisammen und können den Container mit der Webseite starten, zunächst wieder im Vordergrund.
docker-compose -f ./docker-compose.hugo-nginx.yml up
Geben wir nun im Browser http://www.example.com
ein, bekommen wir die Hugo-Nginx-Webseite zu sehen und zwar nach https://
weitergeleitet und mit gültigem Zertifikat per HTTPS ausgeliefert. Wenn alles läuft stoppen/starten wir den Dienst und schicken ihn in den Hintergrund.
docker-compose -f ./docker-compose.hugo-nginx.yml up -d
Gestoppt werden kann die Webseite jederzeit mit
docker-compose -f ./docker-compose.hugo-nginx.yml down