PostgreSQL'de satır düzeyi güvenlik (row level security, RLS), aynı tabloyu kullanan rollerin yalnızca kendilerine izin verilen satırları görmesini ve değiştirmesini sağlar. Önce tabloda ALTER TABLE tablo ENABLE ROW LEVEL SECURITY ile özelliği açarsınız, ardından CREATE POLICY ad ON tablo USING (kosul) ile hangi satırların görüneceğini, gerekirse WITH CHECK (kosul) ile hangi satırların yazılabileceğini tanımlarsınız. RLS açık olup hiç politika yoksa varsayılan kural hepsini reddettir: kullanıcı tabloda tek bir satır bile görmez. Süper kullanıcılar, BYPASSRLS özniteliğine sahip roller ve normalde tablonun sahibi politikaların dışında kalır.
Bu yazıda USING ile WITH CHECK farkını, komut bazlı politikaları, permissive ve restrictive politikaların nasıl birleştiğini, tablo sahibi için FORCE ROW LEVEL SECURITY ayarını, row_security parametresini ve çok kiracılı (multi-tenant) bir sipariş tablosu örneğini adım adım göreceksiniz. RLS, tablo yetkilerinin yerine geçmez, onların üzerine eklenir; bu yüzden önce GRANT ve REVOKE yazısındaki temeli bilmeniz gerekir. Rol öznitelikleri için rol yönetimi yazısına, bütün seri için PostgreSQL öğrenme rehberi sayfasına bakabilirsiniz. Örneklerin hepsi PostgreSQL 18.1 üzerinde, her rol için ayrı psql bağlantısıyla çalıştırıldı.
RLS ne işe yarar, GRANT ile farkı nedir?

GRANT tablonun bütünü üzerinde çalışır: bir rol tabloyu okuyabiliyorsa bütün satırlarını okur. Satır güvenliği politikaları bu sistemin üzerine ikinci bir filtre ekler ve kullanıcı bazında hangi satırların sorgulardan döneceğini ya da hangi satırların eklenip güncellenebileceğini sınırlar. Politika ifadesi, kullanıcının sorgusundaki koşullardan ve fonksiyonlardan önce her satır için değerlendirilir. Belgelerdeki tek istisna leakproof olarak işaretlenmiş, bilgi sızdırmayacağı garanti edilen fonksiyonlardır; planlayıcı bunları güvenlik kontrolünden önce uygulayabilir.
Bazı işlemler satır güvenliğinden hiç etkilenmez. TRUNCATE ve REFERENCES gibi tablonun tamamına uygulanan işlemler, ayrıca primary key, unique ve foreign key gibi bütünlük kontrolleri her zaman RLS'yi atlar. Bu ikinci nokta aşağıda göreceğimiz gibi bir bilgi sızıntısı kanalı oluşturabilir. RLS'yi açmak, kapatmak ve politika eklemek ise yalnızca tablonun sahibine aittir.
Örnek tablo ve varsayılan ret
Senaryomuzda birden fazla firmanın (kiracının) siparişleri tek bir tabloda tutuluyor. Tablonun sahibi veri_sahibi, uygulamanın bağlandığı rol uygulama, denetim için kullanılacak rol ise BYPASSRLS özniteliğine sahip denetci:
CREATE ROLE veri_sahibi LOGIN;
CREATE ROLE uygulama LOGIN;
CREATE ROLE denetci LOGIN BYPASSRLS;
GRANT CREATE ON SCHEMA public TO veri_sahibi;
SET ROLE veri_sahibi;
CREATE TABLE siparisler (
no int PRIMARY KEY,
kiraci_id int NOT NULL,
musteri text,
tutar numeric(10,2),
silindi boolean NOT NULL DEFAULT false
);
INSERT INTO siparisler VALUES
(101, 1, 'Ankara Kırtasiye', 1250.00, false),
(102, 1, 'Ankara Kırtasiye', 300.00, true),
(201, 2, 'İzmir Gıda', 980.50, false),
(202, 2, 'İzmir Gıda', 75.25, false);
GRANT SELECT, INSERT, UPDATE, DELETE ON siparisler TO uygulama, denetci;
ALTER TABLE siparisler ENABLE ROW LEVEL SECURITY;
RESET ROLE;Tabloda dört satır var ve uygulama rolünün tam okuma yazma yetkisi bulunuyor. Buna rağmen bağlanıp sayım yaptığında sonuç sıfır çıkar, çünkü RLS açık ama henüz hiçbir politika tanımlanmadı:
-- psql -U uygulama -d ornek_rls
SELECT count(*) FROM siparisler; -- sonuç: 0Hata mesajı da gelmez; satırlar sessizce filtrelenir. Bu davranış güvenli bir varsayılandır, ama RLS'yi açtıktan sonra politika yazmayı unutursanız uygulamanız boş tablo görüyormuş gibi çalışmaya devam eder. Bu yüzden RLS'yi açtığınız değişikliğin içinde politikaları da tanımlayın.
İlk politika: USING ile kiracı yalıtımı
Uygulama her bağlantıda hangi kiracı adına çalıştığını bir oturum değişkenine yazacak, politika da satırın kiraci_id değerini bu değişkenle karşılaştıracak. Noktalı iki parçalı adlar (app.kiraci_id gibi) PostgreSQL'de özel ayar olarak kabul edilir; belgelere göre sunucu, tanımlayan bir modül olmasa bile bu adlara değer verilmesine izin verir ve onları yer tutucu olarak saklar. current_setting(ad, true) çağrısındaki ikinci argüman, değişken hiç tanımlanmamışsa hata yerine null döndürür:
SET ROLE veri_sahibi;
CREATE POLICY kiraci_izolasyonu ON siparisler
USING (kiraci_id = current_setting('app.kiraci_id', true)::int);
RESET ROLE;Politikada FOR verilmediği için ALL komutlarına, TO verilmediği için PUBLIC'e, yani bütün rollere uygulanır. Şimdi uygulama olarak önce değişkeni ayarlamadan, sonra ayarlayarak deniyoruz:
-- psql -U uygulama -d ornek_rls
SELECT count(*) FROM siparisler; -- sonuç: 0
SET app.kiraci_id = '1';
SELECT no, kiraci_id, musteri, tutar FROM siparisler ORDER BY no; no | kiraci_id | musteri | tutar
-----+-----------+------------------+---------
101 | 1 | Ankara Kırtasiye | 1250.00
102 | 1 | Ankara Kırtasiye | 300.00
(2 rows)Değişken yokken ifade null döndürdü ve hiçbir satır görünmedi; değişken 1 olunca yalnızca 1 numaralı kiracının satırları geldi. USING ifadesi true dönen satırları gösterir; false ya da null dönen satırlar SELECT sonucundan çıkarılır, UPDATE ve DELETE için de erişilemez hâle gelir.
WITH CHECK: yazılan satırın kontrolü

