背景 使用 Let’s Encrypt 或 ZeroSSL 申请的免费 SSL 证书有效期只有 90 天,需要定期续期。如果你的网站使用了阿里云 CDN,每次证书续期后还需要手动上传新证书到阿里云并更新 CDN 配置,这个过程繁琐且容易出错。
本文介绍一套完整的自动化方案,实现:
证书自动续期
自动上传到阿里云 CAS(证书中心)
自动更新 CDN 域名证书
自动刷新 CDN 缓存 (关键步骤!)
自动清理旧证书
自动更新本地服务器证书
飞书通知 (实时掌握证书状态)
架构设计 ┌─────────────────┐ │ acme.sh │ 自动续期 Let's Encrypt/ZeroSSL 证书 │ (定时任务) │ 每天 2:00 检查 └────────┬────────┘ │ 证书续期成功 ▼ ┌─────────────────────────┐ │ deploy .sh (钩子脚本) │ 执行证书部署 └────────┬────────────────┘ │ ┌────┴────┬──────────┬──────────┐ ▼ ▼ ▼ ▼ ┌───────┐ ┌────────┐ ┌────────┐ ┌──────────┐ ┌────────┐ │阿里云 │ │阿里云 │ │CDN缓存 │ │本地Nginx │ │飞书通知│ │CAS │ │CDN │ │刷新 │ │证书更新 │ │ │ │上传证书│ │更新绑定│ │ │ │reload │ │ │ └───────┘ └────────┘ └────────┘ └──────────┘ └────────┘
通知效果图 部署成功后,飞书会收到如下消息:
前置条件
已安装 acme.sh
已安装 阿里云 CLI
已申请并配置好域名证书(示例使用 ZeroSSL)
阿里云账号具有 CAS 和 CDN 管理权限的 RAM 子账号
完整脚本 将以下脚本保存到 /opt/aliyun-cert-deploy/deploy.sh:
#!/bin/bash if [ -f /opt/aliyun-cert-deploy/.env ]; then source /opt/aliyun-cert-deploy/.envfi set -eexport ALIBABA_CLOUD_ACCESS_KEY_ID="${ALIBABA_CLOUD_ACCESS_KEY_ID:-YOUR_ACCESS_KEY_ID} " export ALIBABA_CLOUD_ACCESS_KEY_SECRET="${ALIBABA_CLOUD_ACCESS_KEY_SECRET:-YOUR_ACCESS_KEY_SECRET} " export ALIBABA_CLOUD_REGION_ID="${ALIBABA_CLOUD_REGION_ID:-cn-hangzhou} " export DOMAIN="${DOMAIN:-example.com} " export CERT_NAME="${CERT_NAME:-example-cert} " CDN_DOMAINS="${CDN_DOMAINS:-www.example.com} " FEISHU_WEBHOOK="${FEISHU_WEBHOOK:-} " FEISHU_SECRET="${FEISHU_SECRET:-} " KEEP_CERT_COUNT=3 CERT_FULLCHAIN_PATH="$HOME /.acme.sh/${DOMAIN} _ecc/fullchain.cer" CERT_KEY_PATH="$HOME /.acme.sh/${DOMAIN} _ecc/${DOMAIN} .key" LOG_FILE="/var/log/aliyun-cert-deploy.log" generate_feishu_sign () { local timestamp=$1 local secret=$2 if [ -z "$secret " ]; then echo "" return fi echo -ne "${timestamp} \n${secret} " | openssl dgst -sha256 -hmac "$secret " -binary | base64 }send_feishu_notify () { local title="$1 " local content="$2 " local is_success="$3 " if [ -z "$FEISHU_WEBHOOK " ]; then return fi local timestamp=$(date +%s) local sign=$(generate_feishu_sign "$timestamp " "$FEISHU_SECRET " ) local status_color="green" [ "$is_success " != "true" ] && status_color="red" local json_body if [ -n "$sign " ]; then json_body=$(cat <<EOF { "timestamp": "${timestamp}", "sign": "${sign}", "msg_type": "interactive", "card": { "config": {"wide_screen_mode": true}, "header": { "title": {"tag": "plain_text", "content": "${title}"}, "template": "${status_color}" }, "elements": [ { "tag": "div", "text": { "tag": "lark_md", "content": "**域名:** ${DOMAIN}\\n**状态:** $([ "$is_success" == "true" ] && echo "✅ 成功" || echo "❌ 失败")\\n${content}" } } ] } } EOF ) else json_body=$(cat <<EOF { "msg_type": "interactive", "card": { "config": {"wide_screen_mode": true}, "header": { "title": {"tag": "plain_text", "content": "${title}"}, "template": "${status_color}" }, "elements": [ { "tag": "div", "text": { "tag": "lark_md", "content": "**域名:** ${DOMAIN}\\n**状态:** $([ "$is_success" == "true" ] && echo "✅ 成功" || echo "❌ 失败")\\n${content}" } } ] } } EOF ) fi curl -s -X POST "$FEISHU_WEBHOOK " -H "Content-Type: application/json" -d "$json_body " > /dev/null }log () { echo "[$(date '+%Y-%m-%d %H:%M:%S') ] $*" | tee -a $LOG_FILE } aliyun configure set \ --access-key-id "$ALIBABA_CLOUD_ACCESS_KEY_ID " \ --access-key-secret "$ALIBABA_CLOUD_ACCESS_KEY_SECRET " \ --region "$ALIBABA_CLOUD_REGION_ID " log "========================================" log "开始部署证书到阿里云..." log "域名: $DOMAIN " log "CDN域名列表: $CDN_DOMAINS " if [ ! -f "$CERT_FULLCHAIN_PATH " ] || [ ! -f "$CERT_KEY_PATH " ]; then log "错误: 证书文件不存在" send_feishu_notify "SSL证书部署失败" "证书文件不存在" "false" exit 1fi CERT_CONTENT=$(cat "$CERT_FULLCHAIN_PATH " ) KEY_CONTENT=$(cat "$CERT_KEY_PATH " )log "步骤1: 上传证书到阿里云 CAS..." UPLOAD_RESULT=$(aliyun cas UploadUserCertificate \ --Name "${CERT_NAME} -$(date +%Y%m%d-%H%M%S) " \ --Cert "$CERT_CONTENT " \ --Key "$KEY_CONTENT " \ --region "$ALIBABA_CLOUD_REGION_ID " 2>&1)if [ $? -ne 0 ]; then log "上传证书失败: $UPLOAD_RESULT " send_feishu_notify "SSL证书上传失败" "${UPLOAD_RESULT} " "false" exit 1fi CERT_ID=$(echo "$UPLOAD_RESULT " | grep -oE '"CertId": [0-9]+' | grep -oE '[0-9]+' )if [ -z "$CERT_ID " ]; then log "无法获取证书 ID: $UPLOAD_RESULT " send_feishu_notify "SSL证书上传失败" "无法获取证书 ID" "false" exit 1fi log "证书上传成功, CertId: $CERT_ID " log "步骤2: 清理旧证书(保留最近 $KEEP_CERT_COUNT 个)..." CERT_LIST=$(aliyun cas ListUserCertificateOrder \ --RegionId "$ALIBABA_CLOUD_REGION_ID " \ --OrderType UPLOAD \ --ShowSize 100 2>&1) OLD_CERTS=$(echo "$CERT_LIST " | grep -E '"CertificateId":|"Name":' | paste - - | grep "$CERT_NAME " | grep -oE '"CertificateId": [0-9]+' | grep -oE '[0-9]+' | sort -rn) TOTAL_COUNT=$(echo "$OLD_CERTS " | wc -l) DELETE_COUNT=$((TOTAL_COUNT - KEEP_CERT_COUNT))log "发现 $TOTAL_COUNT 个证书" if [ "$DELETE_COUNT " -gt 0 ]; then log "将删除 $DELETE_COUNT 个旧证书,保留最新的 $KEEP_CERT_COUNT 个" echo "$OLD_CERTS " | tail -n "$DELETE_COUNT " | while read -r OLD_CERT_ID; do if [ "$OLD_CERT_ID " != "$CERT_ID " ]; then log "删除旧证书: $OLD_CERT_ID " aliyun cas DeleteUserCertificate \ --CertId "$OLD_CERT_ID " \ --RegionId "$ALIBABA_CLOUD_REGION_ID " 2>&1 | tail -1 fi done else log "证书数量 ($TOTAL_COUNT ) 未超过保留数量 ($KEEP_CERT_COUNT ),无需清理" fi log "步骤3: 更新 CDN 域名证书..." for CDN_DOMAIN in $CDN_DOMAINS ; do log "正在更新 CDN 域名: $CDN_DOMAIN ..." UPDATE_RESULT=$(aliyun cdn SetCdnDomainSSLCertificate \ --DomainName "$CDN_DOMAIN " \ --CertName "${CERT_NAME} -$(date +%Y%m%d) " \ --CertType "cas" \ --SSLProtocol "on" \ --CertId "$CERT_ID " \ --CertRegion "cn-hangzhou" \ --region "$ALIBABA_CLOUD_REGION_ID " 2>&1) if [ $? -ne 0 ]; then log " 更新 $CDN_DOMAIN 失败: $UPDATE_RESULT " else log " 更新 $CDN_DOMAIN 成功" fi done log "步骤4: 刷新 CDN 缓存..." for CDN_DOMAIN in $CDN_DOMAINS ; do log "正在刷新 CDN 域名缓存: $CDN_DOMAIN ..." REFRESH_RESULT=$(aliyun cdn RefreshObjectCaches \ --RegionId "$ALIBABA_CLOUD_REGION_ID " \ --ObjectType Directory \ --ObjectPath "https://${CDN_DOMAIN} /" 2>&1) if [ $? -eq 0 ]; then REFRESH_TASK_ID=$(echo "$REFRESH_RESULT " | grep -oE '"RefreshTaskId": "[^"]*"' | cut -d'"' -f4) log " 刷新任务提交成功,TaskId: $REFRESH_TASK_ID " else log " 刷新 $CDN_DOMAIN 失败: $REFRESH_RESULT " fi done log "步骤5: 更新服务器本地证书..." NGINX_CERT_DIR="/etc/nginx/ssl" mkdir -p "$NGINX_CERT_DIR " cp "$CERT_FULLCHAIN_PATH " "$NGINX_CERT_DIR /${DOMAIN} .crt" cp "$CERT_KEY_PATH " "$NGINX_CERT_DIR /${DOMAIN} .key" chmod 644 "$NGINX_CERT_DIR /${DOMAIN} .crt" chmod 600 "$NGINX_CERT_DIR /${DOMAIN} .key" log "本地证书已更新到 $NGINX_CERT_DIR " log "步骤6: 测试并重载 nginx..." if nginx -t 2>&1 | grep -q "successful" ; then systemctl reload nginx log "nginx 重载成功" else log "nginx 配置测试失败" exit 1fi log "证书部署完成! CDN 缓存刷新可能需要 5-10 分钟生效" log "========================================" FEISHU_CONTENT="**部署详情:**\\n- 证书 ID:${CERT_ID} \\n- CDN 更新:${SUCCESS_DOMAINS} \\n- 缓存刷新:${REFRESH_TASKS} " send_feishu_notify "SSL证书部署成功" "$FEISHU_CONTENT " "true"
配置步骤 1. 创建飞书机器人(可选,用于通知)
打开飞书群 → 点击右上角 设置 → 群机器人 → 添加机器人
选择 自定义机器人
设置机器人名称(如”证书更新通知”)
安全设置:
推荐 :关闭签名校验,开启 IP 白名单 ,添加服务器 IP
或:开启 签名校验 ,复制签名密钥(需要确保服务器时间准确)
复制 Webhook 地址
2. 创建 RAM 子账号并授权 为了安全,建议创建专用子账号:
登录 阿里云 RAM 控制台
创建用户,选择 “OpenAPI 调用访问”
记录 AccessKey ID 和 AccessKey Secret
添加权限策略:
AliyunCASFullAccess - SSL 证书管理
AliyunCDNFullAccess - CDN 管理
2. 安装阿里云 CLI curl -fsSL https://aliyuncli.alicdn.com/install.sh | bashsource ~/.bashrc aliyun configure set --access-key-id YOUR_AK --access-key-secret YOUR_SK --region cn-hangzhou
3. 创建部署脚本 mkdir -p /opt/aliyun-cert-deploychmod +x /opt/aliyun-cert-deploy/deploy.sh
4. 配置 acme.sh 续期钩子 ~/.acme.sh/acme.sh --renew-hook "/opt/aliyun-cert-deploy/deploy.sh" -d example.com crontab -l | grep acme.sh || echo "0 2 * * * /root/.acme.sh/acme.sh --cron --home /root/.acme.sh >> /var/log/acme.sh.log 2>&1" | crontab -
5. 配置环境变量(推荐) 创建 /opt/aliyun-cert-deploy/.env 文件:
export FEISHU_WEBHOOK="https://open.feishu.cn/open-apis/bot/v2/hook/413ff0ff-48ee-4c86-8e63-6ca304a7bb2e" export FEISHU_SECRET="VVoXnUMsjQKEZtWbkZhIoh" export ALIBABA_CLOUD_ACCESS_KEY_ID="你的AccessKeyID" export ALIBABA_CLOUD_ACCESS_KEY_SECRET="你的AccessKeySecret" export DOMAIN="yourdomain.com" export CERT_NAME="yourdomain-cert" export CDN_DOMAINS="www.yourdomain.com api.yourdomain.com"
注意 :飞书机器人要求消息内容必须包含 “SSL” 关键字,脚本中的通知标题已包含 “SSL证书部署成功/失败” 等字样。
或者直接在脚本中修改配置项:
export ALIBABA_CLOUD_ACCESS_KEY_ID="xxx" export ALIBABA_CLOUD_ACCESS_KEY_SECRET="xxx" export DOMAIN="yourdomain.com" export CDN_DOMAINS="www.yourdomain.com" /opt/aliyun-cert-deploy/deploy.sh
工作流程详解 自动续期流程 每天 2:00 │ ▼ acme.sh --cron (检查证书是否到期) │ ├── 未到期 → 跳过 │ └── 到期前 30 天 │ ▼ 自动续期证书 │ ▼ 触发 renew-hook │ ▼ 执行 deploy .sh
脚本执行流程
步骤
操作
API
说明
1
上传证书
UploadUserCertificate
上传到阿里云 CAS
2
清理旧证书
DeleteUserCertificate
只保留最新的 N 个
3
更新 CDN 证书
SetCdnDomainSSLCertificate
绑定新证书到 CDN
4
刷新 CDN 缓存
RefreshObjectCaches
关键!确保新证书生效
5
更新本地证书
cp
复制到 nginx 目录
6
重载 nginx
nginx -s reload
使新证书生效
7
飞书通知
Webhook
发送部署结果通知
关键问题:为什么要刷新 CDN 缓存? 如果不刷新 CDN 缓存,用户可能仍然看到旧证书!
CDN 节点会缓存 SSL 证书信息。即使你在控制台更新了证书,边缘节点可能还在使用缓存的旧证书,导致:
浏览器显示证书过期
证书指纹不匹配
部分用户访问正常,部分用户报错
解决方案:
脚本中添加了 RefreshObjectCaches 调用,更新证书后立即提交缓存刷新任务。通常 5-10 分钟后全局生效。
验证和测试 1. 手动测试部署脚本 /opt/aliyun-cert-deploy/deploy.sh
2. 强制续期测试完整流程 ~/.acme.sh/acme.sh --renew -d yourdomain.com --force
3. 查看日志 tail -f /var/log/aliyun-cert-deploy.logtail -f /var/log/acme.sh.log
4. 验证 CDN 证书 openssl s_client -servername www.yourdomain.com -connect www.yourdomain.com:443 </dev/null 2>/dev/null | openssl x509 -noout -dates
安全建议
使用 RAM 子账号 :不要直接使用主账号 AccessKey
最小权限原则 :只授予 CAS 和 CDN 必要权限
定期轮换密钥 :每 3-6 个月更换一次 AccessKey
保护密钥 :不要将密钥提交到代码仓库
启用 MFA :为 RAM 用户开启多因素认证
总结 这套方案实现了 SSL 证书全生命周期自动化:
✅ 自动续期(acme.sh)
✅ 自动上传阿里云 CAS
✅ 自动更新 CDN 证书
✅ 自动刷新 CDN 缓存 (避免证书不生效问题)
✅ 自动清理旧证书
✅ 自动更新本地服务器证书
全程无需人工干预,再也不用担心证书过期导致网站无法访问!
故障排查 飞书通知发送失败 配置方式一:签名校验(推荐)
飞书群机器人设置 → 开启 签名校验
复制签名密钥到 .env 文件的 FEISHU_SECRET
确保服务器时间与标准时间同步:
配置方式二:IP 白名单(更简单)
飞书群机器人设置 → 关闭签名校验
开启 IP 白名单 ,添加服务器 IP(如 8.152.212.165)
修改 .env 文件:export FEISHU_SECRET=""
证书更新后浏览器仍显示旧证书
等待 5-10 分钟,让 CDN 缓存刷新生效
强制刷新浏览器缓存(Ctrl+F5)
检查脚本中的 CDN 刷新步骤是否执行成功
参考链接
文章作者: 阿文
版权声明: 本博客所有文章除特别声明外,均采用
CC BY-NC-SA 4.0 许可协议。转载请注明来自
阿文的博客 !
评论
0 条评论