Eine kleine Traefik-Geschichte

Wie bereits öfters erwähnt, nutze ich inzwischen Traefik anstatt der Kombination aus Nginx-Proxy und Nginx-Proxy-Companion im Docker-(Swarm-)Cluster. Traefik dient somit als Einstiegspunkt oder auch Proxy für alle Dienste, die von Docker – entweder im Cluster als Docker Stack oder auf einzelnen Maschinen als Service mit Docker-Compose bereitgestellt werden. Traefik selbst bezeichnet sich als “Edge-Router”, was natürlich gleich viel besser klingt. Jedenfalls hat mich Traefik letztlich überzeugt, doch es zeigten sich durchaus einige Hürden, die es zu bewältigen gab.

Von Nginx-Proxy-Container zu Traefik

In diesem Beitrag möchte ich den Weg vom bisher eingesetzten Nginx-Proxy-Container zu Traefik skizzieren. Dabei sollen weder die Probleme und deren Lösungen, noch die Gründe für diese oder jene Entscheidung zu kurz kommen. Fertige Konfigurationsdateien fürs Copy&Paste werden sich voraussichtlich allenfalls ganz weit unten befinden, und ich werde auch nicht jede Option im Einzelnen erläutern. Dafür ist die Dokumentation auch meiner Ansicht nach viel zu gut, insofern will ich diese nicht wiederholen, jedoch auf diejenigen Aspekte eingehen, die mir beim Einstieg erst einmal ein paar Fragezeichen auf die Stirn projiziert haben.

Traefik – Vorüberlegungen

Gegeben war ein Docker-Swarm-Cluster auf mehreren VMs. Im ersten Schritt wollte ich auf einer Maschine den Nginx-Proxy-Container mitsamt Nginx-Proxy-Companion durch Traefik ersetzen, um Traefik erst einmal genauer unter die Lupe nehmen zu können. Traefik unterstützt neben vielen anderen Cluster-Technologien auch Docker Swarm, kann ebenso mit Docker direkt eingesetzt werden, die erste Frage lautete somit, ob Traefik als einzelner Docker-Container oder im Docker Swarm-Mode laufen sollte. Die Lösung bestand letztlich darin, Traefik nur als Docker-Container zu starten, was jedoch nicht bei Traefik selbst, sondern in den unterschiedlichen Services begründet war, die auf der Maschine liefen. Diese teilen sich die PowerDNS-Container inkl. MariaDB, der eine oder anderen Web-Server-Container und zu guter Letzt GitLab mit Redis, PostgreSQL usw., wobei der Nameserver sowie GitLab an eine zweite IP gebunden sind, was mit Docker Swarm nicht möglich ist. Die Web-Server-Container hingegen laufen mittels Docker Stack im Docker Swarm. Nun war es aber nicht möglich, von einem Container im “Nicht-Swarm-Mode” auf den Traefik-Container zuzugreifen, d.h. die Konfiguration per Labels zu übermitteln, wenn dieser im Swarm-Mode lief… Umgekehrt war dies jedoch kein Problem. Reichlich verwirrend, zugegeben, aber letztlich sollte eben jede VM ihren eigenen Traefik-Container erhalten, wofür ich zunächst zwecks Zugriff auf die Web-UI einen CNAME angelegt habe, in dem Fall einfach “traefik1.xyzcdn.xyz“.

Bei den ersten Experimenten habe ich übrigens auf die Hinweise im Artikel “Traefik Proxy with HTTPS” zurück gegriffen. Darin werden die ersten Schritte sehr ausführlich beschrieben, beim Einstieg hat mir dieses vollständige Beispiel insofern gut geholfen, einige Einstellungen habe ich jedoch schnell modifiziert, beispielsweise die Verwendung des Docker-Volumes für die Speicherung der Zertifikate.

Statische und dynamische Konfigurationselemente

Zunächst einmal unterscheidet Traefik zwischen statischen und dynamischen Konfigurationselementen. Dabei können die dynamischen während der Laufzeit von Traefik geändert werden, während die statische Konfiguration zum Start festgelegt wird. Traefik besitzt so genannte “Provider” für die Konfigurationsoptionen, dies können beispielsweise Dateien im TOML- oder YAML-Format sein, aber auch Labels von Docker oder Docker Swarm, ebenso Kobernetes-Spezifikationen und weitere. Tatsächlich können auch Teile der dynamischen Konfiguration in Dateien abgelegt werden, obwohl diese Art der Speicherung anfänglich vielleicht nicht sonderlich “dynamisch” anmutet. Doch Traefik liest bei Änderungen die entsprechenden Dateien einfach neu ein.

