Ein Webserver(-Service) mit Traefik und ein paar Tipps am Rande

Der Edge-Router oder auch Proxy Traefik ist schon irgendwie cool. Zugegeben, dieser Satz hat null Aussagekraft, ist sehr subjektiv und es fehlen jegliche Argumente, aber wie wäre es denn mit folgenden: Traefik hat mich vor allem aufgrund der unfassbar vielfältigen Konfigurationsmöglichkeiten überzeugt. Auch wenn anfangs nicht alles direkt “rund” lief, so scheint Traefik für alle Anforderungen irgendwo noch eine Option zu besitzen, die nur noch gefunden und eingesetzt werden muss. In diesem Artikel möchte ich zunächst ein Beispiel eines einfachen Webserver-Services zeigen, anschließend folgen noch einige Hinweise zu konkreten Anwendungen, etwa GitLab oder PowerDNS-Admin.

Traefik und ein Webserver-Service

Der Webserver in folgendem Beispiel läuft unter Nginx und hostet einfach nur ein paar statische Seiten. Genauso könnten jedoch beliebige Anwendungen dahinter stecken, etwa ein typischer Web-Stack, bestehend aus Nginx, PHP, MySQL/MariaDB, Redis und weiteren Services, denn letztlich sorgt Traefik nur dafür, dass eingehende Requests an den Nginx-Server weiter geleitet werden und die Response wieder beim anfragenden Client ankommt. Das entsprechende Docker-Compose-File für diese WordPress-Installation sieht in Bezug auf den Teil, der für Traefik relevant ist, insofern identisch aus. Nun aber zum Docker-Compose-File:

version: '3.3'
services:
  nginx:
    image: geschke/nginx-swrm
    volumes:
      - type: bind
        source: ./html
        target: /var/www/html
    deploy:
      replicas: 1
      placement:
        constraints:
          - node.hostname == stralsund
    labels:
      - "traefik.enable=true" # enable traefik
      - "traefik.docker.network=traefik-public" # put it in the same network as traefik

      # http part
      - "traefik.http.routers.website_geschkenet.rule=Host(`www.geschke.net`) || Host(`geschke.net`)" 
      - "traefik.http.routers.website_geschkenet.entrypoints=http"

      - "traefik.http.middlewares.def-geschkenet-domainredirect.redirectregex.regex=^http://(?:www.)?geschke.net/(.*)"
      - "traefik.http.middlewares.def-geschkenet-domainredirect.redirectregex.replacement=https://www.geschke.net/$${1}"
      - "traefik.http.middlewares.def-geschkenet-domainredirect.redirectregex.permanent=true"

      - "traefik.http.routers.website_geschkenet.middlewares=def-geschkenet-domainredirect"

      # https part
      - "traefik.http.routers.website_geschkenet-secured.rule=Host(`www.geschke.net`) || Host(`geschke.net`)" 
      - "traefik.http.routers.website_geschkenet-secured.entrypoints=https"
      - "traefik.http.routers.website_geschkenet-secured.tls=true"


      - "traefik.http.middlewares.def-geschkenet-secured_domainredirect.redirectregex.regex=^https://geschke.net/(.*)"
      - "traefik.http.middlewares.def-geschkenet-secured_domainredirect.redirectregex.replacement=https://www.geschke.net/$${1}"
      - "traefik.http.middlewares.def-geschkenet-secured_domainredirect.redirectregex.permanent=true"


      - "traefik.http.services.srv-website_geschkenet-secured.loadbalancer.server.port=80" 
      - "traefik.http.routers.website_geschkenet-secured.service=srv-website_geschkenet-secured"

      - "traefik.http.routers.website_geschkenet-secured.middlewares=secHeaders@file,def-compress,def-geschkenet-secured_domainredirect"

    configs:
      - source: nginx_config_default
        target: /etc/nginx/sites-enabled/default
        mode: 0440
    networks:
      - "traefik-public"


configs:
  nginx_config_default:
    file: ./nginx/sites-enabled/default


networks:
  traefik-public:
    external: true

Tatsächlich handelt es sich dabei um ein Docker-Compose-File, das als Docker Stack Verwendung findet, wobei der Traefik-Teil genauso aussehen würde, wenn er nicht im Docker Swarm mit Docker Stack liefe.

