Server mit Traefik als Reverse Proxy für Docker einrichten

Server mit Docker Containern einrichten, mithilfe von Docker Compose Traefik installieren und als Reverse Proxy zu nutzen.

Server mit Traefik als Reverse Proxy für Docker einrichten-heroimage

Server installieren

Der erste Schritt besteht darin, sich einen Server zu besorgen. In dieser Anleitung benutze ich einen Server, der sich nicht bei mir zu Hause befindet, dies bietet meistens einige Vorteile, wie beispielsweise:

  • Einfachere Erreichbarkeit durch eine feste IPv4 Adresse

  • Schnellere Internetgeschwindigkeit

  • Bessere Kostenprognose möglich

  • Keine Abnutzung eigener Hardware

  • Keine Stromkosten

  • Geringere Anschaffungskosten

  • Keine Geräuschemission zu Hause

Selbstverständlich sollte man sich auch den Nachteilen bewusst sein. Jedenfalls lässt sich diese Anleitung mit jedem Server befolgen, auf dem Docker installiert werden kann. Probleme hatte ich in der Vergangenheit mit virtuellen Servern, die mit OpenVZ statt KVM virtualisiert waren.

Ich werde hier einen VPS S SSD von Contabo nutzen (Kosten liegen bei ca. 6 € / Monat).

Empfehlenswert sind meiner Meinung auch die VPS und insbesondere auch Root-Server (dezidierte CPU) von Netcup. Falls du dort noch nicht Kunde sein solltest, bekommst du mit folgenden Gutscheinen einen Rabatt in Höhe von 5 €, oder einen VPS oder Root-Server günstiger. Ich bekomme eine Provision für geworbene Neukunden. Sollten die Gutscheine abgelaufen sein, kannst du mich gerne nach neuen anschreiben.

Nachdem der Zugriff auf den Server erteilt wird, lässt sich dieser aus dem Kundenbereich heraus neu installieren. Ich wähle als Betriebssystem Ubuntu 20.04 aus. Ein anderes wäre auch möglich, aber ich habe einige Erfahrung mit Ubuntu und sehe keinen Grund zu wechseln. Des Weiteren ist das Ende des Standard-Supports erst im April 2025, somit wird es vermutlich erst mal nicht nötig sein, das Betriebssystem auf die nächste Version zu upgraden.

Nach ein paar Minuten sollte der Server mit der Installation fertig sein. Nun wird es Zeit sich mit dem Server zu verbinden. Dies lässt sich unter Windows mit dem Windows Terminal, PuTTY oder einem ähnlichen Programm bewerkstelligen. Ich nutze das Windows Terminal.

Windows Terminal

Anschließend gegebenenfalls die Frage mit „yes“ beantworten und das Passwort eingeben. Während der Eingabe wird das Passwort nicht angezeigt. Folgende Meldung sollte nun erscheinen (oder ähnlich):

Login

Als Erstes schauen wir mal, ob es Updates gibt:

apt update

Apt Update Falls Updates existieren, installieren wir diese anschließend:

apt upgrade

Die Frage, ob man updaten möchte, mit Y bestätigen.

Der Texteditor Nano sollte schon standardmäßig installiert sein, aber zur Sicherheit kann man ihn so installieren:

apt install nano

Als Nächstes benennen wir den Server um:

nano /etc/hostname

Hostname

In Nano wird die Datei mit Strg + X verlassen. Nano wird euch Fragen, ob ihr die Änderungen speichern wollt (mit Y bestätigen) und wie ihr die Datei nennen wollt. Da wir sie überschreiben möchten, ändern wir den Namen nicht und bestätigen einfach mit Enter.

Sobald du den Server neu startest reboot und dich anschließend wieder mit ihm verbindest ssh admin@{ip-Adresse} ist der neue Host-Name sichtbar.

Neuer Hostname

Jetzt ist es sinnvoll einen Nutzer anzulegen, der nicht root ist. Ohne Rechteeinschränkungen ist es mit root nämlich ziemlich einfach sein System zu zerstören.

useradd -m -G sudo namedesneuennutzers
passwd namedesneuennutzers

useradd ist der Befehl, um einen neuen Nutzer anzulegen. Das -m sorgt dafür, dass dieser Nutzer einen Home-Ordner eingerichtet bekommt. Das -G packt den Nutzer in eine Gruppe, anschließend wird die Gruppe genannt. Die Sudo Gruppe darf Befehle mit sudo ausführen. Zuletzt folgt der Name des neuen Nutzers. Mit passwd wird ein Password für den Nutzer erstellt.

Sollte die sudo Gruppe nicht existieren, musst du eine Gruppe erstellen.

groupadd namederneuengruppe

Diese Gruppe sollte das Recht haben Sudo-Befehle auszuführen:

EDITOR=nano visudo

In der Datei muss dann die neue Gruppe erwähnt werden.

Visudo

Nun stellen wir bei Bedarf die richtige Zeit ein.

date

Falls nicht die richtige Zeit angezeigt wird:

tzselect

Über die Zahlen könnt ihr eure Zeitzone auswählen.

timedatectl set-timezone 'Europe/Berlin'

Es ist an der Zeit den Server neu zu starten und sich als neu angelegter Nutzer anzumelden.

reboot

Als Nächstes installiere ich Htop (ein Monitoring-Programm).

apt install htop

Jetzt müsste der Server die Ausführung verweigern und fragen, ob man root sei.

