Multi-tenant Architecture: Row-Level Security, data isolation — Từ hy vọng sang thực thi
Row-Level Security (RLS) biến tenant isolation từ 'hy vọng' trong code thành thực thi trong engine, ngăn chặn data leak chỉ bằng một dòng config quên lọc.
Khi agent của bạn phục vụ 100 doanh nghiệp khác nhau trên cùng một database, một lỗi WHERE tenant_id = ? trong JOIN phức tạp là đủ để khách hàng A nhìn thấy dữ liệu của khách hàng B. Không phải vì lỗi logic nghiêm trọng, mà chỉ vì developer quên thêm điều kiện lọc ở một dòng code. Row-Level Security (RLS) chính là lớp phòng thủ cuối cùng biến "hy vọng không quên" thành "bất khả thi để lộ".
Vấn đề
Mô hình multi-tenant truyền thống dựa trên "hy vọng-based security": bạn hy vọng developer nhớ thêm WHERE tenant_id = $1 vào mọi câu query, mọi stored procedure, mọi JOIN và subquery. Trong thực tế:
- Context switching: Developer chuyển từ tính năng A sang B, copy-paste query và quên sửa điều kiện lọc
- AI-generated SQL: Agent sử dụng tool để tự động generate truy vấn, không có "muscle memory" về tenant isolation
- Report queries: Câu query báo cáo phức tạp với 5 bảng JOIN dễ bỏ sót predicate ở một bảng con
- Data leak cascade: Một dòng quên lọc trong query log analytics có thể expose toàn bộ CRM data của tất cả tenants
Khi leak xảy ra, không phải lỗi của một người mà là lỗi của kiến trúc: chúng ta để quyền quyết định ai được xem dữ liệu nằm ở tầng application, nơi con người mắc lỗi.
Ý tưởng cốt lõi
RLS (Row-Level Security) chuyển nhiệm vụ isolation từ application code xuống database engine. Đây là sự thay đổi từ "perimeter defense" (canh gác cổng) sang "ambient reality" (môi trường tự động phân cách).
Ba mô hình multi-tenant
Trước khi đi sâu RLS, cần phân biệt 3 kiến trúc:
| Mô hình | Mô tả | RLS phù hợp? |
|---|---|---|
| Silo | Mỗi tenant một database riêng biệt | Không cần (đã cách ly vật lý) |
| Bridge | Shared database, schema riêng cho mỗi tenant | Không cần RLS row-level (dùng schema isolation) |
| Pool | Shared database, shared schema, tenant_id column | RLS bắt buộc - đây là mô hình scalable nhất |
RLS tỏa sáng ở mô hình Pool - nơi bạn muốn economies of scale nhưng không trade-off security.
Cơ chế "vũ trụ song song"
RLS hoạt động bằng cách biến tenant_id từ một label (metadata) thành một dimension (chiều không gian dữ liệu):
- Tenant Discriminator Column: Mỗi bảng có cột
TenantId(UUID hoặc integer) xác định chủ sở hữu row - Security Policies: Database policies attach predicates vào tables:
- FILTER predicate (SELECT): Chỉ trả về rows có
TenantIdkhớp session context - BLOCK predicate (INSERT/UPDATE): Chặn write vào row không thuộc về tenant hiện tại
- FILTER predicate (SELECT): Chỉ trả về rows có
- Session Context: Application set
SESSION_CONTEXT(Azure SQL) hoặcSET app.current_tenant(PostgreSQL) ngay sau authentication - Engine-Level Enforcement: Query planner tự động append predicate vào execution plan, trước cả khi data được đọc từ disk
Ví dụ PostgreSQL:
-- Bước 1: Enable RLS trên bảng chứa PII
ALTER TABLE customer_orders ENABLE ROW LEVEL SECURITY;
-- Bước 2: Tạo policy "mỗi tenant chỉ thấy của mình"
CREATE POLICY tenant_isolation ON customer_orders
USING (tenant_id = current_setting('app.current_tenant')::UUID);
-- Bước 3: Application set context ngay sau khi xác thực JWT
SET app.current_tenant = 'tenant-abc-123';Ví dụ Azure SQL:
-- Tạo security predicate function
CREATE FUNCTION fn_securitypredicate(@TenantId INT)
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN SELECT 1 AS fn_accessResult
WHERE @TenantId = CAST(SESSION_CONTEXT(N'TenantId') AS INT);
-- Tạo security policy
CREATE SECURITY POLICY TenantFilterPolicy
ADD FILTER PREDICATE dbo.fn_securitypredicate(TenantId)
ON dbo.CustomerData,
ADD BLOCK PREDICATE dbo.fn_securitypredicate(TenantId)
ON dbo.CustomerData AFTER INSERT
WITH (STATE = ON, SCHEMABINDING = ON);"Aha" moment: Không phải filter, mà là đa vũ trụ
Điểm then chốt: RLS không phải là "lọc data sau khi đọc". Query planner tái cấu trúc không gian truy vấn - khi Tenant A kết nối, các rows của Tenant B literally không tồn tại trong không gian truy cập của họ. Database engine tối ưu indexes, JOIN orders, và partition pruning dựa trên assumption rằng chỉ có data của tenant hiện tại là visible.
Điều này tạo ra fail-closed architecture: nếu application quên set SESSION_CONTEXT, policy mặc định trả về empty set (hoặc error tùy config), thay vì expose toàn bộ table.
Tại sao nó hoạt động
Logic then chốt nằm ở separation of concerns giữa authorization và business logic:
- Application layer: Quyết định ai là tenant hiện tại (authentication, JWT parsing)
- Database layer: Quyết định dữ liệu nào có thể tồn tại trong query result (authorization)
Điều này tạo ra defense in depth thực sự: ngay cả khi attacker bypass application và connect trực tiếp vào DB (insider threat, compromised connection string), họ vẫn chỉ thấy data của tenant được set trong session context của họ.
Trade-off: Performance vs Safety
RLS thêm overhead vào query planning. Nếu thiếu index trên TenantId, engine buộc phải full table scan vì predicate áp dụng trước other filters. Tuy nhiên, đây là trade-off correctness: chậm nhưng đúng vẫn hơn nhanh nhưng leak data.
Debug complexity: Khi user báo "không thấy data", developer phải kiểm tra cả application logic (có set context đúng không) và RLS policy (có chặn nhầm không). Đây là chi phí operational để đổi lấy security guarantee.
Ý nghĩa thực tế
So sánh: Hy vọng vs Thực thi
| Tiêu chí | Application-Level Filter | Row-Level Security |
|---|---|---|
| Failure mode | Fail-open (quên lọc = expose all) | Fail-closed (thiếu context = empty set) |
| AI Agent safety | Agent generate SQL có thể bỏ WHERE clause | Agent generate SQL bị engine giới hạn scope |
| Audit compliance | Khó prove rằng tất cả queries đều filter | Policy là single source of truth, auditable |
| Developer cognitive load | Phải nhớ context mọi lúc | Set context 1 lần sau auth, quên cũng an toàn |
| Cross-tenant analytics | Cần explicit UNION hoặc ETL | Dùng superuser hoặc bypass policy cho reporting role |
AI Agent context: Khi agent có "quyền" query
Trong kiến trúc GoClaw/OpenClaw, agent có thể dùng tool để query database. Nếu tool đó connect với superuser privileges, prompt injection có thể khiến agent chạy SELECT * FROM users và expose toàn bộ tenant. Với RLS:
- Tool connection dùng scoped connection (set tenant context từ JWT của user đang chat với agent)
- Agent dùng tool → Tool generate SQL → Database engine apply policy → Agent chỉ thấy data của user đó
- Prompt injection attack: Agent bị ép chạy
SELECT * FROM sensitive_table→ Policy vẫn filter, chỉ trả về rows thuộc về conversation owner
Đây là architectural defense chống lại cả lỗi lập trình và prompt injection.
Hạn chế cần biết
- Context confusion: Nếu application bug set wrong
TenantIdtrong session context, RLS sẽ "bảo vệ" bạn bằng cách trả về data của tenant khác (nhưng vẫn là wrong data, không phải leak) - Schema rigidity: Khó làm tenant-specific schema customization (column thêm cho tenant A nhưng không cho B) trong shared table model
- Superuser bypass: Users with
BYPASSRLSprivilege (PostgreSQL) hoặcCONTROLpermission (Azure SQL) thấy toàn bộ data - cần strict RBAC cho DBAs - Backup/Restore: Dump file chứa toàn bộ rows mọi tenant - cần mã hóa hoặc logical backup với tenant filter
Đào sâu hơn
5-layer Security
Kiến trúc defense-in-depth từ rate limiting đến encryption, bảo vệ agent runtime toàn diện
Agent Permission Model
Tách biệt "muốn làm" và "được phép làm" - kiến trúc quyền hạn cho AI agent
Compliance & Audit
Logging mọi action, GDPR data retention, và audit trail cho multi-tenant systems
Đọc tiếp
Hooks & Quality Control
Giám sát và kiểm soát chất lượng agent qua lifecycle hooks trước khi triển khai production
Production Deployment
Triển khai multi-tenant agent platform từ development lên staging và production
Tài liệu tham khảo
- PostgreSQL Row-Level Security: Official Documentation - CREATE POLICY syntax và performance tuning
- Azure SQL Row-Level Security: Microsoft Docs - SESSION_CONTEXT và security predicates
- AWS Multi-Tenant SaaS: SaaS Factory Reference Architecture - Patterns cho Silo/Bridge/Pool models
- Paper: "A Systematic Taxonomy of Security Vulnerabilities in the OpenClaw" (2025) - Phân tích SSRF và injection trong multi-tenant agent environments
5-layer Security: Defense-in-depth từ Rate limiting đến Encryption — kiến trúc bảo vệ Agent runtime
Kiến trúc 5 lớp bảo mật cho AI Agent: Rate limiting chống brute-force, Injection detection phân tích AST, SSRF protection chặn metadata IP, Shell sanitizatio...
Agent Permission Model: Tách 'muốn làm' khỏi 'được phép làm'
Mô hình phân quyền agent tách biệt ý định LLM (muốn làm) khỏi lớp thực thi (được phép làm), ngăn chặn prompt injection và confused deputy attacks.