Chuyển đến nội dung
Làm cứng cho production và lớp bảo vệ RLS

Làm cứng cho production và lớp bảo vệ RLS

Tổng quan

Khép lại roadmap: thêm row-level security của Postgres làm lớp bảo vệ chiều sâu (defence-in-depth) dưới scoped store (không bao giờ thay cho nó), chạy một đợt rà soát bảo mật theo các rủi ro OWASP API, chứng minh hành vi dưới tải và soak, thêm CI bằng GitHub Actions với một Postgres service, tạo một bản release build có đóng dấu, và viết hướng dẫn deploy cùng tài liệu kiến trúc cuối cùng.

Phase này là **một đợt refactor tường minh**, không phải một migration cắm vào là chạy. Bật `FORCE ROW LEVEL SECURITY` với một policy dựa trên một GUC của session khiến mọi query không đặt GUC đó trả về không dòng nào và làm hỏng mọi insert. Vì vậy kế hoạch có ba phần phải cùng đáp xuống: (1) một **system role có `BYPASSRLS`** trên một connection pool thứ hai, nhỏ, dùng cho các đường đi hợp lệ chạy trước hoặc ngoài một tenant scope — tra cứu API key, lượt quét reclaim run, poller run đang queued, sweeper hết hạn approval, migration và kiểm tra readiness; (2) **mọi query store có tenant scope được chuyển vào bên trong một `WithTenantTx` ngắn** phát lệnh `SET LOCAL hango.tenant_id`; (3) bất biến rằng **không `WithTenantTx` nào được trải qua một lời gọi LLM hay một tool call** — transaction mở và đóng quanh một bước lưu duy nhất, đúng như phase 5 đã yêu cầu.

Yêu cầu

