In Teil 1 dieser Reihe ging es um strukturelle Fehler: fehlende Fremdschlüssel, falsche Indizes, Abfragen die unter Last zusammenbrechen. In Teil 2 um Transaktionen und warum ein fehlendes BEGIN die Datenbank in Zustände bringt aus denen es keinen sauberen Ausweg gibt.
Teil 3 ist unangenehmer. Nicht weil das Thema komplexer ist, sondern weil die Fehler die hier beschrieben werden selten aus bösem Willen entstehen. Manchmal ist es Zeitdruck. Manchmal Bequemlichkeit. Oft schlicht, dass in diese Richtung noch gar nicht gedacht wurde. ‚Die Datenbank läuft, der Zugriff ist geregelt, das reicht‘ — und damit endet die Sicherheitsbetrachtung, bevor sie die entscheidenden Punkte erreicht hat.
Genau da beginnen die Lücken die diesen Artikel ausmachen.
Ein Hinweis vorab: Dieser Artikel fokussiert sich auf MySQL — weil es in der Webentwicklung noch immer eines der am weitesten verbreiteten Datenbanksysteme ist und weil die meisten Beispiele aus der Praxis dort herkommen. Vieles lässt sich sinngemäß auf MariaDB, PostgreSQL oder andere Systeme übertragen — aber nicht alles ist 1:1 zutreffend. Wer mit einem anderen System arbeitet, sollte die jeweiligen Mechanismen und Optionen gesondert prüfen.
1. Root ohne Passwort — oder: Der Entwicklungs-Default der in Produktion landet
Wer MySQL frisch installiert, bekommt in vielen Setups einen root-Account ohne Passwort. Oder mit dem Passwort ‚root‘. Oder ‚1234‘. Das ist für lokale Entwicklung vielleicht akzeptabel — in der Praxis landet dieser Default erschreckend oft auf Produktionsservern.
Das Problem ist nicht nur das fehlende Passwort. Das Problem ist root selbst.
Root hat uneingeschränkten Zugriff auf alle Datenbanken, alle Tabellen, alle Systemoperationen. Wer mit root auf eine Anwendungsdatenbank zugreift, gibt der Anwendung Rechte die sie nie haben sollte: Datenbanken anlegen und löschen, andere Nutzer verwalten, Systemtabellen lesen, File-Operationen ausführen. Eine SQL-Injection in einer Laravel-App die mit root verbunden ist, ist nicht mehr nur ein Datenleck — sie ist ein vollständiger Systemzugang.
-- Falscher Ansatz: Anwendung läuft als root
DB_USERNAME=root
DB_PASSWORD=
-- Richtiger Ansatz: dedizierter User, minimale Rechte
CREATE USER 'app_user'@'localhost' IDENTIFIED BY 'starkes-passwort';
GRANT SELECT, INSERT, UPDATE, DELETE ON app_db.* TO 'app_user'@'localhost';
-- Kein DROP, kein CREATE, kein FILE, kein SUPER
FLUSH PRIVILEGES;
Das Prinzip dahinter heißt Least Privilege: Jede Komponente bekommt genau die Rechte die sie für ihre Aufgabe braucht — nicht mehr. Eine Anwendung die nur liest und schreibt braucht SELECT, INSERT, UPDATE, DELETE. Kein DROP TABLE, kein CREATE DATABASE, kein Zugriff auf andere Schemas.
Ehrlich gesagt ist mir genau das beim Schreiben dieses Absatzes aufgefallen: In Laravel-Projekten laufen Migrationen standardmäßig über denselben Datenbankuser wie die laufende Anwendung. Ein sauber getrennter Migrations-User — der ALTER TABLE und CREATE TABLE darf, der App-User aber nicht — ist konzeptionell sinnvoll, aber nicht out-of-the-box vorgesehen. Das ist kein Fehler im Framework, aber eine Lücke die man kennen sollte. Wie man Least Privilege für Laravel-Deployments trotzdem konsequent umsetzen kann — mit separaten Usern, CI/CD-Integration und praktischen Ansätzen aus der Praxis — ist einen eigenen Artikel wert. Wir kommen in Teil 4 dieser Reihe dediziert darauf zurück.
Und noch ein Punkt der gerne übersehen wird: Externe Verbindungen. Viele Setups erlauben MySQL-Verbindungen auf 0.0.0.0 — also von überall. Kombiniert mit einem schwachen oder fehlenden root-Passwort ist das ein offenes Scheunentor. MySQL sollte grundsätzlich nur auf localhost oder einem definierten internen Netzwerk lauschen.
-- In /etc/mysql/mysql.conf.d/mysqld.cnf
bind-address = 127.0.0.1
Einmal gesetzt, akzeptiert MySQL keine externen Verbindungen mehr. Ein Handgriff der viele Angriffsszenarien von vornherein ausschließt.
2. Data-at-Rest: Was passiert wenn jemand an die Datei kommt
Hier ist ein Gedanke der oft ausbleibt: Die Datenbank selbst ist gesichert. Zugriff geregelt, Passwörter stark, Ports geschlossen. Aber was passiert wenn jemand nicht durch die Datenbank kommt — sondern an die Datei?
Wer Zugriff auf das Dateisystem eines Servers bekommt — durch eine Schwachstelle, durch einen kompromittierten Hosting-Account, durch physischen Zugriff — findet dort die Datenbankdateien direkt auf der Festplatte. Ohne Encryption at Rest liegen alle Daten im Klartext. Kein Passwort, kein Auth-System schützt davor.
Encryption at Rest bedeutet: Die Daten werden verschlüsselt auf dem Datenträger gespeichert. MySQL und MariaDB unterstützen das mit InnoDB Tablespace Encryption. Wer auf verwaltete Dienste wie Amazon RDS oder Google Cloud SQL setzt, bekommt das oft als Option mit einem Klick — und lässt sie trotzdem aus, weil sie nicht Standard ist.
PostgreSQL geht hier einen anderen Weg: Es bietet keine eingebaute Encryption at Rest auf Datenbankebene, sondern verlässt sich vollständig auf das Betriebssystem — also auf Lösungen wie dm-crypt oder LUKS auf Linux, oder verschlüsselte Volumes beim Cloud-Anbieter. Ob das ein Problem ist oder eine pragmatische Designentscheidung, hängt stark vom konkreten Setup ab. Das ist eine Abwägung die sich lohnt genauer anzuschauen — wir greifen das in Teil 5 dieser Reihe auf.
Dieser Gedanke — die Datenbank ist sicher, weil der Zugriff geregelt ist — führt direkt zum nächsten übersehenen Problem.
3. Sensible Spalteninhalte im Klartext — und was Encryption at Rest allein nicht löst
Selbst mit aktivierter Disk-Verschlüsselung: Wer legitimen Datenbankzugriff hat — ein kompromittierter App-User, ein Mitarbeiter mit zu vielen Rechten, ein zu weit gefasstes SELECT — liest alle Spalten im Klartext. Encryption at Rest schützt vor Dateizugriff, nicht vor Datenbankzugriff.
Die Frage ist deshalb: Welche Spalten enthalten Daten die auch innerhalb der Datenbank nicht im Klartext stehen sollten?
IBAN und Kontonummern. Steuer-IDs. Sozialversicherungsnummern. API-Keys und Tokens. Vertrauliche interne Notizen. Alles Daten die bei einem Datenbankdump sofort lesbar sind — und für die Verschlüsselung auf Spaltenebene den entscheidenden Unterschied macht.
Laravel kennt dafür den encrypted Cast — und wer schon mal versucht hat sensible Felder sauber zu verschlüsseln, weiß wie viel Aufwand das früher bedeutete. Mit Laravel 13 ist das deutlich eleganter geworden.
--Customer-Modell mit encrypted Cast für iban, tax_id, birth_date
protected casts = [
iban => encrypted,
tax_id => encrypted,
birth_date => encrypted:date
]
Laravel verschlüsselt und entschlüsselt transparent. Wer auf ->iban zugreift bekommt den Klartext — in der Datenbank steht ein verschlüsselter Blob. Der APP_KEY ist der Schlüssel. Er gehört nicht ins Repository, nicht in die Versionierung, nicht in eine unverschlüsselte Backup-Datei.
Eine Einschränkung die man bis Laravel 12 kennen musste: Verschlüsselte Spalten konnten nicht direkt durchsucht werden. Wer eine IBAN finden wollte, brauchte einen Hash als Suchindex.
--iban_hash als Index, SELECT über SHA2-Hash — Workaround bis Laravel 12
ALTER TABLE customers
ADD COLUMN iban_hash VARCHAR(64),
ADD INDEX idx_iban_hash (iban_hash);
SELECT * FROM customers WHERE iban_hash = SHA2(Wert, 256);
Laravel 13 löst das: Verschlüsselte Felder lassen sich jetzt direkt in WHERE-Bedingungen verwenden, ohne Hash-Umweg.
Wichtige Einordnung: Spalten-Verschlüsselung ist kein hundertprozentiger Schutz. Wer Zugriff auf den APP_KEY hat kann die Daten entschlüsseln. Trotzdem ist es unter allen praktikablen Optionen das Weitestgehende was man tun kann, ohne die Daten offen liegen zu lassen. Es erhöht die Hürde erheblich — und genau das ist der Punkt.
Andere Systeme haben eigene Strategien — Symfony bietet ähnliche Mechanismen, WordPress nicht out-of-the-box. Die Daten sind nicht weniger schützenswert weil das Framework keine fertige Lösung hat.
Du arbeitest noch mit Laravel 10 oder 11? Die Migration auf Laravel 13 bringt genau diese Features mit.
4. Das unverschlüsselte Backup — warum eine gute Datenbank und ein schlechtes Backup dasselbe Ergebnis haben
Ein System hat starke Passwörter, Least Privilege, Encryption at Rest, verschlüsselte Spalten — alle Best Practices korrekt umgesetzt. Dann wird täglich ein mysqldump auf einen S3-Bucket geschrieben, ohne Verschlüsselung, mit zu weitem Zugriff weil irgendwann jemand das Debugging vereinfachen wollte.
Alle Sicherheitsmaßnahmen auf Datenbankebene sind wertlos. Das Backup ist ein vollständiger Klartext-Export aller Daten — inklusive der Spalten die in der Datenbank selbst verschlüsselt wären.
Backups sind das am häufigsten übersehene Sicherheitsproblem in Datenbankinfrastrukturen. Nicht weil niemand Backups macht — sondern weil die Backups selbst nicht denselben Sicherheitsstandards unterliegen wie die Produktionsdatenbank.
-- Unsicher (nicht so machen):
mysqldump -u root -p eproi_db > backup.sql
aws s3 cp backup.sql s3://mein-bucket/backup.sql
-- besser (so machen):
mysqldump -u backup_user -p eproi_db
in gzip pipen
dann in gpg --symmetric --cipher-algo AES256 pipen
Output: backup.sql.gz.gpg
-- Dann hochladen:
aws s3 cp backup.sql.gz.gpg s3://mein-bucket/backups/DATUM-backup.sql.gz.gpg
GPG-Verschlüsselung vor dem Upload kostet wenige Sekunden und schützt den gesamten Inhalt. Wer den Schlüssel nicht hat kann das Backup nicht entschlüsseln — auch nicht wenn er Zugriff auf den Bucket bekommt.
S3 ist hier nur ein Beispiel. Ob Backblaze B2, Hetzner Storage Box, ein eigener SFTP-Server oder ein anderer Objektspeicher — das Prinzip bleibt dasselbe: Verschlüsseln vor dem Upload, Zugriff einschränken, Rotation definieren, Restore testen. Das konkrete Tool ist zweitrangig, die Disziplin dahinter nicht.
Du bist unsicher wie das Backup-Setup für dein Projekt aussehen sollte — oder willst wissen ob deine aktuelle Lösung den Anforderungen standhält? Melde dich gern für eine unverbindliche Beratung.
Drei weitere Punkte die bei Backups regelmäßig fehlen:
Zugriffskontrolle. Wer kann auf den Bucket tatsächlich zugreifen? S3-Buckets mit zu weitem Zugriff sind keine Seltenheit — oft das Ergebnis eines schnellen Tests der nie zurückgesetzt wurde.
Backup-Rotation. Backups ohne Aufbewahrungsregeln wachsen unbegrenzt. Ältere Dumps enthalten oft Daten von Nutzern die längst gelöscht haben. DSGVO-relevant — und problematisch wenn der Dump kompromittiert wird.
Restore-Tests. Ein Backup das sich nicht wiederherstellen lässt ist kein Backup. In der Praxis werden Restores erschreckend selten getestet — oft erst dann wenn sie wirklich gebraucht werden. Solche Tests gehören idealerweise auf ein produktionsnahes Testsystem, nicht in den laufenden Betrieb.
Fazit: Sicherheit ist keine Schicht die man oben drauflegt
Die Muster in diesem Artikel haben eine gemeinsame Wurzel: Sicherheit wird als separater Schritt behandelt, den man nach der eigentlichen Entwicklung irgendwann noch macht. Root-Zugang weil das schneller geht. Klartext weil das Verschlüsseln umständlich wirkt. Kein Encryption at Rest weil die Option nicht im Standard aktiviert war. Unverschlüsselte Backups weil das Backup an sich schon ein Fortschritt war.
Das Ergebnis ist eine Datenbank die von außen sicher wirkt — und die an einer Stelle die niemand zuerst prüft vollständig offen ist.
Sicherheit auf Datenbankebene ist keine Frage des Aufwands allein. Es ist eine Frage des Denkens: Least Privilege als Standard, nicht als Ausnahme. Verschlüsselung dort wo Daten vertraulich sind, nicht nur dort wo das Framework es vorgibt. Backups die dieselben Anforderungen erfüllen wie die Produktionsdatenbank — weil sie im Ernstfall dieselben Daten enthalten.
Wer das von Anfang an richtig denkt spart sich die Diskussion über Schäden, Ursachen und Haftung wenn es zu spät ist.
Reicht ein starkes Passwort für den root-User aus?
Nein. Das Problem ist nicht allein das Passwort, sondern die Rechte die root mitbringt. Root darf Datenbanken anlegen und löschen, andere User verwalten, File-Operationen ausführen. Eine SQL-Injection in einer App die mit root verbunden ist ist kein Datenleck mehr, sondern ein vollständiger Systemzugang. Die Lösung ist ein dedizierter App-User mit minimalen Rechten (d.h. nur die wirklich benötigten Rechte), unabhängig davon wie stark das root-Passwort ist.
Was genau schützt Encryption at Rest und was nicht?
Encryption at Rest schützt vor Dateizugriff. Wer an die Datenbankdateien auf dem Server kommt, also über eine Schwachstelle im Dateisystem oder physischen Zugriff, sieht ohne Encryption at Rest alle Daten im Klartext. Was Encryption at Rest nicht schützt: legitimen Datenbankzugriff. Wer sich regulär verbindet liest alle Spalten normal. Dafür braucht man zusätzlich Spalten-Verschlüsselung für wirklich sensible Felder. Encryption at Rest schützt ansonsten auch, falls die Datenbankdateien vom Server abfließen. Theoretisch kann man Datenbankdateien in mysql einhängen, wenn es die gleiche Datenbankversion ist. Hat man aber den Encryption Key nicht kann man die Daten anschließend nicht auslesen.
Kann ich verschlüsselte Spalten in MySQL noch sinnvoll durchsuchen?
Mit Laravel 13 ja, direkt über WHERE-Bedingungen ohne Umweg. Bis Laravel 12 war ein Hash-Workaround nötig: eine separate Spalte mit einem SHA2-Hash des Klartextwerts, über die gesucht wird, während der eigentliche Wert verschlüsselt bleibt. Aufwendiger, aber funktional. Laravel 13 macht das transparenter. Reine SELECTs über shell bzw. über Tools wie phpmyadmin bringen dagegen nichts mehr und auch im Backup landen derartig verschlüsselte Spalteninhalte nicht mehr in Klarform. Das ist wichtig zu bedenken z.B. auch für Wiederherstellungsszenarien.
Muss ich das Backup verschlüsseln wenn der S3-Bucket schon zugangsbeschränkt ist?
Ja. Zugangsbeschränkungen auf Bucket-Ebene schützen vor unautorisiertem Zugriff über die API. Sie schützen nicht vor einem kompromittierten Account, zu weit gefassten Zugriffsrechten oder einer Fehlkonfiguration. Ein verschlüsseltes Backup ist auch dann wertlos für einen Angreifer wenn er an die Datei kommt. Ein unverschlüsseltes Backup enthält alle Daten im Klartext, inklusive der Spalten die in der Produktionsdatenbank selbst verschlüsselt wären. Zusätzlich hilft ein verschlüsseltes Backup immer dann, wenn es z.B. Mitarbeiter gibt, die potentiell Zugriff haben aber nicht unbedingt alles lesen können sollen. Nicht aus misstrauen sondern einfach aus DSGVO-Sicht und dem least-privilege-Ansatz. Und gleichwohl: Auch wenn S3 und Co selbst zugriffsbeschränkt sind aber die Hoheit über das System hat man selbst nicht: Eine Fehlkonfiguration vom Anbieter und schon kann – ggf. auch nur zeitweise – ein Zugriff prinzipiell möglich sein.
Wie oft sollte ich einen Restore aus dem Backup testen?
Zunächst einmal am Anfang bei Einrichtung des Backupsystems. Dann natürlich auch zu jedem Zeitpunkt, an dem gravierende systemische Änderungen am Backupsystem vorgenommen werden. Und ganz allgemein: Mindestens einmal pro Quartal auf einem produktionsnahen Testsystem. Backup-Tests werden in der Praxis erschreckend selten durchgeführt, meistens erst wenn ein echter Ernstfall eintritt. Ein Backup das sich nicht sauber wiederherstellen lässt ist kein Backup. Rotation, Integrität und Restore gehören zur Backup-Strategie, nicht nur das Erstellen der Dump-Datei.

Kommentar verfassen