同步Caddy证书到阿里云CDN
文章目录
之前阿里云和腾讯免费的证书都是一年,现在只有3个月.使得自动部署证书是必须要弄的事了.
httpsok
这是一个免费部署证书的,可以自动部署到CDN还有其他地方.
but,它免费用户配额有限, 只能免费3个证书,50次刷新CDN证书配额.
一个证书3个月,也就是你最多免费用9个月.就没办法免费用了.
当然也可以购买它高级版
https://httpsok.com/console/vip
25一年,但是只够一个通配符域名,普通用户也够了.不过我域名比较多.光刷新CDN证书次数就不够用.
自己部署
我自己服务器用的Caddy,它本身就有自动的证书请求.
在不到30天的时候,会自动更新证书,不需要任何外部介入.一切都是无缝处理.
然后我看看有没有上传证书到阿里云CDN的,经过GITHUB上找了下,发现一个比较简单好使的.
https://github.com/git9527/aliyun-cdn-https-cert-updater
克隆到服务器任意文件夹即可.
启用阿里云子账号
如果主账号调用API,权限很高,不安全.
在阿里云后台,
AliyunCDNFullAccess 管理CDN的权限
AliyunPCDNFullAccess 管理PCDN的权限
AliyunSCDNFullAccess 管理安全加速(SCDN)的权限
AliyunDCDNFullAccess 管理全站加速(DCDN)的权限
AliyunYundunCertFullAccess 管理云盾证书服务的权限
自定义一个权限组,添加权限
cas:ListTagResources 这个是管理资源,不开这个无法选择已经上传证书,AliyunYundunCertFullAccess不包含这个,所以这个一定要选.
测试脚本
上面的脚本是python的,我服务器有2.7版本测试可以用,理论上3.X也应该可以.
上传新的key文件和cert文件
python updater.py -i LTAI**** -s aBPGz3**** -d vshop.getce.cn -p ./certs/*.key -c ./certs/fullchain.cer
执行成功会返回
{"RequestId":"B764E9E0-77C8-5F73-BFE0-32C79ABC6C47"}
失败的话 有code字段.
上传后,不是自动部署的证书的,还要自己部署.
注意上面脚本中我指定的一个域名vshop.getce.cn这是我其中一个CDN域名.
上面脚本执行成功后,在阿里云盾证书,可以看到一个"vshop_年月日"的证书. 比如"vshop_20250106"
应用证书到域名
每次只能更新一个域名,-n后面的参数就是上一步的名字.
python updater.py -i LTAI**** -s aBPGz3**** -d vshop.getce.cn -n vshop_20250106
python updater.py -i LTAI**** -s aBPGz3**** -d www.getce.cn -n vshop_20250106
阿里云证书刷新很快,基本上更新后,刷新浏览器就看到证书变化了.
写一个自动更新脚本
上面测试了可行性,那么开始写脚本.
分几步.
第一步,判断CDN证书有效期.
第二步,判断本地证书有效期.
第三步,更新
判断CDN证书有效期
# 联网获取子域名的到期时间
echo | openssl s_client -servername vshop.getce.cn -connect vshop.getce.cn:443 2>/dev/null | openssl x509 -noout -enddate | cut -d= -f2
#输出类似Apr 8 12:33:45 2025 GMT
判断本地证书有效期
openssl x509 -enddate -noout -in 证书crt文件 | cut -d= -f2
#返回Apr 8 12:33:45 2025 GMT
更新
比较上面两个日期,然后上传证书.配置证书.
完整脚本
是同一个,文件,我分几段发.好理解.
下面几段拼成一个文件即可.
下面脚本大部分都是让百度AI写的,然后调整下就可以用了.
配置部分
# 指定证书根目录
ROOT_DIR="/var/lib/caddy/.local/share/caddy/certificates/"
# 自己的根域名
DOMAIN="getce.cn"
# 阿里云CDN要更新子域名数组
SUB_DOMAIN_ARRAY=("vshop" "www" "webapi" "login")
# 阿里云SECRET id和key
ALI_ACCESS_SECRET_ID=LT*******************
ALI_ACCESS_SECRET_KEY=sG*******************
# python可执行程序路径,我是2.7版本,可能3.x也可以用
PYTHON_BIN_PATH="/usr/bin/python"
# 阿里云证书更新脚本路径
ALI_SCRIPT_PATH="/data/aliyun-cdn-https-cert-updater/updater.py"
# ------------------- 上面是配置,下面代码一般不需要改动 --------------------------------
# 联网校验该子域名CDN的证书到期时间
TEST_SUB_HOST="${SUB_DOMAIN_ARRAY[0]}" # 提取第一个子域名
TEST_PORT=443
# 拼接完整域名
FULL_TEST_HOST="${TEST_SUB_HOST}.${DOMAIN}"
# 上传后证书的名字
CERT_NAME="${TEST_SUB_HOST}_$(date +%Y%m%d)"
联网获取有效期
超过30天,不做任何动作,退出脚本.
# ------------------------- 开始联网读取域名的证书 --------------------
# 联网获取子域名的到期时间
TEST_HOST_CERT_DATE=$(echo | openssl s_client -servername ${FULL_TEST_HOST} -connect ${FULL_TEST_HOST}:${TEST_PORT} 2>/dev/null | openssl x509 -noout -enddate | cut -d= -f2)
# 将到期日期转换为Unix时间戳(秒)
TEST_HOST_CERT_EXPIRY_TIMESTAMP=$(date -d "$TEST_HOST_CERT_DATE" +%s)
# 计算剩余天数
TEST_HOST_CERT_EXPIRY_DAY=$(( (TEST_HOST_CERT_EXPIRY_TIMESTAMP - $(date +%s)) / (3600 * 24) ))
# 根据剩余天数执行不同操作
if [ "$TEST_HOST_CERT_EXPIRY_DAY" -gt 30 ]; then
echo "证书有效期还有 $TEST_HOST_CERT_EXPIRY_DAY 天,无需更新证书."
exit 0
else
echo "证书有效期只有 $TEST_HOST_CERT_EXPIRY_DAY 天,开始更新证书."
fi
获取本地证书
caddy颁发的证书是多个中间商的,每个中间商有不同文件夹.所以要搜索文件夹.找到wildcard通配符证书.
/var/lib/caddy/.local/share/caddy/certificates/acme-v02.api.letsencrypt.org-directory/
#acme-v02.api.letsencrypt.org-directory这个就是中间证书商文件夹,不同中间商可能文件夹不同.
搜到后,然后用openssl提取证书到期时间.
在所有枚举的证书中,找最新的一个.其他的不要.
# 用于存储最晚到期证书的变量
LATEST_CERT_FILE=""
LATEST_CERT_EXPIRY=""
LATEST_KEY_FILE=""
LATEST_CERT_EXPIRY_TIMESTAMP=0 # 初始化为0,或者一个足够小的值以确保第一个证书会被更新
LATEST_CERT_EXPIRY_DAY=0
# 使用for循环遍历find命令的输出,从指定目录的任何子目录里找到最新通配符证书(按到期时间)
# 之所以要枚举是因为caddy 自动生成letsencrypt证书可能有多个中间证书,放不同文件夹.
for CERT_FILE in $(find "$ROOT_DIR" -type f -name "wildcard_.${DOMAIN}.crt"); do
# 提取目录和文件名(不带扩展名)
CERT_DIR=$(dirname "$CERT_FILE")
CERT_BASENAME=$(basename "$CERT_FILE" .crt)
# 构造 .key 文件的路径
KEY_FILE="$CERT_DIR/${CERT_BASENAME}.key"
# 检查 .key 文件是否存在
if [[ -f "$KEY_FILE" ]]; then
# 使用 openssl 获取证书的到期时间
EXPIRY_DATE=$(openssl x509 -enddate -noout -in "$CERT_FILE" | cut -d= -f2)
# 将到期时间转换为时间戳(秒)
# 注意:这里假设EXPIRY_DATE的格式是"notAfter=YYYYMMDDHHMMSSZ",如果不是,需要调整date命令
EXPIRY_TIMESTAMP=$(date -d "$EXPIRY_DATE" '+%s' 2>/dev/null || echo 0) # 添加错误处理,以防date命令失败
EXPIRY_DAY=$(( (EXPIRY_TIMESTAMP - $(date +%s)) / (3600 * 24) ))
# 如果时间戳转换失败(比如因为日期格式问题),则使用0作为时间戳,这样当前证书就不会被选为最新的
if [[ "$EXPIRY_TIMESTAMP" -eq 0 ]]; then
echo "Warning: Failed to parse expiry date from $CERT_FILE. Skipping this certificate." >&2
continue # 跳过当前循环迭代
fi
# 如果这是我们找到的最晚到期的证书,则更新变量
if [[ -z "$LATEST_CERT_EXPIRY" || "$EXPIRY_TIMESTAMP" -gt "$LATEST_CERT_EXPIRY_TIMESTAMP" ]]; then
LATEST_CERT_FILE="$CERT_FILE"
LATEST_CERT_EXPIRY="$EXPIRY_DATE"
LATEST_CERT_EXPIRY_TIMESTAMP="$EXPIRY_TIMESTAMP"
LATEST_CERT_EXPIRY_DAY="$EXPIRY_DAY"
LATEST_KEY_FILE="$KEY_FILE"
fi
fi
done
比较到期时间
按到期时间,如果本地证书到期时间不到20天,不更新.
如果本地证书比服务器到期还早到期,也不更新.
# 输出最晚到期证书的 .crt 和 .key 文件路径以及到期时间
if [[ -n "$LATEST_CERT_FILE" ]]; then
# echo "Latest Certificate File: $LATEST_CERT_FILE"
# echo "Private Key File: $LATEST_KEY_FILE"
# echo "Expiry Date: $LATEST_CERT_EXPIRY"
# echo "Expiry Date(Timestamp): $LATEST_CERT_EXPIRY_TIMESTAMP"
# 根据剩余天数执行不同操作
if [ "$LATEST_CERT_EXPIRY_DAY" -lt 20 ]; then
echo "证书有效期只有 $LATEST_CERT_EXPIRY_DAY 天,小于20天,此证书没有更新必要."
exit 1
else
# 本地证书和web证书到期日期比较
if [ "$LATEST_CERT_EXPIRY_DAY" -lt "$TEST_HOST_CERT_EXPIRY_DAY" ]; then
echo "本地证书比CDN证书更旧,此证书没有更新必要."
exit 1
else
echo "开始上传证书"
fi
fi
else
echo "没有找到 wildcard_.${DOMAIN}.crt"
exit 1
fi
更新
阿里云API执行后有两种返回格式,
因为失败的有code字段,所以简单判断是否有code就可以判断执行状态.
# 开始上传证书
UPLOAD_CERT_INFO=$(${PYTHON_BIN_PATH} ${ALI_SCRIPT_PATH} -i ${ALI_ACCESS_SECRET_ID} -s ${ALI_ACCESS_SECRET_KEY} -d ${FULL_TEST_HOST} -p ${LATEST_KEY_FILE} -c ${LATEST_CERT_FILE} )
if echo "$UPLOAD_CERT_INFO" | grep -q '"code":'; then
echo "上传本地证书错误,返回${UPLOAD_CERT_INFO}"
exit 1
else
# 更新每个子域名证书
for item in "${SUB_DOMAIN_ARRAY[@]}"; do
_name="${item}.${DOMAIN}"
_update_sub_domain_info=$(${PYTHON_BIN_PATH} ${ALI_SCRIPT_PATH} -i ${ALI_ACCESS_SECRET_ID} -s ${ALI_ACCESS_SECRET_KEY} -d ${_name} -n ${CERT_NAME} )
if echo "$_update_sub_domain_info" | grep -q '"code":'; then
echo "更新${_name}证书失败,返回${_update_sub_domain_info}"
else
echo "更新${_name}证书成功!"
fi
done
fi
脚本执行测试
这里列举我两个域名,实际上所有域名都更新了.
加入计划任务
每天 0点0分执行.
0 0 * * * /root/get_new_crt.sh