Chức năng

  • RLS được bật và ép (forced) trên mọi bảng thuộc về tenant, với các policy USING (tenant_id = current_setting('hango.tenant_id')::uuid).
  • Một application role không phải owner (hango_app) không sở hữu bảng nào, để các policy không thể bị vượt qua; FORCE ROW LEVEL SECURITY trên mọi bảng để ngay cả một kết nối owner bị nhầm cũng được che chắn.
  • Một role thứ hai, `hango_system`, có `BYPASSRLS`, được dùng bởi một **pool nhỏ riêng** (`HANGO_SYSTEM_DB_MAX_CONNS`, mặc định 4). Đúng năm vị trí gọi được phép dùng nó, được liệt kê trong code và được một test khẳng định rằng không package nào khác giữ tham chiếu tới nó: `auth.ResolveKey` (chạy trước khi biết tenant), lượt quét reclaim run, poller run đang queued, sweeper hết hạn approval, và migration/readiness. Mọi thứ còn lại dùng pool `hango_app`.
  • `store.WithTenantTx(ctx, fn)`: bắt đầu một transaction trên pool `hango_app`, phát `SET LOCAL hango.tenant_id = $1` từ scope trong context, chạy callback với một scoped store trên transaction đó, rồi commit. **Mọi lời gọi store có tenant scope đều chạy bên trong một cái như vậy**; không có đường "query pool thường vẫn chạy được", vì dưới RLS `FORCE` với GUC chưa đặt thì không có đường nào như thế. Phần thân các method của scoped store không đổi — đợt refactor chỉ dời nơi chúng được gọi, nên các predicate `tenant_id` tường minh của chúng vẫn là lớp cô lập đầu tiên.
  • Quy tắc cứng, có test: một `WithTenantTx` không bao giờ bọc một lời gọi provider, một tool call hay một lần chạy subagent. Helper nhận một callback mà việc duy nhất của nó là lưu dữ liệu; một test kiểu lint làm fail nếu `WithTenantTx` xuất hiện trong cùng một hàm với `provider.` hoặc `Chain.ToolCall`.
  • `audit_events` nằm trong danh sách RLS như mọi bảng thuộc tenant khác, và policy của nó chỉ cấp cho role `hango_app` quyền `SELECT` và `INSERT` — không `UPDATE`, không `DELETE` — để tính append-only được chính database thực thi chứ không chỉ package store. Không lần ghi audit nào cần system pool: việc tạo tenant insert dòng `tenants` (không thuộc tenant nên không chịu RLS), rồi ghi user owner, key đầu tiên và dòng audit `tenant.create` bên trong `WithTenantTx(newTenantID)`. Bootstrap ghi user và key của platform bên trong `WithTenantTx(platformTenantID)`, và các route platform thao tác trên tenant X (`GET /v1/admin/usage?tenant_id=`, cập nhật quota) chạy bên trong `WithTenantTx(X)`. Đó là điều giữ system pool ở đúng năm nơi gọi.
  • Retention: v1 không xóa gì khỏi `audit_events`. `docs/deployment-guide.md` nêu tốc độ tăng trưởng theo mỗi request có thay đổi dữ liệu và ghi lại việc theo sau — chia partition theo tháng hoặc một job archive-rồi-detach — như con đường dự kiến khi khối lượng đủ lớn để biện minh. Âm thầm cắt xén một audit trail là thao tác duy nhất mà thiết kế này từ chối làm cho dễ dàng.
  • Một test xác minh rằng RLS thực sự chặn được: khi predicate WHERE tenant_id của scope bị cố ý bỏ đi bằng một query thô chỉ dùng trong test, việc đọc chéo tenant vẫn trả về không dòng nào.
  • Checklist rà soát bảo mật bao phủ OWASP API Security Top 10 áp dụng cho hango, mỗi mục được đánh dấu là đã xử lý, chấp nhận hay ngoài phạm vi, kèm bằng chứng cho từng mục.
  • Load và soak là **các Go test**, không phải các chương trình driver tự chế: `TestConcurrentRuns` (trong `internal/run`) chạy N run đồng thời với fake provider và chạy hai lần, với policy RLS tắt và bật, so p95 với một **ngân sách được ghi ra trước phép đo đầu tiên** trong `docs/deployment-guide.md` (ngân sách ban đầu: p95 độ trễ của run dưới 2s ở mức đồng thời 32 với fake provider, RLS cộng thêm không quá 10%). Soak là cùng thân test đó nhưng điều khiển theo thời lượng (`-count` hoặc `HANGO_SOAK_DURATION`), chạy workload hỗn hợp (compaction, subagent, approval). Không có `scripts/loadtest`, không có `scripts/soaktest`, không có metrics exporter tự chế — các test dùng các khẳng định test thông thường và telemetry sẵn có.
  • Khẳng định profile `goroutineleak` của Go 1.27 chỉ nằm **trong soak test**, nơi một tiến trình sống lâu làm nó có ý nghĩa. Các bộ unit và integration dùng khẳng định số goroutine và số topic của broadcaster trước/sau.
  • CI bằng GitHub Actions: build, vet, golangci-lint, unit test, và integration test với một Postgres service container, cộng một bước build Docker image.
  • Release build: -trimpath, strip, đóng dấu version/commit/date, các cờ tái lập được ghi lại; hango -version in cả ba.
  • docs/deployment-guide.md và một docs/system-architecture.md cuối cùng, chính xác.

Phi chức năng

  • Bật RLS không được làm tụt độ trễ đáng kể: mọi index thuộc tenant đã bắt đầu bằng tenant_id, và load test so p95 trước và sau khi bật policy.
  • Pool không bao giờ được giữ một kết nối qua một lời gọi provider, điều này được load test kiểm chứng lại với một pool cố ý để nhỏ.
  • Không có secret nào trong image, file compose, log CI hay bất kỳ attribute telemetry nào; một test và một bước CI kiểm tra các mẫu dễ thấy.

Kiến trúc

// internal/store/pg/rls.go
func (s *pgStore) WithTenantTx(ctx context.Context, fn func(store.Scoped) error) error
// BEGIN; SET LOCAL hango.tenant_id = $1; ...; COMMIT   (SET LOCAL only, never SET)
// Red team: F1 — never spans a provider call, a tool call or a subagent run.

// Red team: F1 — the BYPASSRLS pool, with a deliberately tiny max size and five allowed callers.
func (s *pgStore) System() SystemQuerier // auth lookup, reclaim sweep, queued poller,
                                         // approval sweeper, migrations/readiness