Die ersten Zeilen sollten bekannt sein, das Verzeichnis der HTML-Dateien wird in den Container gemountet, die Angabe des Hostnamens als Constraint im Deploy-Teil sorgt dafür, dass der Service nicht beliebig auf einem Node im Cluster, sondern auf einem speziellen Host läuft – zu den Hintergründen hatte ich in einem früheren Artikel über Nginx-Proxy & -Companion mal etwas geschrieben.

Die Konfiguration von Traefik findet wieder im Abschnitt “labels:” statt, dabei sollten einige Optionen aus dem ersten Artikel über Traefik bekannt sein.

Nachdem Traefik mit “.enable=true” aktiviert und das zu nutzende Docker Network ausgewählt wurde, folgt die Konfiguration für den Router, der sich für das http-Protokoll zuständig zeigt. Als Name des Routers fungiert “website_geschkenet“, und mit der Regel “.http.routers.website_geschkenet.rule=Host(`www.geschke.net`) || Host(`geschke.net`)"” wird festgelegt, dass der Webserver sowohl mit als auch ohne “www” funktionieren soll. In der nächsten Zeile wird der Entrypoint namens “http” eingesetzt, der in der statischen Konfiguration im Docker-Compose-File von Traefik definiert und auf Port 80 gesetzt wurde.

Redirects, oder: Aus vier mach eins

Die nächsten drei Zeilen sind schon etwas interessanter. Darin wird eine Middleware unter der etwas sperrigen Bezeichnung “def-geschkenet-domainredirect” definiert, deren Aufgabe es ist, Weiterleitungen durchzuführen. Denn falls sich noch jemand auf die unverschlüsselte Seite mit http-Protokoll verirrt, soll dieser auf die verschlüsselte Variante mit https weitergeleitet werden. Dies soll sowohl für einen Request auf die Domain ohne vorangestelltes “www” gelten, als auch in der Variante mit vollständigem Hostnamen inkl. “www”. Zwar verbergen aktuelle Browser inzwischen diese ursprüngliche Bezeichnung des World Wide Web im Hostnamen, für den Nutzer wäre es somit kaum merkbar, aber schließlich soll die Website bei Google & Co. nur einmal indiziert werden, um Duplicate Content zu vermeiden.

An dieser Stelle zeigt sich auch wieder einmal die Mannigfaltigkeit der Konfigurationsoptionen von Traefik. Natürlich könnte eine solche Weiterleitung auch in der Webserver-Konfiguration von Nginx eingebaut werden, aber das würde die Komplexität nur weiter nach hinten verlagern, während sich Traefik an vorderster Front um die Aufgabe kümmern kann.

Die Definition der Middleware sieht wie folgt aus:

- "traefik.http.middlewares.def-geschkenet-domainredirect.redirectregex.regex=^http://(?:www.)?geschke.net/(.*)"
- "traefik.http.middlewares.def-geschkenet-domainredirect.redirectregex.replacement=https://www.geschke.net/$${1}"
- "traefik.http.middlewares.def-geschkenet-domainredirect.redirectregex.permanent=true"

Der reguläre Ausdruck in der ersten Zeile besagt, dass eine Übereinstimmung oder auch Match vorliegt, wenn die URL mit “http://” startet, danach entweder nichts oder “www.” folgt, und schließlich mit dem Domainnamen endet, wobei sich dahinter eine beliebige Zeichenkette befinden kann. Da Traefik selbst in Go geschrieben ist, muss die Syntax des Regexp-Packages verwendet werden. In der nächsten Zeile ist angegeben, wodurch der vollständige Match ersetzt werden soll, in dem Fall ist es die https-Variante einschließlich “www” der Domain, und falls dahinter eine Pfad-Angabe oder ein Query-String enthalten war, wird dieser ebenfalls wieder hinzugefügt. D.h. alles, was durch “(.*)” gefunden wurde, wobei der Ausdruck eben besagt “beliebige Zeichen in beliebiger Anzahl (auch 0)”, wird an die Stelle von “$${1}” gepackt, wobei das erste Dollar-Zeichen fürs Escapen des nachfolgenden “$”-Zeichens zuständig ist. Zu guter Letzt wird der Middleware die Art des Redirects genannt, der Default wäre ein temporärer Redirect, hier soll aber der permanente Redirect zum Einsatz kommen.