Docker-Compose-File für Traefik

Nach einigen Interationen sieht der aktuelle Stand des Docker-Compose-Files wie folgt aus:

version: '3.3'

services:
  traefik:
    # Use the latest Traefik image
    image: traefik:v2.3
    ports:
      # Listen on port 80, default for HTTP, necessary to redirect to HTTPS
      - "80:80"
      # Listen on port 443, default for HTTPS
      - "443:443"
    restart: always
    dns:
      - 8.8.8.8
      - 8.8.4.4
    labels:
      # Enable Traefik for this service, to make it available in the public network
      - "traefik.enable=true"
      # Use the traefik-public network (declared below)
      - "traefik.docker.network=traefik-public"
      # admin-auth middleware with HTTP Basic auth
      - "traefik.http.middlewares.admin-auth.basicauth.users=admin:<PASSWORD>"

      # Enable gzip compression
      - "traefik.http.middlewares.def-compress.compress=true"
      # https-redirect middleware to redirect HTTP to HTTPS
      # It can be re-used by other stacks in other Docker Compose files
      - "traefik.http.middlewares.https-redirect.redirectscheme.scheme=https"
      - "traefik.http.middlewares.https-redirect.redirectscheme.permanent=true"
      # traefik-http set up only to use the middleware to redirect to https
      # 
      - "traefik.http.routers.traefik-public-http.rule=Host(`traefik1.xyzcdn.xyz`)"
      - "traefik.http.routers.traefik-public-http.entrypoints=http"
      - "traefik.http.routers.traefik-public-http.middlewares=https-redirect"
      # traefik-https the actual router using HTTPS
      - "traefik.http.routers.traefik-public-https.rule=Host(`traefik1.xyzcdn.xyz`)"
      - "traefik.http.routers.traefik-public-https.entrypoints=https"
      - "traefik.http.routers.traefik-public-https.tls=true"

      # Use the special Traefik service api@internal with the web UI/Dashboard
      - "traefik.http.routers.traefik-public-https.service=api@internal"
      # Use the "le-tls" (Let's Encrypt) resolver created below
      - "traefik.http.routers.traefik-public-https.tls.certresolver=le-tls"
      # Enable HTTP Basic auth, using the middleware created above
      - "traefik.http.routers.traefik-public-https.middlewares=secHeaders@file,admin-auth,def-compress"
      # Define the port inside of the Docker service to use
      - "traefik.http.services.traefik-public.loadbalancer.server.port=8080"
    volumes:
      # Add Docker as a mounted volume, so that Traefik can read the labels of other services
      - "/var/run/docker.sock:/var/run/docker.sock:ro"
      # Mount the volume to store the certificates
      - ./data/certificates:/certstore
      # Volume to store dynamic configuration files
      - ./conf:/conf
      # Volume to store external generated certificates
      - ./certificates:/certs
    command:
      # Enable Docker in Traefik, so that it reads labels from Docker services
      - "--providers.docker"
      - "--pilot.token=<PILOT_TOKEN>"
      - "--global.sendAnonymousUsage"

      # Do not expose all Docker services, only the ones explicitly exposed
      - "--providers.docker.exposedbydefault=false"
      # Enable Docker Swarm mode
      # - "--providers.docker.swarmmode"
      # SSL improvements
      #- "--providers.file.filename=/dynamic.yml"
      # directory is recommended
      - "--providers.file.directory=/conf"
      - "--providers.file.watch=true"
      # Create an entrypoint "http" listening on address 80
      - "--entrypoints.http.address=:80"
      # Create an entrypoint "https" listening on address 80
      - "--entrypoints.https.address=:443"

      # Create the first certresolver with PowerDNS DNS server
      - "--certificatesresolvers.le-ns1.acme.email=email@example.com"
      # Store the Let's Encrypt certificates in the mounted volume
      - "--certificatesresolvers.le-ns1.acme.storage=/certstore/acme.json"
      - "--certificatesResolvers.le-ns1.acme.dnschallenge.provider=pdns"
      - "--certificatesResolvers.le-ns1.acme.dnschallenge.resolvers=1.1.1.1:53,8.8.8.8:53"

      # second certresolver tls
      # Use the TLS Challenge for Let's Encrypt
      - "--certificatesresolvers.le-tls.acme.tlschallenge=true"
      - "--certificatesresolvers.le-tls.acme.storage=/certstore/acme-tls.json"
      - "--certificatesresolvers.le-tls.acme.email=email@example.com"

    
      # Enable the access log, with HTTP requests
      - "--accesslog"
      # Enable the Traefik log, for configurations and errors
      - "--log"
      - "--log.level=DEBUG"
      # Enable the Dashboard and API
      - "--api=true"
      - "--api.dashboard=true"
    environment:
      PDNS_API_KEY: "<POWERDNS API KEY>"
      PDNS_API_URL: "https://ns1.xyzcdn.xyz"

    networks:
      # Use the public network created to be shared between Traefik and
      # any other service that needs to be publicly available with HTTPS
      - "traefik-public"


