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 时你就知道它的威力了。

我的经验:密钥算法优先选 ECDSA(P-256),性能比 RSA 好,证书体积也小。除非你有遗留系统只支持 RSA,否则别用 RSA 2048。

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 来处理,更安全。

避坑指南:千万不要在循环里重复初始化 CA!我曾经犯过这个错,结果每个证书都是用不同的 CA 签的,客户端验证时全部失败。CA 只初始化一次,后续所有证书都用同一个 CA 签发。

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
核心思路:API 化之后,你的 CI/CD 流水线、配置管理工具(Ansible、Terraform)、甚至 Kubernetes 的 cert-manager,都可以通过 HTTP 调用 cfssl 来签发证书。这才是真正的自动化。

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 自动化签发的完整流程:

cfssl 自动化证书签发架构 配置模板层 ca-config.json csr-template.json profile 定义 cfssl 服务层 HTTP API (端口 8888) CA 密钥管理 签名引擎 调用触发层 CI/CD 流水线 Ansible / Terraform 手动 curl 调用 输出产物 证书 (.pem) | 私钥 (.pem) | CA 证书链 存储与分发 Vault / Kubernetes Secret / 本地文件系统 核心流程:配置模板 → cfssl API → 证书输出 → 安全存储 → 应用部署

4.8 安全注意事项

API 化虽然方便,但也带来了安全风险。我总结几个必须注意的点:

  • API 鉴权不能少:cfssl 原生 API 没有鉴权,必须用反向代理(Nginx/Envoy)加 mTLS 或 Token 认证。我曾经见过直接把 cfssl API 暴露在公网上的案例,结果被人用来签发非法证书。
  • 密钥传输要加密:API 返回的私钥是明文,建议在传输层用 TLS 加密,或者在 cfssl 前面加一层加密代理。
  • 审计日志必须开:记录每次签发的请求来源、CN、时间。出了问题能追溯。
  • CA 密钥离线存储:CA 的私钥建议放在 HSM 或 Vault 里,cfssl 服务启动时通过远程接口加载,不要直接写在磁盘上。
血的教训:有一次我把 CA 密钥和 cfssl 服务放在同一个目录,权限设置成了 644。结果被一个挖矿脚本扫到,直接下载了 CA 密钥,然后签了一堆证书用来加密 C2 通信。从那以后,CA 密钥我都是放在 Vault 里,cfssl 通过 Vault 的 PKI 引擎来签发,密钥永远不落盘。

4.9 小结

这一章我们重点讲了 cfssl 的批量签发和 API 集成。核心就三件事:模板化配置(把重复字段抽成 JSON 模板)、批量脚本(循环调用 cfssl gencert)、API 化(启动 cfssl serve,用 HTTP 请求签发)。

你想想看,有了这套体系,你的证书签发就从「手动操作」变成了「配置驱动」和「事件驱动」。不管是扩容 10 个节点还是 1000 个节点,工作量几乎不变。这才是自动化该有的样子。