Die Middleware muss auch hier dem Router hinzugefügt werden, was mittels “.http.routers.website_geschkenet.middlewares=def-geschkenet-domainredirect” passiert. Damit ist der Router vollständig, denn bei einem Request auf die unverschlüsselte Variante soll nicht mehr passieren als diesen auf die https-Website zu leiten.

Im Bereich der Konfiguration für Requests per https sehen die Namen von Router, Service etc. noch etwas sperriger aus, da ich hierbei jeweils einfach eine Zeichenkette “-secured” angehängt habe. Die erste Zeile ist analog zum vorherigen Teil, nur trägt der Router nun den Namen “website_geschkenet-secured“. Als Entrypoint soll “https” genutzt werden, global definiert auf Port 443. Mit “.tls=true” wird die Verschlüsselung aktiviert, wobei in der gesamten Definition keine Angabe folgt, auf welchem Wege das Let’s-Encrypt-Zertifikat generiert werden soll. Das liegt daran, dass das Zertifikat bereits vorliegt und somit Traefik bekannt ist, da es extern generiert worden ist und die Pfade der Zertifikats- und Private-Key-Dateien innerhalb der dynamischen Konfiguration von Traefik vorhanden sind. Vergleiche dazu die Ausführungen im letzten Artikel. Andernfalls müsste der Certresolver angegeben werden, für TLS “.tls.certresolver=le-tls” oder “.tls.certresolver=le-ns1“, falls diese so definiert wurden wie beschrieben.

Die Middleware namens “def-geschkenet-secured_domainredirect” ist ebenfalls sehr ähnlich der bereits beschriebenen im Bereich für “http”. Hier soll allerdings nur ein Redirect erfolgen, wenn ein Request zwar per https, aber ohne Angabe von “www” eingehen sollte, in dem Fall wird zur vollständigen Variante umgeleitet.

Doch nun sollen Requests auch wirklich beantwortet und nicht nur redirected werden. Dazu wird ein Service definiert, und zwar mittels “.http.services.srv-website_geschkenet-secured.loadbalancer.server.port=80“. Um den Namen noch ein wenig zu verlängern bzw. genaugenommen um auf den ersten Blick zu erkennen, dass es sich um eine Service-Definition von Traefik handelt, wurde “srv-” als Prefix verwendet. Da der Nginx-Webserver hier standardmäßig auf Port 80 lauscht, wird der Loadbalancer auf diesen Port angesetzt. Mit “.http.routers.website_geschkenet-secured.service=srv-website_geschkenet-secured” wird der (Traefik-)Service schließlich dem Router hinzugefügt. Somit wird letztlich Nginx und damit der Webserver für das Hosting der statischen Websiten angesprochen.

In der letzten Zeile der Traefik-Labels werden alle Middlewares hinzugefügt, die der Router verwenden soll:

- "traefik.http.routers.website_geschkenet-secured.middlewares=secHeaders@file,def-compress,def-geschkenet-secured_domainredirect"

Dies sind die bereits bekannten und globalen “secHeaders@file” für die Verbesserung der Sicherheit, “def-compress” für gzip-komprimierte Übertragung sowie die hier beschriebene “def-geschkenet-secured_domainredirect“.

Damit wäre der reine Traefik-Teil fertig, alle anderen Abschnitte des Docker-Compose-Files haben sich gegenüber der bisherigen Variante nicht geändert bzw. sollten bekannt sein. Da ich auf dem betreffenden Server Docker Swarm einsetze, wird der Webserver-Service mit als Docker Stack gestartet:

geschke@stralsund:~/services/www.geschke.net$ docker stack deploy -c website_geschkenet.yml geschkenet

Genauso kann “docker-compose” zum Einsatz kommen, in dem Fall wäre es:

geschke@oc01:~/services/www.geschke.net$ docker-compose -f website_geschkenet.yml up -d

Sobald der Service gestartet wurde, macht sich dies auch in der Traefik-UI bemerkbar. Ein Klick in die Detailansicht des neuen Routers offenbart dessen Konfiguration.

Traefik - Router-Konfiguration für einen Webserver-Service