// internal/store/pg/rls_test.go
// test-only raw querier that omits the tenant predicate, proving the policy is load-bearing
-- migrations/00011_rls.sql
-- one block per tenant-owned table: tenants' children, users, api_keys, agents, sessions,
-- messages, runs, run_events, agent_files, subagent_calls,
-- usage_records, tenant_quotas, pending_approvals   -- Red team: D1 — no provider_credentials in v1
-- audit_events                                      -- Updated: Validation Session 1 - audit everything
alter table agents enable row level security;
alter table agents force row level security;
create policy agents_tenant_isolation on agents
  using (tenant_id = current_setting('hango.tenant_id', true)::uuid)
  with check (tenant_id = current_setting('hango.tenant_id', true)::uuid);
-- repeated for every tenant-owned table; `true` in current_setting means a missing GUC
-- yields NULL, and NULL = uuid is false, so the default is deny.

-- application roles: hango_app (no ownership, RLS applies) and hango_system (BYPASSRLS)
-- Red team: F1 — both documented in docs/deployment-guide.md and created by an operator, not by
-- a migration, because role management needs superuser and differs per environment.
-- The compose file creates both so development matches production.

Instrumentation

  • Span INTERNAL store.tenant_tx với hango.tenant.id và hango.rls.enabled, để thấy được những lần đọc nào chạy dưới RLS.
  • Metric: counter hango.rls.denials tăng khi một query trả về không dòng nào một cách bất ngờ trong lúc GUC đã được đặt (một canary cho cấu hình policy sai), histogram hango.db.pool.acquire_duration và gauge hango.db.pool.in_use lấy từ thống kê của pgx pool, gauge hango.build.info mang version, commit và date làm attribute.
  • Các load và soak test không tự phát gì cả: chúng chạy trên một server mà telemetry thông thường đã bật sẵn, nên các dashboard của phase 14 là nơi đọc kết quả và các khẳng định của test là cổng chặn.
  • `hango.rls.denials` được thêm một alert rule và một panel vào `hango-api` trong phase này, để một GUC cấu hình sai được nhìn thấy thay vì âm thầm trả về các danh sách rỗng.
  • Các alert rule từ phase 14 được kích hoạt có chủ đích trong lúc chạy soak test để xác nhận chúng kêu.

Các file liên quan

Tạo mới

  • migrations/00011_rls.sql.
  • internal/store/pg/rls.go, internal/store/pg/rls_test.go.
  • internal/store/pg/poolstats.go — bộ xuất metric của pool.
  • internal/telemetry/buildinfo.go — gauge hango.build.info.
  • internal/run/load_test.go — TestConcurrentRuns (RLS tắt/bật) và TestSoak (điều khiển theo thời lượng, workload hỗn hợp, khẳng định profile goroutineleak).
  • .github/workflows/ci.yaml, .github/workflows/release.yaml.
  • docs/security-review.md — checklist OWASP API kèm bằng chứng.
  • docs/deployment-guide.md.
  • docs/learning/16-hardening-and-rls.md.

Sửa

  • internal/store/pg/store.go — phơi ra WithTenantTx và system pool; app pool kết nối bằng hango_app.
  • Mọi vị trí gọi có tenant scope trong `internal/server/handlers_*.go`, `internal/run/worker.go`, `internal/agent/mw*.go` và `internal/fs/pgbackend.go` — được bọc trong `WithTenantTx`. Đây là phần lớn công việc của phase và là lý do effort của nó cao hơn một phase chỉ có migration.
  • internal/auth, internal/run/reconcile.go, internal/run/poller.go, internal/run/approval_sweeper.go — chuyển sang system pool.
  • internal/config/config.go — HANGO_DB_RLS_ENABLED, HANGO_SYSTEM_DATABASE_URL, HANGO_SYSTEM_DB_MAX_CONNS, HANGO_VERSION/các dấu build.
  • internal/server/health.go — một kiểm tra readiness khẳng định RLS đã bật trên mọi bảng mong đợi khi HANGO_DB_RLS_ENABLED=true, chạy trên system pool.
  • deploy/grafana/provisioning/alerting.yaml, deploy/grafana/dashboards/hango-api.json — alert và panel cho RLS denial.
  • Makefile — loadtest và soaktest trở thành các wrapper mỏng quanh go test -run TestConcurrentRuns / -run TestSoak; cộng release, security-scan.
  • Dockerfile — các cờ đóng dấu cuối cùng; xác minh nonroot và không có shell.
  • compose.yaml — tài liệu về app role và một ví dụ DSN không phải owner.
  • docs/system-architecture.md — lượt rà soát cuối bao phủ mọi phase; docs/learning/README.md; README.md.

