一次把阿里云个人测试证书自动申请、DNS 验证和 Nginx 替换跑通
这篇文章记录的是一次真实的证书自动化改造。目标很直接:阿里云个人测试证书有效期只有 90 天,不想每三个月手工点控制台、复制 DNS 记录、下载证书、替换 Nginx。最后做成了一个定时脚本加一个轻量 Cert Admin 管理面板:证书没过期就跳过,过期才自动申请、验证、下载、替换并 reload Nginx。
本文只记录技术流程和关键参数。AccessKey、AccessSecret、后台密码、联系人手机号邮箱等敏感信息都不会出现在文章里,脚本日志里也做了脱敏。
最终效果
- 证书目标可配置:例如
www.bytealien.com、admin.bytealien.com。 - 脚本先检查本地证书是否已经过期;未过期直接跳过,不会下单,不会申请。
- 证书过期后,自动查询阿里云个人测试证书实例;没有待申请实例时,按 0 元参数购买一张。
- 自动读取阿里云联系人,不把姓名、手机号写入本地 env。
- 自动提交证书申请,读取 DNS 验证记录,写入 AliDNS TXT/CNAME。
- 签发后自动下载证书和私钥,校验证书与私钥匹配,备份旧文件,替换到 Nginx 使用的路径。
- 替换后执行
nginx -t,通过后 reload Nginx。 - Cert Admin 只监听
127.0.0.1:5055,通过 SSH 隧道访问,使用 supervisor 管理。
为什么不能只用旧接口
一开始最容易想到的是阿里云 SSL 证书服务里的几个老接口:CreateCertificateRequest、CreateCertificateWithCsrRequest、CreateCertificateForPackageRequest。文档里确实能看到 digicert-free-1-free,表示 3 个月个人测试证书免费版。
但实际跑下来,旧接口路径容易卡在资源包额度上。CreateCertificateRequest 会要求联系人信息;补齐联系人之后又可能返回 InsufficientQuota。CreateCertificateForPackageRequest 同样依赖可用资源包。这和现在控制台“个人测试证书(原免费证书)”的实例式流程不是一回事。
真正跑通的是新实例流程:先有一个 inactive 的个人测试证书实例,再对这个实例 UpdateInstance、ApplyCertificate、等待签发,最后用证书 ID 下载证书。
阿里云接口链路
当前自动化脚本使用的主链路如下:
ListContact:查询证书联系人,拿ContactId。ListInstances:查有没有Status=inactive的个人测试证书实例。GetSubscriptionPrice:下单前确认个人测试证书实例价格为 0。CreateInstance:没有待申请实例时,自动购买一个 0 元个人测试证书实例。UpdateInstance:把域名、联系人、CSR 生成方式、验证方式写到实例上。ApplyCertificate:提交证书申请。GetTaskAttribute:确认提交任务是否成功。GetInstanceDetail:轮询状态,读取 DNS 验证记录和最终CertificateId。GetUserCertificateDetail:下载证书和私钥。
0 元个人测试证书实例的 BSS 参数
关键坑在这里。不要用旧的 productCode:digicert-free-1-free,service_num,time 那组参数。那组参数可能创建出控制台看不到、状态一直 Creating 的实例。这次最终验证成功的是下面这组“个人测试证书实例”参数。
GetSubscriptionPrice:
ProductCode: cas
ProductType: cas_dv_public_cn
SubscriptionType: Subscription
OrderType: NewOrder
ServicePeriodUnit: Month
ServicePeriodQuantity: 3
Quantity: 1
ModuleList.1.ModuleCode: certdomain
ModuleList.1.Config: certdomain:dv
ModuleList.2.ModuleCode: fullDomainCount
ModuleList.2.Config: fullDomainCount:1
ModuleList.3.ModuleCode: fullSpec
ModuleList.3.Config: fullSpec:ss.dv.t
ModuleList.4.ModuleCode: merge
ModuleList.4.Config: merge:0
ModuleList.5.ModuleCode: product
ModuleList.5.Config: product:testCert_product
CreateInstance:
ProductCode: cas
ProductType: cas_dv_public_cn
SubscriptionType: Subscription
Period: 3
RenewalStatus: ManualRenewal
Parameter.1.Code: certdomain
Parameter.1.Value: dv
Parameter.2.Code: fullDomainCount
Parameter.2.Value: 1
Parameter.3.Code: fullSpec
Parameter.3.Value: ss.dv.t
Parameter.4.Code: merge
Parameter.4.Value: 0
Parameter.5.Code: product
Parameter.5.Value: testCert_product
UpdateInstance 参数
拿到 inactive 实例后,给实例补全申请信息。CSR 使用阿里云在线生成,避免本地私钥和阿里云签发结果不一致。
UpdateInstance:
InstanceId: cas_dv-cn-xxxx
CertificateName: cert-www-example-com
Domain: www.example.com
KeyAlgorithm: RSA_2048
AutoReissue: disable
ContactIdList.1: 15230
GenerateCsrMethod: online
ValidationMethod: DNS
City: Beijing
Province: Beijing
CountryCode: CN
ContactIdList.1 来自 ListContact。这里不需要在 env 文件里保存姓名和手机号,运行时查询即可。日志只输出脱敏后的联系人摘要。
DNS 验证
提交 ApplyCertificate 之后,脚本轮询 GetInstanceDetail。当返回 DomainValidationList 时,根据 ValidationType 自动选择 TXT 或 CNAME。
DomainValidationList:
Domain: www.example.com
RootDomain: example.com
ValidationType: TXT
ValidationKey: _dnsauth
ValidationValue: 20260630xxxxxxxx
脚本会把 ValidationKey + RootDomain 组合成实际记录名,调用 AliDNS 的 DescribeDomainRecords、AddDomainRecord、UpdateDomainRecord。如果记录已经存在且值一致,就直接复用;如果记录存在但值不同,就更新。
证书下载和替换
实例进入 Status=normal 且 CertificateStatus=issued 后,脚本用 CertificateId 调用 GetUserCertificateDetail 下载证书和私钥。
落盘前做两件事:
- 用
openssl x509 -noout -modulus和openssl rsa -noout -modulus校验证书私钥是否匹配。 - 替换目标文件前自动备份旧证书和旧私钥,备份文件带时间戳。
openssl x509 -in /path/to/fullchain.pem -noout -subject -issuer -dates -ext subjectAltName
openssl x509 -noout -modulus -in /path/to/fullchain.pem
openssl rsa -noout -modulus -in /path/to/private.key
nginx -t
systemctl reload nginx
这次实际替换后的证书由 DigiCert 签发,两个域名的到期时间都是 2026-09-28 23:59:59 GMT。
定时任务策略
个人测试证书有效期 90 天。因为需求是“过期了才申请替换”,所以脚本默认检查阈值是 0 秒:没过期就跳过。cron 可以跑得频繁一点,真正的保护逻辑在脚本里。
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
25 */6 * * * root /usr/local/sbin/renew-aliyun-free-cert.py
这样每 6 小时检查一次,但不会提前消耗免费额度。如果确实要提前重签,可以手动加 --force --domain example.com。
Cert Admin 管理面板
为了不每次 SSH 上去改 JSON 或看日志,又做了一个轻量 Cert Admin。它不是暴露公网的后台,只监听 127.0.0.1:5055,通过 SSH 隧道访问。
ssh -L 5055:127.0.0.1:5055 root@your-jump-host
open http://127.0.0.1:5055
面板目前支持:
- 查看证书目标、证书状态、过期时间、证书路径。
- 新增、编辑、启用、停用、删除证书目标。
- 保存自动化开关,例如实例流、自动 0 元购买、购买数量、DNS 根域。
- 全量只检查、全量续期。
- 单域名只检查。
- 单域名立即重签并替换;这个操作会消耗一张个人测试证书额度,所以前端有确认框。
- 查看运行日志和证书申请日志。
- 可选生成 Nginx 托管配置,生成前后都会执行
nginx -t。
Cert Admin 用 supervisor 管理,不用 systemd 管这个服务。这样和原先服务器习惯一致,服务异常也能自动拉起。
[program:cert-admin]
command=/usr/bin/python3 /opt/cert-admin/server.py
directory=/opt/cert-admin
autostart=true
autorestart=true
stdout_logfile=/var/log/cert-admin.log
stderr_logfile=/var/log/cert-admin.err.log
核心配置
续期脚本的关键开关如下。生产环境里 AK/SK 单独放在 env 文件,日志里必须脱敏。
ALIYUN_USE_INSTANCE_FLOW=1
ALIYUN_AUTO_BUY_FREE_CERT=1
ALIYUN_FREE_CERT_BUY_QUANTITY=1
ALIYUN_USE_PACKAGE_FLOW=0
ALIYUN_USE_LOCAL_CSR=0
ALI_DNS_ZONES=bytealien.com,mongona.com,luxefuture.com,pyfuture.com
ALIYUN_FREE_CERT_BUY_QUANTITY=1 是故意的:多个域名同时过期时,脚本逐个域名购买、逐个申请,不一次囤多张。
排错记录
1. 证书实例控制台看不到
如果 BSS 下单参数走错,可能会出现订单成功但 SSL 控制台“个人测试证书”页面看不到,BSS 实例状态还停在 Creating。解决方式是改用 cas_dv_public_cn + ss.dv.t + testCert_product 这组参数。
2. CreateCertificateRequest 返回 InsufficientQuota
这说明当前接口链路在消耗旧资源包额度。当前控制台的免费证书更像“测试证书实例”流程,所以应切到 ListInstances / UpdateInstance / ApplyCertificate。
3. 联系人不要写死
联系人信息用 ListContact 查询。脚本只需要 ContactId,不需要把手机号、邮箱、姓名明文写到 env。
4. 先验证再 reload
证书替换后必须先 nginx -t。如果证书路径、私钥权限或 server block 写错,直接 reload 会影响线上服务。
这套方案的边界
- 个人测试证书是 90 天,不适合作为有 SLA 要求的商业证书方案。
- 免费 DV 证书只按单域名处理;不要把通配符或多域名场景混到同一个目标里。
- 自动下单前必须查价格,确认是 0 元再创建实例。
- Cert Admin 不应直接暴露公网;保持 127.0.0.1 监听,通过 SSH 隧道访问。
- 日志可以记录请求参数,但必须屏蔽 AccessKey、Secret、Token、手机号、邮箱和私钥内容。
小结
这次真正重要的不是“写了一个续期脚本”,而是把阿里云个人测试证书现在实际可用的接口链路摸清楚了。旧接口文档能看到免费产品码,但控制台里的免费证书额度和实例流才是目前更稳定的路径。把“检查过期、购买实例、申请证书、DNS 验证、下载替换、Nginx reload”串起来之后,后面新增域名只需要在 Cert Admin 里加一个目标。