Passwörter rotieren, ohne sich auszusperren

Ausgangslage

Die neuen, starken Datenbank-Passwörter lagen vorbereitet in der .env — generiert und dokumentiert, aber nicht aktiv. Die laufenden MariaDB-Instanzen nutzten weiterhin die alten. Der Grund: MariaDB liest seine Passwort-Umgebungsvariablen nur beim allerersten Start mit leerem Datenverzeichnis, WordPress nur beim Erstellen des Containers. Ein docker restart genügt für keines von beiden.

Entscheidung

Rotation in zwei Phasen: zuerst die Anwendungsbenutzer, root erst danach. root ist der Reparaturzugang — wäre er im selben Durchgang geändert worden und dabei etwas schiefgegangen, hätte für eine Korrektur kein administrativer Zugriff mehr bestanden. Und bewusst kein docker restart, weil ein bestehender Container seine Umgebungsvariablen dabei nicht neu einliest; nur ein echtes Neuerstellen zählt.

Vor der ersten Änderung: wissen, wovon man ausgeht

Zwei Dinge wurden geprüft, bevor überhaupt etwas geändert wurde. Erstens die Images:

docker inspect --format='{{.Image}}' projects-mariadb-odabas-1
docker images --no-trunc | grep mariadb

Die Image-IDs der laufenden Container stimmten exakt mit den lokal getaggten Images überein — seit der Erstellung hatte kein docker pull stattgefunden. Das war wichtig, weil ein ungewollter Versionssprung bei MariaDB auf ein bestehendes Datenverzeichnis potenziell destruktiv gewesen wäre.

Zweitens ein vollständiger Dump beider Instanzen mit mariadb-dump --all-databases (3,3 MB und 3,8 MB, je 42 Tabellen, einschliesslich der Systemtabellen der Datenbank mysql), danach ein Abgleich: Verglichen wurden die Passwort-Werte in den Container-Umgebungen mit denen in der .env, per SHA-256 — nicht im Klartext. Container und .env stimmten überein. Was die Datenbank selbst akzeptierte, zeigte erst der spätere Login.

Eine Korrektur gegenüber der ursprünglichen Planung zeigte sich dabei gleich: Im Container heisst die Variable MYSQL_ROOT_PASSWORD, nicht MARIADB_ROOT_PASSWORD wie auf der Host-Seite in der .env.

Umsetzung

Phase 1 — Anwendungsbenutzer. Zwei getrennte Instanzen, zwei getrennte Anweisungen:

-- auf der odabas-Instanz
ALTER USER 'odabasuser'@'%' IDENTIFIED BY '<neues_passwort>';
-- auf der waverider-Instanz
ALTER USER 'waverideruser'@'%' IDENTIFIED BY '<neues_passwort>';

Danach docker compose up -d, um die WordPress-Container mit den neuen Umgebungsvariablen neu zu erstellen. Dabei wurden, ohne dass es der Plan vorsah, auch die beiden MariaDB-Container neu erstellt — Compose tut das automatisch, sobald sich Umgebungsvariablen ändern. Für die Daten war das folgenlos, sie liegen im Bind-Mount. Es erklärt aber, warum Container-Umgebung und Datenbank in Phase 2 auseinanderlagen.

Phase 2 — root. MYSQL_ROOT_PASSWORD wirkt nur beim erstmaligen Start eines leeren Datenverzeichnisses. Nach der ungeplanten Container-Neuerstellung aus Phase 1 wichen Container-Umgebung und Datenbank also bereits auseinander, und der Zugriff musste über die zuvor gesicherten alten Werte erfolgen. In beiden Datenbanken existierten je zwei root-Konten (root@localhost und root@%), rotiert in einer gemeinsamen Sitzung — nach dem ersten ALTER USER wäre eine zweite Anmeldung mit dem alten Passwort nicht mehr möglich gewesen.

Laut Abschlussnotiz lagen zwischen dem ALTER USER und dem Abschluss von docker compose up -d rund 17 Sekunden Unterbrechung, ohne bemerkte Auswirkungen.

Der Beleg

«Es hat funktioniert» ist keine Verifikation, nur eine Hoffnung. Geprüft wurde stattdessen:

  • Bei jeder Prüfung HTTP 200 auf beiden Sites.
  • Manuelle Anmeldung in beiden wp-admin-Oberflächen erfolgreich — der Login schreibt Session-Daten in wp_usermeta und wird nicht gecacht, belegt also echten Schreibzugriff.
  • Der alte Zugang schlägt fehl:
  docker exec -it mariadb-shared mariadb -u odabasuser -p odabasdb

Beim Eintippen des alten Passworts: ERROR 1045 (28000): Access denied for user 'odabasuser'@'localhost' (using password: YES). Der neue Wert funktioniert.

  • Ein Dump mit dem neuen root-Passwort aus der Container-Umgebung gelingt.
  • Der Traefik-Teststack blieb während der gesamten Rotation unbeeinflusst.

Der dritte Punkt ist der eigentliche Beweis: Eine Rotation ist erst bewiesen, wenn das alte Passwort nachweislich scheitert, nicht wenn das neue zufällig funktioniert.

Aufräumen

Die temporär gesicherten alten Passwörter wurden mit shred -u gelöscht, die Shell-Variablen entfernt. Die beiden SQL-Dumps bleiben mit Modus 600 in einem Verzeichnis mit Modus 700 liegen und sind über *.sql in der .gitignore vom Repository ausgeschlossen — durch --all-databases enthalten sie die Passwort-Hashes aus mysql.user.

Erkenntnis

Der administrative Zugang rotiert man zuletzt, nicht zuerst — er ist der einzige Weg zurück, falls beim Rest etwas schiefgeht. Eine geplante Änderung kann unterwegs eine ungeplante nach sich ziehen, wie hier die Neuerstellung der Datenbank-Container als Nebenwirkung von Phase 1 — und eine Prüfsumme sagt mehr über den Ausgangszustand als die Annahme, alles sei schon synchron.