Các bước triển khai

  1. Viết migrations/00011_rls.sql bật và ép RLS với một policy cho mỗi bảng thuộc tenant, dùng current_setting('hango.tenant_id', true) để một GUC thiếu sẽ từ chối thay vì báo lỗi. Liệt kê mọi bảng được thêm ở các phase 2-15 và đối chiếu với bảng test chéo tenant để không sót bảng nào.
  2. Viết internal/store/pg/rls.go với WithTenantTx dùng SET LOCAL trong một transaction tường minh, cộng pool hango_system và danh sách cho phép năm nơi gọi của nó. Thêm comment nêu vì sao SET thường bị cấm (nó tồn tại suốt đời của kết nối trong pool và làm rò ngữ cảnh của tenant trước đó) và vì sao system pool phải giữ ở cỡ rất nhỏ.
  3. Thực hiện refactor: chuyển **mọi** lời gọi store có tenant scope vào bên trong một `WithTenantTx`, và chuyển năm đường hệ thống sang system pool. Làm từng package một và giữ integration suite xanh sau mỗi package, vì một vị trí gọi chuyển đổi dở dang dưới RLS `FORCE` trả về kết quả rỗng thay vì báo lỗi, đó là kiểu lỗi ẩn mình. Thêm test kiểu lint cấm `WithTenantTx` trong cùng một hàm với một lời gọi provider hoặc tool.
  4. Viết internal/store/pg/rls_test.go: một querier chỉ dùng trong test phát SQL thô không có predicate tenant, chạy một lần với GUC đặt thành tenant A và một lần không đặt, khẳng định không có dòng chéo tenant nào trong cả hai trường hợp. Test này là bằng chứng rằng RLS có sức nặng thật chứ không phải để trang trí.
  5. Ghi lại cả hai role trong docs/deployment-guide.md (hango_app: không sở hữu, RLS áp dụng; hango_system: BYPASSRLS, chỉ dùng cho năm đường đã liệt kê, với DSN riêng và một pool rất nhỏ) và cập nhật compose.yaml để tạo và dùng cả hai, để môi trường dev khớp với production.
  6. Viết internal/store/pg/poolstats.go và internal/telemetry/buildinfo.go.
  7. Viết docs/security-review.md theo OWASP API Security Top 10, với bằng chứng riêng của hango cho từng mục: broken object-level authorization (scoped store cộng test chéo tenant theo từng bảng cộng RLS), broken authentication (key SHA-256, so sánh thời gian hằng, revoke, hết hạn), broken object property-level authorization (tầng DTO, khả năng hiển thị giới hạn theo người tạo với member, không secret nào xuất hiện trong response), unrestricted resource consumption (quota, rate limit, limiter trước xác thực, giới hạn iteration, timeout của tool, deadline của run, giới hạn kích thước file, các cận của buffer SSE và giai đoạn replay), broken function-level authorization (ma trận phân quyền theo từng route, gồm cả việc không tự duyệt run của chính mình), unrestricted access to sensitive business flows (chặn bằng approval, ghi đặc quyền vào /memories/ và /skills/), SSRF (allowlist của http_fetch cộng việc từ chối IP riêng sau khi resolve, và kiểm tra https-hoặc-loopback đối với base URL của provider do operator đặt), security misconfiguration (distroless nonroot, không có shell, HANGO_TRACE_LLM_CONTENT mặc định tắt, collector loại bỏ), inventory management (route /v1 có version, hango -version, danh sách migration), unsafe consumption of third-party APIs (timeout của provider, giới hạn retry, lỗi có kiểu, kiểm tra schema đối số của tool). Bằng chứng của mỗi mục trích dẫn các action audit_events làm cho nó quan sát được sau sự việc, và checklist ghi lại rằng trail này append-only ở mức database và không có job retention nào ở v1.
  8. Ghi ngân sách hiệu năng vào `docs/deployment-guide.md` **trước khi** đo, rồi viết `TestConcurrentRuns`: tạo một tenant, agent và N session; chạy `HANGO_LOAD_CONCURRENCY` run đồng thời qua fake provider có tool call; khẳng định p50/p95/p99 so với ngân sách, không có lỗi, không tăng goroutine và mức dùng pool bị chặn trên; chạy cả thân test hai lần, với policy RLS tắt và bật, và khẳng định chi phí phụ của RLS nằm trong mức cho phép đã ghi.
  9. Viết `TestSoak` như cùng bộ khung đó điều khiển bằng `HANGO_SOAK_DURATION` với workload hỗn hợp (các session dài kích hoạt compaction, giao việc cho subagent, approval do test quyết định), khẳng định heap và goroutine ổn định và không có run nào bị mất hay bị kẹt. Nó bị bỏ qua dưới `-short` và trong CI thông thường.
  10. Đặt khẳng định profile `goroutineleak` của Go 1.27 chỉ trong `TestSoak`; `internal/run`, `internal/agent` và `internal/server` giữ các khẳng định số goroutine và số topic trước/sau đã được đưa vào ở phase 6.
  11. Viết .github/workflows/ci.yaml: một job duy nhất không có matrix trên ubuntu-latest với một service postgres:17, các bước go build ./..., go vet ./..., make sqlc-check (code query được sinh ra không bị cũ), golangci-lint-action v2, go test ./... -race, và một docker build. Viết .github/workflows/release.yaml build binary và image có đóng dấu khi có tag.
  12. Hoàn thiện việc đóng dấu trong Dockerfile và thêm make release tạo ra hango với version, commit và date; hango -version in cả ba.
  13. Viết docs/deployment-guide.md: các biến môi trường bắt buộc và tùy chọn (trỏ tới .env.example thay vì lặp lại), các role hango_app và hango_system cùng việc thiết lập RLS, chính sách migration, ngân sách hiệu năng, ràng buộc single-replica và những gì phải đổi trước khi scale ngang (lưu ý rằng lease sở hữu run từ phase 5 đã an toàn cho nhiều replica), lựa chọn backend telemetry (managed, không phải lgtm), backup và restore, và hành vi shutdown/drain mà operator phải cấu hình timeout xung quanh — gồm quy tắc HANGO_DRAIN_TIMEOUT phải lớn hơn một turn.
  14. Lượt rà soát cuối docs/system-architecture.md: các package, mô hình dữ liệu với mọi bảng, các luồng request và run, thứ tự middleware stack kèm lý do, mô hình tenancy với cả hai lớp, pipeline observability, topology deploy, và các giới hạn đã được ghi lại.
  15. Thêm alert rule và panel cho RLS denial vào các file provisioning của phase 14.
  16. Chạy tất cả: toàn bộ gate, -race, integration test với RLS bị ép, make loadtest, make soaktest, make obs-verify, một docker compose up sạch từ volume rỗng, và một release build có tag.