Die Traefik-Konfiguration für einen einfachen Webserver-Dienst ist zwar angesichts der Vielzahl von Labels, die zur Konfiguration benötigt werden, aufwändiger als wenn Nginx-Proxy und dessen -Companion genutzt werden, aber insgesamt auch wesentlich flexibler, wie das Beispiel des Redirect per regulärem Ausdruck zeigt. Im Folgenden möchte ich noch ein paar Hinweise zur Konfiguration einzelner Anwendungen geben.

Traefik mit GitLab und Fido U2F

Auf einem Host läuft auch eine GitLab-Instanz, natürlich ebenfalls in einer Docker-Ausführung. Die meisten Labels sind analog zu der vorgestellten Konfiguration einzusetzen, und so konnte GitLab zunächst erfolgreich gestartet werden. Jedoch gab es Probleme beim Login, und zwar im Zusammenhang mit der Zwei-Faktor-Authentisierung FIDO U2F. Die Logs zeigten nichts Auffälliges, bis auf die Angabe des Headers "X-Forwarded-Server":["a0dfbdea8cff"].  Anstatt des korrekten Hostnamens wurde hier die Container-ID von GitLab genutzt. Auch hier konnte mit einer Middleware Abhilfe geschaffen werden, die wie folgt aussieht und wie üblich schlussendlich zum betreffenden Router hinzugefügt werden muss:

      - "traefik.http.middlewares.def-gitkuerbisorg.headers.customrequestheaders.X-Forwarded-Ssl=on"
      - "traefik.http.middlewares.def-gitkuerbisorg.headers.customrequestheaders.X-Forwarded-Server=git.kuerbis.org"

[...]
      - "traefik.http.routers.gitkuerbisorg-secured.middlewares=secHeaders@file,def-gitkuerbisorg,def-compress"

Damit war auch der Login-Vorgang wieder erfolgreich möglich.

Traefik und PowerDNS-Admin

Ein ähnliches Problem zeigte sich bei PowerDNS-Admin. Dabei war der Login zwar augenscheinlich erfolgreich, es gab keine Fehlermeldung oder ähnliches. Stattdessen erschien nach dem Login eine Seite mit dem HTTP-Fehlercode “403 – Access denied”. Das Log der mittels Python-Webframework Flask geschriebenen PowerDNS-Admin-Anwendung zeigte folgendes an:

[2020-06-09 09:29:10 +0000] [11] [WARNING] Forbidden (Referer checking failed: no referer.): /login
[2020-06-09 09:29:10,146] [flask_seasurf.py:249] WARNING - Forbidden (Referer checking failed: no referer.): /login

Anscheinend wurde der Referer hier nicht korrekt übermittelt, weshalb erneut einer Middleware eine entsprechende Option hinzugefügt wurde:

- "traefik.http.middlewares.def-ns1xyzcdnxyz.headers.referrerPolicy=origin"

Nach dieser Änderungen konnte der Login-Prozess erfolgreich abgeschlossen werden.

Traefik und Pfade für (Micro-)Services

Traefik bezeichnet sich selbst als Edge-Router für die Veröffentlichung von Services, und genau diese Aussage habe ich im folgenden Beispiel beim Wort genommen. Auf der “Welcome”-Seite von Traefik befindet sich ein schönes Schaubild, das ich für dieses Beispiel schamlos kopiert und ein wenig ergänzt habe.

Traefik Edge-Router für Services, Grafik (c) bei den Traefik-Autoren

Quelle: https://doc.traefik.io/traefik/

Traefik und die PowerDNS-API

Auf einem der Hosts läuft ein PowerDNS-Nameserver, der aus mehreren Komponenten besteht. Zu den Details zur Installation und Konfiguration verweise ich auf frühere Artikel, eine Übersicht findet sich auf der Themen-Seite zu DNS & Pi-hole. Zur einfacheren Verwaltung der Domains dient PowerDNS-Admin, das unter dem Hostnamen des Nameservers erreichbar ist, konkret wäre dies ns1.xyzcdn.xyz. Traefik kümmert sich um die Erreichbarkeit, die Konfiguration ist analog zu den bereits beschriebenen Beispielen.

Nun bietet der PowerDNS Authoritative Server eine API, die zwar auch unter einem eigenen Hostnamen erreichbar sein könnte (wie im Schaubild mit api.domain.com), aber mir stellte sich die Frage, ob es nicht auch möglich wäre, die API mit demselben Hostnamen wie PowerDNS-Admin, jedoch unter dem Pfad “/api/” zur Verfügung zu stellen. Die Frage zeigt sich sehr rhetorisch, denn die umfassenden Features von Traefik, in dem Fall eine Routing-Regel ermöglichen eine solche Konfiguration.

