Cybersicherheit

MariaDB-Konten mit minimalen Rechten für kleine PHP-Anwendungen - Nur erlauben, was die Anwendung braucht

MariaDB-Konten mit minimalen Rechten für kleine PHP-Anwendungen - Nur erlauben, was die Anwendung braucht

Eine kleine PHP-Anwendung benötigt möglicherweise nur vier gewöhnliche Datenbankaktionen: eine Zeile lesen, eine einfügen, sie aktualisieren und gelegentlich löschen. Dennoch kann ihre Verbindung unbemerkt ein Konto verwenden, das Tabellen ändern, Benutzer anlegen oder auf jede Datenbank des Servers zugreifen darf. Die Anwendung funktioniert weiterhin, sodass dieses Missverhältnis unsichtbar bleibt, bis ein Fehler, offengelegte Zugangsdaten oder eine Injection-Schwachstelle unnötige Berechtigungen in zusätzlichen Schaden verwandeln.

Die hilfreiche Frage lautet nicht: „Welcher GRANT beseitigt die Fehlermeldung?“ Sie lautet: „Welche möglichst eng begrenzte Datenbankberechtigung benötigt dieser Prozess für seine dokumentierte Aufgabe?“ Dieser Artikel entwickelt anhand dieser Frage eine Prüfmethode für eine kleine PHP- und MariaDB-Anwendung. Das SQL-Beispiel verwendet einen fiktiven Notizdienst. Es muss nach Prüfung der Anwendungsabfragen angepasst werden und ist keine Aussage über die Konfiguration dieser Website.

Least Privilege begrenzt Befugnisse, nicht Schwachstellen

Das NIST-Glossar definiert Least Privilege als die Beschränkung eines Benutzers oder eines in seinem Auftrag handelnden Prozesses auf den minimalen Zugriff, der für die zugewiesene Funktion nötig ist. Auf eine Datenbankverbindung übertragen sollte der PHP-Prozess ausreichend Berechtigungen für normale Requests erhalten, aber nicht mehr.

Das begrenzt Auswirkungen, schafft aber keine Immunität. Findet ein Angreifer eine SQL Injection, repariert Least Privilege nicht die verwundbare Abfrage. Darf die Anwendung berechtigterweise alle E-Mail-Adressen ihrer Kunden lesen, kann ihr Runtime-Konto diese Adressen nicht vor einer kompromittierten Anwendung verbergen. Engere Grants können jedoch sachfremde Fähigkeiten entfernen: Ein Read-only-Endpunkt muss keine Zeilen löschen, ein normaler Request muss kein Schema ändern und eine Anwendung muss nicht die Datenbank einer anderen Anwendung öffnen können.

Diese Unterscheidung ist wichtig, denn „nicht root“ ist nur der Anfang. Ein Konto mit ALL PRIVILEGES auf einer einzelnen Anwendungsdatenbank ist zwar enger begrenzt als ein Serveradministrator, kann aber weiterhin über CREATE, ALTER und DROP verfügen, obwohl gewöhnliche Web-Requests diese Rechte nie benötigen.

Verhalten inventarisieren, bevor ein GRANT geschrieben wird

Eine aus einem Tutorial kopierte Berechtigungsliste ist eine Vermutung. Ausgangspunkt sollten die tatsächlichen Pfade der Anwendung sein: Controller, Repositories, Queue Worker, geplante Jobs und Migrationsdateien. Jede Datenbankoperation wird nach dem ausführenden Prozess gruppiert. Eine kleine Inventarliste könnte so aussehen:

ProzessErwartete AufgabeWahrscheinliche DatenrechteSchemaänderungen
Öffentliche Web-RuntimeVeröffentlichte Notizen auflisten und anzeigenSELECTKeine
Authentifizierte Web-RuntimeNotizen erstellen und bearbeitenSELECT, INSERT, UPDATE; DELETE nur, wenn das Produkt tatsächlich löschtKeine
Deployment-MigrationGeprüfte Schemaänderungen anwendenVon der Migration abhängigAusgewählte DDL-Rechte für das Deployment-Fenster
Menschliche AdministrationKonten und besondere Wartungsaufgaben verwaltenAufgabenabhängigAufgabenabhängig