networks:
  # Use the previously created public network "traefik-public", shared with other
  # services that need to be publicly available via this Traefik
  traefik-public:
    external: true

Ungewöhnlich für ein Docker-Compose-File mag die Anzahl der Labels und Command-Angaben erscheinen, alles andere dürfte bekannt sein. Für die Details verweise ich auf die Dokumentation von Traefik bzw. auf die Kommentare im Compose-File. Traefik wird mit den Parametern im “Command”-Teil gestartet, dabei wird z.B. festgelegt, in welchem Verzeichnis bzw. Mount Konfigurationsdateien erwartet werden, außerdem werden zwei unterschiedliche Methoden definiert, um an Let’s-Encrypt-Zertifikate zu gelangen, zum einen per TLS, zum anderen per DNS mittels PowerDNS-API. Ebenfalls enthalten sind Angaben zum Logging, oder dass der Docker-Provider benutzt wird, nicht jedoch der Provider für dessen Swarm-Mode (jene Zeile ist einkommentiert). Traefik Pilot ist ein zusätzlicher Dienst für das Management einer oder mehrerer Traefik-Instanzen, in dem Fall dient ein Token zur Authentifizierung. Die Nutzung erfolgt per Traefik-UI, ist bislang noch kostenlos, allerdings erscheint mir der aktuelle Stand auch noch etwas hakelig, beispielsweise ließen sich Metriken, die über einen Tag hinaus gehen, des Öfteren nicht aufrufen.

Im unter /conf/ gemounteten Verzeichnis können sich Konfigurationsdateien befinden, die zur dynamischen Konfiguration gehören. Tatsächlich wird der Weg per Verzeichnis anstatt der Angabe einer einzelnen Datei empfohlen – in den ersten Versionen hatte ich noch eine spezielle Datei gemountet.

Traefik-Nomenklatur: Entrypoints, Router, Middleware…

Für die Ports 80 und 443 werden so genannte “Entrypoints” definiert, die in der Definition der “Router” verwendet werden. Diese Entrypoints müssen einmal statisch definiert werden, so dass alle je in einem Traefik-Container-Lebenszyklus verwendeten Ports zu Beginn festgelegt werden müssen. Falls z.B. Traefik auch als Proxy für DNS eingesetzt werden soll, müssten Entrypoints für Port 53 UDP und TCP hinzugefügt werden.

Auf die Definition der Komponenten zur Generierung der Let’s-Encrypt-Zertifikate gehe ich später noch ein – inklusive der Probleme und deren Lösung.

Der Bereich der Labels ist dafür zuständig, einen konkreten Proxy, bestehend aus Router, Middleware und Service zu definieren und diese Angaben an die laufende Traefik-Instanz zu übergeben. Was zunächst etwas abstrakt klingt, wird vielleicht klarer beim Vergleich mit Nginx-Proxy und -Companion. Dabei erfolgt die Konfiguration der Web-Server, die der Nginx-Proxy-Container berücksichtigen soll, per Environment-Variablen, dabei reicht die Angabe des VIRTUAL_HOST. Bei Traefik muss ein wenig mehr definiert werden, und anstatt der Environment-Variablen werden Labels genutzt. In der hier angegebenen Docker-Compose-Datei werden die Labels nun dazu verwendet, die Traefik-UI zu konfigurieren, die per http und https erreicht werden soll.

Ein neues Docker Network

