Ugrás a tartalomhoz
← Blog'a dön

Postgres FORCE RLS — 18 aylık çok kiracılı mimari deneyimi ve parola sıfırlamayı neredeyse çökerten sessiz hata

RLS, çok kiracılı güvenliğimizin temelidir. Ancak "sessiz 0 satır güncelleme" tuzağı, parola sıfırlama akışını tam bir gün boyunca devre dışı bıraktı. İşte tarif.

RLS, 0 satırı etkilediğinizde hata vermez. Bu yüzden her zaman RETURNING kullanın — aksi hâlde "başarılı" sonucu size yalan söyler.

Ekim 2024'te çok kiracılı veri izolasyonunu uygulama düzeyindeki `WHERE tenantId = ?` filtreleri yerine Postgres FORCE Row-Level Security ile yönetmeye karar verdik. On sekiz ay sonra, 47 tablo, 23 kiracı ve 11 milyon satırla çalışırken hâlâ pişman değiliz. Ama acı dersler de oldu. Bunlardan biri, burada adım adım anlattığımız Haziran 2026 parola sıfırlama olayı.

Neden düz RLS değil de FORCE RLS?

Düz Postgres RLS tablo sahibi tarafından atlanır. Migration'larımız veritabanı sahibi kullanıcıyla çalışıyor (DDL yetkisi gerekiyor) ve herhangi bir uygulama bağlantısı yanlışlıkla bu kullanıcıyı kullansaydı, izolasyon tek bir `if` ile çökerdi. FORCE RLS bunu çözer: politikalar tablo sahibine de uygulanır. Bedeli: migration'lar açıkça `SET LOCAL row_security = off` kullanmalı ya da bir bypass rolüyle çalışmalıdır. Biz ikincisini seçtik — her migration, BYPASSRLS yetkili `app_migration` rolüyle çalışır.

Sessiz hata: anonim kullanıcı parola belirlemeye çalıştığında

Parola sıfırlama akışı: kullanıcı e-postayla tek kullanımlık bir token alır, bunu girer ve yeni bir parola belirler. Bu istek anonimdir (kimlik doğrulaması yoktur). Backend token'ı doğrular, kullanıcıyı bulur ve parolayı UPDATE ile günceller — ancak oturum olmadığı için `app.current_tenant_id` GUC değerinin ayarlanmadığı bir bağlantı üzerinde. `users` tablosundaki FORCE RLS politikası şöyle der: `tenantId = current_setting('app.current_tenant_id')::uuid`. GUC boş olduğunda ifade `NULL` döndürür, `=` de `NULL` döndürür, satır görünmez olur ve UPDATE 0 satırı etkiler. ORM (Prisma) hata vermez, çünkü UPDATE sözdizimsel olarak başarılıdır. Kullanıcı "parolanız değiştirildi" mesajıyla giriş sayfasına yönlendirilir — ve giriş yapamaz.

Çözüm: RLS'yi atlayan işlem + RETURNING kontrolü

  • `/auth/reset-password` uç noktası standart `app_runtime` yerine ayrı bir `auth_bypass` Postgres rolüyle (BYPASSRLS) çalışır
  • Her anonim değişiklik açık bir `BEGIN; SET LOCAL ROLE auth_bypass; ... COMMIT;` bloğu içinde çalışır; böylece yükseltilmiş yetki yalnızca işlem süresince geçerli olur
  • Her UPDATE/DELETE en az bir satır için `RETURNING id` döndürmek zorundadır — döndürmezse istisna fırlatılır ve denetim olayı oluşturulur
  • CI, RETURNING içermeyen her yeni `UPDATE` ifadesini işaretleyen bir "0 satır kontrolü" lint kuralı çalıştırır
  • Pino, sözleşme gereği her değişiklikte `affectedRows` değerini günlüğe yazar — Grafana 5 dakika boyunca 0 değer görürse alarm verir

18 ayda öğrendiklerimiz

Mevcut bir uygulamaya RLS'yi sonradan eklemeyin. Ya ilk günden onunla tasarlarsınız ya da zahmetine değmez. 47 tablomuzdaki performans etkisi: ~%3 (politikalar her indekse uygulanıyor), ihmal edilebilir düzeyde. En pahalı kısım: havuzlanmış bağlantılar arasında GUC durumunu korumak (PgBouncer'ı transaction modunda havuzla kullanmanız gerekir, statement modu bunu bozar). En büyük kazanç: 18 ayda kiracılar arası sıfır veri sızıntısı. Junior bir geliştirici kiracı filtresini "unutamaz" — veritabanı buna izin vermez.

RLS bir güvenlik özelliği değildir. Bir mimari karardır. Çekinerek kullanırsanız yalnızca başınıza dert olur.
Norbert Kubinyi

Bir konuyu ele almamızı ister misiniz?

Bize yazın — bu konuda gerçek deneyimimiz varsa yayımlarız.