Unzureichende Berechtigung

Also noch mal mit Superuser do.

sudo apt install htop

Über die Eingabe von htop kann man sich nun die Auslastung des Servers anschauen.

Damit lässt sich gut verfolgen, wie stark die CPU und der RAM des Servers beansprucht werden. Mit F10 oder STRG + C verlässt du die Ansicht wieder.

Ich möchte gerne noch eine andere Shell haben.

sudo apt install git curl
sudo apt install zsh

Jetzt installiere ich zim und ändere die Standard-Shell auf zsh.

curl -fsSL https://raw.githubusercontent.com/zimfw/install/master/install.zsh | zsh
chsh -s /bin/zsh
exec bash

Jetzt müsste die Shell (subjektiv) besser aussehen.

zsh

Der wichtigste Part der Installation sind Docker und Docker Compose. Außerdem optional noch Lazydocker, für eine sehr einfache Möglichkeit sich seine Docker-Container und deren aktuelle Logs über SSH anzusehen.

sudo apt install ca-certificates curl gnupg lsb-release
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \
  $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose

curl https://raw.githubusercontent.com/jesseduffield/lazydocker/master/scripts/install_update_linux.sh | bash

Jetzt muss noch Docker aktiviert werden.

sudo systemctl enable --now docker

Sollte die nächste Zeile nicht erscheinen mit STRG + C nachhelfen. Nun noch den angelegten Nutzer der Dockergruppe zuordnen.

sudo gpasswd -a namedesneuennutzers docker

Sicherheitstechnisch sollte man das nicht so machen. Dann muss man aber immer wieder sudo vor die Docker Befehle zu schreiben. Überprüfe noch, ob der Benutzer tatsächlich in der Gruppe gelandet ist.

id namedesneuennutzers

Als Letztes richten wir einen Ordner im Heimverzeichnis des neuen Nutzers an.

cd ~
mkdir docker

Mit cd (“change directory”) bewegen wir uns durch die Ordner, wobei die Tilde (~) für home steht. mkdir erstellt einen neuen Ordner. Man kann Ordner bei Bedarf auch wieder löschen.

mkdir docker2
# erstellt Ordner
mkdir -rf docker2
# löscht Ordner

Folgende Eingabe sollte nun den neu erstellten Ordner zeigen.

ls

Wir können uns auch die Berechtigungen des Ordners anschauen und den Eigentümer.

ls -l

Oder auch die ausgeblendeten Ordner.

ls -A
# bzw.
ls -Al

Wir können uns in den neuen Ordner bewegen.

cd docker
# und wieder zurück
cd ..

Im Übrigen kann man viel Zeit mit der Tabulator-Taste sparen. Diese Taste vervollständigt Befehle. Gibt man im Home-Ordner cd d ein und drückt Tab, wird das Wort automatisch vervollständigt. Über die Pfeiltasten oben und unten lassen sich die letzten Befehle anzeigen. Pfeiltaste nach rechts übernimmt den vollständigen Befehl, wenn einer vorgeschlagen wird.

Sollte sich Lazydocker öffnen lassen, ist die Einrichtung abgeschlossen.

sudo lazydocker

Mit Q, ESC oder STRG + C lässt sich Lazydocker wieder beenden. Mit exit verlassen wir den Server.

exit

Einrichtung einer Domain

Der Server besitzt nur eine IP-Adresse, wir möchten aber eventuell mehrere Dienste auf dem Server einrichten. Möglichkeit 1 wäre es, diese über verschiedene Ports laufen zu lassen, dann würde man die Dienste über die Port-Nummer ansteuern müssen (123.45.67.89:443, 123.45.67.89:9000, usw.). Das ist erstens schwer zu merken und zweitens gibt es ein Problem mit den SSL Zertifikaten, es entsteht ein Sicherheitsrisiko. Deswegen ist es besser einen Proxy Manager auf Port 80 und 443 (die Standardports für http und https) zu schalten, dieser leitet dann die Anfragen weiter.

Dafür benötigen wir (Sub-)Domains. So ist es beispielsweise mit der Subdomain “mail.domain1.de” das Mailprogramm zu erreichen und mit” blog.domain1.de” den Blog. Möglich wäre das zwar auch mit Unterverzeichnissen (wie z. B. “domain1.de/mail” und “domain1.de/blog”), aber ich finde Subdomains schöner.

Es gibt zwar kostenlose Domains bei bspw. DuckDNS, aber die sind meist mit Einschränkungen verbunden. Studenten können sich bei Github auch eine vollwertige kostenlose Domain für ein Jahr besorgen.

The best developer tools, free for students. Get your GitHub Student Developer Pack now.

Jedoch ist es auch nicht so teuer, sich eine Domain zu kaufen. Bei Netcup kostet eine .de Domain dauerhaft 5 € im Jahr. Jeder andere Registrar ist auch in Ordnung, solange das DNS und am besten auch die Nameserver eingestellt werden können. Achte auch darauf, dass es bei einigen Anbietern ab dem zweiten Jahr teurer wird, insbesondere dann, wenn es im ersten Jahr nur 1 € kostet.

Im CCP von Netcup rufe ich den Menüpunkt Domains auf:

CCP

Uns interessiert der Tab mit den DNS Einstellungen: DNS Einstellungen

Ich verringere zunächst die TTL ein wenig (24h ist mir zu lang). TTL