Zur Vorbereitung muss zunächst noch ein Docker Network angelegt werden, das von Traefik und den Docker Services mit Docker Swarm, als auch von einzelnen Containern auf dem Host verwendet werden kann. Dazu ist das Netzwerk mit dem Flag “--attachable” zu versehen.

geschke@stralsund:~/services/traefik$ docker network create --driver=overlay --attachable  traefik-public

Der Name des Netzwerks wird Traefik mit der Angabe “traefik.docker.network=traefik-public” übermittelt.

Globale Middleware-Komponenten

Da der Zugriff auf die Traefik-UI nur mit Authentifizierung erlaubt sein soll, wird eine Middleware namens “admin-auth” definiert, die dazu eine HTTP-Basic-Authentifizierung einsetzt. Das Kommando “openssl passwd -apr1” generiert das Passwort, die daraufhin angezeigte Zeichenkette wird einfach hinter dem Usernamen getrennt durch einen Doppelpunkt eingesetzt.

Eine weitere Middleware namens “def-compress” wird genutzt, um die gzip-Kompression zu aktivieren. An dieser Stelle wäre ein Wort zur einheitlichen Benennung angebracht. Wie erwähnt hatte ich anfangs das Docker-Compose-File des o.g. Beispiels übernommen, daher stammt auch der Name der Middleware für die Authentifizierung. Da jedoch Middleware-Komponenten zunächst einmal definiert werden müssen, um anschließend in einem so genannten Router eingesetzt zu werden. Zur den grundsätzlichen Eigenschaften von Middlewares und deren “Magic” hatte ich mich an anderer Stelle bereits geäußert, denn zugegebenermaßen zeigten sich dabei einige Startschwierigkeiten.

Danach erfolgt die Definition der Middleware “https-redirect“, die genau dafür zuständig ist, was ihr Name nahelegt – der permanenten Umleitung von http zu https. Sobald also diese Middleware in einem Router verwendet wird, erfolgt die Umleitung.

Die in diesem Bereich definierten Middleware-Komponenten gelten im Übrigen global und können von anderen Services in gleicher Weise wie hier eingesetzt werden.

Die Kernkomponenten: Router

In den nächsten Zeilen werden die Router definiert, zunächst für http, anschließend für https. Der Name “traefik-public-http” ist auch hier aus dem Beispiel übernommen und orientiert sich an der Bezeichnung des verwendeten Netzwerks. Zunächst wird eine Regel definiert, die besagt, dass der Hostname “traefik1.xyzcdn.xyz” lautet. In der darauf folgenden Zeile wird festgelegt, dass der Entrypoint “http” ist. Dieser im Command-Abschnitt definierte Entrypoint nutzt sinnigerweise Port 80. Danach wird die zu verwendende Middleware festgelegt, dabei handelt es sich um die zuvor definierte Weiterleitung von http auf https. Damit ist der erste Router vollständig eingerichtet, der nichts Anderes macht, als sämtliche http-Requests, die auf dem Web-Server “traefik1.xyzcdn.xyz” eingehen, direkt an dessen https-Pendant weiterzuleiten.

Im zweiten Router namens “traefik-public-https” passiert schon ein wenig mehr. Analog zum http-Router werden Hostname und Entrypoint festgelegt, wobei der Entrypoint “https” genutzt wird, der wie beschrieben auf Port 443 gelegt wurde. Transport Layer Security wird aktiviert mit “.tls=true“, wobei dies allein noch nichts aussagt, auf welchem Wege das Zertifikat zu Traefik gelangt. Dazu dient wiederum “.tls.certresolver=le-tls“. Die zu verwendende Komponente zur Zertifikatsauflösung, im Folgenden auch als Certresolver bezeichnet, wurde im Command-Abschnitt definiert, dazu später mehr.

Zu guter Letzt noch Services

Die Option “.service=api@internal” gibt an, dass der interne Service für die Traefik-API eingesetzt wird, wobei die API in der statischen Konfiguration mittels “--api=true” aktiviert werden muss. Das Dashboard ist auf Port 8080 verfügbar, auch dieses wurde in der statischen Konfiguration durch “--api.dashboard=true” eingeschaltet. Der Port wiederum muss dem Router bekannt gegeben werden, dies geschieht mittels “.loadbalancer.server.port=8080“.

