CFSSL certdb 实战指南:为证书签发、吊销与 OCSP 缓存引入数据库持久化
certdb 是 CFSSL(Cloudflare 的 SSL/TLS 工具集)中负责把证书生命周期状态写入关系型数据库的模块。在 Moby 仓库中它以 vendored 依赖形式存在于 vendor/github.com/cloudflare/cfssl/certdb,版本为 github.com/cloudflare/cfssl v1.6.5(见 vendor/modules.txt)。本文以该目录的 README.md 为骨架,结合其 certdb.go 接口定义与签名器写入路径,说明如何启用数据库、如何用 goose 迁移三种后端,以及 -db-config 配置格式;读完你可独立为 CFSSL 类 CA 部署证书持久化、吊销与 OCSP 缓存能力。
certdb 解决了什么问题:两种能力层级
CFSSL 本身可以在完全离线的状态下完成证书签名。只有提供了数据库配置之后,证书相关的"生命周期状态"(签发了谁、吊销了谁、OCSP 响应缓存在哪)才有处安放。README 将这种能力明确区分为两层:
提供 -db-config 后额外开启的功能:
sign与gencert在完成签名后,会把证书写进 certdb(即"签发即入库");serve使/sign与/revoke这两个 HTTP 端点具备数据库能力。
没有数据库就无法工作的功能(强依赖):
revoke:在数据库中把证书标记为已吊销,并可选填吊销原因码;ocsprefresh:刷新 OCSP 响应缓存表;ocspdump:以 base64 拼接格式导出缓存的 OCSP 响应。
这层区分意味着一个常见部署模式:日常只做签发的内网 CA 可以不接数据库,一旦需要吊销列表或 OCSP 响应服务,就必须把 certdb 建起来并让相关命令共享同一个库实例。
从源码看数据模型与访问抽象
该目录在 Moby 仓库中只保留了 README.md 与 certdb.go 两个文件,全部后端无关逻辑都收敛在后者中。理解这两个类型即可预判数据库表结构。
CertificateRecord(见 certdb.go)用 db:"..." tag 映射列名,核心字段如下:
| Go 字段 | 数据库列 | 含义 |
|---|---|---|
Serial |
serial_number |
证书序列号,字符串存储 |
AKI |
authority_key_identifier |
签发者密钥标识,十六进制编码 |
CALabel |
ca_label |
签发所用 CA/Profile 的标签 |
Status |
status |
状态,正常签发写入为 "good" |
Reason |
reason |
吊销原因码(RFC 5280),未吊销时为零值 |
Expiry / RevokedAt |
expiry / revoked_at |
过期时间 / 吊销时间 |
PEM |
pem |
证书的 PEM 原文 |
IssuedAt / NotBefore |
issued_at / not_before |
签发时间与生效时间,均为指针 |
MetadataJSON |
metadata |
任意元数据(JSON 文本) |
SANsJSON |
sans |
SAN 域名列表(JSON 数组) |
CommonName |
common_name |
证书主体 CN,sql.NullString |
值得注意的兼容性细节也写在源码注释里:IssuedAt、NotBefore、MetadataJSON、SANsJSON、CommonName 这些字段"在 migration 002 执行之前写入的数据将为空"。也就是说数据库 schema 是分版本演进的,旧库升级后新写入才有这些列,回查历史记录时可能取到空值。
OCSPRecord(见 certdb.go)则只关心四个列:serial_number、authority_key_identifier、body(OCSP 响应体)与 expiry,为的是支撑 ocspdump/ocsprefresh 对"最近响应的缓存"进行读写。
面向这些结构体,certdb 通过 Accessor 接口(见 certdb.go)抽象所有后端差异。接口同时提供"按序列号+AKI 精确读写""取全部未过期证书""取已吊销且未过期的证书""按 CA label 过滤"等查询,以及 InsertCertificate、RevokeCertificate、InsertOCSP、UpsertOCSP 等写操作——不同数据库驱动只需各自实现这一接口,上层命令完全不感知底层是 MySQL、PostgreSQL 还是 SQLite。这也是 README 中 sign/revoke/ocsp* 命令能"开箱即用"接入数据库的原因。
用 goose 完成数据库的建表与拆除
README 明确本目录配套使用 goose 数据库迁移工具来管理各后端脚本。在 CFSSL 的完整源码树中,迁移脚本按后端分别存放在 mysql、pg、sqlite 三个子目录(本仓库 vendored 快照仅保留 README 与源码,不含迁移 SQL)。goose 通过 -path 指定脚本目录、以 up/down 子命令执行迁移或回滚。
安装 goose
使用 Go module 之前的经典安装方式(命令来自 README):
go get bitbucket.org/liamstask/goose/cmd/goose
MySQL:启动与拆除
goose -path certdb/mysql up
goose -path certdb/mysql down
PostgreSQL:启动与拆除
goose -path certdb/pg up
goose -path certdb/pg down
SQLite:启动与拆除
goose -path certdb/sqlite up
goose -path certdb/sqlite down
三组命令的 up 方向都是"向前迁移建表",down 方向是"回滚拆除"。README 特别强调了一个易踩的坑:goose 不做数据库实例的创建与权限管理。文档原话指出"MySQL/PostgreSQL 的管理不包含在内",它假定要连接的库已经预先创建好,访问控制也已妥善配置;SQLite 则因文件型数据库的特性而无需此步骤。因此在执行 goose up 之前,请先自行 CREATE DATABASE 并准备好具备建表/读写权限的账号。
CFSSL 数据库配置:-db-config 与 JSON 格式
若干 CFSSL 命令(如 sign、gencert、serve、revoke、ocsprefresh、ocspdump)都接受 -db-config 标志,其值指向一个 JSON 配置文件,内容是一个仅含两个键的字典:driver 指定数据库驱动名,data_source 指定连接串。README 给出的三种后端示例:
SQLite(驱动名 sqlite3):
{"driver":"sqlite3","data_source":"certs.db"}
PostgreSQL(驱动名 postgres):
{"driver":"postgres","data_source":"postgres://user:password@host/db"}
MySQL(驱动名 mysql):
{"driver":"mysql","data_source":"user:password@tcp(hostname:3306)/db?parseTime=true"}
实际使用要点:
driver字符串必须与后端驱动注册名严格一致,sqlite3/postgres/mysql即上文三种;- MySQL 连接串中的
parseTime=true不是可选项而是推荐必需项——只有开启它,驱动才会把DATETIME列解析为time.Time,否则CertificateRecord.Expiry、RevokedAt等time.Time字段的读写会遇到类型转换问题; - PostgreSQL 使用标准 URL 风格 DSN,host 部分可省略端口(默认 5432);
- SQLite 的
data_source就是一个文件路径(如certs.db),goose 的-path certdb/sqlite up建出的表结构与该配置指向的文件需保持一致; - 同一套配置要同时传给签发的服务端(
serve/sign/gencert)和吊销/OCSP 命令(revoke/ocsprefresh/ocspdump),使它们操作同一个库、同一批证书记录,否则"吊销后依然签发"或"OCSP 刷新不到新吊销"。
源码级验证:签发时证书是如何落库的
readme 声称的"sign 会入库"并非空泛描述,可以在签名器实现中找到完整证据链。签名器抽象接口 signer.go 中暴露了一对方法:
SetDBAccessor(certdb.Accessor)
GetDBAccessor() certdb.Accessor
在本地签名实现 local/local.go 的签名主流程 Sign 中,证书签名成功后会检查 s.dbAccessor != nil,然后组装一个 CertificateRecord 并调用 s.dbAccessor.InsertCertificate(certRecord)。入库记录包含了以下关键信息:
Serial:取certTBS.SerialNumber.String();AKI:由刚签出的证书解析得到AuthorityKeyId并做十六进制编码(源码注释特别说明:这依赖 Gox509.CreateCertificate会用签发者的 SubjectKeyId 回填 AuthorityKeyId 的行为,因此从签名结果反解是拿到正确 AKI 的可靠手段);CALabel:来自请求的req.Label;Status:硬编码为"good";Expiry/NotBefore/IssuedAt:证书有效期与当前时间;PEM:签名产物的 PEM 原文,方便日后审计或重发;CommonName、SAN 列表与自定义元数据:分别经sql.NullString、SetSANs与SetMetadata序列化写入。
注意 SetMetadata/SetSANs(见 certdb.go)内部通过 json.Marshal 把任意 map 或字符串切片编码成 JSON 文本存入 types.JSONText 列,这正是 metadata/sans 两列在库中以 JSON 形态存在的原因,反向读取则用 GetMetadata/GetSANs 解码回 Go 结构。
与之呼应,只要签名器被接上 DB,sign、gencert 乃至 serve 的 /sign 端点都会走同一条入库路径;而 revoke 则对应 Accessor.RevokeCertificate(serial, aki, reasonCode),把状态从 "good" 改为吊销并写入 reason/revoked_at。
在 Moby 项目中的位置:作为 Swarm 外部 CA 的基础能力
在 Moby 仓库中,cfssl 并非被直接调用实现 Docker 自管 CA,而是以第三方依赖形式为生态提供能力:go.mod/go.sum 声明了该依赖,SwarmKit 的 CA 模块(vendor/github.com/moby/swarmkit/v2/ca)在实现外部签名服务器对接等场景时引用了 github.com/cloudflare/cfssl 的包;集成测试如 integration-cli/docker_cli_swarm_test.go、integration-cli/docker_api_swarm_test.go 中也覆盖了依赖外部 CA/cfssl 的 Swarm 流程。因此如果你在追踪 Docker 对自定义外部 CA 的支持链路,certdb 就是这套外部签名工具链中负责"证书记账"的部分——它保证了被外部 CA 签发的证书同样有据可查、可吊销。
常见问题与排查方向
- 未配置
-db-config时 revoke/ocsprefresh/ocspdump 无法工作:这不是 bug,而是设计使然。README 明确指出这三个命令"需要数据库",请为它们补齐配置。 - 只配了签名、没配吊销:签名侧与吊销侧应指向同一数据库与同一
driver,否则状态不一致。 - MySQL 时间字段异常:确认
data_source带parseTime=true。 - 旧库查不到新字段:先确认迁移版本是否已越过 002;certdb.go 的注释说明 002 之前的记录在
issued_at、not_before、metadata、sans、common_name上会是空值。 - 执行 goose 前先建库:goose 只负责建表/拆表,不负责创建数据库实例与授权。
小结
certdb 的价值在于把 CA 从"一次性签名工具"升级为"可审计、可吊销、可服务 OCSP 的状态机":sign/gencert/serve 在接入数据库后自动记录每一张证书,revoke/ocsprefresh/ocspdump 则依赖同一份记录完成生命周期管理。落地时只需三件事——用 goose 对 MySQL/PostgreSQL/SQLite 建表,写一个 {"driver":...,"data_source":...} 配置给相关命令传 -db-config,并确保所有命令共享同一个库。相关源码入口见 certdb.go、signer/signer.go 与 signer/local/local.go。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00