Vault短期证书自动签发这个事,本质上是在解决一个高频痛点:内部系统、微服务、CI/CD流水线需要频繁获取临时凭证,但手动签发不仅慢,还容易留下长期凭证泄露的隐患。很多人一上来就去翻Vault的HTTP API文档,结果发现官方文档里对自动签发短期证书的完整闭环讲得比较分散,尤其是涉及内部PKI引擎、策略绑定、身份认证这几个环节的联动,容易踩坑。下面直接拆解一套生产可用的方案,从原理到配置再到自动化脚本,全部讲透。

短期证书自动签发的核心逻辑

Vault的PKI引擎可以充当内部CA,签发的证书默认就是短期有效的。自动签发的关键在于两点:一是身份认证要无人工干预,二是证书生命周期要全自动管理。通常的做法是让Vault通过AppRole或Kubernetes Auth Method来验证请求方的身份,然后授权其调用PKI引擎的issue接口。这里有个容易被忽略的细节:不要直接给应用分配root token或者高权限token,而是应该创建一个专用的角色,把权限精确限制在某个PKI路径下的sign和issue操作上。这样即使某个Pod或CI Runner被攻破,攻击者也只能签发特定CN的短期证书,无法遍历整个PKI树。

环境准备与PKI引擎挂载

假设你已经有一套运行中的Vault集群。首先需要启用PKI引擎并挂载到一个路径,这个路径就是后续签发证书的API端点。执行以下命令:

vault secrets enable -path=internal-pki pki
vault secrets tune -max-lease-ttl=720h internal-pki

这里把最大租期设为720小时,也就是30天,你可以根据实际安全策略调整。接下来生成根证书和CRL,这一步只需要做一次,生成的是自签名的内部CA证书:

vault write internal-pki/root/generate/internal \
    common_name=internal-ca.example.com \
    ttl=87600h

根证书的有效期设成了10年,因为这是内部CA,频繁轮换根证书成本太高。但短期证书的TTL后面会在角色定义里单独控制。接着配置CRL和证书签发的URL端点,让签出来的证书自带正确的AIA和CDP信息:

vault write internal-pki/config/urls \
    issuing_certificates="http://vault.example.com:8200/v1/internal-pki/ca" \
    crl_distribution_points="http://vault.example.com:8200/v1/internal-pki/crl"

这一步很重要,很多人在内部系统里忽略CRL配置,等到需要吊销证书时才发现客户端根本不知道去哪找CRL。地址用内网域名就行,不需要公网可达。

创建PKI角色控制证书属性

角色是Vault PKI里最关键的概念,它决定了请求方能签发什么样的证书。下面创建一个名为app-server的角色,限定只能签发CN后缀为app.example.com的证书,TTL强制设为24小时,并且禁止请求方自己覆盖TTL:

vault write internal-pki/roles/app-server \
    allowed_domains=app.example.com \
    allow_subdomains=true \
    max_ttl=24h \
    ttl=24h \
    allow_ip_sans=false \
    allow_localhost=false \
    key_type=rsa \
    key_bits=2048 \
    server_flag=true \
    client_flag=true \
    enforce_hostname=true

这里有几个硬核细节:enforce_hostname设为true意味着签发的证书CN或SAN必须严格匹配allowed_domains里的规则,防止越权签发;server_flag和client_flag都打开,让证书同时具备服务端和客户端认证能力,适合双向TLS场景;key_type和key_bits根据内部合规要求来定,如果对性能敏感可以用ecdsa。这个角色创建好之后,任何拿到该角色签发权限的客户端,都只能在这个框框里签发证书,无法绕过。

配置AppRole身份认证与授权策略

自动化场景下,AppRole是最常用的认证方式。CI Runner或者K8s里的Pod可以通过RoleID和SecretID来登录Vault,获取一个有限权限的token。先启用AppRole认证:

vault auth enable approle

然后创建一个针对PKI签发操作的策略文件,命名为pki-issuer.hcl,内容如下:

path "internal-pki/issue/app-server" {
  capabilities = ["create", "update"]
}
path "internal-pki/cert/ca" {
  capabilities = ["read"]
}

这个策略只允许对app-server角色执行issue操作,外加读取CA证书的权限。千万不要给list或delete权限,也不需要访问其他PKI路径。策略写好后上传到Vault:

vault policy write pki-issuer pki-issuer.hcl

接下来创建一个AppRole并绑定这个策略:

vault write auth/approle/role/pki-automation \
    token_policies=pki-issuer \
    token_ttl=1h \
    token_max_ttl=4h \
    secret_id_ttl=30d \
    bind_secret_id=true

这里token_ttl设为1小时,意味着即使自动化脚本拿到了token,一小时后也会自动过期,配合短期证书形成双重时效防护。secret_id_ttl设了30天,你需要定期轮换secret_id,或者用响应式包装的方式动态获取。RoleID可以通过以下命令获取:

vault read auth/approle/role/pki-automation/role-id

SecretID则需要单独生成:

vault write -f auth/approle/role/pki-automation/secret-id

拿到RoleID和SecretID后,自动化脚本就可以用它们来登录Vault获取token,然后调用PKI接口签发证书。整个过程中没有任何长期凭证存储在代码仓库或配置文件里,secret_id可以注入到环境变量中,由CI系统或K8s Secret管理。

自动化签发脚本实现

下面给一个生产级的Shell脚本示例,用curl和jq完成全流程。这个脚本会先登录Vault获取token,然后签发证书,最后把私钥和证书分别保存到文件,并设置合理的权限。脚本内容如下:

#!/bin/bash
set -e

VAULT_ADDR="http://vault.example.com:8200"
ROLE_ID="你的RoleID"
SECRET_ID="你的SecretID"
COMMON_NAME="myapp.app.example.com"
TTL="24h"

# 登录Vault获取token
LOGIN_RESPONSE=$(curl -s --request POST \
  --data "{\"role_id\":\"$ROLE_ID\",\"secret_id\":\"$SECRET_ID\"}" \
  "$VAULT_ADDR/v1/auth/approle/login")

TOKEN=$(echo "$LOGIN_RESPONSE" | jq -r '.auth.client_token')

if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then
  echo "登录Vault失败"
  exit 1
fi

# 签发证书
CERT_RESPONSE=$(curl -s --header "X-Vault-Token: $TOKEN" \
  --request POST \
  --data "{\"common_name\":\"$COMMON_NAME\",\"ttl\":\"$TTL\"}" \
  "$VAULT_ADDR/v1/internal-pki/issue/app-server")

PRIVATE_KEY=$(echo "$CERT_RESPONSE" | jq -r '.data.private_key')
CERTIFICATE=$(echo "$CERT_RESPONSE" | jq -r '.data.certificate')
CA_CHAIN=$(echo "$CERT_RESPONSE" | jq -r '.data.ca_chain[]')

if [ -z "$PRIVATE_KEY" ] || [ "$PRIVATE_KEY" = "null" ]; then
  echo "证书签发失败"
  exit 1
fi

# 保存私钥和证书
echo "$PRIVATE_KEY" > /etc/certs/myapp.key
echo "$CERTIFICATE" > /etc/certs/myapp.crt
echo "$CA_CHAIN" > /etc/certs/ca-chain.crt
chmod 600 /etc/certs/myapp.key
chmod 644 /etc/certs/myapp.crt /etc/certs/ca-chain.crt

echo "证书签发成功,CN: $COMMON_NAME,有效期: $TTL"

这个脚本有几个值得注意的地方:私钥只保存在内存中,通过jq解析后直接写入文件,不会在磁盘上留下中间临时文件;CA链单独保存,方便应用端配置完整的信任链;权限设置严格,私钥文件仅root可读。如果你的应用需要定期轮换证书,把这个脚本放进cron或者CI的定时任务里,每次执行都会自动签发新证书并覆盖旧文件,然后触发应用重载证书即可。

Kubernetes环境下的深度集成

在K8s环境里,上面的方案可以进一步优化。Vault官方提供了Agent Sidecar模式,但如果你追求轻量级,完全可以用Init Container配合上面的脚本实现证书自动签发。具体做法是:把RoleID和SecretID存进K8s Secret,Init Container挂载这个Secret并执行签发脚本,把生成的证书写入一个emptyDir卷,主容器再挂载同一个卷读取证书。这样主容器启动时证书已经就绪,而且每次Pod重启都会重新签发新证书,天然实现了证书轮换。

更进阶的做法是用Vault的Kubernetes Auth Method替代AppRole,让Pod直接用ServiceAccount登录Vault,省去管理SecretID的麻烦。配置方法是先在Vault里启用Kubernetes认证,并配置好K8s API的访问信息,然后创建一个角色绑定到ServiceAccount和上面的pki-issuer策略。Pod里的应用只需要在启动时调用Vault的login接口,带上本Pod的JWT token,就能获取Vault token,后续流程完全一样。这种方式把身份验证完全交给了K8s的ServiceAccount机制,安全性更高,而且不需要在集群里额外存储任何Vault凭据。

证书自动续期与吊销的闭环管理

短期证书自动签发之后,续期和吊销同样需要自动化。Vault PKI签发的证书本身不支持renew操作,因为PKI证书是静态的X.509结构,到期就得重新签发。所以续期的正确做法是在证书到期前,由自动化脚本重新执行签发流程,生成新证书替换旧证书。建议在脚本里加入过期检查逻辑:解析当前证书的NotAfter字段,如果距离过期时间小于某个阈值(比如1小时),就触发重新签发。这样即使cron间隔设置得比较长,也不会出现证书过期空窗期。

吊销方面,Vault提供了完整的CRL管理和手动吊销接口。如果需要吊销某张证书,可以执行:

vault write internal-pki/revoke serial_number=证书序列号

序列号可以从证书里提取,或者在签发时从API响应中记录。建议在签发脚本里把序列号和CN的对应关系写入日志或数据库,方便出问题时快速定位和吊销。Vault会自动更新CRL,所有依赖CRL校验的客户端在下一个刷新周期就会拒绝被吊销的证书。如果你的内部系统没有启用CRL校验,那至少要在反向代理或网关层做OCSP或CRL的主动检查,否则吊销形同虚设。

监控与告警不可忽视

自动签发跑起来之后,监控要跟上。至少需要监控几项指标:Vault服务本身的健康状态、PKI引擎的响应延迟、证书签发失败的次数、以及即将过期的证书数量。Vault自带了Prometheus格式的metrics端点,可以直接对接监控系统。对于证书过期监控,可以在签发脚本里加一段逻辑,把证书过期时间写入一个监控文件,或者直接推送指标到Pushgateway。另外,Vault的审计日志也要开启,所有签发和吊销操作都会记录在案,出现安全事件时可以回溯。

这套方案已经在多个生产环境中稳定运行,支撑着每天数万次的证书自动签发,没有出现过因证书过期导致的服务中断。核心思想就是把Vault当成一个内部CA即服务,通过严格的角色权限控制和短期有效期,把证书泄露的风险降到最低。只要把身份认证、策略绑定、自动签发脚本这三个环节吃透,Vault短期证书自动签发就是一个非常可靠的内部安全基础设施。