„Wahrscheinlich“ steht dort bewusst. Eine Suchfunktion kann eine View verwenden, eine Queue Statuszeilen aktualisieren, ein Upload-Ablauf Metadaten schreiben und ein Soft-Delete-Design UPDATE statt DELETE einsetzen. Eine statische Codeprüfung findet vieles, während Staging-Tests übersehene Pfade sichtbar machen. Keine der beiden Methoden beweist allein die Vollständigkeit.

Aus einem einmaligen Installationsschritt sollte keine dauerhafte Berechtigung abgeleitet werden. Wenn das Setup Tabellen anlegt und die Anwendung anschließend monatelang Requests verarbeitet, gehört die Schemaerstellung zum Deployment und nicht zu jedem Request.

Unterschiedliche Aufgaben erhalten unterschiedliche Identitäten

Das OWASP Database Security Cheat Sheet empfiehlt ein eigenes Konto je Anwendung oder Dienst, keine eingebauten Administratorkonten, Zugriff nur von erlaubten Hosts und ausschließlich die benötigten Datenbanken und Berechtigungen. Eine praktische Erweiterung besteht darin, Identitäten nach ihrer betrieblichen Rolle zu trennen:

  • Runtime-Konto: wird von PHP-FPM und gewöhnlichen Hintergrundjobs genutzt und ist normalerweise auf Datenoperationen der Anwendung beschränkt.
  • Migrationskonto: wird von einem kontrollierten Deployment-Schritt genutzt und nicht in der Umgebung der Web-Runtime gespeichert.
  • Administratorkonto: wird interaktiv für Kontoverwaltung oder besondere Wartung eingesetzt und nie in die Anwendungskonfiguration eingebettet.

Diese Trennung macht die Grenze technisch durchsetzbar. Ein PHP-Fehler kann kein Migrationsrecht verwenden, das der PHP-Prozess nie erhalten hat. Auch Logs und die Rotation von Zugangsdaten werden leichter nachvollziehbar. Dafür entsteht betrieblicher Aufwand: Deployments benötigen ein eigenes Secret und ein dokumentiertes Verfahren. Bei einer sehr kleinen Anwendung wirkt das möglicherweise schwerfälliger als ein einziges Allzweckkonto, doch die Alternative überträgt die Befugnisse des Deployments auf jeden Web-Request.

Ein MariaDB-Konto umfasst auch den Host

MariaDB identifiziert ein Konto anhand von Benutzername und Host in der Form 'user'@'host'. Laut der Dokumentation zu CREATE USER wird ein ausgelassener Host als %, also als Wildcard, interpretiert. Diese bequeme Schreibweise ist weiter gefasst als ein bestimmter Anwendungshost.

Auch der Verbindungstransport ist relevant. Unter Linux dokumentiert MariaDB 'user'@'localhost' als Zuordnung für lokale Unix-Socket-Verbindungen. Eine TCP-Verbindung zu 127.0.0.1 ist nicht dieselbe Zuordnung. Vor dem Anlegen eines Kontos sollte deshalb der PHP-DSN geprüft werden: Nutzt die Anwendung einen Socket, Loopback-TCP oder einen separaten Datenbankhost? Das Konto sollte auf diesen tatsächlichen Pfad beschränkt werden, statt es bei einem Authentifizierungsfehler auf % zu erweitern.

Die Host-Beschränkung ist eine Schutzschicht, aber kein Ersatz für eine Firewall. OWASP empfiehlt zusätzlich, die Erreichbarkeit der Datenbank im Netzwerk auf die wenigen notwendigen Hosts zu begrenzen. Konto- und Netzwerkregel sollten zueinander passen.

Einen expliziten Runtime-Grant erstellen

Nach der MariaDB-Dokumentation legt CREATE USER ein Konto ohne Berechtigungen an. Das ist eine nützliche leere Ausgangslage. Angenommen, die fiktive Runtime von notes_app verwendet einen lokalen Socket und benötigt nach der Prüfung vier Datenoperationen in dieser Datenbank:

CREATE USER 'notes_runtime'@'localhost'
  IDENTIFIED BY 'REPLACE_WITH_A_GENERATED_SECRET';

GRANT SELECT, INSERT, UPDATE, DELETE
  ON `notes_app`.*
  TO 'notes_runtime'@'localhost';

