gbrain Row Level Security(RLS)实战指南:让每个 public 表默认拒绝 anon 读取
【免费下载链接】gbrainGarry's Opinionated OpenClaw/Hermes Agent Brain项目地址: https://gitcode.com/gh_mirrors/gb/gbrain
本篇指南以 gbrain 官方文档 docs/guides/rls-and-you.md 为骨架,结合 doctor 检查实现、v35 schema 迁移 与 schema.sql 基线 源码,系统讲解 gbrain 的 RLS 安全模型:为什么public模式中每一张表都必须开启行级安全、gbrain doctor报错后如何修复、自动 RLS 事件触发器与一次性回填的运作原理,以及"刻意让某张表对 anon key 可读"的唯一官方逃生通道GBRAIN:RLS_EXEMPT注释契约。读完你将能独立处理 Supabase / 自托管 Postgres 场景下与 RLS 相关的全部运维与合规问题。
为什么 RLS 如此重要:anon key 就是事实上的公开凭据
简短版本:gbrain 的public模式中每一张表都必须开启 Row Level Security(RLS)。如果有一张表没开,gbrain doctor的检查会从"警告"升级为"失败",进程以退出码 1 结束。
深层原因在于 gbrain 的典型部署形态:
- Supabase 通过 PostgREST 将
public模式中的一切暴露为 REST API; - 客户端应用持有的是anon key,它在设计上就是一个客户端侧的秘密——任何拿到前端代码的人都能看到它;
- 如果某张 public 表 RLS 关闭,anon key 就能直接读取它。对于认证令牌、聊天记录、财务数据这类敏感表,这不再是"小坑",而是一条实实在在的数据外泄通道(exfiltration vector)。
gbrain 自身的连接使用的是service-role(实际是拥有BYPASSRLS权限的角色,通常是postgres)。因此:
开启 RLS 但不建任何 policy,并不会破坏 gbrain 自身——它只是阻断 anon key 的默认读取。
这正是 gbrain 的安全姿态(security posture):对 anon 默认拒绝(deny-by-default),对 service role 完全放行(full access)。这一姿态在 src/schema.sql 中有明确注释与落地:schema 基线只在当前角色具备BYPASSRLS(含继承角色、rolsuper,见 issue #1385 的处理)时才对全部 gbrain 表执行ALTER TABLE ... ENABLE ROW LEVEL SECURITY,否则只发出RAISE WARNING,避免把自己锁在门外。
当 doctor 失败时:三步修复流程
gbrain doctor的 RLS 检查会扫描public模式中的每一张基表(而非硬编码白名单),实现见 src/commands/doctor.ts。检查失败时,它会逐表点名并给出可直接复制执行的ALTER TABLE修复语句,典型输出如下:
1 table(s) WITHOUT Row Level Security: expenses_ramp. Fix: ALTER TABLE "public"."expenses_ramp" ENABLE ROW LEVEL SECURITY; If a table should stay readable by the anon key on purpose, see docs/guides/rls-and-you.md for the GBRAIN:RLS_EXEMPT comment escape hatch.99% 的情况下,你需要的正是这条修复语句。操作流程:
- 以
postgres(或任意拥有BYPASSRLS的角色)连接数据库; - 执行 doctor 给出的 SQL;
- 重新运行
gbrain doctor,确认检查通过。
几个值得注意的实现细节(均来自 src/commands/doctor.ts):
- doctor 使用单条 SQL 左连接
pg_description,在一次往返内同时取回表的rowsecurity状态与可选注释; - 生成修复 SQL 时会对标识符中的双引号做转义(
"→""),保证连weird"table这种病态表名渲染出的 SQL 也能安全复制粘贴; - 有豁免表时,失败信息末尾会附带
(N other table(s) explicitly exempt.)提示,避免误以为豁免被撤销。
自动 RLS:事件触发器 + 一次性回填
仅靠 doctor 事后发现仍然存在"时间窗":一张表可能在public模式中存在一段时间而完全没有 RLS,成为静默风险源。为此 gbrain 通过schema migration v35(auto_rls_event_trigger)内置了两套机制,完整 DDL 见 src/core/migrate.ts。
1. DDL 事件触发器(event trigger)
名为auto_rls_on_create_table的 Postgres 事件触发器,会对每一个新建的public.*表执行ALTER TABLE … ENABLE ROW LEVEL SECURITY。它覆盖 Postgres 报告为建表命令的全部三种语法:
CREATE TABLECREATE TABLE AS … SELECTSELECT … INTO
这意味着无论表是由 gbrain 自身、共享同一个 Supabase 项目的其他应用,还是由人直接执行裸 SQL 创建,从它存在的第一毫秒起 RLS 就已开启。
关键设计决策(源码注释有完整说明):
- 只作用于
public模式:auth、storage、realtime等非 public 模式被显式忽略——这些由 Supabase 自己管理,gbrain 不应触碰; - 触发器函数不包裹 EXCEPTION:
ddl_command_end在建表事务内部触发,一旦ALTER TABLE失败就会中止整个 CREATE TABLE——这是响亮的失败信号,而不是静默的缺口; - create-if-absent 而非 DROP+CREATE(issue #3603):
CREATE EVENT TRIGGER是超级用户保留操作,在 RDS/Aurora 等托管 Postgres 上普通应用角色没有rolsuper(rds_superuser也不够)。因此迁移改为"不存在才创建",运维可以先用 master 用户预创建函数与触发器,迁移随后自动收敛;权限不足时会抛出一条可操作的中文错误信息(提示预创建 SQL 的位置),而不是原始权限报错。
2. 一次性回填(one-time backfill)
首次应用 v35 迁移时,回填逻辑会遍历public.*中每一张满足以下条件的基表并开启 RLS:
- 当前 RLS 关闭(
relrowsecurity = false); - 表注释不携带
GBRAIN:RLS_EXEMPT豁免标记(正则与 doctor 完全一致:^GBRAIN:RLS_EXEMPT\s+reason=\S.{3,})。
回填前会先校验当前角色是否具备BYPASSRLS(识别超级用户与继承角色的rolbypassrls,见 #1385);不具备则抛异常中止迁移,保证config.version不被推进,下次initSchema时自动重试。
迁移完成后,gbrain doctor的rls检查在所有 brain 上应当成为 no-op。
让表在回填中保持 RLS 关闭
如果你有刻意保持 RLS 关闭的 public 表,必须在 v35 迁移运行之前就给表加上GBRAIN:RLS_EXEMPT注释(契约格式见下文)。回填会对任何不带该注释的 RLS-off public 表强制执行 RLS。注意:该迁移没有--dry-run旗标。
搞错的最低代价是一次往返:运维先执行 SQL 开启了本应豁免的表的 RLS,然后执行ALTER TABLE … DISABLE ROW LEVEL SECURITY并补上豁免注释,防止下次 doctor 再把它翻回去。全程不会丢失数据。
跨应用影响:其他应用共享同一个项目时
如果非 gbrain 应用(side project、自写脚本等)在同一个 Supabase 项目中建表,触发器同样会给这些表开启 RLS。有两种妥善的应对方式:
- 应用连接角色拥有
BYPASSRLS(例如同样使用postgres角色):新表 RLS 开启,但应用读写畅通——BYPASSRLS完全绕过 policy; - 应用角色没有
BYPASSRLS:应用需要在建表后立即CREATE POLICY授予自己所需的读写权限。触发器不会添加任何 policy——它只开启 RLS,在应用的 policy 落地之前保持 deny-by-default 姿态。
如果两种情况都不满足,应用将无法读取自己刚建的表。修复点在应用侧而非 gbrain 侧:要么授予BYPASSRLS,要么提供 policy。
触发器被删了怎么办
gbrain doctor内置rls_event_trigger检查(实现见 src/commands/doctor.ts),用于验证触发器是否安装且启用。它查询pg_event_trigger中auto_rls_on_create_table的状态:
- 记录不存在 →
warn,提示按本指南重建; evtenabled既不是O(origin)也不是A(always)→warn(R表示仅副本触发,普通会话不会触发;D表示已禁用),提示执行ALTER EVENT TRIGGER auto_rls_on_create_table ENABLE;;- 正常 →
ok。
如果你出于调试等原因手动删除了触发器,可用以下 DDL 重建(与 v35 迁移运行的完全相同,幂等:CREATE OR REPLACE+DROP EVENT TRIGGER IF EXISTS,可安全地以postgres等BYPASSRLS角色粘贴到 psql):
CREATE OR REPLACE FUNCTION auto_enable_rls() RETURNS event_trigger AS $$ DECLARE obj record; BEGIN FOR obj IN SELECT * FROM pg_event_trigger_ddl_commands() WHERE object_type = 'table' AND schema_name = 'public' LOOP EXECUTE format('ALTER TABLE %s ENABLE ROW LEVEL SECURITY', obj.object_identity); END LOOP; END; $$ LANGUAGE plpgsql; DROP EVENT TRIGGER IF EXISTS auto_rls_on_create_table; CREATE EVENT TRIGGER auto_rls_on_create_table ON ddl_command_end WHEN TAG IN ('CREATE TABLE', 'CREATE TABLE AS', 'SELECT INTO') EXECUTE FUNCTION auto_enable_rls();注意:此处%s是正确的格式化符,因为obj.object_identity已被 Postgres 预加引号(如"public"."My Table"),若改用%I会造成双重引号。
规范的触发器 DDL 存放在src/core/migrate.ts的MIGRATIONS数组中。没有 CLI 快捷方式:gbrain apply-migrations --force-retry面向的是 vX.Y.Z 编排器(orchestrator)注册表,而非 v35 这种数字 schema 迁移。
为什么不用 FORCE ROW LEVEL SECURITY?
Postgres 有两只 RLS 旋钮:
ENABLE:阻断 anon/authenticated 角色的读取;FORCE:额外阻断表 OWNER 的访问(除非其持有BYPASSRLS)。
gbrain 只用ENABLE,与 src/schema.sql、migration v24、v29 的姿态保持一致。原因:FORCE会把非BYPASSRLS应用锁在它们自己刚建的表之外——触发器函数继承的是调用者的角色,而非 gbrain 角色——从而破坏上文"跨应用共存"的方案。如果你确实想对某张 gbrain 专属表做纵深防御(defense-in-depth),请在你自己的迁移中显式添加FORCE;gbrain 的自动 RLS 默认不会替你开启它。
那 1% 的情况:刻意豁免
偶尔,某张 public 表本来就该对 anon key 可读,例如:
- 支撑公开仪表盘的 analytics 视图;
- 只读参考表;
- 自带前端、刻意使用 anon key 读取数据的插件。
gbrain 为此提供了逃生通道,并且故意把它设置得很麻烦——这正是设计的一部分。
豁免注释契约
-- 在 psql 中,以 BYPASSRLS 角色(如 postgres)连接执行: COMMENT ON TABLE public.your_table IS 'GBRAIN:RLS_EXEMPT reason=<why this is anon-readable on purpose>';规则(doctor 与 v35 回填共用同一正则^GBRAIN:RLS_EXEMPT\s+reason=\S.{3,}):
- 注释值必须以
GBRAIN:RLS_EXEMPT开头(区分大小写); - 必须包含
reason=,且理由至少 4 个字符; - 没有任何其他途径:无前缀变体、配置文件里的开关、环境变量统统不算——只有 Postgres 表注释算数;
- 如果该表 RLS 也确实关闭(anon key 真正可读的必要条件),你还需要显式执行
ALTER TABLE ... DISABLE ROW LEVEL SECURITY;。仅关闭 RLS 不够——注释才是告诉 doctor "这是有意的"的关键。
完整示例
ALTER TABLE public.expenses_ramp DISABLE ROW LEVEL SECURITY; COMMENT ON TABLE public.expenses_ramp IS 'GBRAIN:RLS_EXEMPT reason=analytics-only, anon-readable ok, owner=you, 2026-04-22';之后gbrain doctor报告:
rls: ok — RLS enabled on 20/21 public tables (1 explicitly exempt: expenses_ramp)注意:每次成功运行 doctor 都会按名字重新枚举你的豁免表。这是有意为之——逃生通道不是一次性签字,而是反复出现的提醒。你随时可以运行gbrain doctor查看哪些表处于开放状态。
为什么是 SQL 而不是 CLI 子命令
gbrain没有提供gbrain rls-exempt add <table>这类 CLI 命令。原因是安全设计上的深思熟虑:一个 CLI 命令会让 agent 轻易地静默向 anon 开放一张表。而"在 psql 里手写注释"这条约束,强制操作者用 SQL 打出豁免理由,且这个动作会暴露在多个可审计的面上:
- 出现在shell history中;
- 出现在git 追踪的 schema dump中;
- 下次恢复时出现在
pg_dump输出中; - 每次运行都出现在
gbrain doctor输出中。
agent 依然可以执行这段 SQL,但它无法在用户看不到的情况下完成。这就是文档所称的 "write it in blood"(用血写下)设计。
事后审计豁免
查看当前数据库中所有豁免表:
SELECT c.relname AS table_name, obj_description(c.oid, 'pg_class') AS comment FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace WHERE n.nspname = 'public' AND c.relkind = 'r' AND obj_description(c.oid, 'pg_class') LIKE 'GBRAIN:RLS_EXEMPT%';如果这份清单比你记忆里签过字的还要长,那就是警报信号——该去核实是谁、为什么开放了这些表。
移除一个豁免
只要删掉注释并重新开启 RLS 即可:
ALTER TABLE public.expenses_ramp ENABLE ROW LEVEL SECURITY; COMMENT ON TABLE public.expenses_ramp IS NULL;gbrain doctor会停止将该表列为豁免,并像对待其他表一样继续检查它。
PGLite:检查自动跳过
如果你使用 PGLite(零配置默认项),doctor完全跳过RLS 检查:PGLite 是嵌入式、单用户的,前面没有 PostgREST,public-schema 暴露风险不存在。输出为:
rls: ok — Skipped (PGLite — no PostgREST exposure, RLS not applicable)同理,rls_event_trigger检查也会跳过(PGLite 不支持事件触发器)。v35 迁移在 PGLite 上是 no-op(sqlFor.pglite为空字符串)。
一旦你迁移到 Supabase 或自托管 Postgres,检查立即开始运行,并会标记任何从 PGLite 带过来但未开启 RLS 的表。
自托管 Postgres
如果运行 Postgres 时前面没有 PostgREST,anon key 暴露风险并不存在。但 gbrain仍然会在缺少 RLS 时使检查失败,原因有三:
- "所有 public 表开启 RLS"是 gbrain 的安全不变量(security invariant),而非 Supabase 特有的变通方案;
ALTER TABLE ... ENABLE RLS修复在任何 Postgres 上都是无害的:它只约束非 bypass 角色,而 gbrain 不使用这类角色;- 如果将来你在前面部署 PostgREST 或类似工具,这道防线已经就位。
如果你认为这个框架不适合你的部署形态,请带着具体细节提 issue,以便团队评估是否值得引入"自托管豁免模式"。
结语:从修复到预防的完整闭环
gbrain 的 RLS 体系是一条完整的闭环:schema.sql 基线 + v24/v29 迁移 兜底存量表,v35 事件触发器杜绝新建表缺口,v35 回填收敛历史遗留表,doctor 的rls与rls_event_trigger双检查 持续监控,最后以GBRAIN:RLS_EXEMPT注释作为唯一、可审计、需手写理由的豁免通道。对运维而言,日常需要记住的只有三点:新表无需操心(触发器自动处理);doctor 报错就执行它给出的ALTER TABLE;只有确实需要 anon 可读的表,才用 psql 写下理由并打上豁免注释。
【免费下载链接】gbrainGarry's Opinionated OpenClaw/Hermes Agent Brain项目地址: https://gitcode.com/gh_mirrors/gb/gbrain
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考