Wie bereits in früheren Artikel beschrieben, nutze ich für meine Web-Auftritte (das klingt jetzt viel, aber letztlich ist es nur noch dieses Blog und ein paar statische Seiten) als erste Bastion gegenüber den Besuchern aus dem Internet den „Cloud Native Application Proxy“ Traefik. Dieser Open Source Reverse Proxy und Load Balancer, der speziell für Container-Umgebungen entwickelt wurde, bietet einfach unschätzbare Features wie automatisches Handling von Let’s-Encrypt-Zertifikaten, flexibles Routing, dynamische Konfiguration ohne Restart und eine hervorragende Integration in die Docker-(Compose-)Landschaft.
Weitere Features und Möglichkeiten finden sich auf der Website von traefiklabs, die als Entwickler von Traefik die Eigenschaften auch viel besser beschreiben können als ich. Dieses Blog zum Beispiel wird in einer Docker-Compose-Umgebung betrieben, jeder Dienst findet sich in einem Container wieder, im Einzelnen sind dies die MariaDB-Datenbank, das PHP-Backend und natürlich Nginx als Web-Server. Traefik als vorgeschalteter Proxy sorgt nun dafür, dass ankommende Requests an den richtigen Container weitergeleitet werden, für die Antwort gilt dasselbe, nur in der anderen Richtung. Traefik sorgt auch dafür, dass Anfragen per HTTP-Protokoll, also quasi ungeschützter Verkehr, direkt umgeleitet werden auf die HTTPS-Variante. Somit muss sich Nginx als Web-Server mit dem Handling der notwendigen Zertifikate erst gar nicht beschäftigen, da dies an zentraler Stelle von Traefik übernommen wird. Die Konfiguration der einzelnen Dienste erfolgt mittels Labels im Docker-Compose-File, was wiederum angenehm ist, da alle service-spezifischen Einstellungen an einer Stelle zugänglich sind.
Diese Konfiguration läuft seit geraumer Zeit absolut stabil und zuverlässig, auch das kürzlich vorgenommene Update von Traefik 2.11 auf die neueste Version 3.x (aktuell ist 3.3) verlief ohne größere Probleme, einzig musste eine veraltete Einstellung entfernt werden.
Da ich mich somit wieder einmal etwas genauer mit Updates und Upgrades – nicht nur von Traefik, sondern auch mit den zugrunde liegenden Docker-Images beschäftigt, und mich daher auch ein wenig in der Admin-Oberfläche von WordPress aufgehalten hatte, fielen mir wieder einmal die vielen Zugriffe auf die Login-Seite auf. Laut Statistik-Plugin zeigten mehr Besucher Interesse an diesen Seiten als an den Inhalten.
Mal ehrlich, was soll das? Russland, Pakistan, Indien, Brasilien, Frankreich, USA usw., ist ja schön, dass Ihr Euch für meine Web-Auftritte interessiert, aber auf der Login-Seite habt Ihr nichts zu suchen! Klar, die allgemeine Hacker- und Angriffs-Problematik ist mir bekannt, aber ich bin weder Regierung, noch Bank, noch Partei, noch Unternehmen, sondern betreibe hier nur ein kleines, privates Blog, also haut gefälligst ab von meinen Login-Seiten! Auch wenn die Tür verschlossen war und bislang niemand eingedrungen ist, aber das ständige Anklopfen nervt einfach – ganz davon abgesehen, dass WordPress bekanntlich nicht unfehlbar ist und es mit einiger Sicherheit in absehbarer Zeit auch mal wieder neue Sicherheitslücken geben wird.
Back to Basic(s Auth)
Aus heutiger Sicht fast schon anachronistisch, aber nach wie vor effektiv ist die Möglichkeit, einen Login per HTTP-Basic-Authentication zu realisieren. Dieser wird einfach für die betreffenden Seiten bzw. Pfade vorgeschaltet, so dass sich Besucher zunächst authentifizieren müssen, bevor sie überhaupt die Login-Seite oder die Admin-UI erreichen. Das mag ein wenig old-school wirken, aber immerhin sind die Browser-eigenen Login-Fenster inzwischen ein wenig hübscher als in den Anfangstagen des Web. Und während unerwünschte Besucher bereits von der HTTP-Authentifizierung auf Netzwerk-Ebene abgewehrt werden und gar nicht zu WordPress weiter geleitet werden, folglich auch keine Ressourcen von WordPress oder der Datenbank bei Brute-Force-Angriffen blockieren, stört sich WordPress nach dem erfolgreichen Überwinden dieser Hürde überhaupt nicht daran.
Erinnerungen werden wach – HTTP-Authentication
Dank Traefik ist die Einrichtung der HTTP-Basic-Authentication auch nicht mehr von einer solchen Fummelei begleitet wie in vergangenen Zeiten mit .htaccess-Dateien und Apache-Modulen. Außerdem werden angesichts vorheriger Umleitung auf die verschlüsselte HTTPS-Variante Usernamen und Passwörter auch nicht mehr im Klartext übertragen, wie es früher gerüchteweise durchaus vorgekommen sein soll.
Nicht großartig geändert haben sich die Vorbereitungen, denn zunächst muss ein Hash des Passworts bzw. der Passwörter angelegt werden. Wobei sich die folgenden Beispiele auf einen User und ein Passwort beziehen, Traefik ermöglicht jedoch ebenfalls die Nutzung mehrerer Usernamen inkl. deren Passwörter, diese können auch in einer Datei abgelegt werden, die in diesem Fall in der Konfiguration referenziert wird.
Vorbereitungen – Passwörter & Co.
Klassischerweise kommt für die Einrichtung der Passwörter das Tool htpasswd
zum Einsatz, da dieses auf meinem System nicht installiert war und ich auch sonst nichts aus den Apache-Kosmos benötige, habe ich die CLI von openssl
benutzt:
geschke@moabit:~$ openssl passwd -apr1 Password: <<EINGABE>> Verifying - Password: <<EINGABE WIEDERHOLUNG>> $apr1$IPqdz1q4$cYWQSIIP2P9JZ75lyJsi/1
Dass hierbei ein gutes und langes Passwort gewählt werden soll und eine Eingabe von „1234“ nicht gerade sinnvoll ist, dürfte sich von selbst verstehen. Zur Nutzung in Docker-Compose-Files müssen außerdem die enthaltenen Dollar-Zeichen („$
„) escaped werden, und zwar mit einem weiteren Dollar-Zeichen, da Docker Compose sonst davon ausgeht, es handele sich um eine Umgebungsvariable. Der endgültige Passwort-Hash aus dem Beispiel wäre somit „$$apr1$$IPqdz1q4$$cYWQSIIP2P9JZ75lyJsi/1
„.
Falls htpasswd
doch installiert ist, findet sich in der Traefik-Dokumentation sogar ein Kommando, mit dem alle Schritte in einem realisiert werden können. Hier nur wiedergegeben und somit ungetestet wäre dies „echo $(htpasswd -nb user password) | sed -e s/\\$/\\$\\$/g
„.
Traefik-Labels im Docker-Compose-File
Das folgende Beispiel zeigt den kompletten Bereich des Nginx-Webservers aus dem Docker-Compose-File, das für dieses Blog zuständig ist:
services: nginx_kuerbisorg: image: geschke/nginx-swrm:1.24-3 restart: always volumes: - type: bind source: ./html target: /var/www/html - type: bind source: ./nginx/conf.d target: /etc/nginx/conf.d configs: - source: nginx_config_default target: /etc/nginx/sites-enabled/default networks: - website_net - "traefik-public" labels: - "traefik.enable=true" # enable traefik - "traefik.docker.network=traefik-public" # put it in the same network as traefik - "traefik.constraint-label=traefik-public" # assign the same label as traefik so it can be discovered - "traefik.http.routers.website_kuerbisorg.rule=Host(`www.kuerbis.org`) || Host(`kuerbis.org`)" - "traefik.http.routers.website_kuerbisorg.entrypoints=http" - "traefik.http.middlewares.website_kuerbisorg.redirectscheme.scheme=https" # redirect traffic to https - "traefik.http.middlewares.website_kuerbisorg.redirectscheme.permanent=true" - "traefik.http.routers.website_kuerbisorg.middlewares=https-redirect" - "traefik.http.routers.website_kuerbisorg-secured.rule=(Host(`www.kuerbis.org`) || Host(`kuerbis.org`)) && !Path(`/wp-login.php`) && !PathPrefix(`/wp-admin`)" - "traefik.http.routers.website_kuerbisorg-secured.entrypoints=https" - "traefik.http.routers.website_kuerbisorg-secured.tls.certresolver=le-tls" # use the Let's Encrypt certificate we set up earlier - "traefik.http.services.website_kuerbisorg-secured.loadbalancer.server.port=80" # ask Traefik to search for port 80 of the website service container - "traefik.http.routers.website_kuerbisorg-secured.middlewares=secHeaders@file,def-compress@file" - "traefik.http.routers.website_kuerbisorg-secured.priority=1" - "traefik.http.routers.website_kuerbisorg-seclogin.rule=(Host(`www.kuerbis.org`) || Host(`kuerbis.org`)) && (Path(`/wp-login.php`) || PathPrefix(`/wp-admin`))" - "traefik.http.routers.website_kuerbisorg-seclogin.entrypoints=https" - "traefik.http.routers.website_kuerbisorg-seclogin.tls=true" - "traefik.http.routers.website_kuerbisorg-seclogin.priority=10" - "traefik.http.routers.website_kuerbisorg-seclogin.middlewares=secHeaders@file,website_kuerbisorg_mwlogin,def-compress@file" - "traefik.http.routers.website_kuerbisorg-adminajax.rule=(Host(`www.kuerbis.org`) || Host(`kuerbis.org`)) && PathPrefix(`/wp-admin/admin-ajax.php`)" - "traefik.http.routers.website_kuerbisorg-adminajax.entrypoints=https" - "traefik.http.routers.website_kuerbisorg-adminajax.tls=true" - "traefik.http.routers.website_kuerbisorg-adminajax.priority=15" - "traefik.http.routers.website_kuerbisorg-adminajax.middlewares=secHeaders@file,def-compress@file" - "traefik.http.middlewares.website_kuerbisorg_mwlogin.basicauth.users=username:$$apr1$$IPqdz1q4$$cYWQSIIP2P9JZ75lyJsi/1"
Auf die allgemeine Docker-Compose- bzw. Traefik-Konfiguration werde ich jetzt nicht weiter eingehen, sondern den Fokus auf die letzten drei Abschnitte und den Änderungen für die HTTP-Authentifizierung legen.
Router-Definition
Ein Unterschied zur bisherigen Konfiguration ist in Zeile 29 zu sehen: „traefik.http.routers.website_kuerbisorg-secured.rule=(Host(`www.kuerbis.org`) || Host(`kuerbis.org`)) && !Path(`/wp-login.php`) && !PathPrefix(`/wp-admin`)
„. Zuvor wurde dabei nur der Host-Name für den Router definiert. Hinzu gekommen sind zwei Ausnahmen, und zwar einerseits die Definition des Dateinamens für den WordPress-Login und andererseits der Ausschluss eines kompletten Pfades inkl. aller darin befindlichen Dateien. Verknüpft mit logischen Operatoren berücksichtigt diese Regel somit alles, bis auf die definierten Ausnahmen, für die weiter unten ab Zeile 36 ein eigener Router definiert wird.
Dieser neue Router mit dem Namen „website_kuerbisorg-seclogin
“ ist nur für die Login-Seite und die Admin-UI zuständig, die betreffende Regel ist in Zeile 36 angegeben, es handelt sich quasi um die Umkehrung der ersten Regel. Sie soll genau dann angewendet werden, wenn Nutzer die Login-Seite oder den Admin-Bereich betreten. Auch diese Definition liest sich leicht verständlich: „traefik.http.routers.website_kuerbisorg-seclogin.rule=(Host(`www.kuerbis.org`) || Host(`kuerbis.org`)) && (Path(`/wp-login.php`) || PathPrefix(`/wp-admin`))
„.
Der ausschlaggebende Punkt und somit unterschiedlich zur ersten Regel ist die hier verwendete Middleware namens „website_kuerbisorg_mwlogin
„, die in Zeile 40 eingesetzt und weiter unten (Zeile 48) definiert wird. In Zeile 40 sind alle Middlewares (ich verzichte an dieser Stelle gerne auf die etwas holprigen Übersetzungen wie „Zwischenanwendung“ oder „Diensteschicht“) des Routers angegeben: „traefik.http.routers.website_kuerbisorg-seclogin.middlewares=secHeaders@file,website_kuerbisorg_mwlogin,def-compress@file
„. Diese neu definierte Middleware sorgt nun dafür, dass die HTTP-Authentifizierung stattfindet.
In der Definition der Middleware kommt nun auch endlich das zuvor erstellte Passwort bzw. dessen Hash zum Einsatz (Zeile 48): „traefik.http.middlewares.website_kuerbisorg_mwlogin.basicauth.users=username:$$apr1$$IPqdz1q4$$cYWQSIIP2P9JZ75lyJsi/1
„. Weitere Paare von Benutzerkennungen und Passwörtern können einfach per Komma getrennt hinzugefügt werden, eine Alternative wäre die bereits erwähnte Nutzung einer externen Datei, in diesem Fall würde der Inhalt lauten „.basicauth.usersFile=/path/to/my/usersfile"
. Die Traefik-Dokumentation listet weitere Möglichkeiten wie die Vergabe eines Realms oder Optionen bzgl. des übermittelten Headers, beides wurde hier bei der Default-Einstellung belassen.
Die Ausnahme der Ausnahmen…
Damit wäre die Konfiguration bereits beendet, wenn nicht WordPress selbst zu internen Zwecken den Pfad „/wp-admin/admin-ajax.php
„, verbunden mit einigen Parametern, aufrufen würde. Dies verlangt nach einer Ausnahme der Ausnahme, realisiert mit einem neuen Router namens „website_kuerbisorg-adminajax
„, definiert in den Zeilen 42 bis 46. Damit die Regel in Zeile 42 berücksichtigt wird und dabei die zuvor definierte HTTP-Authentifizierung außer Kraft setzt, ist zudem eine Priorisierung notwendig. Traefik berechnet die Priorität der Routen nach einem einfachen Schema, und zwar nach der Länge der jeweiligen Regel. Dabei gilt ein höherer Wert als vorrangig gegenüber niedrigeren Werten, so dass letztlich der Router mit der längsten Regel „gewinnt“. Im Beispiel würde somit der Router für die HTTP-Authentifizierung gegenüber der Ausnahme für „admin-ajax.php
“ vorgezogen werden, so letzterer Router nie zum Einsatz käme.
Diese Traefik-eigene Priorisierung kann jedoch individuell verändert werden, und zwar mit der Option „priority=n
„. In der hier dargestellten Konfiguration finden sich daher in den Zeilen 45 (Ausnahme der Ausnahme), 39 (HTTP-Authentifizierung) und 34 (Default-Zugriff) entsprechende Priorisierungen.
Anklopfen unerwünscht!
Das war es auch schon! In der Admin-UI von Traefik werden alle Router, Services und Middlewares wie üblich übersichtlich dargestellt, der folgende Screenshot zeigt die Detail-Seite für den Router der Pfade für der HTTP-Authentifizierung und der entsprechenden Middleware:
Natürlich ist diese Admin-UI ebenfalls nicht frei zugänglich, selbst bei Kenntnis der URL würden unerwünschte Besucher hier ebenfalls scheitern.