← zurück Ein Vintage-Terminal-Monitor, der auf einem dunklen Holzschreibtisch leuchtet, mit verstreuten Papieren und weichem Überlichtwetter

Supabase RLS: Die Richtlinien, die ich in jedem Projekt schreibe

Im Jahr 2021 kam ein SaaS-Kunde etwa sechs Wochen nach dem Launch zu mir. Ihr Produkt war ein Projektmanagement-Tool. Schöne UI, solides Onboarding, anständige Bindungsquote. Dann bemerkte einer ihrer Beta-Nutzer etwas: Durch Änderung der project_id in einer GET-Anfrage konnten sie die Daten eines anderen Nutzers lesen. Keine Auth-Umgehung. Keine SQL-Injection. Nur eine fehlende Richtlinie. Die Tabelle hatte RLS aktiviert, aber die SELECT-Richtlinie war weit offen: USING (true). Jemand hatte es aus einem Tutorial kopiert und nie wieder angesehen.

Dieser Vorfall ist mir in Erinnerung geblieben. Seitdem behandle ich RLS als Architektur, nicht als Nachgedanken. Und nach dem Bau von über 12.000 Websites und Apps bei Seahawk habe ich eine Handvoll Richtlinien, die ich fast reflexartig schreibe, bevor ich eine einzige Zeile Frontend-Code schreibe.

Das ist diese Liste.

Warum sich der Aufwand für RLS lohnt

Supabase läuft auf PostgreSQL, was bedeutet, dass Row Level Security ein erstklassiges Datenbankfeature ist, keine Nachrüstung. Wenn Sie RLS auf einer Tabelle aktivieren und ein Benutzer sie über den Supabase-Client abfragt, wird jede Zeile automatisch durch Ihre Richtlinien gefiltert. Egal was Ihre API-Schicht tut oder nicht tut.

Das ist der springende Punkt. Ich habe mit Teams gearbeitet, die REST, GraphQL, Edge Functions und Background Jobs alle gegen dieselbe Datenbank laufen lassen. Zugriffskontrolle auf der Anwendungsebene über alle hinweg zu erzwingen ist ein Koordinationsnachteil. Sie auf der Datenbankebene zu erzwingen ist einfach... erledigt.

Ehrlich gesagt bezahlt sich der Aufwand für das Schreiben von Richtlinien selbst, wenn ein Junior-Entwickler zum ersten Mal vergisst, eine WHERE-Klausel hinzuzufügen.

Es gibt da eine Sache, die Menschen missverstehen. RLS ohne Richtlinien zu aktivieren bedeutet nicht „offener Zugang". Es bedeutet gar kein Zugang für normale Rollen. Jede Zeile wird standardmäßig abgelehnt. Wenn Sie RLS aktivieren und Ihre App funktioniert sofort nicht mehr, dann ist das der Grund.

Die vier Tabellen, die ich zuerst RLS-schütze

Nicht alles braucht dasselbe Maß an Überprüfung. Aber diese vier Tabellentypen werden gesperrt, bevor ich alles andere anfasse.

  • Benutzerprofilltabellen (profiles, users, accounts): Das Offensichtliche. Benutzer sollten ihre eigene Zeile lesen. Vielleicht können Admins alle lesen. Niemand sollte das Profil einer anderen Person schreiben.
  • Ressourcen-/Inhalt-Tabellen (projects, documents, posts): Was auch immer das Kernding in Ihrer App ist. Das Eigentum ist hier normalerweise unkompliziert.
  • Billing- und Abonnementtabellen: Wenn Sie Stripe verwenden und Plandaten, Abonnementstatus oder Rechnungsverlauf in Supabase speichern, muss dies fest gesperrt sein. Ich habe gesehen, wie Apps versehentlich Testphase-Enddaten an andere Benutzer preisgegeben haben.
  • Audit-Logs: Dies sind schreibgeschützt für Benutzer (falls überhaupt). Nur Service Role sollte sie schreiben.

Die Richtlinien, die ich tatsächlich schreibe

1. Die Only-Owner-Richtlinie (Mein am häufigsten verwendetes Muster)