Dàn ý tài liệu học

docs/learning/16-hardening-and-rls.md phải có:

  • Khái niệm: defence in depth — vì sao RLS được thêm dưới scoped store thay vì thay thế nó, và mỗi lớp bắt được loại lỗi nào.
  • Cơ chế RLS và các góc cạnh sắc: SET so với SET LOCAL và việc rò rỉ trên kết nối trong pool, table-owner bypass và FORCE ROW LEVEL SECURITY, current_setting(..., true) để GUC thiếu thì từ chối, mọi index đều cần tenant_id đứng đầu nếu không policy sẽ thoái hóa thành sequential scan, và vì sao giữ một transaction qua một lời gọi LLM là không chấp nhận được.
  • Lựa chọn thiết kế: policy trên mọi bảng thuộc tenant, một app role không phải owner, một system role BYPASSRLS rất nhỏ cho năm đường đi hợp lệ đứng trước tenant scope, WithTenantTx là đường duy nhất đặt GUC, và test đối kháng có chủ đích bỏ predicate để chứng minh policy hoạt động.
  • Vì sao "bật RLS" là một refactor chứ không phải một migration: dưới RLS `FORCE` một query chưa chuyển đổi trả về không dòng nào thay vì thất bại, nên toàn bộ integration suite xanh dưới các policy bị ép là phép nghiệm thu trung thực duy nhất, và đường auth — chạy trước khi biết bất kỳ tenant nào — phải được thiết kế riêng một cách tường minh.
  • Đánh đổi: RLS ở mọi nơi so với chỉ trên các bảng rủi ro cao; đọc trong phạm vi transaction so với đọc qua pool; schema-per-tenant và database-per-tenant là các phương án thay thế hango đã bác bỏ và vì sao.
  • Vì sao các driver load và soak là Go test: một driver tự chế là code không ai chạy, và một ngân sách bịa ra sau phép đo đầu tiên thì không phải là ngân sách.
  • Sẵn sàng cho production: các load và soak test thực sự chứng minh được gì và không thể chứng minh được gì ở single replica.
  • So sánh với goclaw: không có RLS nào qua 160 migration, dựa hoàn toàn vào QueryScope cộng một visibility_filter.go để vá các chỗ rò, đó là bằng chứng cụ thể cho cách tiếp cận hai lớp của hango.
  • So sánh với deepagents: không có database, không có tenancy, không có câu chuyện deploy — toàn bộ phase này là công việc server mà phần nghiên cứu harness không thể soi sáng.
  • Cách debug tầng này: hango.rls.denials là canary cho một GUC hoặc policy cấu hình sai; span store.tenant_tx cho thấy những lần đọc nào chạy dưới RLS; metric của pool cộng các dashboard load-test cho thấy một sự thụt lùi là do tranh chấp hay do chi phí của policy; log CI cộng hango -version trên một instance đang chạy trả lời “chính xác cái gì đang được deploy”.
  • Bài tập: chạy test RLS khi đã bỏ các policy và xem nó fail; kết nối bằng table owner không có FORCE và quan sát việc bypass; trỏ một vị trí gọi đã chuyển đổi về pool thường và xem nó trả kết quả rỗng thay vì lỗi; so p95 của load test có và không có policy.
  • Ngân sách thời gian: 13h cài đặt + 3h tài liệu học (nó đồng thời là ghi chú vận hành RLS).

