首页
/ CFSSL certdb 实战指南:为证书签发、吊销与 OCSP 缓存引入数据库持久化

CFSSL certdb 实战指南:为证书签发、吊销与 OCSP 缓存引入数据库持久化

2026-09-06 18:33:33作者:卓艾滢Kingsley

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 后额外开启的功能:

  • signgencert 在完成签名后,会把证书写进 certdb(即"签发即入库");
  • serve 使 /sign/revoke 这两个 HTTP 端点具备数据库能力。

没有数据库就无法工作的功能(强依赖):

  • revoke:在数据库中把证书标记为已吊销,并可选填吊销原因码;
  • ocsprefresh:刷新 OCSP 响应缓存表;
  • ocspdump:以 base64 拼接格式导出缓存的 OCSP 响应。

这层区分意味着一个常见部署模式:日常只做签发的内网 CA 可以不接数据库,一旦需要吊销列表或 OCSP 响应服务,就必须把 certdb 建起来并让相关命令共享同一个库实例。

从源码看数据模型与访问抽象

该目录在 Moby 仓库中只保留了 README.mdcertdb.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

值得注意的兼容性细节也写在源码注释里:IssuedAtNotBeforeMetadataJSONSANsJSONCommonName 这些字段"在 migration 002 执行之前写入的数据将为空"。也就是说数据库 schema 是分版本演进的,旧库升级后新写入才有这些列,回查历史记录时可能取到空值。

OCSPRecord(见 certdb.go)则只关心四个列:serial_numberauthority_key_identifierbody(OCSP 响应体)与 expiry,为的是支撑 ocspdump/ocsprefresh 对"最近响应的缓存"进行读写。

面向这些结构体,certdb 通过 Accessor 接口(见 certdb.go)抽象所有后端差异。接口同时提供"按序列号+AKI 精确读写""取全部未过期证书""取已吊销且未过期的证书""按 CA label 过滤"等查询,以及 InsertCertificateRevokeCertificateInsertOCSPUpsertOCSP 等写操作——不同数据库驱动只需各自实现这一接口,上层命令完全不感知底层是 MySQL、PostgreSQL 还是 SQLite。这也是 README 中 sign/revoke/ocsp* 命令能"开箱即用"接入数据库的原因。

用 goose 完成数据库的建表与拆除

README 明确本目录配套使用 goose 数据库迁移工具来管理各后端脚本。在 CFSSL 的完整源码树中,迁移脚本按后端分别存放在 mysqlpgsqlite 三个子目录(本仓库 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 命令(如 signgencertserverevokeocsprefreshocspdump)都接受 -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.ExpiryRevokedAttime.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 并做十六进制编码(源码注释特别说明:这依赖 Go x509.CreateCertificate 会用签发者的 SubjectKeyId 回填 AuthorityKeyId 的行为,因此从签名结果反解是拿到正确 AKI 的可靠手段);
  • CALabel:来自请求的 req.Label
  • Status:硬编码为 "good"
  • Expiry / NotBefore / IssuedAt:证书有效期与当前时间;
  • PEM:签名产物的 PEM 原文,方便日后审计或重发;
  • CommonName、SAN 列表与自定义元数据:分别经 sql.NullStringSetSANsSetMetadata 序列化写入。

注意 SetMetadata/SetSANs(见 certdb.go)内部通过 json.Marshal 把任意 map 或字符串切片编码成 JSON 文本存入 types.JSONText 列,这正是 metadata/sans 两列在库中以 JSON 形态存在的原因,反向读取则用 GetMetadata/GetSANs 解码回 Go 结构。

与之呼应,只要签名器被接上 DB,signgencert 乃至 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.gointegration-cli/docker_api_swarm_test.go 中也覆盖了依赖外部 CA/cfssl 的 Swarm 流程。因此如果你在追踪 Docker 对自定义外部 CA 的支持链路,certdb 就是这套外部签名工具链中负责"证书记账"的部分——它保证了被外部 CA 签发的证书同样有据可查、可吊销。

常见问题与排查方向

  • 未配置 -db-config 时 revoke/ocsprefresh/ocspdump 无法工作:这不是 bug,而是设计使然。README 明确指出这三个命令"需要数据库",请为它们补齐配置。
  • 只配了签名、没配吊销:签名侧与吊销侧应指向同一数据库与同一 driver,否则状态不一致。
  • MySQL 时间字段异常:确认 data_sourceparseTime=true
  • 旧库查不到新字段:先确认迁移版本是否已越过 002;certdb.go 的注释说明 002 之前的记录在 issued_atnot_beforemetadatasanscommon_name 上会是空值。
  • 执行 goose 前先建库:goose 只负责建表/拆表,不负责创建数据库实例与授权。

小结

certdb 的价值在于把 CA 从"一次性签名工具"升级为"可审计、可吊销、可服务 OCSP 的状态机":sign/gencert/serve 在接入数据库后自动记录每一张证书,revoke/ocsprefresh/ocspdump 则依赖同一份记录完成生命周期管理。落地时只需三件事——用 goose 对 MySQL/PostgreSQL/SQLite 建表,写一个 {"driver":...,"data_source":...} 配置给相关命令传 -db-config,并确保所有命令共享同一个库。相关源码入口见 certdb.gosigner/signer.gosigner/local/local.go

登录后查看全文
热门项目推荐
相关项目推荐