把官方扩展放进自己的仓库,DuckDB 拒绝安装
DuckDB 2.0 里藏了一个大部分用户还没注意到的能力:
关键在方向性。我实测:运行期把设置从
CREATE EXTENSION REPOSITORY
。一条 SQL,你就能注册自己的扩展仓库,用自己生成的密钥签名,团队成员
INSTALL
安装时的体验和装官方扩展完全一样。
我昨天在本机把这个机制完整跑了一遍,撞上了一个反直觉的结果:我把一个
官方签名的 icu 扩展二进制
原封不动复制进自己的仓库,然后从这个仓库安装——DuckDB 拒绝了,报错是"doesn't have a valid signature"。
官方亲签的文件,在自己仓库里反而不被信任。这不是 bug,是这个设计里最值得讲的部分。
● ● ●
一条 SQL 建仓库
先说怎么建。整个流程比我预想的短,服务端只需要摆两个东西:extensions.example.com/
├── .well-known/duckdb-extension-repo.json
└── v2.0.0/
└── linux_amd64/
└── 你的扩展.duckdb_extension.gz
.well-known/duckdb-extension-repo.json
是仓库的元数据文件,内容是一个 JSON,
signature_keys
数组里放你的 RSA-2048 公钥。下面那层按「DuckDB 版本/平台」组织扩展二进制,和官方仓库的结构一致。prefix 指向哪都行——本地目录、HTTPS、S3 都支持,我在 WSL 上用本地目录就跑通了。
客户端三步:
SET allow_extension_repositories = 'allowed';第一步是开关,默认值是
CREATE EXTENSION REPOSITORY myrepo WITH PREFIX '/tmp/duckrepo';
INSTALL AND LOAD my_ext FROM myrepo;
undecided
(禁止添加新仓库)。第二步执行时,DuckDB 会去抓
.well-known
里的公钥,把它
钉扎
进仓库定义,并打印密钥的 SHA-256 指纹。这个指纹要和你在文档、内部 wiki 或签名公告里带外公布的值人工比对——因为公钥是走网络抓来的,跟扩展走同一条通道,指纹比对就是防中间人的那道闸。不想信任网络的话,可以在
CREATE
语句里用
USING PUBLIC KEY
直接内联公钥,跳过抓取。
第三步的
INSTALL AND LOAD
是 2.0 的新语法,装和载合成一条。
● ● ●
为什么官方扩展会被拒
回到开头那个实验。我做的操作是:从本机扩展目录里复制一个由 DuckDB 官方密钥签名的 icu 二进制,放进自建仓库的对应目录,然后
INSTALL icu FROM myrepo
。
结果:
IO Error: Attempting to install an extension file that原因在于钉扎逻辑:自建仓库安装的扩展,只验证 这个仓库自己公钥 的签名。官方密钥签的文件在官方仓库里有效,搬到你仓库里就成了「无有效签名」。仓库之间签名不互通,各管各的。 这比 DuckDB 1.x 更严格。旧版本里,每个扩展验证的是 core 和 community 两套密钥的并集;2.0 的实现(PR #24777)把签名密钥按来源隔离——core 扩展只认 core 密钥,community 只认 community 密钥,自建仓库只认自己钉扎的密钥。扩展装完后,实际验证通过的那把密钥的指纹会记录在安装元数据里,通过
doesn't have a valid signature
duckdb_extensions()
的
signature_key_fingerprint
列可以随时查。
这个设计防的事很具体:有人把一个来历不明的二进制塞进你信任的通道,无论这个二进制在别处有多权威的签名,你的仓库只认你的章。
● ● ●
单向棘轮:运行期永远无法扩大信任面
注册仓库等于往系统里添加信任的签名密钥,这个动作的危险级别和打开
allow_unsigned_extensions
相当。所以 DuckDB 给它上了同一套锁,还加了一层。
allow_extension_repositories
是三态开关:
| 状态 | 行为 |
|---|---|
undecided
(默认)
|
禁止添加新仓库,已有仓库的扩展仍可安装加载 |
allowed
|
可以添加自定义仓库 |
forbidden
|
已有仓库也不信任,其扩展验证一律失败 |
allowed
改回
undecided
,直接报错——数据库运行期间,这个开关只能收紧,不能放宽。想开
allowed
,必须在启动时通过客户端配置传入。也就是说,任何一条运行中的 SQL 连接,无论权限多高,都无法注册一个攻击者控制的密钥。
另外两道锁:仓库定义存在
~/.duckdb/extension_repositories
目录(每仓一个 JSON 文件,含钉扎的公钥,重启后钉扎依然有效);在签名校验开启的前提下,这个目录路径本身也只能启动期设置——否则运行期连接可以把它指向一个塞满假仓库文件的目录,绕过整个 opt-in 机制。CREATE 和 DROP REPOSITORY 还会额外受
lock_configuration
约束,可以一键冻结整个信任库。
● ● ●
加载时的信任断言
还有一个细节我比较欣赏:裸
LOAD my_ext
找不到自建仓库的扩展——必须写
LOAD my_ext FROM myrepo
。
自建仓库的扩展安装在独立的子目录里(
repositories/myrepo/
),两个仓库里有同名扩展也不会撞。裸 LOAD 只解析 core 和 community 的扩展,所以永远不可能「不小心」加载到一个来自用户仓库的文件。而
LOAD foo FROM core
这种写法本身是断言:如果 foo 实际装自别处,直接报错。来源写在语句里,审计的人一眼能看出每个扩展信任的是谁。
● ● ●
签名怎么做
官方 extension-template 仓库里的
extension-upload.sh
给出了完整流程:
-
01
生成 RSA-2048 密钥对,私钥放
DUCKDB_EXTENSION_SIGNING_PK环境变量; - 02 对扩展二进制算哈希;
-
03
openssl pkeyutl -sign用私钥签出 256 字节签名,追加到二进制尾部; - 04 gzip 压缩,放到仓库目录。
.gz
版本再找裸文件。不签名也能发,尾部补 256 个零字节凑格式——但那样用户就得开
allow_unsigned_extensions
,回到了老路。
密钥轮换有官方姿势:仓库元数据里可以放多把公钥,
CREATE ... USING PUBLIC KEYS 'k1', 'k2'
,全部钉扎、全部信任。换钥匙时新钥旧钥并行一段时间,装过的客户端不会断供,之后再摘掉旧钥。
● ● ●
谁需要这个
DuckDB 现有的分发通道只有两条:官方 core 仓库,和提交 PR、过 CI、等维护者审核的 community 仓库。对个人开发者够用,但有一类需求一直没着落—— 企业内部的私有扩展 :内部数据连接器、定制算法、还没到开源程度的原型。以前只能
-unsigned
加载裸二进制,等于放弃签名校验;现在可以自建仓库、自管密钥,安全模型和官方通道对齐。
日志类、合规类场景还有个加分项:本地目录就能当仓库。内网机器不联网,U 盘拷一个目录过去、
CREATE EXTENSION REPOSITORY
指向本地路径,签名验证照常工作。
两个注意点。一是语法官方标注 work-in-progress,可能还会调整,落地前盯着 PR #24777 的后续;二是 macOS Intel(osx_amd64)平台正在淘汰,2.0 的 community 扩展已停建该平台,自建仓库如果面向 macOS 用户,只需要管 Apple Silicon。
● ● ●
参考来源
- 01 Extension Repositories 官方文档:https://duckdb.org/docs/2.0/extensions/extension_repositories.html
- 02 PR #24777 External Extension Repositories:https://github.com/duckdb/duckdb/pull/24777
- 03 Extension Distribution(目录结构与平台):https://duckdb.org/docs/current/extensions/extension_distribution.html
- 04 extension-template 签名脚本:https://github.com/duckdb/extension-template/blob/main/scripts/extension-upload.sh