Tiêu chí thành công

  • go build ./... && go vet ./... && go test ./... -race đều qua; golangci-lint run sạch.
  • RLS được bật và ép trên mọi bảng thuộc tenant; một test liệt kê các bảng từ danh sách migration và khẳng định không bảng nào bị thiếu.
  • Toàn bộ integration suite xanh khi RLS được bật và ép. Đây là tiêu chí chứng minh đợt refactor đã đáp xuống: dưới RLS FORCE một vị trí gọi chưa chuyển đổi trả về kết quả rỗng thay vì báo lỗi, nên không gì ngoài toàn bộ suite bắt được nó.
  • Đường xác thực được thử sau RLS: một request với API key hợp lệ xác thực thành công khi policy bị ép (nó chạy trên pool hango_system), và một test khẳng định ResolveKey là query duy nhất của đường auth dùng pool đó.
  • Một test khẳng định chỉ năm vị trí gọi đã liệt kê tham chiếu tới system pool, và không hàm nào chứa một WithTenantTx đồng thời thực hiện một lời gọi provider hay tool.
  • audit_events mang một RLS policy như mọi bảng thuộc tenant khác, và một test khẳng định UPDATE và DELETE của role hango_app trên nó bị database từ chối; docs/deployment-guide.md nêu việc theo sau về retention.
  • Test RLS đối kháng (query thô không có predicate tenant) trả về không dòng chéo tenant nào cả khi GUC đặt thành tenant khác lẫn khi không đặt.
  • /readyz thất bại khi HANGO_DB_RLS_ENABLED=true và một policy bị thiếu.
  • go test -run TestConcurrentRuns ở mức đồng thời đã ghi cho thấy không tăng goroutine, mức dùng pool bị chặn trên, không mất run nào, và p95 nằm trong ngân sách đã ghi từ trước, cả có và không có RLS.
  • go test -run TestSoak cho thấy heap và số goroutine ổn định suốt thời lượng và không có run nào bị kẹt.
  • Khẳng định profile goroutineleak chạy trong TestSoak; các bộ còn lại khẳng định số goroutine và số topic trở về mức cơ sở.
  • CI xanh trên một pull request: build, vet, lint, unit và integration test với Postgres, build Docker.
  • hango -version in version, commit và date từ release build; hango.build.info mang cùng các giá trị.
  • docs/security-review.md có một quyết định và bằng chứng cho cả mười mục OWASP API.
  • docs/deployment-guide.md và docs/system-architecture.md chính xác so với code cuối cùng, và docs/learning/README.md lập chỉ mục cả mười sáu tài liệu học.
  • Mọi tiêu chí nghiệm thu trong plan.md được đánh dấu hoàn thành kèm bằng chứng.