Das schreibe ich häufiger als alles andere. Einfache Prämisse: Benutzer können nur Zeilen sehen, die ihnen gehören.

`` create policy "Users can view own rows" on profiles for select using (auth.uid() = user_id); ``

auth.uid() ist ein Supabase-Helfer, der die UUID des aktuell authentifizierten Benutzers zurückgibt. Sauber, schnell, indiziert wenn user_id indiziert ist. Ich kombiniere dies mit einer Insert-Richtlinie, die user_id standardmäßig auf auth.uid() setzt, damit Benutzer nicht so tun können, als wären sie jemand anders, wenn sie Zeilen einfügen.

`` create policy "Users can insert own rows" on profiles for insert with check (auth.uid() = user_id); ``

Die with check Klausel ist für Schreiboperationen. using ist für Lesezugriffe. Viele Leute verwechseln diese und landen bei Richtlinien, die richtig aussehen, aber schlechte Inserts nicht wirklich verhindern.

2. Die Org/Team-Richtlinie (Multi-Tenant Apps)

Hier wird es interessant. Bei jeder Multi-Tenant-SaaS brauche ich, dass Benutzer Zeilen sehen, die zu ihrer Organisation gehören, nicht nur zu sich selbst.

Das Muster, bei dem ich lande: eine memberships Join-Tabelle, die Benutzer mit Organisationen verbindet.

`` create policy "Org members can view org resources" on projects for select using ( exists ( select 1 from memberships where memberships.org_id = projects.org_id and memberships.user_id = auth.uid() ) ); ``

Seahawk hatte ein Fintech-Projekt, bei dem die Organisation Dutzende von Benutzern hatte, einige mit schreibgeschützten Rollen, einige mit Schreibzugriff. Wir erweiterten dieses Muster mit einer role Spalte auf memberships und nutzten sie direkt in der Richtlinie. So konnte eine viewer Rolle UPDATE oder DELETE auf Datenbankebene nicht ausführen, Punkt. Nicht von der API erzwungen. Von der Datenbank erzwungen.

3. Die Public Read / Owner Write Richtlinie

Für Inhalte, die öffentlich sichtbar sind, aber nur vom Besitzer bearbeitbar sind. Blogbeiträge, öffentliche Profile, Produktlisten.

``` create policy "Anyone can read published posts" on posts for select using (published = true);

create policy "Authors can update own posts" on posts for update using (auth.uid() = author_id) with check (auth.uid() = author_id); ```

Zwei separate Richtlinien. Ich sehe, wie Leute versuchen, diese in eine zu kombinieren und landen bei Logik, die schwer zu durchschauen ist. Halte sie getrennt. Postgres wird sie automatisch für die gleiche Operation zusammen-OR-en, wenn nötig.

4. Das Service Role Escape Hatch

Einige Operationen müssen RLS legitim umgehen. Background Jobs, Webhooks, Admin-Skripte. Dafür nutze ich den service_role Schlüssel, der RLS komplett umgeht.

Aber hier ist das Ding: Ich stelle den service_role Schlüssel nie in Frontend-Code zur Verfügung. Niemals. Er lebt nur in Umgebungsvariablen auf der Serverseite. Ich habe Codebases überprüft, wo er in einem Next.js pages/ Verzeichnis hardcodiert war. Das ist deine ganze Datenbank, weit offen.

Wenn du Supabase Edge Functions verwendest, kannst du den service_role Client darin sicher nutzen, weil Edge Functions serverseitig laufen. Supabase's eigene Docs zu Auth und Service Roles sind lesenswert, von vorne bis hinten, wenn du sie noch nicht gelesen hast.

5. Die Admin Override Richtlinie

Für Apps mit einem Admin-Panel füge ich eine Richtlinie hinzu, die Admins vollständigen Zugriff gewährt, überprüft gegen eine in den JWT-Metadaten des Benutzers gespeicherte Rolle oder eine separate user_roles Tabelle.

`` create policy "Admins can do everything" on projects for all using ( exists ( select 1 from user_roles where user_roles.user_id = auth.uid() and user_roles.role = 'admin' ) ); ``

