之前阿里云和腾讯免费的证书都是一年,现在只有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,权限很高,不安全.

在阿里云后台,RAM 访问控制,申请一个子账号,并申请access Key

存下来.

 

设置子账号权限

 

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

或者用-n指定自定义证书名字.

python updater.py -i LTAI**** -s aBPGz3**** -d vshop.getce.cn -p ./certs/*.key -c ./certs/fullchain.cer -n vshop_20250106

 

 

执行成功会返回

{"RequestId":"B764E9E0-77C8-5F73-BFE0-32C79ABC6C47"}

失败的话 有code字段.所以只要判断返回字符串是否包含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

 

阿里云证书刷新很快,基本上更新后,ctrl+F5强制刷新浏览器,就看到证书变化了.

 

 

写一个自动更新脚本

 

上面测试了可行性,那么开始写脚本.

分几步.

第一步,判断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

 

更新CDN

比较上面两个日期,然后上传证书.配置证书.

 

完整脚本

是同一个,文件,我分几段发.好理解.

下面几段拼成一个文件即可.

下面脚本大部分都是让百度AI写的,然后调整下就可以用了.

 

 

配置部分

 

#!/bin/bash

# 指定证书根目录
ROOT_DIR="/var/lib/caddy/.local/share/caddy/certificates/" 

# 自己的根域名
DOMAIN="getce.cn"

# 阿里云CDN要更新子域名数组,有几个子域名就自己添加
SUB_DOMAIN_ARRAY=("vshop" "www" "webapi" "login" "ipv4api")


# 阿里云SECRET id和key
ALI_ACCESS_SECRET_ID=*******************
ALI_ACCESS_SECRET_KEY=*****************

# python可执行程序路径,我是2.7版本,可能3.x也可以用
PYTHON_BIN_PATH="/usr/bin/python"

# 阿里云证书更新脚本路径
ALI_SCRIPT_PATH="/data/aliyun-cdn-https-cert-updater/updater.py"




# ------------------- 上面是配置,下面代码一般不需要改动 --------------------------------



 

 

 

 

联网获取有效期

 

 

对每个子域名进行判断,如果所有子域名证书到期时间超过30天,那么退出脚本,后续脚本无需执行.

哪怕有一个低于30天,那么就要更新证书.

 

# 初始化一个空数组来存储证书到期天数
EXPIRATION_DAYS=()

# 当前时间的时间戳
CURRENT_TIMESTAMP=$(date +%s)

# 遍历子域名数组
for SUB_DOMAIN in "${SUB_DOMAIN_ARRAY[@]}"; do
  FULL_DOMAIN="${SUB_DOMAIN}.${DOMAIN}"
  
  # 使用openssl获取证书到期日期
  EXPIRATION_DATE=$(echo | openssl s_client -servername ${FULL_DOMAIN} -connect ${FULL_DOMAIN}:443 2>/dev/null | openssl x509 -noout -enddate | cut -d= -f2)
  
  # 将日期转换为时间戳(秒)
  EXPIRATION_TIMESTAMP=$(date -d "${EXPIRATION_DATE}" +%s)
  
  # 计算到期天数
  EXPIRATION_DAY_DIFF=$(( (EXPIRATION_TIMESTAMP - CURRENT_TIMESTAMP) / 86400 )) # 86400秒 = 1天
  
  # 将到期天数添加到数组中
  EXPIRATION_DAYS+=($EXPIRATION_DAY_DIFF)
done

# 检查是否有任何子域名的证书到期天数不超过30天
EXIT_FLAG=0
for DAYS in "${EXPIRATION_DAYS[@]}"; do
  if [ "$DAYS" -le 30 ]; then
    EXIT_FLAG=1
    break
  fi
done

# 根据检查结果决定是否退出脚本
if [ "$EXIT_FLAG" -eq 1 ]; then
  echo "所有子域名的证书到期天数均不超过30天,继续执行后续内容。"
else
  echo "所有子域名的证书到期天数都超过30天,无需更新,脚本退出。"
  exit 0
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天,不更新.

这种情况一般不会发生,caddy会自动更新脚本

# 输出最晚到期证书的 .crt 和 .key 文件路径以及到期时间
if [[ -n "$LATEST_CERT_FILE" ]]; then
	# 根据剩余天数执行不同操作
	if [ "$LATEST_CERT_EXPIRY_DAY" -lt 20 ]; then
	   echo "证书有效期只有 $LATEST_CERT_EXPIRY_DAY 天,小于20天,此证书没有更新必要."
	   exit 1
	else
	   echo "开始上传证书"		   
	fi	
else
	echo "没有找到 wildcard_.${DOMAIN}.crt"
	exit 1
fi

 

上传并更新证书

 

阿里云API执行后有两种返回格式,

因为失败的有code字段,所以简单判断是否有code就可以判断执行状态.

 

# 开始上传证书



# 提取第一个子域名
TEST_SUB_HOST="${SUB_DOMAIN_ARRAY[0]}"  

# 拼接完整域名
FULL_TEST_HOST="${TEST_SUB_HOST}.${DOMAIN}"

# 上传后证书的名字
CERT_NAME="${TEST_SUB_HOST}_$(date +%Y%m%d)"

echo "上传后证书名:${CERT_NAME}"
echo "上传后的域名:${FULL_TEST_HOST}"
echo "上传后的证书路径${LATEST_CERT_FILE}"


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} -n ${CERT_NAME} )

echo "$UPLOAD_CERT_INFO"

if echo "$UPLOAD_CERT_INFO" | grep -iq '"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 -iq '"code":'; then
			echo "更新${_name}证书失败,返回${_update_sub_domain_info}"
		else
		    echo "更新${_name}证书成功!"
		fi
	done
fi

 

脚本执行测试

这里列举我两个域名,实际上所有域名都更新了.

 

加入计划任务

每天 0点0分执行.

0 0 * * * /root/get_new_crt.sh

如果有多个域名,那么再复制一份脚本,调整下上面配置,同样加入计划任务.

 

补充

本篇博文发布于2025-01-08
本次更新是2025-03-24

经过测试,脚本可以完美工作,会自动更新阿里云证书.无需人工介入.