Anweisungen zur Kontoverwaltung sollten über eine autorisierte administrative Verbindung ausgeführt werden, nicht über die Webanwendung. Der Platzhalter ist keine Passwortempfehlung. Die echten Zugangsdaten müssen im Secret-Prozess des Deployments erzeugt und übermittelt, von der Versionsverwaltung und dem öffentlichen Webverzeichnis ferngehalten und vor einer Offenlegung in Shell-Verlauf oder Dokumentation geschützt werden.

Die MariaDB-Referenz zu GRANT unterscheidet globale sowie datenbank-, tabellen-, spalten- und routinenbezogene Geltungsbereiche. Hier steht notes_app.* für alle Objekte dieser Datenbank, einschließlich zutreffender zukünftiger Objekte. Es bedeutet nicht jede Datenbank des Servers. Trotzdem müssen die aufgeführten Aktionen zum Inventar passen. Wenn die Runtime nie löscht, entfällt DELETE. Ein nur lesendes Veröffentlichungs-Frontend könnte Folgendes erhalten:

GRANT SELECT
  ON `notes_app`.*
  TO 'notes_reader'@'localhost';

Feingranularer ist nicht automatisch besser. Grants auf Tabellenebene können verhindern, dass ein öffentlicher Reader private Kontentabellen öffnet:

GRANT SELECT
  ON `notes_app`.`published_notes`
  TO 'notes_reader'@'localhost';

Dutzende fragile Tabellengrants können jedoch bei jeder Schemaänderung brechen und schwerer zu prüfen sein als ein sorgfältig gewählter Datenbankgrant. Sinnvoll ist der engste Geltungsbereich, den das Team zuverlässig aktuell halten kann. Views oder Routinen können in manchen Designs eine sauberere Schnittstelle bieten, bringen aber eigene Fragen zu Eigentümerschaft und Ausführungskontext mit sich, die über dieses Grundmuster hinausgehen.

Schemabefugnisse von normalen Requests fernhalten

Ein Runtime-Konto hat normalerweise keinen Grund, CREATE, ALTER, DROP, CREATE USER, FILE oder GRANT OPTION zu besitzen. Die genauen Migrationsrechte sind nicht universell: Das Hinzufügen eines Index, das Erstellen einer Tabelle und das Ersetzen einer View sind unterschiedliche Operationen. Grants für das Migrationskonto müssen aus den geprüften Migrationsdateien und der Deployment-Strategie abgeleitet werden. Diese Zugangsdaten bleiben für PHP-FPM unerreichbar.

Hinzu kommt ein Zuverlässigkeitskompromiss. DDL-Rechte für jedes Deployment vorübergehend zu vergeben und wieder zu entziehen kann präzise sein, ist aber fehleranfällig. Ein eigenes Migrationskonto mit geschütztem Secret kann einfacher sein. Beide Varianten sind besser, als dem Runtime-Konto stillschweigend sämtliche Datenbankrechte zu geben. Least Privilege muss auch bei einem fehlgeschlagenen Deployment bedienbar bleiben, sonst führt der Zeitdruck irgendwann zu einem GRANT ALL als Notlösung.

Berechtigung und Ablehnung verifizieren

Konfigurationstext allein genügt nicht. MariaDB stellt SHOW GRANTS bereit, um die für ein Konto gespeicherten Grants aufzulisten:

SHOW GRANTS FOR 'notes_runtime'@'localhost';

Jede Zeile muss geprüft werden, insbesondere globale Grants, weiter gefasste Datenbankgrants, Rollen und die genaue Host-Komponente. Ein enger Tabellengrant hilft nicht, wenn eine andere Zeile bereits umfassende Befugnisse erteilt.

Danach wird das Verhalten im Staging mit derselben Verbindungsmethode und Konfiguration getestet, die später die Anwendung verwendet. Positive Tests müssen alle normalen Pfade abdecken. Negative Tests müssen beweisen, dass verbotene Aktionen scheitern: Das Runtime-Konto darf beispielsweise keine temporäre Staging-Tabelle anlegen oder verändern und keine unabhängige Datenbank lesen. Ein destruktiver Ablehnungstest darf nie auf Produktionsdaten ausgeführt werden.

