Phân quyền trong PostgreSQL có bốn lớp, và mỗi lớp chặn được một thứ khác nhau. Phần này đo từng lớp trên một bảng nhân viên bốn dòng, và tìm ra ba chỗ chúng không chặn.
Lớp 1: mặc định
Một vai trò mới tạo, chưa cấp gì:
| Thao tác | Kết quả |
|---|---|
| Kết nối | được |
select bảng nv |
ERROR: permission denied for table nv |
Tạo bảng trong public |
ERROR: permission denied for schema public |
Dòng thứ ba là thay đổi của PostgreSQL 15. Trước đó, mọi vai trò đều tạo được bảng trong schema public — một mặc định nguy hiểm tồn tại nhiều năm. Nếu bạn chạy phiên bản 14 trở xuống, hãy chạy:
revoke create on schema public from public;
Nhưng lớp mặc định không chặn được mọi thứ:
thấy 69 bảng catalog
thấy 18 vai trò
đọc được shared_buffers = 16384
Một vai trò không có quyền nào trên dữ liệu vẫn đọc được toàn bộ catalog: tên mọi bảng, mọi cột, mọi vai trò, mọi tham số cấu hình. Không có GRANT nào chặn được phần này — nó là thiết kế, vì bản thân bộ phân tích SQL cần đọc catalog.
Điều đó có nghĩa: đừng đặt thông tin nhạy cảm vào tên bảng, cột, hay vai trò.
Lớp 2 và 3: GRANT ở mức bảng và mức cột
grant select on nv to r_doc;
đọc 4 dòng ok
đọc cột lương ok (90.000.000)
sửa bảng từ chối
Mức cột thì hẹp hơn:
grant select (id, ten, phong) on nv to r_ghi;
đọc id, ten, phong ok
đọc cột lương ERROR: permission denied for table nv
chạy select * ERROR: permission denied for table nv
Chú ý dòng cuối: select * bị từ chối vì nó bao gồm cột luong. Đây là hành vi đúng nhưng nó phá vỡ mã ứng dụng hay viết select * — và thông báo lỗi nói "permission denied for table" chứ không nói cột nào, nên việc chẩn đoán mất thời gian hơn cần thiết.
Quyền mức cột ít được dùng vì lý do đó, nhưng nó là cách duy nhất để một vai trò đọc được bảng mà không đọc được một cột cụ thể.
Lớp 4: bảo mật ở mức dòng
alter table nv enable row level security;
create policy chi_phong_minh on nv for select
using (phong = current_setting('app.phong', true));
| Phiên đặt gì | Số dòng thấy được |
|---|---|
Chưa đặt app.phong |
0 |
set app.phong = 'kt' |
2 |
set app.phong = 'it' |
2 |
Cùng một truy vấn select count(*) from nv, ba kết quả khác nhau tuỳ biến phiên.
Tham số thứ hai của current_setting là true — nghĩa là trả về NULL thay vì báo lỗi khi biến chưa được đặt. Không có nó, mọi truy vấn từ phiên chưa đặt biến sẽ ném lỗi thay vì trả 0 dòng. Với ứng dụng thì lỗi rõ ràng có khi tốt hơn, nhưng phải chọn có ý thức.
Mẫu này gắn với phần 43: nếu bạn dùng PgBouncer ở chế độ transaction, set app.phong có thể rò rỉ sang client khác — và ở đây rò rỉ đó không chỉ là hiệu năng mà là dữ liệu của phòng khác. Phải dùng set local trong giao dịch.
Ba người vượt qua được RLS
| Ai | Thấy bao nhiêu |
|---|---|
Chủ sở hữu bảng, chưa FORCE |
3 / 3 |
Chủ sở hữu bảng, sau FORCE ROW LEVEL SECURITY |
2 / 3 |
Superuser, dù đã FORCE |
3 / 3 |
Vai trò có thuộc tính BYPASSRLS |
4 / 4 |
Hai điều đáng nhớ.
Chủ sở hữu bảng mặc định không bị chính sách ràng buộc. Đây là chỗ dễ sai nhất: nếu ứng dụng của bạn kết nối bằng chính vai trò đã tạo ra bảng, RLS không có tác dụng gì cả. Phải thêm:
alter table nv force row level security;
Đo được: sau FORCE, chủ sở hữu thấy 2 trên 3 dòng — chính sách đã áp.
Superuser luôn vượt qua, kể cả sau FORCE. Không có cách nào chặn. Nghĩa là RLS không phải cơ chế bảo vệ dữ liệu khỏi quản trị viên; nó là cơ chế phân tách dữ liệu giữa những người dùng bình thường.
Nếu bạn cần bảo vệ khỏi quản trị viên, câu trả lời là mã hoá ở tầng ứng dụng, không phải RLS.
RLS giấu được dòng, nhưng thông báo lỗi thì không
Đây là hạn chế tôi thấy đáng nhớ nhất.
r_doc với app.phong = 'kt' không nhìn thấy dòng id = 3 (thuộc phòng it). Nhưng thử chèn một dòng có id = 3:
ERROR: duplicate key value violates unique constraint "nv_pkey"
Thông báo đó khẳng định dòng id = 3 tồn tại — đúng thứ mà chính sách đang cố giấu.
Ràng buộc duy nhất được kiểm ở tầng lưu trữ, trước khi chính sách được xét. Không có cách nào tắt chuyện này mà vẫn giữ ràng buộc.
Hệ quả thực tế: với khoá tuần tự thì rò rỉ này vô hại. Với khoá có ý nghĩa — số căn cước, mã đơn hàng, địa chỉ email — kẻ tấn công dò được sự tồn tại của bản ghi bằng cách thử chèn. Đó là lý do khoá chính công khai nên là uuid hoặc số tuần tự, không phải dữ liệu thật.
Ba việc nên làm
1. Ứng dụng không dùng vai trò sở hữu bảng. Tách ba vai trò:
create role so_huu nologin; -- sở hữu lược đồ, không đăng nhập
create role ung_dung login password '...'; -- ứng dụng dùng
create role chi_doc login password '...'; -- báo cáo, giám sát
grant usage on schema public to ung_dung, chi_doc;
grant select, insert, update, delete on all tables in schema public to ung_dung;
grant select on all tables in schema public to chi_doc;
Vai trò sở hữu không đăng nhập được, nên không ai dùng nó để chạy truy vấn — và RLS áp đúng cho vai trò ứng dụng.
2. Đặt quyền mặc định cho bảng tương lai. grant ... on all tables chỉ áp cho bảng đang có. Bảng tạo sau đó không có quyền:
alter default privileges for role so_huu in schema public
grant select, insert, update, delete on tables to ung_dung;
Đây là bước hay bị quên nhất, và triệu chứng là "bảng mới tạo tuần trước thì ứng dụng báo permission denied".
3. Kiểm ai đang có quyền gì:
select grantee, table_name, string_agg(privilege_type, ', ') as quyen
from information_schema.table_privileges
where table_schema = 'public' and grantee <> 'postgres'
group by grantee, table_name order by grantee, table_name;
Và ai là superuser:
select rolname, rolsuper, rolbypassrls, rolcreatedb, rolcreaterole
from pg_roles where rolsuper or rolbypassrls or rolcreaterole;
Danh sách thứ hai nên rất ngắn. Mỗi dòng trong đó là một vai trò mà RLS không chặn được.
Xác thực: pg_hba.conf
Bốn lớp ở trên đều nói về "vai trò này làm được gì sau khi đã kết nối". Lớp trước đó là pg_hba.conf, và nó quyết định ai kết nối được từ đâu:
# TYPE DATABASE USER ADDRESS METHOD
host all all 10.0.0.0/8 scram-sha-256
host all all 0.0.0.0/0 reject
Ba điều đáng biết:
Dòng khớp đầu tiên thắng — thứ tự quan trọng, và dòng reject phải nằm cuối.
trust nghĩa là không cần mật khẩu. Ảnh Docker mặc định của PostgreSQL đặt trust cho kết nối local, và đó là lý do mọi lệnh psql trong bài này không cần mật khẩu. Trên máy chủ thật thì đó là lỗ hổng.
scram-sha-256 thay cho md5. Từ PostgreSQL 14, scram-sha-256 là mặc định. Nếu hệ thống của bạn còn md5, đổi sang — nó chống được tấn công phát lại mà md5 không chống được.
Thử ba mươi giây
select rolname, rolsuper, rolbypassrls, rolcanlogin
from pg_roles where rolcanlogin order by rolsuper desc, rolname;
Hai câu hỏi:
- Có bao nhiêu vai trò vừa
rolsupervừarolcanlogin? Mỗi cái là một tài khoản bỏ qua mọi lớp ở trên. Ứng dụng không bao giờ nên là một trong số đó. - Vai trò mà ứng dụng dùng có sở hữu bảng nào không?
select tablename, tableowner from pg_tables where schemaname = 'public';
Nếu tableowner trùng vai trò ứng dụng, RLS của bạn không có tác dụng cho tới khi thêm FORCE.
Phần sau đo mã hoá và kết nối an toàn: TLS, chi phí của nó, và những gì nó không bảo vệ.