Somit müssen dem PowerDNS-Service nur noch die entsprechenden Labels hinzugefügt werden, die meisten sind aus den beschriebenen Beispielen bekannt, nur dass sich die Host-Rule jetzt nicht nur auf einen oder mehrere Hostnamen bezieht, sondern noch ein weiteres Element beinhaltet:

- "traefik.http.routers.ns1xyzcdnxyzapi-secured.rule=Host(`ns1.xyzcdn.xyz`) && PathPrefix(`/api`)"

Der Router “ns1xyzcdnxyzapi-secured” wird von Traefik also genau dann verwendet, wenn der Pfad “/api” aufgerufen wird. Die Definition der Middleware, Services etc. erfolgen wie bisher, nur wird in diesem Fall die PowerDNS-API angesprochen, die, falls aktiviert, standardmäßig auf Port 8081 verfügbar ist. Insofern nutzt diesen Port auch der Service:

- "traefik.http.services.ns1xyzcdnxyzapi-secured.loadbalancer.server.port=8081"

Dieser Port muss innerhalb des Docker-Compose-Files natürlich nicht nach außen hin freigegeben werden! Das wäre auch alles andere als geschickt, denn PowerDNS unterstützt kein TLS, weshalb sich Traefik um die verschlüsselte Übertragung kümmert.

Noch ein Service, noch eine API

In ähnlicher Form, aber noch mit einer kleinen Ergänzung wird der dynpower-Service angesprochen. Mit diesem kleinen in Go geschriebenen Programm wird PowerDNS als Dynamic DNS Server genutzt – Details dazu und weitere Hinweise zur “Traefik Magic” finden sich im Artikel “Dynamic DNS in Eigenregie“.

Dabei soll der Pfad “/dynapi/” lauten, was folgende Regel ermöglicht:

- "traefik.http.routers.ns1xyzcdnxyzdynapi-secured.rule=Host(`ns1.xyzcdn.xyz`) && PathPrefix(`/dynapi`)"

Nun ist – zumindest aktuell – der Pfad bei dynpower fix eingestellt auf “/api/“, d.h. dynpower erwartet von Requests, dass sie ausschließlich unter diesem Pfad eingehen. Für eine spätere Version wäre eine flexible Konfiguration zugegebenermaßen eine gute Idee, aber momentan existiert eben diese Einschränkung. Wenn jedoch die URL in der Form ns1.xyzcdn.xyz/dynapi/api/ aufgerufen wird, erkennt dynpower dies nicht an, da der komplette Pfad von Traefik mitgegeben wird, d.h. inkl. “/dynapi/“.

Doch auch für dieses Problem bietet eine Middleware eine Lösung:

- "traefik.http.middlewares.def-ns1xyzcdnxyzdynapi-strip.stripprefix.prefixes=/dynapi"

Die Namen der Middlewares werden zwar immer sperriger, aber die Aufgabe wird hervorragend erfüllt. Bevor der Request an den dynpower-Service weitergegeben wird, wird einfach der Prefix “/dynapi” aus dem Pfad entfernt. Somit kommt bei dynpower nur der gewünschte Pfad “/api/” an. Die zusätzliche Middleware muss nur noch in der üblichen Weise eingefügt werden:

- "traefik.http.routers.ns1xyzcdnxyzdynapi-secured.middlewares=secHeaders@file,def-ns1xyzcdnxyzdynapi-strip,def-ns1xyzcdnxyzdynapi,def-compress"

Cool, oder?

Wer bis hierhin durchgehalten hat, wird mir doch hoffentlich zustimmen, oder? Traefik ist cool! Insbesondere die zuletzt geschilderten Lösungen habe mich zu dieser Ansicht veranlasst, es scheint, als biete Traefik für jedes Problem, das in irgendeiner Form bei der Bereitstellung von Services auftreten kann, eine Lösung. Obwohl – beim Thema Loadbalancing bin ich bislang nicht fündig geworden. Für Tipps und Hinweise…

 

 

 

Ein Gedanke zu „Ein Webserver(-Service) mit Traefik und ein paar Tipps am Rande“

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.

Tags: