PostgreSQL row-level security cho SaaS: các chính sách hiệu quả
PostgreSQL row-level security cho SaaS giúp thực thi cô lập tenant trong cơ sở dữ liệu. Tìm hiểu khi nào nên dùng, cách viết policy và những điều cần tránh.

Vấn đề thực sự mà RLS cố giải quyết trong ứng dụng SaaS
Trong một app SaaS, lỗi bảo mật nguy hiểm nhất là lỗi xuất hiện khi bạn đã scale. Bạn bắt đầu với một quy tắc đơn giản như “người dùng chỉ thấy dữ liệu của tenant họ”, rồi nhanh chóng đưa ra endpoint mới, thêm một truy vấn báo cáo, hoặc giới thiệu một join vô tình bỏ qua kiểm tra.
Ủy quyền chỉ ở phía app sẽ vỡ dưới áp lực vì quy tắc bị rải rác. Một controller kiểm tra tenant_id, controller khác kiểm tra membership, một job nền quên, và một đường dẫn “admin export” giữ trạng thái “tạm thời” trong nhiều tháng. Ngay cả các đội cẩn thận cũng bỏ sót chỗ.
PostgreSQL row-level security (RLS) giải quyết một vấn đề cụ thể: bắt cơ sở dữ liệu phải thực thi hàng nào được nhìn thấy cho một request. Mô hình tư duy đơn giản: mọi SELECT, UPDATE, và DELETE đều bị lọc tự động bởi các policy, giống như mọi request bị lọc bởi middleware xác thực.
Phần “row” quan trọng. RLS không bảo vệ mọi thứ:
- Nó không tự động ẩn từng cột (dùng views hoặc quyền cột).
- Nó không làm cho các hàm không an toàn trở nên an toàn (một hàm vẫn có thể rò rỉ dữ liệu nếu chạy với quyền nâng cao).
- Nó không xác thực các quy tắc nghiệp vụ (ví dụ, “chỉ chủ sở hữu mới thay đổi cài đặt thanh toán”).
Một ví dụ cụ thể: bạn thêm endpoint liệt kê projects với join tới invoices cho dashboard. Với ủy quyền chỉ phía app, bạn dễ lọc projects theo tenant nhưng quên lọc invoices, hoặc join trên một khóa làm chéo tenant. Với RLS, cả hai bảng có thể thực thi cô lập tenant, nên truy vấn sẽ fail-safe thay vì rò rỉ dữ liệu.
Đổi chác có thật. Bạn viết ít mã ủy quyền lặp lại hơn và giảm số chỗ có thể rò rỉ. Nhưng bạn cũng phải làm việc mới: thiết kế policy cẩn thận, test sớm, và chấp nhận rằng một policy có thể chặn truy vấn bạn mong đợi chạy.
Khi nào RLS đơn giản hóa ủy quyền (và khi nào nó gây đau đầu)
RLS có thể cảm thấy là việc thêm vào cho tới khi app của bạn vượt quá vài endpoint. Nếu bạn có ranh giới tenant nghiêm ngặt và nhiều đường dẫn truy vấn (màn hình danh sách, tìm kiếm, xuất dữ liệu, công cụ admin), đặt quy tắc trong DB nghĩa là bạn không phải nhớ thêm cùng bộ lọc ở mọi chỗ.
RLS phù hợp mạnh khi quy tắc buồn tẻ và phổ quát: “người dùng chỉ thấy hàng của tenant họ” hoặc “người dùng chỉ thấy project mà họ là thành viên”. Trong những thiết lập đó, policy giảm sai sót vì mọi SELECT, UPDATE, DELETE đều đi qua cùng cổng, ngay cả khi truy vấn được thêm sau này.
Nó cũng hữu ích trong các app đọc nhiều nơi mà logic lọc nhất quán. Nếu API của bạn có 15 cách khác nhau để tải invoices (theo trạng thái, theo ngày, theo khách hàng, theo tìm kiếm), RLS cho phép bạn dừng việc cài đặt lại lọc tenant trên từng truy vấn và tập trung vào tính năng.
RLS gây phiền khi các quy tắc không dựa trên hàng. Các quy tắc theo trường như “bạn có thể thấy salary nhưng không thấy bonus” hoặc “che cột này trừ khi bạn là HR” thường biến thành SQL vụng về và ngoại lệ khó bảo trì.
Nó cũng không phù hợp cho báo cáo nặng thực sự cần truy cập rộng. Đội thường tạo các role bypass cho “chỉ job này”, và đó là nơi sai sót tích tụ.
Trước khi cam kết, quyết định liệu bạn muốn DB là cổng cuối cùng hay không. Nếu có, lập kế hoạch kỷ luật: test hành vi DB (không chỉ phản hồi API), coi migration là thay đổi bảo mật, tránh bypass tạm thời, quyết định cách job nền xác thực, và giữ policy nhỏ gọn và có thể tái dùng.
Nếu bạn dùng công cụ tạo backend, nó có thể tăng tốc delivery, nhưng không loại trừ nhu cầu về roles rõ ràng, tests và mô hình tenant đơn giản. (Ví dụ, Koder.ai dùng Go và PostgreSQL cho backend sinh tự động, và bạn vẫn muốn thiết kế RLS một cách có chủ ý thay vì “rắc nó vào sau”.)
Những nguyên tắc mô hình dữ liệu giúp quản lý policy RLS dễ hơn
RLS dễ nhất khi schema của bạn đã rõ ràng ai sở hữu gì. Nếu bạn bắt đầu với mô hình mơ hồ và cố “fix trong policy”, thường bạn sẽ gặp truy vấn chậm và bug khó hiểu.
Đặt khóa tenant ở mọi nơi nó thuộc về
Chọn một khóa tenant duy nhất (như org_id) và dùng nhất quán. Hầu hết các bảng thuộc tenant nên có nó, ngay cả khi chúng tham chiếu tới bảng khác đã có org_id. Điều này tránh join bên trong policy và giữ các kiểm tra USING đơn giản.
Một quy tắc thực tế: nếu một hàng nên biến mất khi khách hàng hủy, nó có lẽ cần org_id.
Mô hình membership rõ ràng
Policy RLS thường trả lời một câu hỏi: “Người này có phải là thành viên của org này, và họ có thể làm gì?” Khó suy ra điều đó từ các cột ad hoc.
Giữ các bảng lõi nhỏ và đơn giản:
users(một hàng cho mỗi người)orgs(một hàng cho mỗi tenant)org_memberships(user_id,org_id,role,status)- tùy chọn:
project_membershipscho truy cập theo project
Với cấu trúc đó, policy có thể kiểm tra membership bằng một lookup có index.
Tách dữ liệu tham chiếu chia sẻ khỏi dữ liệu thuộc tenant
Không phải mọi thứ đều cần org_id. Bảng tham chiếu như countries, product categories, hay plan types thường được chia sẻ giữa các tenant. Làm chúng chỉ đọc cho hầu hết vai trò, và đừng gắn chúng với một org cụ thể.
Dữ liệu thuộc tenant (projects, invoices, tickets) nên tránh kéo chi tiết tenant qua các bảng chia sẻ. Giữ bảng chia sẻ tối thiểu và ổn định.
FK, cascade, và indexing
Foreign keys vẫn hoạt động với RLS, nhưng deletes có thể làm bạn ngạc nhiên nếu role thực hiện xóa không “thấy” các hàng phụ thuộc. Lập kế hoạch cascade cẩn thận và test luồng xóa thực tế.
Index các cột mà policy lọc, đặc biệt org_id và các khóa membership. Một policy đọc như WHERE org_id = ... không nên trở thành quét toàn bảng khi bảng có hàng triệu dòng.
RLS hoạt động như thế nào, không cần phần đáng sợ
RLS là một công tắc per-table. Khi bật, PostgreSQL ngừng tin mã app sẽ nhớ bộ lọc tenant. Mọi SELECT, UPDATE, DELETE bị lọc bởi policy, và mọi INSERT, UPDATE bị xác thực bởi policy.
Thay đổi tư duy lớn nhất: khi bật RLS, các truy vấn từng trả về dữ liệu có thể bắt đầu trả về 0 hàng mà không báo lỗi. Đó là PostgreSQL thực thi kiểm soát truy cập.
Policy thực ra làm gì
Policy là các quy tắc nhỏ gắn với bảng. Chúng dùng hai kiểm tra:
USINGlà bộ lọc đọc. Nếu một hàng không khớpUSING, nó vô hình choSELECT, và không thể là mục tiêu củaUPDATEhayDELETE.WITH CHECKlà cửa cho ghi. Nó quyết định các hàng mới hoặc đã thay đổi được phép choINSERThoặcUPDATE.
Một mẫu SaaS phổ biến: USING đảm bảo bạn chỉ thấy hàng từ tenant của bạn, và WITH CHECK đảm bảo bạn không chèn hàng vào tenant người khác bằng cách đoán tenant_id.
PERMISSIVE vs RESTRICTIVE, tóm tắt trong một câu
Khi bạn thêm nhiều policy sau này, điều này quan trọng:
PERMISSIVE(mặc định): một hàng được cho phép nếu bất kỳ policy nào cho phép nó.RESTRICTIVE: một hàng chỉ được cho phép nếu tất cả policy restrictive cho phép nó (bên cạnh hành vi permissive).
Nếu bạn dự định xếp lớp các quy tắc như khớp tenant cộng thêm kiểm tra role và membership project, policy restrictive có thể làm ý định rõ hơn, nhưng cũng dễ khiến bạn tự khóa nếu quên một điều kiện.
Postgres biết ai là user bằng cách nào
RLS cần một giá trị “ai đang gọi” đáng tin cậy. Các lựa chọn phổ biến:
- Biến session đặt theo request (ví dụ
app.user_idvàapp.tenant_id). - Claim trong JWT ánh xạ vào session settings bởi lớp API.
- Chuyển role (
SET ROLE ...) theo request, có thể hoạt động nhưng tăng chi phí vận hành.
Chọn một cách và áp dụng khắp nơi. Trộn nguồn identity giữa các dịch vụ là con đường nhanh dẫn tới bug khó hiểu.
Đặt tên policy để có thể debug sau này
Dùng quy ước dễ đoán để dump schema và log dễ đọc. Ví dụ: {table}__{action}__{rule}, như projects__select__tenant_match.
Từng bước: thêm RLS vào một bảng và chứng minh nó hoạt động
Nếu bạn mới với RLS, bắt đầu với một bảng và một proof nhỏ. Mục tiêu không phải phủ kín hoàn hảo. Mục tiêu là khiến DB từ chối truy cập chéo-tenant ngay cả khi app bị lỗi.
Một bảng nhỏ để thực hành
Giả sử bảng projects đơn giản. Trước tiên, thêm tenant_id theo cách không làm hỏng ghi.
ALTER TABLE projects ADD COLUMN tenant_id uuid;
-- Backfill existing rows (example: everyone belongs to a default tenant)
UPDATE projects SET tenant_id = '11111111-1111-1111-1111-111111111111'::uuid
WHERE tenant_id IS NULL;
ALTER TABLE projects ALTER COLUMN tenant_id SET NOT NULL;
Tiếp theo, tách ownership khỏi access. Mẫu phổ biến: một role sở hữu bảng (app_owner), role khác dùng bởi API (app_user). Role API không nên là owner bảng, nếu không nó có thể bypass policy.
ALTER TABLE projects OWNER TO app_owner;
REVOKE ALL ON projects FROM PUBLIC;
GRANT SELECT, INSERT, UPDATE, DELETE ON projects TO app_user;
Bây giờ quyết định cách request báo cho Postgres biết đang phục vụ tenant nào. Một cách đơn giản là setting theo request. App đặt nó ngay sau khi mở transaction.
-- inside the same transaction as the request
SELECT set_config('app.current_tenant', '22222222-2222-2222-2222-222222222222', true);
Bật RLS và bắt đầu với quyền đọc.
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
CREATE POLICY projects_tenant_select
ON projects
FOR SELECT
TO app_user
USING (tenant_id = current_setting('app.current_tenant')::uuid);
Chứng minh nó hoạt động bằng thử hai tenant khác nhau và kiểm tra số hàng thay đổi.
Thêm quy tắc ghi (WITH CHECK)
Policy đọc không bảo vệ ghi. Thêm WITH CHECK để chặn inserts và updates chui vào tenant sai.
CREATE POLICY projects_tenant_write
ON projects
FOR INSERT, UPDATE
TO app_user
WITH CHECK (tenant_id = current_setting('app.current_tenant')::uuid);
Một cách nhanh để kiểm tra hành vi (bao gồm failure) là giữ một script SQL nhỏ bạn có thể chạy lại sau mỗi migration:
BEGIN; SET LOCAL ROLE app_user;SELECT set_config('app.current_tenant', '\u003ctenant A\u003e', true); SELECT count(*) FROM projects;INSERT INTO projects(id, tenant_id, name) VALUES (gen_random_uuid(), '\u003ctenant B\u003e', 'bad');(sẽ fail)UPDATE projects SET tenant_id = '\u003ctenant B\u003e' WHERE ...;(sẽ fail)ROLLBACK;
Nếu bạn chạy script đó và nhận kết quả giống nhau mỗi lần, bạn có một baseline tin cậy trước khi mở rộng RLS sang bảng khác.
Các mẫu policy bạn sẽ tái sử dụng trong hầu hết app SaaS
Hầu hết đội bắt đầu dùng RLS vì chán lặp lại cùng kiểm tra ủy quyền trong mọi truy vấn. Tin tốt là hình dạng policy cần thường nhất quán.
Bảng owned bởi owner vs bảng theo membership
Một số bảng tự nhiên thuộc sở hữu của một user (notes, API tokens). Những bảng khác thuộc tenant mà truy cập phụ thuộc vào membership. Xem hai loại này là hai mẫu khác nhau.
Với dữ liệu owned-by-owner, policy thường kiểm tra created_by = app_user_id(). Với dữ liệu tenant, policy thường kiểm tra user có một hàng membership cho org hay không.
Một cách thực tế để giữ policy dễ đọc là tập trung identity vào các helper SQL nhỏ và tái dùng chúng:
-- Example helpers
create function app_user_id() returns uuid
language sql stable as $$
select current_setting('app.user_id', true)::uuid
$$;
create function app_is_admin() returns boolean
language sql stable as $$
select current_setting('app.is_admin', true) = 'true'
$$;
Tách quy tắc đọc khỏi quy tắc ghi
Đọc thường rộng hơn ghi. Ví dụ, bất kỳ thành viên org nào cũng có thể SELECT projects, nhưng chỉ editors mới UPDATE, và chỉ owners mới DELETE.
Giữ rõ ràng: một policy cho SELECT (membership), một policy cho INSERT/UPDATE với WITH CHECK (role), và một policy cho DELETE (thường nghiêm ngặt hơn update).
Override admin mà không tắt RLS
Tránh “tắt RLS cho admin.” Thay vào đó, thêm một cửa thoát bên trong policy, như app_is_admin(), để bạn không vô tình cấp quyền toàn bộ cho một role dịch vụ chung.
Soft deletes và các flag trạng thái
Nếu bạn dùng deleted_at hoặc status, đưa nó vào policy SELECT (deleted_at is null). Nếu không, ai đó có thể “hồi sinh” hàng bằng cách đổi flag mà app tưởng là cuối cùng.
UPSERT: giữ WITH CHECK thân thiện
INSERT ... ON CONFLICT DO UPDATE phải thỏa WITH CHECK cho hàng sau khi ghi. Nếu policy yêu cầu created_by = app_user_id(), đảm bảo upsert đặt created_by lúc insert và không ghi đè nó lúc update.
Nếu bạn sinh code backend, các mẫu này đáng được biến thành template nội bộ để bảng mới bắt đầu với mặc định an toàn thay vì một canvas trống.
Các rủi ro phổ biến của RLS làm việc debug trở nên đau đầu
RLS tuyệt vời cho tới khi một chi tiết nhỏ làm nó trông như PostgreSQL “ngẫu nhiên” ẩn hoặc hiển thị dữ liệu. Những lỗi dưới đây tiêu tốn nhiều thời gian nhất.
Bẫy gây rò tenant im lặng
Cạm bẫy đầu tiên là quên WITH CHECK trên insert và update. USING kiểm soát những gì bạn có thể thấy, không phải những gì bạn được phép tạo. Nếu thiếu WITH CHECK, một lỗi app có thể ghi hàng vào tenant sai, và bạn có thể không biết vì cùng user đó không đọc được hàng đó.
Một rò rỉ phổ biến khác là “leaky join.” Bạn lọc projects đúng, rồi join tới invoices, notes hoặc files chưa được bảo vệ giống vậy. Cách sửa nghiêm ngặt nhưng đơn giản: mọi bảng có thể tiết lộ dữ liệu tenant cần policy riêng, và views không nên dựa vào chỉ một bảng được an toàn.
Các mẫu lỗi thường xuất hiện sớm:
- Có policy đọc, nhưng policy ghi thiếu
WITH CHECK. - Điều kiện policy dùng join tới bảng khác chưa được bảo vệ.
- Access được thực thi trong một view, nhưng bảng nền vẫn mở.
- Bạn dựa vào “app luôn set tenant_id”, và một job nền quên.
- Bạn test với role superuser, nên không bao giờ thấy hành vi thực tế.
Bẫy làm hành vi khó hiểu
Policy tham chiếu cùng bảng (trực tiếp hoặc qua view) có thể tạo ra đệ quy bất ngờ. Một policy có thể kiểm tra membership bằng cách query view đọc bảng được bảo vệ nữa, dẫn tới lỗi, truy vấn chậm, hoặc policy không bao giờ match.
Cấu hình role là nguồn nhầm lẫn khác. Owner bảng và role nâng cao có thể bypass RLS, nên test của bạn pass trong khi user thực không thể (hoặc ngược lại). Luôn test với cùng role quyền thấp mà app dùng.
Cẩn trọng với SECURITY DEFINER functions. Chúng chạy với quyền owner của function, nên helper như current_tenant_id() có thể ổn, nhưng một function “tiện lợi” đọc dữ liệu có thể vô tình đọc chéo tenant nếu bạn không thiết kế để tôn trọng RLS.
Cũng đặt search_path an toàn trong security definer functions. Nếu không, function có thể lấy object khác cùng tên, và logic policy của bạn có thể im lặng trỏ vào thứ sai tùy session state.
Debugging RLS: cách thực tế để thấy chuyện gì đang xảy ra
Bug RLS thường là do thiếu bối cảnh, không phải “SQL sai.” Một policy có thể đúng trên giấy nhưng vẫn fail vì session role khác bạn nghĩ, hoặc request không đặt tenant và user mà policy dựa vào.
Cách tin cậy để tái hiện một báo cáo production là mô phỏng cùng setup session cục bộ và chạy đúng query. Thường có nghĩa là:
SET ROLE app_user;(hoặc role API thực tế)SELECT set_config('app.tenant_id', 't_123', true);vàSELECT set_config('app.user_id', 'u_456', true);- Chạy đúng SQL app đã chạy (kể cả tham số)
- Xác nhận Postgres thấy gì:
SELECT current_user, current_setting('app.tenant_id', true), current_setting('app.user_id', true);
Khi bạn không chắc policy nào được áp dụng, kiểm tra catalog thay vì đoán. pg_policies hiển thị mỗi policy, lệnh, và biểu thức USING/WITH CHECK. Kết hợp với pg_class để xác nhận RLS bật trên bảng và không bị bypass.
Vấn đề hiệu năng có thể trông như vấn đề auth. Một policy join table membership hoặc gọi function có thể đúng nhưng chậm khi bảng lớn lên. Dùng EXPLAIN (ANALYZE, BUFFERS) trên query tái hiện và tìm sequential scans, nested loops bất ngờ, hoặc các filter áp muộn. Thiếu index trên (tenant_id, user_id) và bảng membership là nguyên nhân thường gặp.
Cũng nên log ba giá trị mỗi request ở lớp app: tenant ID, user ID, và role DB dùng cho request. Khi những giá trị đó không khớp bạn nghĩ, RLS sẽ hành xử “sai” vì input sai.
Cho tests, giữ vài tenant seed và làm rõ failure. Một suite nhỏ thường bao gồm: “Tenant A không thể đọc Tenant B”, “user không có membership không thấy project”, “owner có thể update, viewer không”, “insert bị chặn trừ khi tenant_id khớp context”, và “override admin chỉ áp dụng nơi dự định”.
Checklist nhanh trước release RLS lên production
Xem RLS như dây an toàn, không phải feature có thể bật/tắt. Sai sót nhỏ biến thành “mọi người thấy dữ liệu của mọi người” hoặc “mọi thứ trả về 0 hàng.”
Mô hình dữ liệu và hình dạng policy
Đảm bảo thiết kế bảng và quy tắc policy khớp mô hình tenant.
- Mọi bảng thuộc tenant nên có khóa tenant rõ ràng (thường
tenant_id). Nếu không, ghi rõ lý do (ví dụ bảng tham chiếu toàn cục). - Bật RLS trên mọi bảng thuộc tenant, không chỉ bảng “chính”. Nếu một số đường dẫn phải không bao giờ bypass, cân nhắc
FORCE ROW LEVEL SECURITYtrên những bảng đó. - Tách quy tắc đọc và ghi. Reads dùng
USING. Writes phải cóWITH CHECKđể inserts/updates không thể chuyển row sang tenant khác. - Giữ predicate của policy thân thiện với index. Nếu policy lọc theo
tenant_idhoặc join qua bảng membership, thêm index phù hợp.
Một kịch bản sanity đơn giản: user tenant A có thể đọc invoices của họ, chỉ có thể chèn invoice cho tenant A, và không thể update invoice để thay đổi tenant_id.
Roles, hiệu năng và tests
RLS chỉ mạnh khi roles app dùng phù hợp.
- Xác nhận app không bao giờ kết nối như superuser, owner bảng, hoặc bất kỳ role có
bypassrls. - Chạy vài query thực với volume dữ liệu giống production và kiểm tra kế hoạch truy vấn.
- Thêm tests negative tự động để chứng minh truy cập chéo-tenant bị chặn.
Ví dụ: app projects đa-tenant với truy cập theo membership
Hình dung một app B2B nơi công ty (orgs) có projects, và projects có tasks. Users có thể thuộc nhiều org, và user có thể là thành viên của một số projects mà không phải tất cả. Đây là một fit tốt cho RLS vì DB có thể thi hành cô lập tenant ngay cả khi một endpoint API quên bộ lọc.
Mô hình đơn giản: orgs, users, org_memberships (org_id, user_id, role), projects (id, org_id), project_memberships (project_id, user_id), tasks (id, project_id, org_id, ...). Việc có org_id trên tasks là có chủ ý. Nó giữ policy đơn giản và giảm bất ngờ khi join.
Rò rỉ kinh điển xảy ra khi tasks chỉ có project_id, và policy kiểm tra quyền thông qua join tới projects. Một lỗi (policy permissive trên projects, một join bỏ điều kiện, hoặc một view thay đổi context) có thể phơi bày tasks từ org khác.
Một đường chuyển an toàn tránh phá traffic production:
- Triển khai thay đổi schema trước (thêm
org_idchotasks, thêm bảng membership). - Backfill
tasks.org_idtừprojects.org_id, rồi thêmNOT NULL. - Thêm policies nhưng giữ RLS tắt trong khi test ở staging.
- Bật RLS, rồi force nó, và chỉ khi đó loại bỏ các bộ lọc cũ ở phía app.
Truy cập support thường xử lý tốt nhất bằng một role break-glass hẹp, không phải bằng tắt RLS. Giữ nó tách biệt khỏi tài khoản support bình thường và làm rõ khi nào được dùng.
Ghi chép các quy tắc để policy không trôi: session variables cần set là gì (user_id, org_id), bảng nào phải mang org_id, “member” nghĩa là gì, và vài ví dụ SQL nên trả về 0 hàng khi chạy dưới org sai.
Bước tiếp theo: triển khai RLS an toàn và giữ nó có thể duy trì
RLS dễ sống chung khi bạn đối xử nó như một thay đổi sản phẩm. Triển khai theo từng phần nhỏ, chứng minh hành vi bằng tests, và giữ hồ sơ rõ ràng lý do mỗi policy tồn tại.
Kế hoạch rollout thường hiệu quả:
- Bắt đầu với một bảng có ownership tenant rõ ràng (ví dụ
projects) và khóa nó. - Thêm tests bao phủ đọc/ghi được phép và bị chặn cho vài role (owner, member, outsider).
- Mở rộng theo lô (một khu vực tính năng một lần) bạn có thể debug trong một session.
- Giám sát lỗi permission trong quá trình rollout và deploy vào khung thời gian rủi ro thấp.
Sau khi bảng đầu ổn, làm các thay đổi policy có chủ ý. Thêm bước review policy vào migrations, và kèm một ghi chú ngắn về ý định (ai nên truy cập gì và vì sao) cùng cập nhật test tương ứng. Điều này ngăn “thêm OR cho xong” dần dần biến thành lỗ hổng.
Nếu bạn di chuyển nhanh, các công cụ như Koder.ai (koder.ai) có thể giúp sinh Go + PostgreSQL khởi điểm qua chat, rồi bạn có thể xếp các chính sách RLS và tests lên trên với cùng kỷ luật như backend viết tay.
Cuối cùng, giữ các lan can an toàn trong suốt rollout. Chụp snapshot trước migration policy, luyện rollback đến khi thấy nhàm chán, và giữ một đường break-glass nhỏ cho support mà không tắt RLS cho toàn hệ thống.
Câu hỏi thường gặp
Vấn đề bảo mật nào RLS thực sự giải quyết trong app SaaS?
RLS bắt buộc PostgreSQL phải quyết định hàng nào có thể nhìn thấy hoặc ghi cho một request, nên việc cô lập tenant không phụ thuộc vào việc mọi endpoint đều nhớ thêm WHERE tenant_id = .... Lợi ích chính là giảm các lỗi “quên lọc” khi ứng dụng mở rộng và truy vấn nhân lên.
Khi nào RLS xứng đáng với độ phức tạp thêm?
Đáng để dùng khi quy tắc truy cập đồng nhất và dựa trên hàng, như cô lập tenant hoặc truy cập theo membership, và khi bạn có nhiều đường dẫn truy vấn (tìm kiếm, xuất dữ liệu, giao diện admin, job nền). Thường không đáng với các quy tắc theo trường (per-field) phức tạp, nhiều ngoại lệ, hoặc các báo cáo cần đọc ngang tenant.
RLS không bảo vệ tôi khỏi những gì?
RLS xử lý hiển thị hàng và kiểm soát ghi cơ bản. Quyền truy cập cột thường cần views hoặc quyền cột, và các quy tắc nghiệp vụ phức tạp (ví dụ: ownership thanh toán, luồng phê duyệt) vẫn thuộc logic ứng dụng hoặc các ràng buộc cơ sở dữ liệu được thiết kế cẩn thận.
Cách an toàn nhất để bắt đầu dùng RLS nếu tôi mới tiếp cận là gì?
Tạo một role quyền thấp cho API (không phải owner bảng), bật RLS, rồi thêm chính sách SELECT và chính sách INSERT/UPDATE với WITH CHECK. Dùng một biến session theo request (ví dụ app.current_tenant) và xác minh rằng đổi giá trị này làm thay đổi hàng bạn thấy và có thể viết.
Ứng dụng nên báo cho Postgres biết tenant và user thực hiện request như thế nào?
Một mặc định phổ biến là biến session theo request, được đặt ngay khi bắt đầu transaction, như app.tenant_id và app.user_id. Điều quan trọng là nhất quán: mọi đường dẫn mã (web requests, jobs, script) phải đặt cùng các giá trị mà policy kỳ vọng, nếu không bạn sẽ thấy hành vi “trả về 0 hàng” gây khó hiểu.
Sự khác nhau giữa USING và WITH CHECK trong policy RLS là gì?
USING quyết định bộ lọc đọc. Nếu một hàng không thỏa USING, nó sẽ vô hình cho SELECT và không thể là target của UPDATE/DELETE. WITH CHECK là cửa cho ghi: nó quyết định các hàng mới hoặc thay đổi được phép khi INSERT hoặc UPDATE. WITH CHECK ngăn chặn việc chèn/đổi row vào tenant khác kể cả khi app truyền tenant_id sai.
Tại sao mọi người cứ nhắc “đừng quên WITH CHECK”?
Bởi vì nếu chỉ thêm USING, endpoint bị lỗi vẫn có thể INSERT hoặc UPDATE hàng vào tenant khác, và bạn có thể không nhận ra vì cùng user đó không thể đọc hàng lỗi đó. Luôn ghép quy tắc đọc với WITH CHECK tương ứng cho ghi để dữ liệu xấu không được tạo ra ngay từ đầu.
Nên cấu trúc schema thế nào để policies RLS giữ đơn giản và nhanh?
Tránh join bên trong policy bằng cách đặt khóa tenant (ví dụ org_id) trực tiếp trên các bảng thuộc tenant, ngay cả khi chúng tham chiếu bảng khác đã có org_id. Thêm bảng membership rõ ràng (org_memberships, tùy chọn project_memberships) để policy chỉ cần một lookup có index thay vì suy luận phức tạp.
Làm thế nào debug “RLS đang ẩn dữ liệu của tôi” mà không suy đoán?
Đầu tiên, tái tạo cùng ngữ cảnh session mà app dùng bằng cách đặt cùng role và session settings, rồi chạy đúng SQL query. Xác nhận RLS đang bật và kiểm tra pg_policies để xem USING và WITH CHECK nào áp dụng, vì lỗi RLS thường do thiếu bối cảnh identity hơn là “SQL sai”.
Nếu tôi sinh backend (ví dụ với Koder.ai), có còn cần thiết kế RLS cẩn thận không?
Có. Xem mã sinh như điểm khởi đầu, không phải hệ thống an ninh hoàn chỉnh. Nếu dùng Koder.ai để sinh backend Go + PostgreSQL, bạn vẫn phải định nghĩa mô hình tenant, đặt identity session nhất quán, và thêm policies cùng tests có chủ ý để các bảng mới không được phát hành thiếu bảo vệ.