Ich habe Rollen früher in JWT Custom Claims gespeichert, was schneller ist (keine Subquery), aber es bedeutet, dass du das JWT neu ausstellen musst, wann immer eine Rolle sich ändert. Für die meisten Apps ist die Subquery ok. Wenn du Performance-Probleme im großen Maßstab siehst, ist JWT Custom Claims via Supabase Auth Hooks der Weg.

Häufige Fehler, die ich gemacht habe (und gesehen habe)

Lass mich direkt über die Dinge sein, die mich oder meine Kunden tatsächlich gebissen haben.

  1. UPDATE und DELETE Richtlinien vergessen. Es ist einfach, eine SELECT Richtlinie zu schreiben und zu denken, man ist fertig. Das ist man nicht. Teste alle vier Operationen: SELECT, INSERT, UPDATE, DELETE. Ich nutze jetzt den eingebauten Policy Tester des Supabase Dashboards, aber jahrelang habe ich rohes SQL in psql geschrieben und manuell getestet.
  2. USING vs WITH CHECK Verwirrung. USING filtert, welche Zeilen eine Abfrage sehen kann. WITH CHECK validiert, ob eine Schreiboperation erlaubt ist. Für UPDATE brauchst du beides: USING um zu steuern, welche Zeilen anvisiert werden können, WITH CHECK um zu steuern, wie die Zeile nach dem Update aussieht.
  3. Rekursive Policy-Schleifen. Wenn deine Policy auf Tabelle A Tabelle B abfragt, und Tabelle B hat eine Policy, die Tabelle A abfragt, bekommst du unendliche Rekursion. Ich bin das einmal mit einer teams und team_members Tabelle durchgegangen, die sich gegenseitig referenzierten. Die Lösung: nutze security definer Funktionen um den Kreis zu durchbrechen.
  4. Nicht als anonymer Benutzer testen. Supabase lässt dich die anonymous Rolle (anon) nutzen. Teste deine Richtlinien immer sowohl als authenticated als auch als anon. Ich nutze Postman mit verschiedenen Auth-Tokens um das zu simulieren, wechsle zwischen kein Token, ein gültiger Benutzer-Token, und der Token eines anderen Benutzers.
  5. RLS auf Storage-Buckets nicht aktivieren. RLS gilt auch für die Tabelle storage.objects. Wenn du einen Supabase Storage-Bucket erstellst und diese Tabelle ungeschützt lässt, kann jeder deine „privaten" Dateien lesen, wenn er den Pfad errät. Ich habe das auf die harte Tour in einem Kundenprojekt gelernt, das benutzerhochgeladene Dokumente speicherte.

So teste ich meine Richtlinien vor dem Deployment

Das ist mein echtes Vorgehen, nicht eine theoretische Checkliste.

  1. Schreibe die Richtlinie im Supabase SQL-Editor.
  2. Öffne einen zweiten Browser-Tab und melde dich als anderer Test-Nutzer an.
  3. Versuche, auf Daten zuzugreifen, die blockiert werden sollten. Bestätige, dass sie blockiert sind.
  4. Versuche, auf Daten zuzugreifen, die sichtbar sein sollten. Bestätige, dass es funktioniert.
  5. Führe ein UPDATE und DELETE auf eine Reihe aus, die dir nicht gehört. Sollte fehlschlagen.
  6. Prüfe die Supabase-Protokolle auf Fehler zu Verstößen gegen Row-Level-Security-Richtlinien.

Bei komplizierteren Fällen, besonders bei Multi-Tenant-Org-Richtlinien, schreibe ich ein kleines Test-Script mit dem supabase-js-Client mit zwei verschiedenen Benutzersessions und prüfe die erwarteten Ergebnisse. Dauert vielleicht 20 Minuten zum Schreiben, spart aber Stunden Debugging in der Produktion.

Wann du RLS NICHT nutzen solltest

RLS ist nicht immer das richtige Werkzeug.

Wenn du ein internes Admin-Tool baust, bei dem alle Benutzer vertraute Mitarbeiter sind, fügt RLS Komplexität hinzu ohne großen Nutzen. Eine einfache serverseitige Auth-Prüfung reicht aus. Wenn dein Datenmodell so komplex ist, dass Richtlinien 5-stufig verschachtelte Subqueries erfordern, bist du vielleicht besser dran, die Zugriffskontrolle in einer API-Schicht mit ordentlicher Service-Zerlegung durchzusetzen.

Auch: Wenn du Supabase rein als Backend mit deiner eigenen API davor nutzt (ohne die Supabase-URL oder den anon-Key jemals an Clients freizugeben), ist RLS optional. Die API wird deine Sicherheitsschicht. Aber ich füge trotzdem grundlegende Richtlinien in solchen Fällen hinzu, weil Verteidigungstiefe wertvoll ist.

Schau, RLS ist ein Werkzeug. Keine Religion. Nutze es, wo es dein System einfacher und sicherer macht. Kopiere es nicht einfach nach, weil ein Tutorial dir das sagte.

FAQ

Brauche ich RLS, wenn ich Supabase nur mit einer serverseitigen API nutze?

Streng genommen nein. Wenn dein Frontend Supabase nie direkt berührt und alles über deinen eigenen Server läuft, ist deine API die Sicherheitsschicht. Aber ich empfehle trotzdem, mindestens Owner-basierte Richtlinien als zweite Verteidigungslinie hinzuzufügen. Wenn jemand einen Bug in deiner API findet, fängt RLS auf, was durchfällt.

Beeinflusst RLS die Performance?

Sie kann, wenn deine Richtlinien teure Subqueries auf großen Tabellen enthalten. Die Lösung ist fast immer Indexierung. Stelle sicher, dass die Spalten, die in deinen Richtlinienbedingungen verwendet werden (user_id, org_id, etc.), Indizes haben. Bei einem Projekt letztes Jahr ließ das Hinzufügen eines Index auf org_id die Policy-Evaluierungszeit von ~40ms auf unter 2ms auf einer Tabelle mit 800k Reihen fallen.

Kann ich RLS mit Supabase Realtime nutzen?

Ja. Realtime-Abonnements respektieren RLS-Richtlinien. Wenn sich ein Benutzer zu Änderungen bei einer Tabelle anmeldet, erhält er nur Events für Reihen, die seine Richtlinien ihm sehen lassen. Das ist eine der wirklich guten Designentscheidungen in Supabase's Architektur.

Was ist der Unterschied zwischen `for all` und getrennten Richtlinien?

`for all` erstellt eine einzige Richtlinie für SELECT, INSERT, UPDATE und DELETE. Es ist praktisch für Admin-Override-Muster. Für alles andere schreibe ich separate Richtlinien pro Operation, weil die Bedingungen meist unterschiedlich sind. SELECT könnte öffentliche Lesevorgänge erlauben, während INSERT Ownership erfordert. Separate Richtlinien sind einfacher zu verstehen, wenn um 23 Uhr etwas schiefgeht.

Wie debugge ich eine Richtlinie, die Anfragen blockiert, die sie nicht sollte?

Erstens: Prüfe, ob auth.uid() tatsächlich einen Wert zurückgibt. Wenn der Benutzer nicht authentifiziert ist, gibt es null zurück und die meisten Richtlinien schlagen fehl. Zweitens: Stelle die Richtlinie vorübergehend auf USING (true), um zu bestätigen, dass die Query selbst funktioniert. Drittens: Füge eine Test-Richtlinie hinzu, die die Werte protokolliert, die du prüfst (mit einer Security-Definer-Funktion, die einen Notice auslöst). Die Supabase-Dashboard-Protokolle geben auch RLS-Verstöße aus, was das deutlich weniger schmerzhaft macht als früher.

---

Sicherheit auf Zeilenebene ist keine glamouröse Arbeit. Niemand schreibt Blog-Beiträge über den Sicherheitsverletzung, der nicht stattgefunden hat. Aber der Vorfall 2021 mit den offengelegten Projektdaten hat mich gelehrt, dass die Lücke zwischen „RLS aktiviert" und „RLS korrekt umgesetzt" größer ist, als die meisten Menschen denken. Diese Richtlinien schließen diese Lücke. Zumindest für mich.

← zurück