Eine nützliche Abnahmeregel ist asymmetrisch: Alle dokumentierten Anwendungspfade funktionieren, während repräsentative Aktionen außerhalb des Geltungsbereichs scheitern. Ein Fehler in einem erwarteten Pfad bedeutet, dass Inventar oder Architektur überarbeitet werden müssen. Er bedeutet nicht automatisch, dass das Konto ALL PRIVILEGES erhalten sollte.

Rollout ohne Raten unter Zeitdruck

  1. Das neue Konto und die expliziten Grants anlegen, ohne die bestehende Runtime zu ändern.
  2. Im Staging testen, einschließlich Queues, geplanter Jobs, Wartungsseiten und selten genutzter Schreibpfade.
  3. Die neuen Zugangsdaten über den üblichen Secret-Mechanismus deployen.
  4. Bestehende Datenbankverbindungen ersetzen, damit sich die Anwendung tatsächlich mit dem neuen Konto authentifiziert.
  5. Fehler durch verweigerte Operationen und den Zustand der Anwendung während eines begrenzten Rollouts beobachten.
  6. Erst nach ausreichender Prüfung die alten Runtime-Zugangsdaten entfernen oder nicht mehr benötigte Grants entziehen.

Die MariaDB-Dokumentation zu REVOKE warnt sinngemäß davor, dass das Entfernen eines Grants nicht unbedingt die Befugnis beseitigt, wenn ein weiter gefasster Grant sie weiterhin erteilt. Nach jedem Entzug sollten die wirksamen Grants erneut geprüft und die Ablehnungstests wiederholt werden. Auch ein Rollback muss vor dem Rollout geplant sein: Es sollte bekannt sein, welche konkrete Berechtigung einen übersehenen Pfad wiederherstellt, statt unter Druck uneingeschränkten Zugriff zu erteilen.

Berechtigungen driften, wenn sich Software verändert. Ein neuer Exportjob benötigt möglicherweise Lesezugriff auf eine weitere Tabelle; eine aufgegebene Funktion kann ein DELETE-Recht zurücklassen. Grants sollten zusammen mit Schemamigrationen geprüft und regelmäßig mit den aktuellen Abfragepfaden verglichen werden. Der Rhythmus sollte sich nach der Änderungshäufigkeit der Anwendung richten und nicht nach einem willkürlichen universellen Intervall.

Die Grenzen dieses Schutzes kennen

  • Er ersetzt keine parametrisierten Abfragen oder andere Abwehrmaßnahmen gegen SQL Injection.
  • Er erzwingt nicht, welcher Anwendungsbenutzer auf welche Zeile zugreifen darf. Das bleibt ein Problem der Anwendungsautorisierung, sofern kein separat entwickelter Datenbankmechanismus dies übernimmt.
  • Er kann keine Daten verbergen, welche die Runtime berechtigterweise lesen muss.
  • Er schützt kein offengelegtes Datenbankpasswort; sichere Speicherung, Rotation und Netzwerkschutz bleiben notwendig.
  • Er ersetzt weder Backups und Wiederherstellungstests noch Patches, Monitoring oder Audit-Logs.

Diese Grenzen machen Least Privilege nicht kosmetisch. Sie beschreiben seinen ehrlichen Zweck: die verfügbaren Befugnisse zu reduzieren, wenn eine andere Kontrolle versagt.

Fazit

Ein Datenbankkonto sollte eine Aufgabe beschreiben. Für eine kleine PHP-Runtime besteht diese Aufgabe normalerweise aus gewöhnlichem Datenzugriff innerhalb einer Anwendungsdatenbank über einen erwarteten Verbindungspfad. Schemamigration und menschliche Administration sind andere Aufgaben und verdienen eigene Identitäten.

Der stärkste Ablauf ist zugleich unspektakulär: reale Abfragen inventarisieren, ein unprivilegiertes Konto anlegen, explizite Grants hinzufügen, die gespeicherte Konfiguration prüfen, erwartete Operationen erfolgreich testen und repräsentative verbotene Operationen nachweislich scheitern lassen. Wenn sich die Anwendung ändert, wird dieser Nachweis erneut geprüft. Das Ziel ist nicht der auf dem Papier kleinstmögliche Grant, sondern der kleinste Grant, der korrekt, testbar und wartbar bleibt.

References