USING var olan satırlara, WITH CHECK ise INSERT ya da UPDATE sonucunda oluşacak yeni satıra uygulanır. ALL ve UPDATE politikalarında WITH CHECK yazılmazsa USING ifadesi her iki iş için de kullanılır; bizim politikamız da böyle. Aynı oturumda birkaç değiştirme komutu çalıştırdık:
-- app.kiraci_id = 1 iken
INSERT INTO siparisler (no, kiraci_id, musteri, tutar) VALUES (103, 1, 'Ankara Kırtasiye', 99.90);
INSERT INTO siparisler (no, kiraci_id, musteri, tutar) VALUES (203, 2, 'İzmir Gıda', 10.00);
UPDATE siparisler SET tutar = 0 WHERE no = 201;
UPDATE siparisler SET kiraci_id = 2 WHERE no = 101;
DELETE FROM siparisler WHERE kiraci_id = 2;INSERT 0 1
ERROR: new row violates row-level security policy for table "siparisler"
UPDATE 0
ERROR: new row violates row-level security policy for table "siparisler"
DELETE 0Sonuçlar iki farklı davranışı gösteriyor. Başka kiracıya satır eklemeye ya da kendi satırını başka kiracıya taşımaya çalışmak hata verdi, çünkü WITH CHECK ihlali her zaman bütün komutu iptal eder. Başka kiracının satırını güncellemeye ya da silmeye çalışmak ise hata vermedi, yalnızca UPDATE 0 ve DELETE 0 döndü: o satırlar kullanıcı için görünmez olduğundan komut onları hiç bulamadı. Uygulama tarafında etkilenen satır sayısını kontrol etmeniz bu yüzden önemlidir.
| Komut | USING (var olan satır) | WITH CHECK (yeni satır) |
|---|---|---|
| SELECT | Görünmeyen satırlar sessizce filtrelenir | Uygulanmaz |
| INSERT | Uygulanmaz (RETURNING ya da ON CONFLICT varsa SELECT politikası kontrol edilir) | Geçmeyen satır hata verir, komut iptal olur |
| UPDATE | Görünmeyen satırlar güncellenmez, hata yok | Güncellenen hâli geçmezse hata verir |
| DELETE | Görünmeyen satırlar silinmez, hata yok | Uygulanmaz |
Boş metin tuzağı: set_config ve NULLIF
Bağlantı havuzu kullanan uygulamalarda kiracı değerini bütün oturum için değil, yalnızca o transaction için ayarlamak daha güvenlidir. Bunun için SET LOCAL ya da üçüncü argümanı true olan set_config() kullanılır. Bunu denediğimizde beklenmedik bir hatayla karşılaştık:
BEGIN;
SELECT set_config('app.kiraci_id', '2', true);
SELECT no, musteri, tutar FROM siparisler ORDER BY no;
COMMIT;
SELECT count(*) AS commit_sonrasi FROM siparisler; set_config
------------
2
(1 row)
no | musteri | tutar
-----+------------+--------
201 | İzmir Gıda | 980.50
202 | İzmir Gıda | 75.25
(2 rows)
COMMIT
ERROR: invalid input syntax for type integer: ""Transaction içinde her şey doğru çalıştı. Ama transaction bittikten sonra değişken bu oturumda artık "hiç tanımlanmamış" değil, boş metin durumundaydı; current_setting(..., true) null yerine boş metin döndürdü ve ''::int dönüşümü hata verdi. Politika hata verdiği için veri sızmadı, ama uygulama da çalışmaz hâle geldi. Çözüm, boş metni null'a çeviren NULLIF eklemektir:
SET ROLE veri_sahibi;
ALTER POLICY kiraci_izolasyonu ON siparisler
USING (kiraci_id = NULLIF(current_setting('app.kiraci_id', true), '')::int);
RESET ROLE;Aynı transaction denemesini tekrarladığımızda içeride yine kiracı 2'nin iki satırı geldi, transaction sonrasındaki sayım ise hatasız biçimde 0 döndü. ALTER POLICY politikanın adını, rollerini ve ifadelerini politikayı silmeden değiştirmenizi sağlar.
Permissive ve restrictive politikalar
Bir komuta birden fazla politika uygulandığında birleştirme kuralı politika türüne bağlıdır. Varsayılan tür olan PERMISSIVE politikalar OR ile birleşir: herhangi birinin izin verdiği satır erişilebilir olur. RESTRICTIVE politikalar ise AND ile birleşir ve her satırın hepsinden geçmesi gerekir. Erişim için en az bir permissive politikanın geçmesi şarttır; yalnızca restrictive politika varsa hiçbir satır görünmez. Silinmiş olarak işaretlenen siparişlerin uygulamada hiç görünmemesi için bir restrictive SELECT politikası ekledik:
SET ROLE veri_sahibi;
CREATE POLICY silinmisleri_gizle ON siparisler AS RESTRICTIVE FOR SELECT
USING (NOT silindi);
RESET ROLE;
-- uygulama olarak, app.kiraci_id = 1
SELECT no, musteri, tutar FROM siparisler ORDER BY no; no | musteri | tutar
-----+------------------+---------
101 | Ankara Kırtasiye | 1250.00
103 | Ankara Kırtasiye | 99.90
(2 rows)Kiracı 1'in silindi = true olan 102 numaralı satırı artık görünmüyor; 103 ise az önce eklediğimiz satır. Farklı komut türlerinin politikaları da AND ile birleşir: bir UPDATE çoğu zaman WHERE içinde sütun okuduğu için hem UPDATE hem de SELECT politikalarından geçmek zorundadır. Bu durumda ALL politikaları, birlikte uygulandıkları komut türünün politikası gibi sayılır.
Tanımlı politikaları psql'de \d siparisler çıktısının sonunda ya da pg_policies görünümünde görebilirsiniz:
SELECT policyname, permissive, roles, cmd, qual FROM pg_policies WHERE tablename = 'siparisler'; policyname | permissive | roles | cmd | qual
--------------------+-------------+----------+--------+-----------------------------------------------------------------------------------------
silinmisleri_gizle | RESTRICTIVE | {public} | SELECT | (NOT silindi)
kiraci_izolasyonu | PERMISSIVE | {public} | ALL | (kiraci_id = (NULLIF(current_setting('app.kiraci_id'::text, true), ''::text))::integer)
(2 rows)qual sütunu, sunucunun ifadeyi kendi yazdığı biçimde (tip dönüşümleri açık olarak) saklar; ALTER POLICY sonrasındaki NULLIF'li hâl burada görünüyor. roles sütunundaki {public}, TO yazılmadığı için politikanın herkese uygulandığını gösterir.
Tablo sahibi, FORCE ROW LEVEL SECURITY ve BYPASSRLS
Tablonun sahibi normalde politikalara tabi değildir. veri_sahibi olarak sayım yaptığımızda bütün satırları (o anda beş) gördü. Sahibin de politikalara uymasını istiyorsanız ALTER TABLE ... FORCE ROW LEVEL SECURITY kullanırsınız:
SET ROLE veri_sahibi;
SELECT count(*) AS sahip_goruyor FROM siparisler; -- sonuç: 5
ALTER TABLE siparisler FORCE ROW LEVEL SECURITY;
SELECT count(*) AS sahip_force FROM siparisler; -- sonuç: 0
ALTER TABLE siparisler NO FORCE ROW LEVEL SECURITY;
RESET ROLE;FORCE sonrasında sahip de sıfır satır gördü, çünkü kendi oturumunda app.kiraci_id tanımlı değildi. Uygulamayı tablonun sahibiyle çalıştırıyorsanız FORCE olmadan RLS'nin hiçbir etkisi olmaz; en temiz çözüm, uygulama rolünü tablo sahibinden ayırmaktır.
Süper kullanıcılar ve BYPASSRLS özniteliğine sahip roller ise FORCE olsa bile her zaman bütün satırları görür. denetci rolüyle bağlandığımızda sayım beş döndü. BYPASSRLS özniteliğini yalnızca süper kullanıcılar ya da kendisi bu özniteliğe sahip roller verebilir. Rol yönetimi yazısında anlatılan pg_read_all_data rolünün ise RLS'yi atlamadığını unutmayın.
row_security = off: sessiz filtreleme yerine hata
Yedek alırken bazı satırların politika yüzünden sessizce eksik kalması ciddi bir sorundur. Belgeler bu durum için row_security parametresini önerir. Parametreyi off yapmak RLS'yi atlatmaz; sorgunun sonucu bir politika tarafından filtrelenecekse hata verdirir:
-- uygulama olarak
SET row_security = off;
SELECT count(*) FROM siparisler;ERROR: query would be affected by row-level security policy for table "siparisler"Aynı komutları denetci ile çalıştırdığımızda hata alınmadı ve sayım yine beş döndü, çünkü bu rolün sorgusu zaten hiçbir politikadan etkilenmiyordu. Belgelere göre pg_dump varsayılan olarak row_security değerini off yapar; yedeği alan kullanıcının yeterli yetkisi yoksa satır kaybı yerine hata alırsınız. PostgreSQL 18 ile pg_dump, pg_dumpall ve pg_restore araçlarına politikaları yedeğe almamak için --no-policies seçeneği de eklendi. Yedek komutlarının ayrıntısı pg_dump yazısında.
Gizli kanal: unique kısıtı ve bilgi sızıntısı
Bütünlük kontrolleri RLS'yi atladığı için kullanıcı göremediği bir satırın varlığını dolaylı olarak öğrenebilir. Kiracı 1 adına çalışırken, kiracı 2'ye ait 201 numaralı siparişle aynı anahtara sahip bir satır eklemeyi denedik:
-- app.kiraci_id = 1 iken
INSERT INTO siparisler (no, kiraci_id, musteri, tutar) VALUES (201, 1, 'Deneme', 1);ERROR: duplicate key value violates unique constraint "siparisler_pkey"Satırın içeriği görünmedi; testimizde hata mesajında çakışan değeri gösteren bir DETAIL satırı da gelmedi. Yine de kullanıcı 201 numarasının başka bir kiracıda kullanıldığını öğrendi. Belgeler şema ve politika tasarlarken bu tür gizli kanallara dikkat edilmesini ister. Çok kiracılı tablolarda anahtarı (kiraci_id, no) gibi kiracıyla birleşik tutmak ya da tahmin edilemeyen anahtarlar kullanmak bu sızıntıyı azaltır. Politikanın başka tablolara alt sorgu ile bakması gerektiğinde ise belgeler eşzamanlı transaction'larda yarış durumu oluşabileceği konusunda uyarır; ayrıntı için izolasyon seviyeleri yazısına bakabilirsiniz.
Sık yapılan hatalar