Wurde im Router für http nur eine Middleware verwendet, sind es im https-Router gleich drei, die durch Komma getrennt angegeben werden, die Option dazu lautet “.middlewares=secHeaders@file,admin-auth,def-compress“. Bereits bekannt sind “admin-auth” und “def-compress” zur Aktivierung der HTTP-Authentifizierung und gzip-komprimierter Übertragung. Bei “secHeaders@file” handelt es sich um Einstellungen, die in einer Datei zur “dynamischen” Konfiguration definiert sind. Diese Datei liegt im Verzeichnis ./conf/, das in ein Verzeichnis /conf/ in den Container gemountet wird, so dass Traefik Zugriff darauf erhält.

Dynamische Konfiguration

Dort liegt eine Datei “dynamic.yml“, in der Teile der dynamischen Konfiguration enthalten sind. Das mag nun möglicherweise doppelt oder unnötig erscheinen, da die Optionen für die  “dynamische” Konfiguration bislang in den Labels des Docker-Compose-Files enthalten war. Jedoch hat die Nutzung der Datei einen ganz einfachen Hintergrund, denn wie der Name nahelegt, sind Änderungen der dynamischen Konfiguration während der Laufzeit von Traefik möglich. Es wäre jedoch nicht möglich, in die Docker-Compose-Datei neue Optionen einzufügen und diese Traefik zugänglich zu machen, ohne dass der Container neu gestartet werden muss. Da die Datei jedoch außerhalb der Service-Definition liegt, können darin Änderungen vorgenommen werden, und Traefik liest die Datei anschließend einfach neu ein. Somit kann Traefik während der Laufzeit darauf reagieren.

Genug der Vorrede, der Inhalt der ./conf/dynamic.yml sieht hier wie folgt aus:

tls:
  certificates:
    - certFile: /certs/xyzcdn.xyz.crt
      keyFile: /certs/xyzcdn.xyz.key
    - certFile: /certs/geschke.net.crt
      keyFile: /certs/geschke.net.key
  options:
    default:
      minVersion: VersionTLS12
      sniStrict : true
      cipherSuites:
        - TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
        - TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
        - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
        - TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
        - TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305
        - TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305
        - TLS_AES_128_GCM_SHA256
        - TLS_AES_256_GCM_SHA384
        - TLS_CHACHA20_POLY1305_SHA256
      curvePreferences:
        - secp521r1
        - secp384r1
    mintls13:
      minVersion: VersionTLS13

http:
  middlewares:
    secHeaders:
      headers:
        browserXssFilter: true
        contentTypeNosniff: true
        frameDeny: true
        sslRedirect: true
        # HSTS Configuration
        stsIncludeSubdomains: true
        stsPreload: true
        stsSeconds: 31536000
        customFrameOptionsValue: "SAMEORIGIN"
        forceSTSHeader: true
        referrerPolicy: "no-referrer"
        customRequestHeaders:
          X-Forwarded-Proto: "https"

Im unteren Abschnitt wird die Middleware “secHeaders” definiert, der obere besteht hauptsächlich aus den Cipher Suites, die beim verschlüsselten Verkehr per TLS verwendet werden sollen. Zugegebenermaßen habe ich mich bei beiden Bereiche von Hinweisen in diversen Blogs (etwa “Traefik v2 – HTTPS Verschlüsselung / Sicherheit verbessern“) inspirieren lassen, was wie immer eine nette Umschreibung für “schamlos geklaut” ist. Denn Traefik erzielt in der Default-Einstellung ohne jene Optionen nur eine Wertung “B” im SSL Server Test von Qualys, was einerseits nicht mehr zeitgemäß, andererseits aber eben auch schnell änderbar ist. Mit den hier angegebenen Einstellungen wird aktuell ein “A+” erreicht, was sich im Laufe der Zeit auch wiederum ändern kann, insofern empfiehlt sich ein regelmäßiger Check.

Weiter oben in der Datei finden sich noch Verweise auf Zertifikatsdateien für die beiden Domains xyzcdn.xyz und geschke.net. Da mich das Thema Let’s-Encrypt-Zertifikate während der Einrichtung von Traefik immer wieder und insgesamt auch die meiste Zeit beschäftigt hat, möchte ich ein wenig auf die Hintergründe eingehen.

SSL-Zertifikate von Let’s Encrypt