Anschließend müssen einige Einträge gemacht werden. Das Zeichen @ bedeutet, dass es um den Root der Zone geht, also meinedomain.de wird auf die IP-Adresse 160.90.80.80 (IP-Adresse meines Servers) verwiesen. Außerdem werden alle (deshalb das ”_”) Subdomains von meinedomain.de wiederum auf meinedomain.de geroutet. Statt _ könnte man auch einzelne Einträge machen. Das kann z. B. dann sinnvoll sein, wenn man mehrere Server unter unterschiedlichen IP-Adressen betreibt.

DNS Einstellungen

Die Einstellungen werden über die Schaltfläche gespeichert. Nun muss man warten. Es kann einen Tag dauern bis die Einstellungen übernommen werden, in seltenen Fällen sogar auch zwei. In der Regel geht es aber deutlich schneller.

Wir testen, ob es geklappt hat, indem wir das Terminal aufrufen und versuchen die Domain zu pingen.

ping dieeingerichtetedomain.de

Erscheint jetzt die Meldung, dass die Ping-Anforderung den Host nicht finden konnte, wissen wir, dass die Einträge noch nicht übernommen wurden.

Manchmal hilft es, die gecachten DNS im System zu leeren.

ipconfig /flushdns

DNS-Flush

Anschließend noch mal versuchen. Wenn die IP-Adresse des Servers angezeigt wird, ist die Domain fürs Erste eingerichtet.

Einrichtung von Traefik

Wir benötigen einen Proxy Manager, der jetzt die Anfragen in Abhängigkeit der (Sub-)Domain weiterleiten kann. Auch wenn ich mich in der Vergangenheit eigentlich mit dem Nginx Proxy Manager angefreundet habe, möchte ich hier Traefik v2 nutzen.

Traefik ist ebenfalls ein open source Reverse Proxy, der aber auf Docker aufsetzt. Zwar ist die Einrichtung meiner Meinung nach komplizierter, dafür ist es anschließend leichter, dynamisch weitere Container mit Traefik zu starten. An dieser Stelle verweise ich außerdem auf zwei andere Anleitungen, die ich zur Konfiguration genutzt habe. Das ist einmal ein Blogeintrag von DigitalOcean und einer von Smarthomebeginner.

Zunächst geben wir der Gruppe Docker die nötigen Rechte am Ordner docker. Dazu benötigen wir ACL.

sudo apt install acl
cd ~
sudo setfacl -Rdm g:docker:rwx docker
sudo setfacl -Rm g:docker:rwx docker
sudo chmod -R 775 docker
getfacl docker

ACL Es müsste nun angezeigt werden, dass die Gruppe Docker am Ordner docker die Rechte zum Lesen ( read), Schreiben (write) und Ausführen (execute) hat.

Wir erstellen eine Datei mit Umgebungsvariablen. Es gibt zwar auch Stimmen, die meinen, man sollte Umgebungsvariablen vermeiden bzw. für sensible Daten nicht nutzen, aber mir erscheint es immer noch besser, als sie direkt einzusetzen (meiner Meinung nach noch unsicherer) oder so was wie Keywhiz zu nutzen (ist mir zu viel Aufwand). Deshalb erstellen wir die Datei mit den nötigen Variablen. Zuvor müssen wir unsere User ID und die Gruppen ID von Docker auslesen.

id

id Diese beiden Zahlen müssen wir in die Umgebungsdatei einfügen.

cd docker
nano .env
PUID=1000
PGID=114

Jetzt sollten noch die Zeitzone, die Verzeichnispfade und die eigene Domain eingefügt werden.

pwd

PWD

nano .env
PUID=1000
PGID=114
TZ=Europe/Berlin
USERDIR=/home/jewgeni
DOCKERDIR=/home/jewgeni/docker
DOMAINNAME=deployn.de

In der Shell ist es überdies möglich über > bzw. >> Dinge direkt in eine Datei zu schreiben. Der Befehl echo wiederholt die Eingabe. Das testen wir mal.

cd ~
echo "hallo"

Es sollte jetzt das Wort „hallo“ erscheinen.

echo "hallo" > test.txt
ls
# Wir sehen, dass im Home-Verzeichnis unseres Benutzers nun eine Textdatei liegt.
# Die Raute am Anfang der Zeile steht übrigens für einen Kommentar.
# Dieser Code wird nicht ausgeführt.
nano test.txt

In der Datei steht „hallo“ :) Diese Datei lässt sich durch weiteren Text ergänzen.

echo "tschüss" > test.txt
# Das gibt einen Fehler, weil die Datei schon exisitiert.
# Mit >> fügen wir einen Text in eine bestehende Datei als neue Zeile ein.
echo "tschüss" >> test.txt
nano test.txt

Wenn bei der Eingabe im Terminal grauer Text erscheint, lässt sich dieser mit der Pfeiltaste nach rechts übernehmen. In der Textdatei sind nun zwei Zeilen. Wir löschen sie wieder.

rm test.txt
ls

Es sollte nur noch der Ordner “docker” sichtbar sein. Nun zum weiteren Vorgang. Wir benötigen als nächstes eine Möglichkeit gehashte Passwörter zu erstellen, dafür installieren wir die Apache2 Hilfsprogramme und testen ob über htpasswd ein gehastes Passwort erstellt wird.

sudo apt install apache2-utils
htpasswd -nb benutzername passwort

Ergebnis (sollte ähnlich aussehen):

htpasswd