- RLS'yi açıp politika yazmamak: hata yerine boş sonuç döner; uygulama sessizce bozulur.
- Uygulamayı tablo sahibiyle bağlamak: sahip politikaları atlar. Ya ayrı bir rol kullanın ya da
FORCE ROW LEVEL SECURITYaçın. - current_setting sonucunu doğrudan int'e çevirmek: transaction'a özel ayardan sonra değer boş metin olabilir;
NULLIF(..., '')ekleyin. - Yalnızca restrictive politika tanımlamak: en az bir permissive politika olmadan hiçbir satır görünmez.
- UPDATE 0 sonucunu başarı saymak: görünmeyen satırlar hata vermeden atlanır; etkilenen satır sayısını kontrol edin.
- pg_read_all_data'nın RLS'yi atladığını sanmak: atlamaz; gerekiyorsa role ayrıca
BYPASSRLSverilir.
Sık Sorulan Sorular
RLS performansı etkiler mi?
Politika ifadesi her sorguya eklenen bir koşul gibi çalışır. Belgeler, yalnızca erişilen satırın kendi değerlerine bakan ifadelerin en basit ve en iyi performanslı durum olduğunu, başka tablolara bakan alt sorguların hem maliyet hem yarış durumu riski taşıdığını belirtir. Politikada kullandığınız sütunlar (örneğin kiraci_id) için uygun bir indeks oluşturun ve planı EXPLAIN ile kontrol edin.
RLS'yi geçici olarak kapatabilir miyim?
ALTER TABLE siparisler DISABLE ROW LEVEL SECURITY politikaları silmeden devre dışı bırakır; politikalar yerinde durur ama yok sayılır ve tablo yalnızca normal SQL yetkilerine göre erişilebilir olur. Bunu yalnızca tablo sahibi yapabilir.
Politikayı belirli bir role nasıl uygularım?
CREATE POLICY ... TO rol_adi yazın. Birden fazla rol verilebilir ve normal üyelik ve kalıtım kuralları geçerlidir; politika bir gruba yazılırsa üyelerine de uygulanır. TO yazılmazsa varsayılan PUBLIC'tir.
Görünümler (view) RLS ile nasıl çalışır?
Belgelere göre görünüm üzerinden erişimde varsayılan olarak alttaki tabloya görünüm sahibinin politikaları ve yetkileri uygulanır. Görünümün sahibi tablonun da sahibiyse, sahip normalde politikalardan muaf olduğu için bu durum RLS'nin etkisiz kalmasına yol açabilir. security_invoker = true seçeneğiyle oluşturulan görünümlerde ise sorguyu çalıştıran kullanıcının politikaları ve yetkileri kullanılır; ayrıntılar view yazısında.
Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.
İlgili içerikler: PostgreSQL öğrenme rehberi · GRANT ve REVOKE · Kullanıcı ve rol yönetimi · View ve materialized view · pg_dump ile yedek alma · İzolasyon seviyeleri ve MVCC