In Teil 1 dieser Reihe ging es um strukturelle Fehler: fehlende Fremdschlüssel, falsche oder fehlende Indizes, zu spät gesetzte Filter und Soft Deletes, die Daten nicht wirklich löschen.
Teil 2 hat gezeigt, warum mehrere zusammengehörige Schreibvorgänge in eine Transaktion gehören. Ohne BEGIN, COMMIT und ROLLBACK kann eine Anwendung Daten in einem Zustand hinterlassen, der nie hätte entstehen dürfen.
In Teil 3 ging es um Datenbanksicherheit: Root-Zugänge, Encryption at Rest, sensible Spalten und Backups. Dieser Teil führt den Gedanken konsequent weiter.
Denn selbst ein starkes Passwort und ein geschlossener Port lösen ein grundlegendes Problem nicht: Wenn die Anwendung mit einem Benutzer verbunden ist, der alles darf, wird aus jeder Schwachstelle in der Anwendung potenziell ein vollständiger Datenbankzugriff.
Root ist kein Anwendungsuser
Der häufigste Fehler ist gleichzeitig der unspektakulärste: Die Anwendung läuft mit dem Datenbankbenutzer root. In der lokalen Entwicklung funktioniert das bequem. Die Anwendung verbindet sich, Migrationen laufen durch, jedes Tool kann auf jede Tabelle zugreifen. Genau deshalb wird diese Konfiguration später gerne nach Produktion übernommen.
Das Problem ist nicht nur ein fehlendes oder schwaches Passwort. Das Problem ist der Umfang der Rechte. Root darf Datenbanken und Benutzer verwalten, Tabellen anlegen und löschen, Systeminformationen lesen und je nach Konfiguration Operationen ausführen, die eine normale Anwendung niemals benötigen sollte.
Eine SQL-Injection in einer Anwendung, die mit einem eingeschränkten Benutzer läuft, ist bereits ernst. Eine SQL-Injection in einer Anwendung, die als Root verbunden ist, kann aber weit darüber hinausgehen: Daten aus anderen Datenbanken lesen, Tabellen löschen, neue Zugänge anlegen oder die Datenbankstruktur verändern.
Root gehört deshalb in einen administrativen Ausnahmeweg. Nicht in die .env der Anwendung. Nicht in den normalen Webprozess. Und auch nicht als bequemer Benutzer für Migrationen.
MySQL sieht dafür ausdrücklich getrennte Konten und Berechtigungen vor. Mit CREATE USER wird ein Konto angelegt, mit GRANT erhält es genau die Rechte, die für seinen Zweck erforderlich sind:
-- Nur ein Beispiel: Werte und Host müssen an das Setup angepasst werden
CREATE USER 'app_runtime'@'localhost' IDENTIFIED BY 'ein-langes-einzigartiges-passwort';
GRANT SELECT, INSERT, UPDATE, DELETE ON app_db.* TO 'app_runtime'@'localhost';
Die Details zu CREATE USER und GRANT gehören zu den Stellen, die man nicht aus dem Gedächtnis zusammenkopieren sollte. Besonders bei produktiven Systemen müssen Host, TLS-Anforderungen, Version und tatsächlicher Berechtigungsbedarf geprüft werden.
Ein Datenbankuser ist nicht automatisch für jeden Weg zuständig
In vielen Projekten gibt es genau einen Zugang: Die Webanwendung nutzt ihn, Queue-Worker nutzen ihn, Cronjobs nutzen ihn, Migrationen nutzen ihn und ein internes Reporting greift ebenfalls darüber zu. Technisch funktioniert das. Für Sicherheit, Auditierung und Fehlersuche ist es aber eine schlechte Trennung.
Sinnvoller ist eine Aufteilung nach Vertrauensgrenze und Aufgabe:
- Runtime-User: liest und verändert Daten im normalen Betrieb.
- Migrations- oder Deployment-User: verändert die Struktur der Datenbank und wird nur kontrolliert eingesetzt.
- API-User: greift nur auf die Daten zu, die ein externer oder besonders exponierter API-Weg benötigt.
- Reporting-User: liest Daten, verändert aber nichts.
- Administrativer User: wird nur für Benutzerverwaltung, Serveraufgaben und echte Notfälle verwendet.
Das ist keine künstliche Aufteilung. Jeder dieser Wege hat ein anderes Risiko und einen anderen technischen Zweck. Wenn ein Zugriff auffällig ist, lässt sich außerdem direkt erkennen, ob er aus dem normalen Anwendungscode, aus einer API, aus einer Migration oder aus einem administrativen Prozess stammt.
Tipp: Neben getrennten Usern für getrennte Aufgaben kann es ebenfalls je nach Projektgröße auch sinnvoll sein für die diversen Teilsysteme auch eigenständige (Laravel) Projekte aufzusetzen
Der Runtime-User: lesen und schreiben, aber keine Struktur verändern
Der laufende Anwendungscode muss in vielen Projekten lesen, neue Datensätze anlegen, vorhandene Daten aktualisieren und Datensätze löschen können. Dafür reichen typischerweise SELECT, INSERT, UPDATE und DELETE.
Was er normalerweise nicht braucht, sind Rechte wie DROP, ALTER, CREATE DATABASE, FILE oder der Zugriff auf andere Datenbanken. Die Anwendung soll ihre Daten verwalten können. Sie soll aber nicht ihre eigene Datenbankstruktur zerstören dürfen.
Das ist der Kern von Least Privilege: Nicht die Frage „Welche Rechte könnten irgendwann praktisch sein?“ entscheidet, sondern die Frage „Welche Rechte braucht dieser Prozess im normalen Betrieb tatsächlich?“
Das gilt auch für Queue-Worker und geplante Aufgaben. Sie gehören zum laufenden Anwendungssystem und sollten deshalb grundsätzlich mit dem Runtime-User arbeiten. Eine Aufgabe, die im Hintergrund läuft, braucht nicht automatisch mehr Datenbankrechte als eine Anfrage über das Web.
Für die PHP- und Laravel-Entwicklung bedeutet das: Die Anwendungskonfiguration sollte einen dedizierten Runtime-Zugang verwenden. Lokal kann ein Entwickler für Tests mit einer großzügigeren Konfiguration arbeiten. Diese Bequemlichkeit darf aber nicht zur Produktionskonfiguration werden.
Der Migrationsuser: mehr Rechte, aber nur im kontrollierten Prozess
Migrationen sind ein Sonderfall. Sie müssen Tabellen anlegen, Spalten ergänzen, Indizes verändern oder alte Strukturen entfernen können. Dafür reichen die Rechte des Runtime-Users nicht immer aus.
Die Lösung ist aber nicht, dem Runtime-User einfach dauerhaft alle Rechte zu geben. Stattdessen bekommt der Deployment- oder Migrationsprozess einen eigenen Zugang. Dieser Zugang wird nur dort verfügbar gemacht, wo Migrationen tatsächlich ausgeführt werden — zum Beispiel in einem geschützten Deployment-Job.
Ein beispielhafter Migrationsuser könnte so angelegt werden:
CREATE USER 'app_migrations'@'localhost' IDENTIFIED BY 'ein-eigenes-migrations-passwort';
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER, INDEX ON app_db.* TO 'app_migrations'@'localhost';
Die Rechte müssen zum tatsächlichen Migrationsprozess passen. Wenn Migrationen Tabellen löschen oder bestimmte weitere Objekte verändern, können zusätzliche Rechte erforderlich sein. GRANT ALL PRIVILEGES ist trotzdem keine gute Standardlösung. Jede zusätzliche Berechtigung sollte begründet werden können.
Damit entsteht eine klare Sicherheitsgrenze:
Webanwendung –> Runtime-User –> Daten lesen und verändern
Deployment –> Migrationsuser –> Schema verändern
Administration –> Administrativer User –> Benutzer und Server verwalten
Der Migrationsuser ist dadurch nicht sicher, weil er weniger Rechte als Root hat. Er ist sicherer, weil er nicht dauerhaft und nicht aus jedem Pfad heraus verwendet wird.
Laravel: mehrere Verbindungen, aber keine automatische Trennung
Laravel trennt Runtime- und Migrationsuser nicht automatisch. Eine neue Anwendung verwendet in der Regel eine Standardverbindung, deren Zugangsdaten aus der Umgebung kommen. Wenn nichts weiter eingerichtet wird, laufen Anwendung und Migrationen über diese Standardverbindung.
Laravel unterstützt aber mehrere Datenbankverbindungen. In config/database.php kann neben der Runtime-Verbindung eine eigene Verbindung für Migrationen definiert werden:
'mysql' => [
'driver' => 'mysql',
'host' => env('DB_HOST'),
'port' => env('DB_PORT',3306),
'database' => env('DB_DATABASE'),
'username' => env('DB_USERNAME'),
'password' => env('DB_PASSWORD'),
],
'mysql_migrations' => [
'driver' => 'mysql',
'host' => env('DB_HOST'),
'port' => env('DB_PORT',3306),
'database' => env('DB_DATABASE'),
'username' => env('DB_MIGRATIONS_USERNAME'),
'password' => env('DB_MIGRATIONS_PASSWORD'),
],
Die Anwendung nutzt weiterhin die Standardverbindung. Der Migrationsprozess wird im Deployment ausdrücklich mit der zweiten Verbindung gestartet:
php artisan migrate --database=mysql_migrations --isolated --force
–isolated verhindert bei geeigneter Cache-Konfiguration, dass mehrere parallel laufende Deployments gleichzeitig Migrationen ausführen. -force erlaubt die Ausführung im Produktionsmodus ohne interaktive Rückfrage. Beide Optionen müssen zum konkreten Deployment passen.
Alternativ kann eine einzelne Migration über die Eigenschaft $connection einer anderen Verbindung zugeordnet werden. Für eine generelle Trennung der Produktionsmigrationen ist der explizite Aufruf im Deployment jedoch meist übersichtlicher als eine versteckte globale Änderung im Anwendungscode.
Die offiziellen Laravel-Dokumentationen erklären mehrere Datenbankverbindungen, das Setzen einer Migrationsverbindung und die isolierte Ausführung von Migrationen.
Der Migrationsuser gehört ins Deployment, nicht in den Webprozess
Die Zugangsdaten für den Migrationsuser sollten nur dort verfügbar sein, wo der Migrationsbefehl ausgeführt wird. Das kann ein CI/CD-Job, ein geschütztes Deployment-Script oder eine vergleichbare administrative Umgebung sein.
Die laufende Webanwendung braucht diese Zugangsdaten nicht. Sie sollte mit dem Runtime-User starten und auch bei einem kompromittierten Prozess keinen zweiten Zugang mit weitreichenden Strukturrechten vorfinden.
Für lokale Entwicklung und Tests kann die Konfiguration abweichen. Auch dort lohnt es sich aber, die Rollen realistisch zu trennen. Ein Projekt, das lokal ausschließlich mit Root funktioniert, zeigt erst beim Deployment, dass die Anwendung oder ein Test-Setup unbemerkt zu viele Rechte voraussetzt.
Exkurs: Ein eigener User für die API
Eine API ist nicht einfach nur ein weiterer Aufruf der Anwendung. Sie ist häufig stärker exponiert, wird von fremden Systemen angesprochen und verarbeitet Eingaben, die nicht aus dem eigenen Frontend stammen. Ein eigener Datenbankuser kann deshalb eine zusätzliche Sicherheitsgrenze schaffen.
MySQL unterstützt Berechtigungen auf Datenbank-, Tabellen- und Spaltenebene. Ein API-User kann auf genau die Tabellen eingeschränkt werden, die die API benötigt. Bei sensiblen Tabellen kann sogar der Zugriff auf einzelne Spalten begrenzt werden. Eine weitere Möglichkeit sind Views:
CREATE VIEW api_public_events AS
SELECT id, title, starts_at, location
FROM events
WHERE is_public = 1;
GRANT SELECT
ON app_db.api_public_events
TO 'api_reader'@'localhost';
Der API-User erhält damit nicht automatisch Zugriff auf sämtliche Tabellen und Spalten der Datenbank. Die View bildet eine kontrollierte Oberfläche für die Daten, die tatsächlich veröffentlicht werden dürfen.
In Laravel wird dafür eine eigene Verbindung definiert und im betreffenden API-Service ausdrücklich verwendet:
'mysql_api' => [
'driver' => 'mysql',
'host' => env('DB_HOST'),
'port' => env('DB_PORT', 3306),
'database' => env('DB_DATABASE'),
'username' => env('DB_API_USERNAME'),
'password' => env('DB_API_PASSWORD'),
],
use Illuminate\Support\Facades\DB;
$events = DB::connection('mysql_api')->table('api_public_events')->get();
Der API-User ist kein Ersatz für Authentifizierung, Autorisierung oder Eingabevalidierung. Die API bleibt die Anwendungsschicht. Die Datenbankrechte begrenzen nur den Schaden, falls die Anwendungsschicht umgangen oder kompromittiert wird.
Mehrere User helfen auch bei Auditierung und Fehlersuche
Der Sicherheitsgewinn ist nur eine Seite. Getrennte Benutzer machen technische Vorgänge auch besser nachvollziehbar.
Wenn ein Datenbank- oder Auditlog einen Zugriff dem Benutzer api_reader, app_runtime oder app_migrations zuordnet, ist die Herkunft des Zugriffs sofort erkennbar. War es ein API-Vorgang? Eine normale Anfrage? Ein Deployment? Eine Migration? Oder ein administrativer Eingriff?
Für die einzelne Endkundenaktion braucht es zusätzlich AnwendungsLogging mit Request-ID, Endpoint und — sofern vorhanden — Benutzer-ID. Der Datenbankuser identifiziert aber bereits den technischen Pfad. Diese Information ist bei Audits, Sicherheitsvorfällen und der Fehlersuche sehr wertvoll.
Laravel kann über Query-Listener zusätzliche Informationen zu ausgeführten Abfragen protokollieren. In Kombination mit getrennten Verbindungen lässt sich so nicht nur sehen, welche Abfrage langsam oder fehlerhaft war, sondern auch, aus welchem technischen Bereich sie kam.
use Illuminate\Database\Events\QueryExecuted;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
DB::listen(function (QueryExecuted $query)
{
Log::debug('Datenbankabfrage', [
'connection' => $query->connectionName,'time_ms' => $query->time,
]);
});
Mit SHOW GRANTS lässt sich außerdem prüfen, welche Rechte ein Konto tatsächlich besitzt. Auch diese Prüfung sollte zum regelmäßigen Betrieb gehören und nicht erst dann erfolgen, wenn bereits ein Problem vorliegt.
Eine praktische Checkliste
- Läuft die Anwendung mit einem dedizierten Benutzer — niemals mit Root?
- Hat der Runtime-User nur die Rechte, die der normale Betrieb benötigt?
- Sind Migrationen und Deployments von den Webanfragen getrennt?
- Werden die Zugangsdaten des Migrationsusers nur im Deployment bereitgestellt?
- Werden API-Zugriffe über einen eigenen, eingeschränkten User abgewickelt?
- Können Views oder Spaltenrechte sensible Daten zusätzlich abschirmen?
- Sind Reporting- und Analysezugänge schreibgeschützt?
- Sind die Benutzer auf die nötigen Hosts oder internen Netze begrenzt?
- Werden Zugangsdaten getrennt verwaltet und regelmäßig geprüft?
- Lässt sich aus dem Logging erkennen, welcher technische Weg auf die Datenbank zugegriffen hat?
- Wurde die Trennung nicht nur eingerichtet, sondern auch mit einem echten Deployment getestet?
Fazit: Rechte gehören zum Prozess, nicht nur zur Datenbank
Ein Root-Zugang in der Anwendung ist kein Zeichen dafür, dass ein Projekt flexibel konfiguriert ist. Er ist ein Zeichen dafür, dass die Verantwortlichkeiten zwischen Anwendung, Deployment, API und Administration nicht sauber getrennt wurden.
Der Runtime-User soll Daten verwalten. Der Migrationsuser soll die Struktur verändern. Der API-User soll nur den Teil sehen, den die API wirklich benötigt. Der administrative Zugang bleibt für die Aufgaben reserviert, die tatsächlich Administration sind.
Diese Trennung macht Systeme nicht automatisch sicher. Sie sorgt aber dafür, dass ein Fehler nicht sofort den größtmöglichen Schaden verursachen kann. Gleichzeitig wird sichtbar, welcher technische Weg auf welche Daten zugegriffen hat.
Das ist kein einzelner Schalter. Es ist eine Architekturentscheidung — und genau deshalb sollte sie nicht erst nach einem Sicherheitsvorfall getroffen werden.
Wer Datenbanken für bestehende Systeme strukturieren, modernisieren oder sicher in PHP- und Laravel-Anwendungen einbinden möchte, findet bei eproi weitere Informationen zur Datenbankentwicklung und zu.
FAQ: Root und Anwendungsuser
Warum darf Root nicht als Anwendungsuser genutzt werden?
Root hat unbeschränkten Zugriff auf alle Datenbanken, Tabellen und Systemfunktionen. Wird eine Anwendung mit Root-Credentials betrieben und ein Angreifer übernimmt die Verbindung, kann er nicht nur die eigene Datenbank auslesen, sondern die gesamte Datenbankinstanz kompromittieren. Ein dedizierter Anwendungsuser mit minimalen Rechten begrenzt den möglichen Schaden auf genau den Bereich, den die Anwendung wirklich braucht.
Was bedeutet „Least Privilege“ in der Praxis?
Jeder (Datenbank-)User erhält nur die Rechte, die er für seine Aufgabe tatsächlich benötigt. Der Runtime-User der Anwendung bekommt SELECT, INSERT, UPDATE und DELETE — aber kein DROP, kein GRANT und keinen Zugriff auf Systemtabellen. Der Migrations-User darf zusätzlich DDL-Operationen wie CREATE TABLE und ALTER TABLE ausführen, wird aber ausschließlich beim Deployment eingesetzt. „GRANT ALL PRIVILEGES“ ist in einer Produktionsumgebung fast immer falsch.
Warum reicht ein einziger Datenbankuser für Laravel nicht aus?
Laravel trennt Runtime und Migrationen nicht automatisch. Ohne explizite Konfiguration läuft alles von der Anwendungslogik, Queue-Worker, API-Abfragen bis zu Schema-Migrationen über denselben User aus der .env. Das bedeutet: Der User, der im laufenden Betrieb Datensätze liest und schreibt, hat automatisch auch die Rechte, Tabellen zu ändern oder zu löschen. Das ist ein unnötiges Sicherheitsrisiko, das sich mit wenigen Zeilen Konfiguration beseitigen lässt. Übrigens: Auch für andere Frameworks und CMS würde dasselbe gelten…
Wie setzt man in Laravel mehrere Datenbankverbindungen um?
In config/database.php werden zusätzliche Verbindungen als separate Einträge definiert, zum Beispiel mysql für den Runtime-User und mysql_migrations für den Migrations-User. Die Zugangsdaten kommen jeweils aus eigenen Umgebungsvariablen. Migrationen können dann über den Artisan-Parameter --database=mysql_migrations auf die dedizierte Verbindung geleitet werden. Queue-Verbindungen werden separat in config/queue.php zugewiesen.
Wo gehören die Credentials des Migrations-Users hin?
Der Migrations-User wird ausschließlich beim Deployment benötigt, nicht im laufenden Betrieb. Seine Credentials gehören deshalb in CI/CD-Secrets — also in GitHub Actions Secrets, GitLab CI Variables, Laravel Forge Deployment-Hooks oder vergleichbare Systeme. In die .env der Produktionsanwendung gehört der Migrations-User nicht. So ist er gar nicht erst erreichbar, wenn die Anwendung läuft.
Gelten diese Prinzipien nur für Laravel oder auch für andere Systeme?
Die Prinzipien sind systemunabhängig. Least Privilege, getrennte User für unterschiedliche Aufgaben und der Schutz von DDL-Rechten im laufenden Betrieb sind Empfehlungen, die OWASP, die MySQL-Dokumentation und gängige Security-Guides für alle Anwendungstypen beschreiben. Laravel ist in diesem Artikel das konkrete Praxisbeispiel, weil die Konfiguration dort besonders klar nachvollziehbar ist.
Kann ich lokal mit Root-Zugriff entwickeln?
Ja, in der lokalen Entwicklungsumgebung ist das praktisch und in der Regel unkritisch — solange die Produktionsumgebung konsequent getrennte User verwendet. Wichtig ist, dass die Konfiguration für Produktion nicht aus der lokalen Entwicklungsumgebung übernommen wird und die .env.production von Anfang an die richtigen, minimalen Credentials enthält. Aber: Besser ist auch lokal ohne root zu entwickeln. So lässt sich leichter auch erkennen, ob produktiv alles funktionieren würde. Denn root darf auch lokal alles und es kommt daher tendenziell weniger häufig zu Fehlern mangels Rechten.
Was bringen Views für eine API-Datenbankverbindung?
Views definieren einen kontrollierten Ausschnitt der Daten, den die API sehen darf. Statt dem API-User direkten Zugriff auf eine Tabelle zu geben, erhält er nur SELECT-Rechte auf eine View, die genau die erlaubten Spalten und Zeilen zurückgibt. So ist der Datenzugriff auch dann eingeschränkt, wenn jemand die API-Verbindung übernimmt — er sieht nur das, was die View preisgibt.

Kommentar verfassen