Jetzt nutzt du die Daten, die wirklich zur Authentifizierung dienen sollen (am besten ein neues Passwort über einen Passwortgenerator erstellen).

mkdir ~/docker/shared/
htpasswd -nb einbenutzername neuespasswort > ~/docker/shared/.htpasswd

Als Nächstes benötigen wir die Daten für unsere Zertifikate. Wenn sich deine Domain nicht bei Netcup befindet, müssen die Angaben angepasst werden. Bei Netcup benötigen wir drei Angaben: Kundennummer, API-Schlüssel und API-Passwort.

In den Stammdaten des CCP gibt es den API-Menüpunkt. Hier kannst du dir einen API-Schlüssel generieren lassen. Lies jedoch vorher die Nutzungsbedingungen durch. Mit dem API-Schlüssel ist es möglich Domainkäufe zu tätigen. Allein aus diesem Grund sollte das Passwort geheim gehalten werden (im Übrigen auch dein Passwort zum CCP).

API Aktivierung im CCP

Diese Werte tragen wir in die .env Datei ein, außerdem noch eine eigene E-Mail Adresse für die Let’s encrypt Zertifikate

nano ~/docker/.env
PUID=1000
PGID=114
TZ=Europe/Berlin
USERDIR=/home/jewgeni
DOCKERDIR=/home/jewgeni/docker
DOMAINNAME=einedomain.de
NETCUP_CUSTOMER_NUMBER=50000
NETCUP_API_KEY=LDBfH4MNyDlgBgww0lGry0OkouhkMqUI5E
NETCUP_API_PASSWORD=uEzCAzISBUBwGqW9oSEzRJuL3D26Gm1yQFMlnqSB1Lhjjb5Z98
EMAIL_ADDRESS=mail@hallo.de

Als Nächstes erstellen wir einen Ordner für Traefik und unsere Zertifikate. Außerdem noch die Zertifikat-Datei und eine Log-Datei.

mkdir ~/docker/traefik
mkdir ~/docker/traefik/acme
touch ~/docker/traefik/traefik.log
touch ~/docker/traefik/acme/acme.json
chmod 600 ~/docker/traefik/acme/acme.json

Jetzt erstellen wir uns ein neues Netzwerk in Docker.

docker network create web
docker network ls

Es sind nun vermutlich vier Einträge zu sehen (bridge, host, none und web). Zeit unsere hauptsächliche docker-compose-Datei zu erstellen.

nano ~/docker/docker-compose.yml

Jetzt den Text eingeben.

version: "3.7"

### NETWORKS ###
networks:
    web:
        external:
            name: web
    default:
        driver: bridge

### SERVICES ###
services:
    traefik:
        container_name: traefik
        image: traefik:latest
        restart: unless-stopped
        command:
            - --global.checkNewVersion=true
            - --global.sendAnonymousUsage=false
            - --entryPoints.http.address=:80
            - --entryPoints.https.address=:443
            - --entryPoints.traefik.address=:8080
            - --api=true
            - --api.dashboard=true
            - --log=true
            - --log.level=DEBUG
            - --accessLog=true
            - --accessLog.filepath=/traefik.log
            - --accessLog.bufferingSize=100
            - --accessLog.filters.statusCodes=400-499
            - --providers.docker=true
            - --providers.docker.endpoint=unix:///var/run/docker.sock
            - --providers.docker.exposedByDefault=false
            - --providers.docker.network=web
            - --providers.docker.swarmMode=false
            - --providers.file.directory=/rules
            - --providers.file.watch=true
            - --certificatesResolvers.dns-netcup.acme.dnsChallenge.provider=netcup
            - --certificatesResolvers.dns-netcup.acme.caServer=https://acme-staging-v02.api.letsencrypt.org/directory
            - --certificatesResolvers.dns-netcup.acme.email=$EMAIL_ADDRESS
            - --certificatesResolvers.dns-netcup.acme.storage=/acme.json
            - --certificatesResolvers.dns-netcup.acme.dnsChallenge.resolvers=1.1.1.1:53,8.8.8.8:53
            - --certificatesResolvers.dns-netcup.acme.dnschallenge.delayBeforeCheck=60
        networks:
            - web
        security_opt:
            - no-new-privileges:true
        ports:
            - target: 80
              published: 80
              protocol: tcp
              mode: host
            - target: 443
              published: 443
              protocol: tcp
              mode: host
            - target: 8080
              published: 8080
              protocol: tcp
              mode: host
        volumes:
            - $DOCKERDIR/traefik/rules:/rules
            - /var/run/docker.sock:/var/run/docker.sock:ro
            - $DOCKERDIR/traefik/acme/acme.json:/acme.json
            - $DOCKERDIR/traefik/traefik.log:/traefik.log
            - $DOCKERDIR/shared:/shared
        environment:
            - NETCUP_CUSTOMER_NUMBER=$NETCUP_CUSTOMER_NUMBER
            - NETCUP_API_KEY=$NETCUP_API_KEY
            - NETCUP_API_PASSWORD=$NETCUP_API_PASSWORD
        labels:
            - "traefik.enable=true"
            - "traefik.http.routers.http-catchall.entrypoints=http"
            - "traefik.http.routers.http-catchall.rule=HostRegexp(`{host:.+}`)"
            - "traefik.http.routers.http-catchall.middlewares=redirect-to-https"
            - "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https"
            - "traefik.http.routers.traefik-rtr.entrypoints=https"
            - "traefik.http.routers.traefik-rtr.rule=Host(`traefik.$DOMAINNAME`)"
            - "traefik.http.routers.traefik-rtr.tls=true"
            - "traefik.http.routers.traefik-rtr.tls.certresolver=dns-netcup"
            - "traefik.http.routers.traefik-rtr.tls.domains[0].main=$DOMAINNAME"
            - "traefik.http.routers.traefik-rtr.tls.domains[0].sans=*.$DOMAINNAME"
            - "traefik.http.routers.traefik-rtr.service=api@internal"
            - "traefik.http.routers.traefik-rtr.middlewares=middlewares-basic-auth@file"

