Die Traefik-Labels im Einzelnen, samt der Dollarfalle

Inhalt
Traefik wird eingerichtet, indem Labels an einen Container geschrieben werden – was nach einer Liste von Einstellungen klingt und in Wahrheit ein Namensraum ist. Die Schlüssel sind Pfade durch einen Baum, und was wie sechs unabhängige Zeilen aussieht, sind drei Gebilde, die über ihre Namen voneinander wissen – nur den Dienst hängt Traefik einem Router ohne service= selbst an, solange der Container genau einen erklärt.
Diese Bauart macht die Einrichtung kurz und ihre Fehler leise. Ein Vertippen im Schlüssel erzeugt ein zweites Gebilde statt einer Beschwerde, und geprüft wird ein Name erst dort, wo ein Router auf ihn verweist – mit einem Eintrag im Protokoll und einer 404 als einzigen Hinweisen nach außen.

Drei Gebilde, ein Name
Ein Router entscheidet, welche Anfragen zu diesem Container gehören, ein Dienst sagt, wohin sie geschickt werden, und eine Middleware verändert sie unterwegs. Jedes lebt unter einem eigenen Zweig von traefik.http, und jedes wird über einen frei gewählten Namen bezeichnet – der Label-Schlüssel ist die Erklärung.
labels:
- "traefik.enable=true"
- "traefik.http.routers.blog.rule=Host(`blog.example.com`)"
- "traefik.http.routers.blog.entrypoints=websecure"
- "traefik.http.routers.blog.tls.certresolver=le"
- "traefik.http.routers.blog.middlewares=blog-auth@docker"
- "traefik.http.middlewares.blog-auth.basicauth.users=admin:$$2y$$05$$e6zX…"
- "traefik.http.services.blog.loadbalancer.server.port=8080"
Dass Router und Dienst zusammengehören, sagt keine Zeile, und der gleiche Name blog ist es nicht, der sie bindet. Nennt ein Router keinen Dienst, hängt Traefik ihm den einzigen Dienst an, den der Container erklärt; erklärt der Container mehrere, verwirft Traefik den Router mit einem Eintrag im Protokoll. Ein vertipptes traefik.http.services.blogg.loadbalancer.server.port bleibt in diesem Beispiel deshalb folgenlos. Ernst wird es, sobald der Router seinen Dienst ausdrücklich nennt: Mit traefik.http.routers.blog.service=blog zeigt er auf einen Dienst, den es nicht gibt; Traefik markiert den Router mit „the service … does not exist“ als fehlerhaft und beantwortet Anfragen an diesen Namen mit einer 404.
Belegt der Container genau einen Port, kann das Dienst-Label ganz entfallen, Traefik schließt es sich zusammen. Diese Bequemlichkeit zu kennen lohnt vor allem, um sie in fremden Dateien wiederzuerkennen – sobald ein zweiter Port auftaucht, nimmt Traefik den niedrigsten, und die Route bricht in dem Augenblick, in dem ein völlig unbeteiligter, niedrigerer Port hinzukommt.
Der Zusatz für die Quelle
Middlewares werden über ihren Namen angesprochen, und Traefik führt je Quelle einen eigenen Namensraum – die Docker-Labels, eine Konfigurationsdatei, eine Kubernetes-Ressource. Ein bloßes blog-auth wird in der Quelle des Routers gesucht, ohne Rückfall auf andere Quellen. Für eine Middleware aus denselben Labels genügt es also, und blog-auth@docker schreibt nur aus, was ohnehin gilt; den Zusatz braucht erst ein Verweis über die Grenze der Quelle, etwa blog-auth@file auf eine Middleware aus Traefiks Konfigurationsdatei.
Ein falscher Verweis – vertippt, oder ohne Zusatz auf eine Middleware einer anderen Quelle – öffnet die Seite dabei nicht ohne Anmeldung. Traefik markiert den Router mit „middleware … does not exist“ als fehlerhaft, richtet ihn nicht ein und beantwortet Anfragen an diesen Namen mit einer 404. Leise bleibt nur ein Verweis, der ganz fehlt: Ein Router, der die Middleware nicht in seiner Liste führt, liefert die Anwendung ohne die Anmeldung aus, die davor stehen sollte, und nichts im Protokoll erwähnt das, denn aus Traefiks Sicht wurde keine verlangt.
Die Dollarfalle
Ein bcrypt-Streuwert sieht aus wie $2y$05$e6zX... und besteht zu einem guten Teil aus Dollarzeichen. Docker Compose ersetzt in jedem gelesenen Wert Variablen, aus $2y wird also eine leere Zeichenkette, aus $05 ebenfalls, und das Passwort, das bei Traefik ankommt, ist ein Bruchstück, das auf nichts passt.
# erzeugen
htpasswd -nbB admin "passwort"
admin:$2y$05$e6zXx8Q0m2yYy1H4o1Yb1uJf2Rr7cKq5dW6vT8sN0pL3aG9hC1dS2
# in der docker-compose.yml wird jedes $ verdoppelt
admin:$$2y$$05$$e6zXx8Q0m2yYy1H4o1Yb1uJf2Rr7cKq5dW6vT8sN0pL3aG9hC1dS2
# in einer reinen Etikettendatei oder auf der Befehlszeile bleibt es einfach
Das Verdoppeln ist eine Compose-Regel und keine Traefik-Regel – deshalb muss dieselbe Zeichenkette je nach Aufenthaltsort verschieden geschrieben werden. Ein Streuwert in Traefiks eigener dynamischer Konfigurationsdatei behält seine einfachen Dollarzeichen, derselbe Streuwert in einer Compose-Datei braucht sie doppelt, und eine funktionierende Einrichtung von der einen in die andere zu übertragen, zerlegt sie genau auf diese Weise.
Das Symptom ist eine Anmeldeaufforderung, die jedes Passwort ablehnt, auch das richtige – was dazu führt, dass das Passwort geprüft wird und nicht die Datei, die es aufgefressen hat.
Wie sich jeder Fehler von außen zeigt
| Fehler | Symptom | Wo nachzusehen ist |
|---|---|---|
| Router nennt einen Dienst, den es nicht gibt | 404 für diesen Namen | die Übersicht: der Router mit Fehler „service … does not exist“ |
| Middleware-Verweis vertippt oder ohne Zusatz auf eine andere Quelle | 404 für diesen Namen | die Übersicht: der Router mit Fehler „middleware … does not exist“ |
| Einfache Dollarzeichen in Compose | 401 bei jedem Passwort | docker inspect: das Label ist kürzer als der Streuwert |
| Container nicht im Proxy-Netz | 502 oder 504 | der Dienst hat keinen erreichbaren Server |
traefik.enable=true fehlt, bei exposedByDefault=false |
404, es erscheint überhaupt nichts | die Übersicht: der Container ist nicht aufgeführt |
| Zwei Container, gleicher Routername, verschiedene Angaben | 404, Traefik verwirft den Router | das Protokoll: „defined multiple times with different configurations“ |
Vier der sechs beantwortet die Übersichtsseite, was dafür spricht, sie auch auf einer kleinen Maschine an einem internen Eingang einzuschalten. Sie zeigt, was Traefik tatsächlich verstanden hat, und jeder dieser Fehler ist ein Unterschied zwischen dem Geschriebenen und dem Verstandenen.
Zwei Gewohnheiten, die das meiste verhindern
Die erste besteht darin, jeden Namen aus dem Dienst abzuleiten und ihn an allen drei Stellen unverändert zu verwenden – Container blog, Router blog, Dienst blog, Middleware blog-auth. Ein mechanisch gebildeter Name lässt sich nicht falsch erinnern, und ein Vergleich der Labels zweier Container zeigt dann nur noch die Zeilen, die sich wirklich unterscheiden.
Die zweite besteht darin, die Labels zurückzulesen statt die Datei. docker inspect zeigt die Werte so, wie der Dienst sie abgelegt hat, nachdem Compose mit dem Ersetzen fertig war – dort wird aus einem verdoppelten Dollarzeichen wieder ein einfaches, und dort ist einem überlebenden Streuwert anzusehen, dass er genauso lang ist wie der erzeugte.
Fragen und Antworten
Lässt sich der Streuwert so hinterlegen, dass Compose ihn gar nicht erst zu sehen bekommt?
Ja, auf zwei Wegen, und bei beiden bleiben die Dollarzeichen einfach. Der erste ist eine Benutzerdatei: Statt basicauth.users nimmt das Label basicauth.usersfile einen Pfad entgegen, und die Datei enthält die Zeilen so, wie htpasswd sie ausgibt. Compose liest nur den Pfad, nicht den Inhalt, und hat damit nichts zu ersetzen. Geöffnet wird die Datei allerdings von Traefik, nicht von der Anwendung; sie muss also in den Traefik-Container eingebunden sein, nicht in den Container, den sie schützt.
Der zweite Weg verlegt die ganze Middleware in Traefiks eigene dynamische Konfigurationsdatei, in der der Streuwert ohnehin mit einfachen Dollarzeichen steht. Damit wechselt sie aber die Quelle, und hier greift der Zusatz aus dem zweiten Abschnitt: Der Router muss dann auf blog-auth@file verweisen statt auf blog-auth@docker. Ein Verweis, der beim Umzug unverändert bleibt, zeigt auf einen Namen, den es in dieser Quelle nicht mehr gibt, und Traefik nimmt den Router mit „middleware … does not exist“ außer Betrieb, bis der Verweis stimmt. Zum Umzug gehört deshalb ein Blick in die Übersicht, ob der Router fehlerfrei ist.
Gilt eine Middleware für jeden Weg, der zum selben Dienst führt?
Nein. Eine Middleware hängt am Router, nicht am Dienst, und wirkt nur auf die Anfragen, die dieser Router annimmt. Führt ein zweiter Router zum selben Dienst, etwa für einen weiteren Hostnamen oder über einen anderen Eingang, liefert er die Anwendung ohne Anmeldung aus, solange die Middleware nicht auch in seiner Liste steht; die Übersicht zeigt das für jeden Router einzeln.
Warum antwortet Traefik bei einem Container außerhalb des Proxy-Netzes mit 502 oder 504 und nicht mit 404?
Weil in diesem Fall alles Geschriebene stimmt. Router und Dienst existieren und passen zueinander, die Anfrage wird also angenommen und weitergereicht. Erst dabei scheitert sie: Traefik kennt aus Docker eine Adresse des Containers, erreicht sie aber nicht, weil beide kein gemeinsames Netz haben. Eine 404 entsteht, wenn Traefik für eine Anfrage kein gültiges Ziel findet; 502 und 504 entstehen, wenn es ein Ziel gefunden hat, das nicht antwortet.
Häufiger als ein ganz fehlendes Netz ist ein Container in mehreren Netzen. Traefik nimmt dann eine seiner Adressen, und ohne Vorgabe ist nicht festgelegt, welche. Das Label traefik.docker.network legt das Netz fest. Es erwartet den Namen, unter dem Docker das Netz führt, und Compose stellt Netzen, die es selbst anlegt, in der Regel den Projektnamen voran. Welcher Name gilt, zeigt wie bei den Dollarzeichen docker inspect.