Let’s Encrypt ist eine wunderbare Sache, der Erfolg dieser Zertifizierungsstelle für kostenlose SSL-Zertifikate spricht für sich. Um ein Zertifikat zu erhalten, wird das ACME-Protokoll genutzt (Automatic Certificate Management Environment), das letztlich dazu dient, Anfragen nach Zertifikaten zu verifizieren. Um ein Zertifikat zum Beispiel für die Domain dieses Blogs, also “kuerbis.org”, zu erhalten, muss die Zertifizierungsstelle prüfen, ob der anfragende Client auch tatsächlich Zugriff auf die Domain bzw. den Web-Server hat. Zur Automatisierung dieses Verfahrens gibt es mehrere Möglichkeiten. Bei der Prüfung per Webserver funktioniert das Schema vereinfacht betrachtet wie folgt: vom Webserver aus wird Request an Let’s Encrypt gestellt, Let’s Encrypt übermittelt einen Token, der nur für die anfragende Domain gültig ist, der Webserver sorgt dafür, dass der Token unter einer bestimmten URL erreichbar ist, und die Zertifizierungsstelle nutzt schließlich diese URL, um zu prüfen, ob der zuvor übermittelte Token korrekt ist. Danach wird dem Client das Zertifikat zur Verfügung gestellt.

Eine zweite Möglichkeit nutzt das DNS zur Verifikation. Dabei stellt ebenfalls ein Client eine Anfrage an Let’s Encrypt, dabei muss es sich jedoch nicht um einen Webserver handeln. Let’s Encrypt übermittelt ebenfalls einen Token, dieser wird vom Client daraufhin als TXT-Eintrag in die DNS-Server der Domain gesetzt. Somit kann die Zertifizierungsstelle durch eine DNS-Abfrage feststellen, ob der richtige Token eingetragen wurde. Falls ja, stellt Let’s Encrypt ebenfalls ein SSL-Zertifikat aus.

Traefik und Let’s Encrypt

Traefik unterstützt beide Methoden, wobei die Prüfung per “Webserver” die so genannte “tlsChallenge” darstellt, während die Prüfung per DNS als “dnsChallenge” bezeichnet wird. Dabei dient Traefik als “Webserver”, es ist somit keine weitere Beteiligung eines Nginx, Apache oder sonstiger Software notwendig, Traefik übernimmt hier alle notwendigen Schritte selbst. Somit ist auch kein weiterer Service – wie etwa beim zuvor eingesetzten Nginx-Proxy-Container der Nginx-Proxy-Companion – notwendig, um SSL-Zertifikate zu erhalten.

Damit das tlsChallenge-Verfahren funktioniert, muss der Webserver, bzw. in diesem Fall Traefik von außen erreichbar sein, schließlich benötigt Let’s Encrypt Zugriff auf die URL, unter der der Token erreichbar ist. Beim dnsChallenge-Verfahren ist dieser direkte Zugriff nicht notwendig, da sich Let’s Encrypt der DNS-Server bedient, die beim Request den korrekten Token liefern müssen. Somit ist es auch möglich, Let’s-Encrypt-Zertifikate für Domains bzw. Hostnamen zu erhalten, die nicht öffentlich zugänglich sind, etwa innerhalb privater Netzwerke, wobei die Domain selbst natürlich existieren muss. Ebenfalls ist es so möglich, von jedem beliebigen Rechner aus, Zertifikate zu erzeugen, gleichgültig, ob dieser öffentlich zugänglich ist oder etwa in einem internen Netzwerk steht. Der dazu notwendige ACME-Client muss in dem Fall nur die Möglichkeit besitzen, die DNS-Einträge der betreffenden Domain zu modifizieren.

Definition der dnsChallenge mit PowerDNS

Im hier dargestellten Docker-Compose-File werden im “Command”-Bereich zwei Certresolver definiert. Der erste trägt den Namen “le-ns1” und nutzt die dnsChallenge-Methode:

   # Create the first certresolver with PowerDNS DNS server
   - "--certificatesresolvers.le-ns1.acme.email=email@example.com" 
   # Store the Let's Encrypt certificates in the mounted volume
   - "--certificatesresolvers.le-ns1.acme.storage=/certstore/acme.json"
   - "--certificatesResolvers.le-ns1.acme.dnschallenge.provider=pdns"
   - "--certificatesResolvers.le-ns1.acme.dnschallenge.resolvers=1.1.1.1:53,8.8.8.8:53"