Damit die letzte Zeile funktioniert, müssen wir die Middleware konfigurieren. Das passiert mit einer Datei im Ordner mit allen Regeln.

mkdir ~/docker/traefik/rules
nano ~/docker/traefik/rules/middlewares.toml

[http.middlewares]
[http.middlewares.middlewares-basic-auth]
[http.middlewares.middlewares-basic-auth.basicAuth]
realm = "Traefik Basic Auth"
usersFile = "/shared/.htpasswd"

Jetzt ist der Zeitpunkt gekommen alles zu testen.

cd ~/docker
docker-compose up -d

Sobald alles fertig ist, schauen wir uns das automatische Protokoll im rechten Fenster an.

lazydocker

Wenn die Meldung kommt, dass auf eine DNS record propagation gewartet wird, ist alles soweit richtig.

Traefik Log

In diesem Fall schrauben wir den Log-Level zurück auf WARN und holen echte Zertifikate, indem wir die Zeile mit --certificatesResolvers.dns-netcup.acme.caServer=https://acme-staging-v02.api.letsencrypt.org/directory auskommentieren.

nano ~/docker/docker-compose.yml

Geänderte Docker-Compose Datei Anschließend löschen wir den Inhalt der acme-Datei.

nano ~docker/traefik/acme/acme.json

In dieser Datei den kompletten Inhalt löschen (auch die Leerzeilen). Jetzt Traefik neu starten.

cd ~/docker
docker-compose down
docker-compose up -d

Warten (5 Minuten), danach versuchen das Dashboard über traefik.dieeigenedomain.de zu öffnen. Falls es nicht klappt, noch mal den Log-Level auf DEBUG setzen, Container neu starten und schauen, was das Log anzeigt.

Traefik Dashboard

Um ins Dashboard zu gelangen, musst du den Benutzernamen und das nicht gehashte Passwort eingeben, das du in die htpasswd Datei geschrieben hast. Wenn alles geklappt hat, können wir Traefik dazu bewegen ein Wildcard-Zertifikat zu nutzen.

nano ~/docker/docker-compose.yml

Auskommentiertes Label

Diesmal muss das Label “traefik.http.routers.traefik-rtr.tls.certresolver=dns-netcup” auskommentiert werden.

cd ~/docker
docker-compose down
docker-compose up -d

Wir ergänzen unsere Middleware um ein Rate-Limit zur Anmeldung und Security Headers. Hier oder hier sind zwei Beispiele für weitere Informationen über Security Headers.

nano ~/docker/traefik/rules/middlewares.toml
[http.middlewares]
  [http.middlewares.middlewares-basic-auth]
    [http.middlewares.middlewares-basic-auth.basicAuth]
      realm = "Traefik Basic Auth"
      usersFile = "/shared/.htpasswd"

  [http.middlewares.middlewares-rate-limit]
    [http.middlewares.middlewares-rate-limit.rateLimit]
      average = 100
      burst = 50

  [http.middlewares.middlewares-secure-headers]
    [http.middlewares.middlewares-secure-headers.headers]
      accessControlAllowMethods= ["GET", "OPTIONS", "PUT"]
      accessControlMaxAge = 100
      hostsProxyHeaders = ["X-Forwarded-Host"]
      sslRedirect = true
      stsSeconds = 63072000
      stsIncludeSubdomains = true
      stsPreload = true
      forceSTSHeader = true
      customFrameOptionsValue = "allow-from https:dieeigenedomain.de"
      contentTypeNosniff = true
      browserXssFilter = true
      referrerPolicy = "same-origin"
      featurePolicy = "camera 'none'; geolocation 'none'; microphone 'none'; payment 'none'; usb 'none'; vr 'none';"
      [http.middlewares.middlewares-secure-headers.headers.customResponseHeaders]
        X-Robots-Tag = "none,noarchive,nosnippet,notranslate,noimageindex,"
        server = ""

Wir haben drei Middlewares eingerichtet:

  • Authentifizierung
  • Rate-Limit
  • Security-Headers

Um sie zu nutzen, können wir sie einzeln in die docker-compose Datei eintragen oder Ketten mit Kombinationen aus ihnen einrichten.

Eintrag ohne Kette

Um eine Kette zu erstellen, kreieren wir eine neue Datei im Rules-Ordner:

nano ~/docker/traefik/rules/middlewares-chains.toml
[http.middlewares]
  [http.middlewares.chain-no-auth]
    [http.middlewares.chain-no-auth.chain]
      middlewares = [ "middlewares-rate-limit", "middlewares-secure-headers"]

  [http.middlewares.chain-basic-auth]
    [http.middlewares.chain-basic-auth.chain]
      middlewares = [ "middlewares-rate-limit", "middlewares-secure-headers", "middlewares-basic-auth"]