Đánh giá rủi ro

Rủi roKhả năng x Tác độngTín hiệuCách xử lý đã định sẵn
RLS bị bypass âm thầm vì app kết nối bằng table ownerCao x Nghiêm trọngTest đối kháng qua nhưng vì lý do saiFORCE ROW LEVEL SECURITY cộng một role hango_app không phải owner; hướng dẫn deploy bắt buộc role này và /readyz kiểm tra các policy tồn tại
Dùng SET thay vì SET LOCAL làm rò ngữ cảnh tenant qua các request trong poolTrung bình x Nghiêm trọngĐọc chéo tenant dưới tảiWithTenantTx là nơi duy nhất đặt GUC, nó luôn dùng SET LOCAL trong một transaction tường minh, và một test khẳng định GUC rỗng trên một kết nối vừa được lấy mới
Bật RLS âm thầm làm rỗng mọi queryCao x Nghiêm trọngEndpoint trả danh sách rỗng, auth trả 401 cho key hợp lệĐợt refactor sang WithTenantTx cộng system pool BYPASSRLS đáp xuống cùng phase với migration, từng package một, và toàn bộ integration suite xanh dưới RLS bị ép là cổng chặn
Một WithTenantTx rốt cuộc bọc một lời gọi LLMTrung bình x Nghiêm trọngCạn pool ở mức đồng thời thấp, transaction bị giữ lâuTest kiểu lint cấm lời gọi provider hay tool bên trong một hàm giữ WithTenantTx, cộng load test với pool nhỏ
Pool BYPASSRLS trở thành lối thoát chungTrung bình x Nghiêm trọngCode mới dùng system pool cho dữ liệu của tenantNăm vị trí gọi được liệt kê, kích thước pool tối đa rất nhỏ, và một test khẳng định không package nào khác tham chiếu tới nó
RLS biến các lần đọc có index thành sequential scanTrung bình x Caop95 của load test thụt lùi sau khi bật policyMọi index thuộc tenant đã bắt đầu bằng tenant_id; phép so p95 trước/sau là một tiêu chí nghiệm thu, và đầu ra EXPLAIN của các query nóng được ghi trong tài liệu học
Một transaction bị giữ qua một lời gọi LLM làm cạn poolThấp x Nghiêm trọngPool bão hòa ở mức đồng thời thấpQuy tắc kiến trúc từ phase 5, được load test kiểm chứng lại với một pool cố ý để nhỏ
Một bảng thuộc tenant được thêm sau này bị thiếu policyTrung bình x Nghiêm trọngBảng mới không có RLSTest RLS liệt kê các bảng từ các migration và fail với bất kỳ bảng thuộc tenant nào không có policy, nên cổng chặn này là vĩnh viễn
Integration test của CI chập chờn trên Postgres serviceTrung bình x ThấpBuild đỏ thất thườngHealth check của service cộng một bước chờ readiness; testcontainers dùng ở máy local và service container trong CI, với cùng đường migration
Secret rò vào image hoặc log CIThấp x Nghiêm trọngMột key hiện trong log hoặc layer.dockerignore loại các file env, một bước CI grep các mẫu key, và telemetry không bao giờ mang credential

Rollback

goose down một bước vô hiệu hóa và xóa các RLS policy, để scoped store là lớp cô lập duy nhất (đã được test). Đợt refactor WithTenantTx vẫn giữ nguyên sau khi rollback policy — nó đúng dù có RLS hay không — nên rollback là một bước migration duy nhất mà không phải revert code. Các artifact CI, release, load và soak là phần cộng thêm và có thể gỡ mà không đụng vào code runtime.