Let’s Encrypt verlangt die Angabe einer Mailadresse, der die Zertifikate zugeordnet werden, wozu die erste Option dient. Es folgt die Angabe einer Datei, in der Traefik die erhaltenen Zertifikate ablegen soll. Diese befindet sich in gemounteten Verzeichnis, innerhalb des Containers unter “/certstore/” erreichbar, auf dem Host unter “./data/certificates/“. Schließlich wird der Provider für den DNS-Service festgelegt, im Beispiel handelt es sich um den selbst gehosteten PowerDNS-Server, der per API zugänglich ist. Deren URL und Authentifizierungs-Key sind im Abschnitt der Umgebungsvariablen angegeben. Traefik nutzt dabei die sehr umfassende ACME-Library LEGO, die eine Vielzahl von DNS-Providern unterstützt. LEGO steht im Übrigen auch als CLI-Anwendung zur Verfügung, nur über die Namenswahl lässt sich trefflich diskutieren…

Mit “.dnschallenge.resolvers” lässt sich festlegen, dass für die Prüfung der DNS-Einträge durch Traefik andere DNS-Server genutzt werden sollen als diejenigen, die auf dem System standardmäßig Gebrauch finden. Die Einstellung war hier notwendig, da auf dem verwendeten Host ebenfalls die Nameserver-Container laufen, somit erschien es sinnvoller, die Existenz der Einträge von externen DNS-Servern bestätigen zu lassen. Tatsächlich prüft Traefik zunächst, ob die TXT-Einträge für die Domain erfolgreich eingetragen wurden, bevor die Let’s-Encrypt-Server zu eben dieser Prüfung wieder kontaktiert werden.

Definition des tlsChallenge Resolvers

Der zweite Certresolver bedarf etwas weniger Konfiguration, für den Resolver der “tlsChallenge” sind folgende Zeilen zuständig:

- "--certificatesresolvers.le-tls.acme.tlschallenge=true"
- "--certificatesresolvers.le-tls.acme.storage=/certstore/acme-tls.json"
- "--certificatesresolvers.le-tls.acme.email=email@example.com"

Das Verfahren wird aktiviert mit “.tlschallenge=true“, eine Mailadresse ist ebenfalls anzugeben, und für die Zertifikate wurde hier eine anderslautende Datei genutzt, was die Unterscheidung beim Debugging vereinfacht.

Spezielle Lösungen für spezielle Anforderungen

Tatsächlich hatte ich beim DNS-Verfahren anfänglich so einige Schwierigkeiten, zunächst schlug die Prüfung der Einträge durch Traefik fehl, so wurden die erzeugten TXT-Einträge in den Nameservern anscheinend nicht gefunden, obwohl sie vorhanden waren, wie eine manuelle Abfrage ergab. Mit der Einstellung “.dnschallenge.disablepropagationcheck=true“, die ausdrücklich nicht empfohlen wird, und die die internen Checks eigentlich abschaltet, konnten die Zertifikate dann wiederum erzeugt werden. Im Log gab Traefik jedoch an, den DNS-Eintrag trotz Deaktivierung zu prüfen – kurzum, es war alles etwas merkwürdig und fehleranfällig. Dazu kam, dass letztlich die Zertifikate auf mehr als einer Traefik-Instanz verwendet werden sollten, da die ursprüngliche Absicht darin bestand, eine Art “GitHub Pages ohne GitHub Pages” zu kreieren. Es musste also ein Weg gefunden werden, für ein- und dieselbe Domain bzw. denselben Hostnamen von mehreren Traefik-Servern aus ein Zertifikat zu generieren, oder eine andere Art der Zertifikatsverteilung zu nutzen. In einer früheren Version beherrschte Traefik ein “Distributed Let’s Encrypt”, dies wurde jedoch als fehleranfällig angesehen und steht aktuell leider nur für die nicht mehr kostenlose “Enterprise“-Variante zur Verfügung.

Da sich Let’s-Encrypt-Zertifikate jedoch von beliebigen ACME-Clients generieren lassen, einer davon wäre besagte CLI-Anwendung von LEGO, musste es nur möglich sein, ein bestehendes bzw. an anderer Stelle erzeugtes Zertifikat in eine Traefik-Instanz zu importieren. Genau das wird mit der Angabe der Zertifikatsdateien, d.h. Zertifikat und dazu gehöriger Private Key in der certificates-Konfiguration der “conf/dynamic.yml“-Datei erreicht:

