Let's Encrypt+CDN自动证书续订

运维 发布于 May 18, 2024 更新于 Jun 15, 2024

背景

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设置的证书
  1. Prerequisite
    1. 一台服务器
    2. 一个域名
    3. BT面板(optional)/Nginx部署
      1. 需要先有个网站,需要知道怎么用Nginx配置SSL,这个此处不展开讲,不用宝塔面板也可以,我主要是懒,用UI配置方便不动脑
      2. 后面的Nginx SSL配置路径替换宝塔的,给出的脚本仅供参考,脚本的功能本身是Nginx证书更新+Tencent CDN部署的超集,参考脚本稍作修改即可自己完成定制化部署
    4. CDN已部署
      1. 这里使用腾讯的CDN,12块钱100GB/yr的流量包
    5. 科学上网
      1. 安装acme.sh需要从被墙的raw.githubusercontent获取
    6. 对web服务器运维技术有基本了解
  2. 安装acme.sh
    1. 参考acme.sh中文Wiki https://github.com/acmesh-official/acme.sh/wiki/说明
    2. 服务器无法连接GitHub可以下载Release压缩包离线安装
  3. 安装TCCLI:https://cloud.tencent.com/document/product/440/34011
    1. 按照文档配置,需要去控制台创建腾讯云API Token
  4. 创建DNSPod API Token:去DNSPod后台创建,记住Secret Key,以后看不到了
  5. 证书签发: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即可

  1. 确定证书安装位置:搞一个路径,比如我用的/root/cert,里面存Nginx需要的证书文件,fullchain.pem和privkey.pem,证书文件和私钥文件,Nginx config里也配置为这个路径。另外还存个cert.cer,用来读取证书信息生成bt需要的info.json
  2. 安装证书,使用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里的文件更新为最新

  1. 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

脚本流程:

    1. 使用openssl导出证书信息(用来搓宝塔的证书信息JSON)
      1. PS:不用像我这样全用sed正则提取,openssl解析证书有很多参数,可以提取出需要的信息
    2. 生成宝塔面板需要的info.json
    3. 将证书拷贝到所有通过宝塔面板部署的网站配置目录
    4. nginx重新加载配置文件,更新证书部署
    5. TCCLI上传证书到腾讯云托管
    6. TCCLI获取可应用本证书的CDN部署
    7. 更新CDN证书配置为上传的托管证书

总结

这篇文章记录了一个highly customized方案,对大部分人来说是没法直接使用的。但是相信SSL免费证书有效期从1年缩短为3月后,应该很多人都会有申请免费泛域名证书并自动更新的需求,所以本文记录的方案应该有较大的参考价值,希望可以帮到一些被证书反复部署烦恼到的读者。

标签

Noam Chi

An Innovative Quant Developer. 2018 VEX World Final THINK Award🏆