4. 自动化证书签发:cfssl 批量签发与 API 集成
证书签发的自动化,是整个生命周期管理的核心环节。手动用 OpenSSL 一条条命令去签,在小规模测试环境还能凑合,一旦上了生产,成百上千的节点、微服务、容器,你根本签不过来。
我个人习惯用 cfssl(CloudFlare's PKI and TLS toolkit)。它设计之初就考虑了批量化和 API 化,说白了就是为自动化而生的。今天我们就把它掰开揉碎,看看怎么用 cfssl 实现批量签发、模板化配置,以及如何集成到你的自动化流水线里。
4.1 为什么是 cfssl?
市面上证书签发工具不少,OpenSSL 是瑞士军刀,功能全但操作繁琐;Easy-RSA 是 OpenVPN 的配套,场景受限。cfssl 的优势在哪?我列个对比表你就明白了。
| 特性 | OpenSSL | Easy-RSA | cfssl |
|---|---|---|---|
| 批量签发 | 需脚本封装 | 支持,但配置复杂 | 原生支持 JSON 批量 |
| 配置文件模板化 | 需手写 conf 文件 | 变量有限 | JSON 模板,灵活度高 |
| API 接口 | 无 | 无 | 内置 HTTP API |
| 输出格式 | PEM 为主 | PEM | PEM / DER / PKCS12 |
| 学习曲线 | 陡峭 | 中等 | 平缓(JSON 即可) |
看到没?cfssl 在批量、模板、API 这三个维度上,几乎是碾压级的优势。我在一次容器化改造项目中,需要给 200 多个微服务签发证书,用 cfssl 配合 Ansible,一个下午就全部搞定了。
4.2 环境准备与基础配置
先把 cfssl 工具集装好。它包含三个核心二进制:cfssl(签发)、cfssljson(解析输出)、cfssl-certinfo(证书信息查看)。
# 下载最新版本(以 Linux amd64 为例)
wget https://github.com/cloudflare/cfssl/releases/download/v1.6.4/cfssl_1.6.4_linux_amd64
wget https://github.com/cloudflare/cfssl/releases/download/v1.6.4/cfssljson_1.6.4_linux_amd64
wget https://github.com/cloudflare/cfssl/releases/download/v1.6.4/cfssl-certinfo_1.6.4_linux_amd64
# 赋予执行权限并移动到 PATH
chmod +x cfssl_1.6.4_linux_amd64 cfssljson_1.6.4_linux_amd64 cfssl-certinfo_1.6.4_linux_amd64
sudo mv cfssl_1.6.4_linux_amd64 /usr/local/bin/cfssl
sudo mv cfssljson_1.6.4_linux_amd64 /usr/local/bin/cfssljson
sudo mv cfssl-certinfo_1.6.4_linux_amd64 /usr/local/bin/cfssl-certinfo
# 验证安装
cfssl version
嗯,这里要注意:版本号可能会更新,建议去 GitHub Releases 页面确认最新版本。我曾经因为用了旧版本,导致生成的证书不兼容 TLS 1.3,排查了半天才发现是 cfssl 版本太老。
4.3 配置文件模板化
cfssl 的核心配置是 JSON 格式。我们把它拆成两部分:CA 配置和证书请求配置。
4.3.1 CA 配置文件(ca-config.json)
这个文件定义了 CA 的策略,比如签名算法、证书有效期、用途等。
{
"signing": {
"default": {
"expiry": "8760h",
"usages": ["signing", "key encipherment", "server auth", "client auth"]
},
"profiles": {
"server": {
"expiry": "43800h",
"usages": ["signing", "key encipherment", "server auth"]
},
"client": {
"expiry": "43800h",
"usages": ["signing", "key encipherment", "client auth"]
},
"peer": {
"expiry": "43800h",
"usages": ["signing", "key encipherment", "server auth", "client auth"]
}
}
}
}
这里我定义了三个 profile:server、client、peer。你想想看,微服务之间通信,有的需要双向 TLS(mTLS),peer profile 就派上用场了。有效期我习惯用 43800 小时(5 年),生产环境建议缩短到 8760 小时(1 年)甚至更短,配合自动续期。
4.3.2 证书请求模板(csr-template.json)
这是批量签发的关键。我们把公共字段提取成模板,每个证书只需要传入不同的 CN 和 SAN。
{
"CN": "{{ .CN }}",
"key": {
"algo": "ecdsa",
"size": 256
},
"names": [
{
"C": "CN",
"ST": "Beijing",
"L": "Beijing",
"O": "MyCompany",
"OU": "DevOps"
}
],
"hosts": [
"{{ .CN }}",
"localhost",
"127.0.0.1"
],
"ca": {
"expiry": "43800h"
}
}
注意看,我用 {{ .CN }} 作为占位符。这是 Go 模板语法,cfssl 原生支持。你可能会问:为什么不直接用环境变量?因为 Go 模板更灵活,可以嵌套、循环,后面我们集成 API 时你就知道它的威力了。
4.4 批量签发实战
现在我们来写一个批量签发的脚本。假设我们有 10 个微服务需要签发证书,服务名列表存在 services.txt 里。
#!/bin/bash
# batch-sign.sh - 批量签发证书
CA_DIR="./ca"
OUTPUT_DIR="./certs"
SERVICES_FILE="services.txt"
# 初始化 CA(如果不存在)
if [ ! -f "$CA_DIR/ca.pem" ]; then
echo "初始化 CA..."
cfssl gencert -initca ca-csr.json | cfssljson -bare $CA_DIR/ca
fi
# 读取服务列表,逐行签发
while IFS= read -r service; do
echo "正在签发: $service"
# 生成证书请求 JSON(替换模板中的 CN)
sed "s/{{ .CN }}/$service/g" csr-template.json > /tmp/$service-csr.json
# 使用 CA 签发
cfssl gencert \
-ca=$CA_DIR/ca.pem \
-ca-key=$CA_DIR/ca-key.pem \
-config=ca-config.json \
-profile=server \
/tmp/$service-csr.json | cfssljson -bare $OUTPUT_DIR/$service
echo "完成: $OUTPUT_DIR/$service.pem"
done < "$SERVICES_FILE"
echo "批量签发完成!"
这个脚本看起来简单,但我在实际项目中踩过一个坑:sed 替换模板时,如果服务名包含特殊字符(比如下划线、点号),可能会破坏 JSON 结构。后来我改用 jq 来处理,更安全。
4.5 API 集成:让签发变成 HTTP 调用
手动跑脚本虽然比一条条命令强,但还不是真正的自动化。真正的自动化,应该是一个 HTTP 请求就能完成签发。cfssl 内置了 HTTP API 服务,我们把它跑起来。
4.5.1 启动 cfssl API 服务
# 启动 cfssl 服务端,监听 8888 端口
cfssl serve \
-address=0.0.0.0 \
-port=8888 \
-ca=./ca/ca.pem \
-ca-key=./ca/ca-key.pem \
-config=ca-config.json
服务启动后,它会暴露几个 RESTful 接口:
| 接口路径 | 方法 | 说明 |
|---|---|---|
/api/v1/cfssl/init_ca |
POST | 初始化 CA |
/api/v1/cfssl/newcert |
POST | 签发新证书 |
/api/v1/cfssl/sign |
POST | 对已有 CSR 签名 |
/api/v1/cfssl/info |
POST | 获取 CA 信息 |
4.5.2 用 curl 调用 API 签发证书
现在,签发一个证书只需要一个 POST 请求:
# 构造请求体
cat > /tmp/cert-request.json <
返回的 JSON 里包含了证书、私钥和 CA 证书。你可以用 jq 提取并保存到文件:
curl -s -X POST http://localhost:8888/api/v1/cfssl/newcert \
-H "Content-Type: application/json" \
-d @/tmp/cert-request.json | jq -r '.result.certificate' > api-gateway.pem
curl -s -X POST http://localhost:8888/api/v1/cfssl/newcert \
-H "Content-Type: application/json" \
-d @/tmp/cert-request.json | jq -r '.result.private_key' > api-gateway-key.pem
4.6 集成到 CI/CD 流水线
我以 GitLab CI 为例,展示如何把证书签发集成到流水线中。
# .gitlab-ci.yml 片段
stages:
- build
- sign-cert
- deploy
variables:
CFSSL_API: "http://cfssl-server:8888"
sign-cert:
stage: sign-cert
script:
- |
# 构造证书请求
cat > /tmp/cert-req.json < ${CI_PROJECT_NAME}.pem
jq -r '.result.private_key' /tmp/cert-result.json > ${CI_PROJECT_NAME}-key.pem
# 安全处理:密钥只保留在制品中,不打印到日志
echo "证书签发完成"
artifacts:
paths:
- ${CI_PROJECT_NAME}.pem
- ${CI_PROJECT_NAME}-key.pem
expire_in: 1 hour
你看,每次代码提交、分支构建,都会自动签发一个对应服务名的证书。密钥作为制品保留 1 小时,足够部署使用。部署完成后,制品自动过期,避免密钥泄露风险。
4.7 整体架构图
下面这张图展示了 cfssl 自动化签发的完整流程:
4.8 安全注意事项
API 化虽然方便,但也带来了安全风险。我总结几个必须注意的点:
- API 鉴权不能少:cfssl 原生 API 没有鉴权,必须用反向代理(Nginx/Envoy)加 mTLS 或 Token 认证。我曾经见过直接把 cfssl API 暴露在公网上的案例,结果被人用来签发非法证书。
- 密钥传输要加密:API 返回的私钥是明文,建议在传输层用 TLS 加密,或者在 cfssl 前面加一层加密代理。
- 审计日志必须开:记录每次签发的请求来源、CN、时间。出了问题能追溯。
- CA 密钥离线存储:CA 的私钥建议放在 HSM 或 Vault 里,cfssl 服务启动时通过远程接口加载,不要直接写在磁盘上。
4.9 小结
这一章我们重点讲了 cfssl 的批量签发和 API 集成。核心就三件事:模板化配置(把重复字段抽成 JSON 模板)、批量脚本(循环调用 cfssl gencert)、API 化(启动 cfssl serve,用 HTTP 请求签发)。
你想想看,有了这套体系,你的证书签发就从「手动操作」变成了「配置驱动」和「事件驱动」。不管是扩容 10 个节点还是 1000 个节点,工作量几乎不变。这才是自动化该有的样子。