Natürlich könnt ihr auch andere Kombinationen ergänzen, aber dann ist es eventuell einfacher, die oben genannte Code-Zeile zu nutzen und die Dinge zu kombinieren, die man haben möchte. Durch die Ketten ist es möglich, die drei oben gezeigten Zeilen in eine zu ändern:

Kette aus Middlewares in docker-compose

Nachdem dieses „Etikett“ in der docker-compose Datei eingetragen ist, muss Traefik neu gestartet werden, damit die Änderungen in Kraft treten.

cd ~/docker
nano docker-compose
# Datei anpassen (- "traefik.http.routers.traefik-rtr.middlewares=middlewares-chain-basic-auth@file" in die labels einfügen)
# Auf die Einrückung beim Einfügen achten
docker-compose down
docker-compose up -d

Jetzt im Dashboard nachsehen, ob die Middleware richtig geladen werden.

Dashboard Middleware

Und prüfen, ob die Kette als Middleware im HTTP-Router von Traefik geladen wird:

Middleware im HTTP Router

Google Authentifizierung

Die Authentifizierung, die wir bereits eingerichtet haben, bringt zwar zusätzliche Sicherheit, aber es gibt Authentifizierungsmethoden mit mehr Funktionen. Deshalb möchten wir uns mal Google OAuth anschauen.

Meet your business challenges head on with cloud computing services from Google, including data management, hybrid & multi-cloud, and AI & ML.
— Cloud Computing Services | Google Cloud

Es ist nötig sich in der Cloud-Konsole zu registrieren (kostenlos). Nach einigen Klicks gelangt man ins Dashboard.

Google Cloud Dashboard

Hier erstellen wir ein neues Projekt und benennen es so, dass wir es wiederfinden. Neues Projekt

Zunächst muss ein Zustimmungsbildschirm erstellt werden. Zustimmungsbildschirm

Dahin gelangen wir über den Menüpunkt APIs & Dienste.

Externer Usertype

Ein Name muss angegeben werden, eine E-Mail-Adresse auch. Optional kann auch ein Logo hochgeladen werden. Anwendungsinformation

Eine Domain muss autorisiert werden. Das Limit liegt bei 10. Domainauthorisierung

Bei den Bereichen wählen wir die drei nicht kritischen Bereiche aus. E-Mail, Profile und OpenID. Bereiche

Bei den Testnutzern haben wir eine Obergrenze von 100. Diese 100 Nutzer dürfen die Anwendung dahinter nutzen. Also trag dich da am besten selbst ein, andere Nutzer lassen sich später nachtragen. Testnutzer

Über APIs & Dienste wählen wir Anmeldedaten. Anmeldedaten

Nun fügen wir eine OAuth-Client-ID hinzu. OAuth-Client-ID

Wir tragen eine autorisierte Weiterleitungsdomain ein. Weiterleitungsdomain

Die Schlüssel, die uns soeben angezeigt werden, speichern wir in die .env Datei ab.

nano ~/docker/.env

Die neuen Einträge nennen wir GOOGLE_CLIENT_ID und GOOGLE_CLIENT_SECRET. Danach erstellen wir ein GOOGLE_OAUTH_SECRET.

echo "GOOGLE_OAUTH_SECRET=$(openssl rand -hex 16)" >> ~/docker/.env
nano ~/docker/.env

Die Datei dürfte jetzt ungefähr so aussehen:

PUID=1000
PGID=114
TZ=Europe/Berlin
USERDIR=/home/jewgeni
DOCKERDIR=/home/jewgeni/docker
DOMAINNAME=irgendeinedomain.de
NETCUP_CUSTOMER_NUMBER=50000
NETCUP_API_KEY=lksjfrjgoigKJLKjlhir
NETCUP_API_PASSWORD=SlkjiojKLhiuuzvTFDENgdeu
EMAIL_ADDRESS=mail@gmail.com
GOOGLE_CLIENT_ID=uhrlkiaungopterktdsnf.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=lkiz537Jkhkr
GOOGLE_OAUTH_SECRET=leaj7GjejmG

Jetzt muss diese Authentifizierungsmethode auch Traefik hinzugefügt werden. Zunächst als Middleware.

nano ~/docker/traefik/rules/middlewares.toml
[http.middlewares]
  [http.middlewares.middlewares-basic-auth]
    [http.middlewares.middlewares-basic-auth.basicAuth]
      realm = "Traefik Basic Auth"
      usersFile = "/shared/.htpasswd"

  [http.middlewares.middlewares-oauth]
    [http.middlewares.middlewares-oauth.forwardAuth]
      address = "http://oauth:4181"
      trustForwardHeader = true
      authResponseHeaders = ["X-Forwarded-User"]

  [http.middlewares.middlewares-rate-limit]
    [http.middlewares.middlewares-rate-limit.rateLimit]
      average = 100
      burst = 50

  [http.middlewares.middlewares-secure-headers]
    [http.middlewares.middlewares-secure-headers.headers]
      accessControlAllowMethods= ["GET", "OPTIONS", "PUT"]
      accessControlMaxAge = 100
      hostsProxyHeaders = ["X-Forwarded-Host"]
      sslRedirect = true
      stsSeconds = 63072000
      stsIncludeSubdomains = true
      stsPreload = true
      forceSTSHeader = true
      customFrameOptionsValue = "allow-from https:example.com"
      contentTypeNosniff = true
      browserXssFilter = true
      referrerPolicy = "same-origin"
      featurePolicy = "camera 'none'; geolocation 'none'; microphone 'none'; payment 'none'; usb 'none'; vr 'none';"
      [http.middlewares.middlewares-secure-headers.headers.customResponseHeaders]
        X-Robots-Tag = "none,noarchive,nosnippet,notranslate,noimageindex,"
        server = ""

Und dann als Kette.

nano ~/docker/traefik/rules/middlewares-chains.toml
[http.middlewares]
  [http.middlewares.chain-no-auth]
    [http.middlewares.chain-no-auth.chain]
      middlewares = [ "middlewares-rate-limit", "middlewares-secure-headers"]

  [http.middlewares.chain-basic-auth]
    [http.middlewares.chain-basic-auth.chain]
      middlewares = [ "middlewares-rate-limit", "middlewares-secure-headers", "middlewares-basic-auth"]

  [http.middlewares.chain-oauth]
    [http.middlewares.chain-oauth.chain]
      middlewares = [ "middlewares-rate-limit", "middlewares-secure-headers", "middlewares-oauth"]

Bevor wir das testen können, müssen wir aber den OAuth-Dienst in Docker starten. Also ergänzen wir die docker-compose Datei.

nano ~/docker/docker-compose.yaml
version: "3.7"

### NETWORKS ###
networks:
    web:
        external:
            name: web
    default:
        driver: bridge

### SERVICES ###
services:
    ### TRAFIK - REVERSE PROXY ###
    traefik:
        container_name: traefik
        image: traefik:latest
        restart: unless-stopped
        command:
            - --global.checkNewVersion=true
            - --global.sendAnonymousUsage=false
            - --entryPoints.http.address=:80
            - --entryPoints.https.address=:443
            - --entryPoints.traefik.address=:8080
            - --api=true
            - --api.dashboard=true
            - --log=true
            - --log.level=WARN
            - --accessLog=true
            - --accessLog.filepath=/traefik.log
            - --accessLog.bufferingSize=100
            - --accessLog.filters.statusCodes=400-499
            - --providers.docker=true
            - --providers.docker.endpoint=unix:///var/run/docker.sock
            - --providers.docker.exposedByDefault=false
            - --providers.docker.network=web
            - --providers.docker.swarmMode=false
            - --providers.file.directory=/rules
            - --providers.file.watch=true
            - --certificatesResolvers.dns-netcup.acme.dnsChallenge.provider=netcup
            # - --certificatesResolvers.dns-netcup.acme.caServer=https://acme-staging-v02.api.letsencrypt.org/directory
            - --certificatesResolvers.dns-netcup.acme.email=$EMAIL_ADDRESS
            - --certificatesResolvers.dns-netcup.acme.storage=/acme.json
            - --certificatesResolvers.dns-netcup.acme.dnsChallenge.resolvers=1.1.1.1:53,8.8.8.8:53
            - --certificatesResolvers.dns-netcup.acme.dnschallenge.delayBeforeCheck=10
        networks:
            - web
        security_opt:
            - no-new-privileges:true
        ports:
            - target: 80
              published: 80
              protocol: tcp
              mode: host
            - target: 443
              published: 443
              protocol: tcp
              mode: host
            - target: 8080
              published: 8080
              protocol: tcp
              mode: host
        volumes:
            - $DOCKERDIR/traefik/rules:/rules
            - /var/run/docker.sock:/var/run/docker.sock:ro
            - $DOCKERDIR/traefik/acme/acme.json:/acme.json
            - $DOCKERDIR/traefik/traefik.log:/traefik.log
            - $DOCKERDIR/shared:/shared
        environment:
            - NETCUP_CUSTOMER_NUMBER=$NETCUP_CUSTOMER_NUMBER
            - NETCUP_API_KEY=$NETCUP_API_KEY
            - NETCUP_API_PASSWORD=$NETCUP_API_PASSWORD
        labels:
            - "traefik.enable=true"
            - "traefik.http.routers.http-catchall.entrypoints=http"
            - "traefik.http.routers.http-catchall.rule=HostRegexp(`{host:.+}`)"
            - "traefik.http.routers.http-catchall.middlewares=redirect-to-https"
            - "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https"
            - "traefik.http.routers.traefik-rtr.entrypoints=https"
            - "traefik.http.routers.traefik-rtr.rule=Host(`traefik.$DOMAINNAME`)"
            - "traefik.http.routers.traefik-rtr.tls=true"
            # - "traefik.http.routers.traefik-rtr.tls.certresolver=dns-netcup"
            - "traefik.http.routers.traefik-rtr.tls.domains[0].main=$DOMAINNAME"
            - "traefik.http.routers.traefik-rtr.tls.domains[0].sans=*.$DOMAINNAME"
            - "traefik.http.routers.traefik-rtr.service=api@internal"
            - "traefik.http.routers.traefik-rtr.middlewares=chain-oauth@file"

    ### GOOGLE OAUTH ###
    oauth:
        container_name: oauth
        image: thomseddon/traefik-forward-auth:latest
        restart: unless-stopped
        networks:
            - web
        security_opt:
            - no-new-privileges:true
        environment:
            - CLIENT_ID=$GOOGLE_CLIENT_ID
            - CLIENT_SECRET=$GOOGLE_CLIENT_SECRET
            - SECRET=$GOOGLE_OAUTH_SECRET
            - COOKIE_DOMAIN=$DOMAINNAME
            - INSECURE_COOKIE=false
            - AUTH_HOST=oauth.$DOMAINNAME
            - URL_PATH=/_oauth
            - WHITELIST=$EMAIL_ADDRESS
            - LOG_LEVEL=info
            - LOG_FORMAT=text
            - LIFETIME=2592000
        labels:
            - "traefik.enable=true"
            - "traefik.http.routers.oauth-rtr.entrypoints=https"
            - "traefik.http.routers.oauth-rtr.rule=Host(`oauth.$DOMAINNAME`)"
            - "traefik.http.routers.oauth-rtr.tls=true"
            - "traefik.http.routers.oauth-rtr.service=oauth-svc"
            - "traefik.http.services.oauth-svc.loadbalancer.server.port=4181"
            - "traefik.http.routers.oauth-rtr.middlewares=chain-oauth@file"