tls:
  certificates:
    - certFile: /certs/xyzcdn.xyz.crt
      keyFile: /certs/xyzcdn.xyz.key
    - certFile: /certs/geschke.net.crt
      keyFile: /certs/geschke.net.key

Dabei ist “/certs/” ein Verzeichnis innerhalb des Containers, dazu wurde das Verzeichnis “./certifcates/” des Hosts gemountet. Dank der “dynamischen” Komponente der Konfiguration können dieser Datei weitere Zertifikate für weitere Domains hinzugefügt werden, ohne dass Traefik einen Neustart benötigt. Bei Erneuerung von Zertifikaten, die bei Let’s Encrypt spätestens in Abständen von drei Monaten fällig wird, reicht es übrigens, nach der Kopie der Dateien in das entsprechende Verzeichnis, ein “touch“-Kommando zur Aktualisierung des Zeitstempels auf die “dynamic.yml” auszuführen.

LEGO – ein Baustein für Zertifikate

Zur Zertifikatsgenerierung nutze ich das CLI-Tool von LEGO, anschließend werden die Zertifikatsdateien in das entsprechende Verzeichnis auf die Hosts kopiert. Als Beispiel ein Kommando zur ersten Erstellung mittels PowerDNS-Nameserver:

geschke@moabit:~$ export PDNS_API_URL="https://ns1.xyzcdn.xyz" 
geschke@moabit:~$ export PDNS_API_KEY="<MEIN GEHEIMER API KEY>" 
geschke@moabit:~$ lego --email="email@example.com" --domains="example.com" --domains="www.example.com" --key-type="rsa4096" --dns="pdns" run

Liegen bereits Zertifikate vor und müssen erneuert werden, so lautete das Kommando “renew“.  Weitere Hinweisen finden sich in der Dokumentation von LEGO. Zur Automatisierung habe ich mir ein Shellskript gebaut, das regelmäßig aufgerufen wird und nach der erfolgreichen Erstellung bzw. Erneuerung die Zertifikate auf die Hosts kopiert und dort ein Reload der “dynamic.yml” auslöst.

3, 2, 1, Traefik!

Zu guter Letzt muss Traefik nur noch mittels “docker-compose” gestartet werden:

geschke@stralsund:~/services/traefik$ docker-compose -f traefik_compose.yml up -d

Wenn Traefik erfolgreich gestartet werden konnte, müsste Zugriff auf die Admin-UI möglich sein. Falls nicht, lohnt sich natürlich ein Blick in die Logs, evtl. dauert die Zertifikatsgenerierung etwas länger und ist noch nicht abgeschlossen. Bei einer Konfiguration wie hier dargestellt, ist die Admin-UI unter einem eigenen Hostnamen zu erreichen, und bei Eingabe der URL sollte zunächst die Basis-Authentifizierung durchlaufen und anschließend die per TLS gesicherte Seite angezeigt werden.

Das Dashboard von Traefik

Das Dashboard zeigt alle Komponenten von Traefik wie Router, Services und Middlewares für die verwendeten Protokolle an. Ebenso können Details aller Komponenten angezeigt werden, es ist jedoch nicht möglich, deren Konfiguration zu ändern. Die Aktualisierung erfolgt sofort, somit lässt sich leicht erkennen, wenn ein Router, Service oder eine Middleware hinzugefügt wird. Initial zeigt das Dashboard nur die Traefik-Komponenten für das Hosting von sich selbst, interessanter wird es dann, wenn weitere Dienste hinzu kommen.

Eine kleine Traefik-Geschichte 1

In der Detailansicht werden Pfad von Entrypoint bis hin zum Service sowie weitere Angaben zur Router-, TLS- und Middleware-Konfiguration angezeigt.

Die Geschichte geht weiter

Nun ist die kleine Traefik-Geschichte beinahe zur unendlichen Geschichte mutiert, daher soll dies als erster Einblick zu Traefik auch zunächst genügen. Dies wird jedoch nicht der einzige Artikel bleiben, in einem späteren und – versprochen – kürzeren Beitrag werde ich die Konfiguration für einen einfachen Webserver vorstellen und evtl. noch auf ein paar Kleinigkeiten eingehen, auf die ich beim Betrieb mit Traefik gestoßen bin. Bis dahin vielen Dank für die Aufmerksamkeit!

 

Schreibe einen Kommentar

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

Tags: