Let's Encrypt+CDN自动证书续订
背景
2024年4月之前腾讯云可以申请一年有效期的免费单域名SSL证书,政策更新后免费证书只能申请3个月有效期的单域名证书,考虑到我有众多HTTPS服务,并且更新博客源站证书需要同步更新CDN证书配置,频繁替换证书为网站/服务运维造成巨大的时间与精力成本(看到政策更新的内容后一口老血吐了出来)。
于是开始寻找一种自动化部署与更新方案,恰好发现著名免费SSL证书机构Let's Encrypt有泛域名证书提供,有效期三个月,之前只是用过Let's Encrypt单域名证书,计划依赖该机构实现自动化源站证书与CDN证书更新。
本文将使用acme.sh客户端+腾讯云API(TCCLI)实现该需求,下面简要介绍涉及到的工具和概念:
acme.sh:acme协议客户端,用于向机构(Let's Encrypt)申请证书,在申请过程中完成发起证书申请和验证域名所有权功能。github repo:https://github.com/acmesh-official/acme.sh
涉及到的腾讯云API:
- UploadCertificate:上传证书加入托管列表
- ModifyDomainConfig:修改证书为已托管的证书(ref-示例4 更新 HTTPS 服务器证书 ID)
API调用使用TCCLI完成:TCCLI简介 https://cloud.tencent.com/document/product/440/6176
搭建步骤
基本方案:
- 证书签发/维护:用acme.sh申请Let's Encrypt的泛域名证书,申请一个证书全家域名可用,并且通过acme.sh提供的renew hook功能触发CDN证书部署和宝塔(Nginx)证书更新替换
- CDN部署:腾讯给的TCCLI几乎支持100%的控制台功能,支持查询启用CDN域名,上传和替换CDN设置的证书
- Prerequisite
- 一台服务器
- 一个域名
- BT面板(optional)/Nginx部署
- 需要先有个网站,需要知道怎么用Nginx配置SSL,这个此处不展开讲,不用宝塔面板也可以,我主要是懒,用UI配置方便不动脑
- 后面的Nginx SSL配置路径替换宝塔的,给出的脚本仅供参考,脚本的功能本身是Nginx证书更新+Tencent CDN部署的超集,参考脚本稍作修改即可自己完成定制化部署
- CDN已部署
- 这里使用腾讯的CDN,12块钱100GB/yr的流量包
- 科学上网
- 安装acme.sh需要从被墙的raw.githubusercontent获取
- 对web服务器运维技术有基本了解
- 安装acme.sh
- 参考acme.sh中文Wiki https://github.com/acmesh-official/acme.sh/wiki/说明
- 服务器无法连接GitHub可以下载Release压缩包离线安装
- 安装TCCLI:https://cloud.tencent.com/document/product/440/34011
- 按照文档配置,需要去控制台创建腾讯云API Token
- 创建DNSPod API Token:去DNSPod后台创建,记住Secret Key,以后看不到了
- 证书签发:example.com替换成自己的主域名,使用acme.sh --issue指令配合--dns dns_dp指定DNSPod作为解析服务商,acme.sh会向CA申请证书,再用API在DNSPod解析记录里添加一个验证用记录,并且启用定时任务按时续期,将续期时的hook脚本设置成
/path/to/on/renew/hook.sh
DP_Id=<DNSPodTokenId> DP_Key=<DNSPodTokenSecret> acme.sh --issue --dns dns_dp -d example.com -d '*.example.com' \
--renew-hook "/path/to/on/renew/hook.sh"
还没创建回调的hook.sh,后面再创建,自己给一个喜欢的路径就好
注意:renew-hook只会在issue的时候设置,后面acme指令再加这个参数就不会有预期的效果,如果需要调整,修改~/.acme.sh/example.com(_ecc)/example.conf
即可
- 确定证书安装位置:搞一个路径,比如我用的/root/cert,里面存Nginx需要的证书文件,fullchain.pem和privkey.pem,证书文件和私钥文件,Nginx config里也配置为这个路径。另外还存个cert.cer,用来读取证书信息生成bt需要的info.json
- 安装证书,使用acme.sh的--install-cert命令
acme.sh --install-cert -d jnn.icu \
--key-file /root/cert/privkey.pem \
--fullchain-file /root/cert/fullchain.pem \
--cert-file /root/cert/cert.cer \
--reloadcmd "service nginx force-reload" # 这一行可选,如果你没有bt面板之类的,直接安装到nginx,更新完证书文件让Nginx reload一下就完事了,后面更新bt的步骤都不需要管了
这一步的指令会被acme.sh保存,后面acme.sh自动renew证书的时候会触发这个指令,把/root/cert里的文件更新为最新
renew_hook.sh
内容,需要chmod +x renew_hook.sh
几分钟随手搓了个shell脚本,比较糙,但是能用🙈
echo on_renew_cert hook invoked
# env
export main_domain=<example.com> # EDITME
export DP_Id=<DNSPodTokenId> # EDITME
export DP_Key=<DNSPodTokenSecret> # EDITME
# extract cert info
openssl x509 -text -noout -in /root/cert.cer 2>$1 > cert_info.txt
cn=$(cat cert_info.txt | grep -E 'Issuer.*CN' | sed -E 's/.*CN = (.*)/\1/' | tr -d '\n')
not_bef=$(cat cert_info.txt | grep -E 'Not Before' | sed -E 's/.*: (.*)/\1/' | tr -d '\n')
not_aft=$(cat cert_info.txt | grep -E 'Not After' | sed -E 's/.*: (.*)/\1/' | tr -d '\n')
subject=$(cat cert_info.txt | grep 'Subject: CN = ' | sed -E 's/.*CN = (.*)/\1/' | tr -d '\n')
dnss=$(cat cert_info.txt | grep 'DNS:' | tr -d ' ' | tr -d '\n' | sed -E 's/DNS:([^,]+)/"\1"/g')
# write bt info.json
echo \{\"issuer\": \"$cn\", \"notAfter\": \"$(date -d "$not_aft" +'%Y-%m-%d')\", \"notBefore\": \"$(date -d "$not_bef" +'%Y-%m-%d')\", \"dns\": \[$dnss\], \"subject\": \"$subject\", \"endtime\": $((($(date +%s -d "$not_aft") - $(date +%s -d "$not_bef")) / 86400 - 1))\} > info.json
# deploy cert to sites
for site in $(ls /www/server/panel/vhost/cert); do
if [[ $site == *$subject ]]; then
echo updating nginx $site cert
# cp certs and info.json to bt cert/ssl path
cp /root/cert/privkey.pem /root/cert/fullchain.pem info.json /www/server/panel/vhost/cert/$site/
cp /root/cert/privkey.pem /root/cert/fullchain.pem info.json /www/server/panel/vhost/ssl/$site/
fi
done
# reload nginx
echo reload nginx
service nginx force-reload
# upload cert to tencent cloud
resp=$(tccli ssl UploadCertificate --cli-unfold-argument --CertificatePublicKey "$(cat /root/cert/fullchain.pem)" --CertificatePrivateKey "$(cat /root/cert/privkey.pem)")
echo $resp
cert_id=$(echo $resp | grep CertificateId | sed -E 's/.*CertificateId.*:.*"(.*)".*/\1/' | tr -d '\n')
# deploy cdn cert
# grep online domain cdn deployments
for domain in $(tccli cdn DescribeDomains --cli-unfold-argument --endpoint cdn.ap-beijing.tencentcloudapi.com --Filters.0.Name status --Filters.0.Value online | grep \"Domain\" | sed -E 's/.*:.*\"(.*)\".*/\1/' | tr '\n' ' '); do
if [[ $domain == *$subject ]]; then
echo update online cdn domain deployment $domain
tccli cdn ModifyDomainConfig --cli-unfold-argument --Domain "$domain" --Route 'Https.CertInfo.CertId' --Value "{\"update\":\"$cert_id\"}"
fi
done
echo done
脚本流程:
- 使用openssl导出证书信息(用来搓宝塔的证书信息JSON)
- PS:不用像我这样全用sed正则提取,openssl解析证书有很多参数,可以提取出需要的信息
- 生成宝塔面板需要的info.json
- 将证书拷贝到所有通过宝塔面板部署的网站配置目录
- nginx重新加载配置文件,更新证书部署
- TCCLI上传证书到腾讯云托管
- TCCLI获取可应用本证书的CDN部署
- 更新CDN证书配置为上传的托管证书
总结
这篇文章记录了一个highly customized方案,对大部分人来说是没法直接使用的。但是相信SSL免费证书有效期从1年缩短为3月后,应该很多人都会有申请免费泛域名证书并自动更新的需求,所以本文记录的方案应该有较大的参考价值,希望可以帮到一些被证书反复部署烦恼到的读者。