DuckDB 装了 gatekeeper:AI 写的 SQL,先过检再执行
把数据库连接交出去的那一刻,问题就变了:你不再担心 SQL 写错,而是担心它写对。
一句 SELECT * FROM users 语法没毛病,跑起来把整张表读走了,日志里看不出任何异常。
社区里刚上了一个扩展补这块,叫 gatekeeper。它不执行 SQL,只把语句绑定后的真实对象拿出来,对着策略判一遍放不放行。
DuckDB 自带的闸门全是路径级的。enable_external_access 能把整个外部访问关掉,allowed_directories 能限定允许访问的目录,lock_configuration 能把这些设置锁死。但你说不出「这条连接只准读 reporting.orders 这一张表」——表级 ACL,DuckDB 一直没有。
gatekeeper 9 月 17 日进的 DuckDB 社区仓库,MIT 协议。
● ● ●
装上是两条命令
INSTALL gatekeeper FROM community;
LOAD gatekeeper;
我在 DuckDB 1.5.5 上装的,版本 0.1.2。作者的仓库 9 月 12 日才建,9 月 17 日就进了社区仓库。
装完先看它管什么:gatekeeper_configure 设策略,gatekeeper_validate 判一条 SQL。旧版的表函数语法,返回一行结构化结果。
● ● ●
四道闸门
一条 SQL 走完的检查,全程不执行
allowed_tables'*' 通配;[] 等于全拒 | ok;没配策略前的默认姿态是所有非内部表都可读 | |
md5SELECT md5('hello') → forbidden | ||
SELECT;INSERT、DROP、COPY、动态 SQL 全拒 | DROP TABLEunsupported;SELECT * FROM duckdb_tables() → forbidden | |
SELECT 1; SELECT 2;forbidden,规则 limit |
第三行值得单独说:duckdb_tables() 这类元数据表函数本身没碰你的数据,但它们能把库里有什么暴露出去。gatekeeper 把它们当普通函数一起管了。
策略还能锁死。CALL gatekeeper_configure(...) 之后加一句 SET lock_configuration = true,之后 SET、RESET、再调用 configure 都改不动,单条请求里的参数只能把范围收窄,不能放宽。
顺手试了两个写错的情况:字段名敲错(把 table 写成 tabl)、结构里塞 NULL,configure 都直接报 Invalid Input Error 退出,不会当成合法策略收下。
● ● ●
真正有意思的地方:它按绑定结果判
前面那些闸门,用字符串匹配也能糊出来一半。gatekeeper 不一样的地方在于,它是先让 DuckDB 把 SQL 绑定一遍,再拿绑定出来的真实对象去比。
我搭了个场景。一张放行的表,一张不许碰的表,然后建一个视图把两张表 join 起来:
CREATE SCHEMA reporting;
CREATE TABLE reporting.orders (customer_id INTEGER, amount DOUBLE);
CREATE TABLE secrets (token VARCHAR);
CREATE VIEW reporting.summary AS
SELECT o.customer_id, s.token FROM reporting.orders o JOIN secrets s ON true;
CALL gatekeeper_configure(
allowed_tables := [{schema: 'reporting', 'table': '*'}],
blocked_tables := [{schema: 'main', 'table': 'secrets'}]
);
视图 reporting.summary 在允许的表里,直接查它应该没问题?
SELECT allowed, code, violations[1].rule AS rule, violations[1]."table" AS v_table
FROM gatekeeper_validate('SELECT * FROM reporting.summary');
allowed | code | rule | v_table
---------+-----------+-------+---------
false | forbidden | table | secrets
视图套了一层,被点名的还是底下那张表
拒绝,而且点的是 secrets——视图底下那张表,不是视图自己的名字。
同样的东西藏在 CTE 加标量子查询里、藏在别名后面,结果一样。路径扫描也一样:SELECT * FROM '/tmp/x.parquet' 会被按 read_parquet 这个函数拦下(顺带发现,它默认不在这 953 个名字里)。
这就是字符串检查挡不住的部分。你可以把表名换成别名、包一层视图、写进宏,文本上看起来干干净净;但引擎绑定之后只有一张真实的依赖表,策略比的是那个。
反过来说,它也不是万能的:函数匹配是按名字,不是对宏和 UDF 的实现做证明。作者在文档里写得很直白——「catalog 完整性是前提假设」。
● ● ●
三种失败要分开看
gatekeeper_validate 返回的 code 有六种值,其中三种值得分开处理:
返回码按「有没有引擎原文」分两类
forbidden/ unsupported:策略拒绝,响应里不带引擎原文。可以直接打给调用方。binding/ parser:引擎自己绑定或解析失败,error_message原样带回来。
我拿一张不存在的表试了下,引擎回的是 Table with name missing_table does not exist! Did you mean "pg_tables"?。
注意后半句。引擎的报错会主动提示库里有哪些表名——这份提示如果直接返回给租户,等于送你一份表名清单。作者在 security.md 里把 error_message 标成敏感数据:记给运维看,返回给调用方要换一句通用错误,而且不要指望「截掉提示」能保密,名字可能出现在任意一行。
● ● ●
我装到的这一版,少一个函数
README 里把 CALL gatekeeper_enforce() 写成了给代理用的首选接法:在这条连接上开强制模式,DuckDB 自己拒绝执行策略不允许的语句,不用宿主额外判断。
但社区仓库编的这一版没有它。
我在里面查 duckdb_functions(),gatekeeper 只注册了两个函数:gatekeeper_validate 和 gatekeeper_configure;设置里也只有 gatekeeper_policy。社区仓库的构建来自 v0.1.2 这个 tag(commit bc6578f,9 月 15 日 06:23),而强制连接的提交是 9 月 17 日 20:24 才进的(#45),在 tag 之后。
所以今天能用的只有两步法:先 validate,拿到 allowed = true 且 code = 'ok',再执行同一段 SQL。听起来简单,坑在最后半句——验证的 SQL 和真正执行的 SQL 必须是同一段、同一条连接,中间不能有拼接,异常和空结果都算拒绝。这一步得宿主自己保证,扩展帮不上忙。
作者在文档里也承认:未做强制的连接上,宿主仍然要控制谁能拿到原始连接。
● ● ●
它不是沙箱
扩展自己在文档里列了一串边界,我照着核了一遍,几条值得抄出来:
这是语句级沙箱,不是操作系统、内存、网络沙箱。 没有行、列级授权;不管内存、不管执行时间、不限制结果大小。 953 个默认函数是「人工审过的名单」,不是「每个重载、每种传参都无害」的证明。 绑定阶段本身可能触发远程 I/O 或求值绑定期表达式,哪怕这条语句最后会被拒。 名单里放行了 now、current_date、uuidv7这类时钟函数和setseed——用了它们,结果没法只从 SQL 文本复现,做查询缓存的要留意。输入上限:SQL 与序列化 AST 8 MiB、AST 遍历 10 万节点、深度 512。
那个 953 我核过一遍,因为它是个容易被随手接受的数字。仓库的 inventories/ 目录里按 DuckDB 1.5.5 的 2903 个注册函数逐个分类:核心 622 个,icu 186 个,spatial 158 个,json 33 个,inet 11 个,加上十几个小项,加起来正好 953。另有 115 个标成 elevated(会读会话或计划器状态、做 I/O、或者按调用方给的名字去查目录),不进默认名单。
● ● ●
回到开头那件事
把连接交出去,本来没有中间态:要么全放开,要么把整个文件系统关掉。
gatekeeper 的贡献是把中间态做出来了——你可以说清「这条连接只能读 reporting 下的东西」,拒绝发生在绑定之后、执行之前,而且拒绝的理由是结构化的,code 加 violations[].rule,能直接进日志和告警。
但它现在是个刚上线几天、版本 0.1.2 的扩展,给代理用的强制连接还没进二进制,函数匹配也还停在名字层。我不建议现在拿它当唯一一道墙。
真正值得抄的是思路:授权要卡在绑定结果上,不要卡在 SQL 文本上。 前者贵,但它是引擎自己算出来的答案;后者便宜,而绕过它的方法有一堆。
如果你的 DuckDB 要开放给 AI 代理、多租户看板或者 BI 自助查询,可以先从最小的一步开始:给连接配一份 allowed_tables,写错也没关系,configure 会直接报错退出。等你确认哪些表真的要被读,再决定要不要上黑名单和 lock。
你会把权限卡在 SQL 文本这一层,还是卡在绑定结果这一层?评论区聊聊。
● ● ●
参考来源
- 01
duckdb/community-extensions PR #2698(gatekeeper v0.1.2 收录),2026-09-17
https://github.com/duckdb/community-extensions/pull/2698
- 01
gatekeeper 仓库与 README(0.1.2 行为、四道闸门、953 默认函数)
https://github.com/nozzle/duckdb-gatekeeper
- 01
gatekeeper 安全模型文档(语句级沙箱边界、error_message 敏感、AST 上限)
https://github.com/nozzle/duckdb-gatekeeper/blob/main/docs/security.md
- 01
gatekeeper 函数清单与基线(core.json、inventories/extensions/*)
https://github.com/nozzle/duckdb-gatekeeper/tree/main/inventories
- 01
DuckDB 1.5.5 release
https://github.com/duckdb/duckdb/releases/tag/v1.5.5
- 01
本文实测环境:DuckDB v1.5.5(Variegata d8cdaa33fd)+ gatekeeper 0.1.2, INSTALL gatekeeper FROM community;文中所有 SQL 与返回结果均来自本机运行