Sollen mehrere E-Mail-Adressen sich anmelden dürfen, muss die Umgebung bei WHITELIST erweitert werden. WHITELIST=$EMAIL_ADDRESS, $EMAIL_ADDRESS2, … Diese Konstanten müssen dann auch in die .env Datei eingetragen werden und ggf. in die Google Cloud Konsole.

Webseite hosten

Als einfachen Beispieldienst möchte ich jetzt eine Webseite (kann auch jeder andere Dienst sein) hosten. Dazu nutze ich das offizielle Docker Image von Nginx.

cd ~/docker
mkdir appdata
mkdir appdata/testseite
cd ~/docker/appdata/testseite
nano index.html

Hier kann nun ein einfacher HTML-Code zum Test eingetragen werden.

<!doctype html>
<html>
	<head>
		<title>Testseite</title>
	</head>
	<body>
		<p>Hallo</p>
	</body>
</html>

Danach benötigen wir eine neue docker-compose Datei für den Dienst.

cd ~/docker
nano docker-compose-testseite.yml
version: "3.7"

### NETWORKS ###
networks:
    web:
        external:
            name: web
    default:
        driver: bridge

### SERVICE ###
services:
    testseite:
        container_name: testseite
        image: nginx:latest
        restart: unless-stopped
        networks:
            - web
        security_opt:
            - no-new-privileges:true
        volumes:
            - $DOCKERDIR/appdata/testseite:/usr/share/nginx/html:ro
        labels:
            - "traefik.enable=true"
            - "traefik.http.routers.testseite-rtr.entrypoints=https"
            - "traefik.http.routers.testseite-rtr.rule=Host(`testseite.$DOMAINNAME`)"
            - "traefik.http.routers.testseite-rtr.tls=true"
            - "traefik.http.routers.testseite-rtr.service=testseite-svc"
            - "traefik.http.services.testseite-svc.loadbalancer.server.port=80"
            - "traefik.http.routers.testseite-rtr.middlewares=chain-no-auth@file"
docker-compose -f docker-compose-testseite.yml up -d

Die Seite müsste daraufhin wenige Augenblicke später unter testseite.domain.de erreichbar sein. Doch was ist, wenn man eine zweite Seite online haben möchte? Gleiche Prozedur:

cd ~/docker
mkdir appdata
mkdir appdata/testseite2
cd ~/docker/appdata/testseite2
nano index.html
<!doctype html>
<html>
	<head>
		<title>Testseite</title>
	</head>
	<body>
		<p>Hallo</p>
		<p>Das ist die zweite Seite.</p>
	</body>
</html>
cd ~/docker
nano docker-compose-testseite2.yml
version: "3.7"

### NETWORKS ###
networks:
    web:
        external:
            name: web
    default:
        driver: bridge

### SERVICE ###
services:
    testseite2:
        container_name: testseite2
        image: nginx:latest
        restart: unless-stopped
        networks:
            - web
        security_opt:
            - no-new-privileges:true
        volumes:
            - $DOCKERDIR/appdata/testseite2:/usr/share/nginx/html:ro
        labels:
            - "traefik.enable=true"
            - "traefik.http.routers.testseite2-rtr.entrypoints=https"
            - "traefik.http.routers.testseite2-rtr.rule=Host(`testseite2.$DOMAINNAME`)"
            - "traefik.http.routers.testseite2-rtr.tls=true"
            - "traefik.http.routers.testseite2-rtr.service=testseite2-svc"
            - "traefik.http.services.testseite2-svc.loadbalancer.server.port=80"
            - "traefik.http.routers.testseite2-rtr.middlewares=chain-basic-auth@file"
docker-compose -f docker-compose-testseite2.yml up -d
lazydocker

In Lazydocker sollten jetzt beide Dienste zu sehen sein. Zwei Nginx Container

Über testseite.domain.de und testseite2.domain.de sind die beiden Seiten erreichbar, Portfreigaben mussten nicht erstellt werden. Wenn die Einstellungen meiner Docker-Compose Dateien übernommen werden, erscheint bei der testseite2 das Anmeldefenster mit der einfachen Authentifizierung. Die Container können indessen wieder gestoppt werden.

cd ~/docker
docker-compose -f docker-compose-testseite.yml down
docker-compose -f docker-compose-testseite2.yml down

Die Einrichtung ist damit fertig 😃


Diese Website verwendet Cookies. Diese sind notwendig, um die Funktionalität der Website zu gewährleisten. Weitere Informationen finden Sie in der Datenschutzerklärung