深夜提醒

现在是深夜,建议您注意休息,不要熬夜哦~

🏮 🏮 🏮

新年快乐

祝君万事如意心想事成!

2024 桐庐半程马拉松
00:00:00
时间
0.00
距离(公里)
--:--
配速
--
步频
--
心率 (bpm)
--
配速
步频
|
share-image
ESC

自动更新 SSL 证书到阿里云 CDN 的完整方案

背景

使用 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 │ │ │
└───────┘ └────────┘ └────────┘ └──────────┘ └────────┘

通知效果图

部署成功后,飞书会收到如下消息:

前置条件

  1. 已安装 acme.sh
  2. 已安装 阿里云 CLI
  3. 已申请并配置好域名证书(示例使用 ZeroSSL)
  4. 阿里云账号具有 CAS 和 CDN 管理权限的 RAM 子账号

完整脚本

将以下脚本保存到 /opt/aliyun-cert-deploy/deploy.sh

#!/bin/bash
# 阿里云 SSL 证书自动部署脚本(带飞书通知)
# 功能:证书续期后自动上传到阿里云 CAS,更新 CDN 证书,刷新 CDN 缓存,发送飞书通知

# 加载环境变量
if [ -f /opt/aliyun-cert-deploy/.env ]; then
source /opt/aliyun-cert-deploy/.env
fi

set -e

# ============================================
# 配置项(根据实际情况修改)
# ============================================
export 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 域名列表(以空格分隔)
CDN_DOMAINS="${CDN_DOMAINS:-www.example.com}"

# 飞书机器人配置
FEISHU_WEBHOOK="${FEISHU_WEBHOOK:-}"
FEISHU_SECRET="${FEISHU_SECRET:-}"

# 保留的证书数量(阿里云 CAS 中保留最新的 N 个证书)
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"

# ============================================
# 飞书通知函数
# ============================================

# 生成飞书签名(如果配置了 secret)
generate_feishu_sign() {
local timestamp=$1
local secret=$2

if [ -z "$secret" ]; then
echo ""
return
fi

# 生成签名:timestamp + "\n" + secret 的 HMAC-SHA256,然后 base64
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
}

# ============================================
# 主流程
# ============================================

# 配置阿里云 CLI
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 1
fi

# 读取证书内容
CERT_CONTENT=$(cat "$CERT_FULLCHAIN_PATH")
KEY_CONTENT=$(cat "$CERT_KEY_PATH")

# ============================================
# 步骤1: 上传证书到阿里云 CAS
# ============================================
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 1
fi

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

log "证书上传成功, CertId: $CERT_ID"

# ============================================
# 步骤2: 清理旧证书
# ============================================
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

# ============================================
# 步骤3: 更新 CDN 域名证书
# ============================================
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

# ============================================
# 步骤4: 刷新 CDN 缓存(关键步骤!)
# ============================================
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

# ============================================
# 步骤5: 更新服务器本地证书
# ============================================
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"

# ============================================
# 步骤6: 测试并重载 nginx
# ============================================
log "步骤6: 测试并重载 nginx..."

if nginx -t 2>&1 | grep -q "successful"; then
systemctl reload nginx
log "nginx 重载成功"
else
log "nginx 配置测试失败"
exit 1
fi

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. 创建飞书机器人(可选,用于通知)

  1. 打开飞书群 → 点击右上角 设置群机器人添加机器人
  2. 选择 自定义机器人
  3. 设置机器人名称(如”证书更新通知”)
  4. 安全设置:
    • 推荐:关闭签名校验,开启 IP 白名单,添加服务器 IP
    • 或:开启 签名校验,复制签名密钥(需要确保服务器时间准确)
  5. 复制 Webhook 地址

2. 创建 RAM 子账号并授权

为了安全,建议创建专用子账号:

  1. 登录 阿里云 RAM 控制台
  2. 创建用户,选择 “OpenAPI 调用访问”
  3. 记录 AccessKey ID 和 AccessKey Secret
  4. 添加权限策略:
    • AliyunCASFullAccess - SSL 证书管理
    • AliyunCDNFullAccess - CDN 管理

2. 安装阿里云 CLI

curl -fsSL https://aliyuncli.alicdn.com/install.sh | bash
source ~/.bashrc
aliyun configure set --access-key-id YOUR_AK --access-key-secret YOUR_SK --region cn-hangzhou

3. 创建部署脚本

mkdir -p /opt/aliyun-cert-deploy
# 将上面的脚本保存到 /opt/aliyun-cert-deploy/deploy.sh
chmod +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 文件:

# 飞书机器人配置(通知消息必须包含 SSL 关键字)
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.log

# acme.sh 日志
tail -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

安全建议

  1. 使用 RAM 子账号:不要直接使用主账号 AccessKey
  2. 最小权限原则:只授予 CAS 和 CDN 必要权限
  3. 定期轮换密钥:每 3-6 个月更换一次 AccessKey
  4. 保护密钥:不要将密钥提交到代码仓库
  5. 启用 MFA:为 RAM 用户开启多因素认证

总结

这套方案实现了 SSL 证书全生命周期自动化:

  • ✅ 自动续期(acme.sh)
  • ✅ 自动上传阿里云 CAS
  • ✅ 自动更新 CDN 证书
  • 自动刷新 CDN 缓存(避免证书不生效问题)
  • ✅ 自动清理旧证书
  • ✅ 自动更新本地服务器证书

全程无需人工干预,再也不用担心证书过期导致网站无法访问!

故障排查

飞书通知发送失败

配置方式一:签名校验(推荐)

  • 飞书群机器人设置 → 开启 签名校验
  • 复制签名密钥到 .env 文件的 FEISHU_SECRET
  • 确保服务器时间与标准时间同步:
    # 检查时间
    date
    # 同步时间
    chronyc makestep

配置方式二:IP 白名单(更简单)

  • 飞书群机器人设置 → 关闭签名校验
  • 开启 IP 白名单,添加服务器 IP(如 8.152.212.165
  • 修改 .env 文件:export FEISHU_SECRET=""

证书更新后浏览器仍显示旧证书

  1. 等待 5-10 分钟,让 CDN 缓存刷新生效
  2. 强制刷新浏览器缓存(Ctrl+F5)
  3. 检查脚本中的 CDN 刷新步骤是否执行成功

参考链接

文章作者:阿文
文章链接: https://www.awen.me/post/bfdcd906.html
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 阿文的博客

评论

0 条评论
😀😃😄 😁😅😂 🤣😊😇 🙂🙃😉 😌😍🥰 😘😗😙 😚😋😛 😝😜🤪 🤨🧐🤓 😎🥸🤩 🥳😏😒 😞😔😟 😕🙁☹️ 😣😖😫 😩🥺😢 😭😤😠 😡🤬🤯 😳🥵🥶 😱😨😰 😥😓🤗 🤔🤭🤫 🤥😶😐 😑😬🙄 😯😦😧 😮😲🥱 😴🤤😪 😵🤐🥴 🤢🤮🤧 😷🤒🤕 🤑🤠😈 👿👹👺 🤡💩👻 💀☠️👽 👾🤖🎃 😺😸😹 😻😼😽 🙀😿😾 👍👎👏 🙌👐🤲 🤝🤜🤛 ✌️🤞🤟 🤘👌🤏 👈👉👆 👇☝️ 🤚🖐️🖖 👋🤙💪 🦾🖕✍️ 🙏💅🤳 💯💢💥 💫💦💨 🕳️💣💬 👁️‍🗨️🗨️🗯️ 💭💤❤️ 🧡💛💚 💙💜🖤 🤍🤎💔 ❣️💕💞 💓💗💖 💘💝💟 ☮️✝️☪️ 🕉️☸️✡️ 🔯🕎☯️ ☦️🛐 🆔⚛️🉑 ☢️☣️📴 📳🈶🈚 🈸🈺🈷️ ✴️🆚💮 🉐㊙️㊗️ 🈴🈵🈹 🈲🅰️🅱️ 🆎🆑🅾️ 🆘 🛑📛 🚫💯💢 ♨️🚷🚯 🚳🚱🔞 📵🚭 ‼️⁉️🔅 🔆〽️⚠️ 🚸🔱⚜️ 🔰♻️ 🈯💹❇️ ✳️🌐 💠Ⓜ️🌀 💤🏧🚾 🅿️🈳 🈂🛂🛃 🛄🛅🛗 🚀🛸🚁 🚉🚆🚅 ✈️🛫🛬 🛩️💺🛰️
您的评论由 AI 智能审核,一般1分钟内会展示,若不展示请确认你的评论是否符合社区和法律规范
加载中...

留言反馈

😀😃😄 😁😅😂 🤣😊😇 🙂🙃😉 😌😍🥰 😘😗😙 😚😋😛 😝😜🤪 🤨🧐🤓 😎🥸🤩 🥳😏😒 😞😔😟 😕🙁☹️ 😣😖😫 😩🥺😢 😭😤😠 😡🤬🤯 😳🥵🥶 😱😨😰 😥😓🤗 🤔🤭🤫 🤥😶😐 😑😬🙄 😯😦😧 😮😲🥱 😴🤤😪 😵🤐🥴 🤢🤮🤧 😷🤒🤕 🤑🤠😈 👿👹👺 🤡💩👻 💀☠️👽 👾🤖🎃 😺😸😹 😻😼😽 🙀😿😾 👍👎👏 🙌👐🤲 🤝🤜🤛 ✌️🤞🤟 🤘👌🤏 👈👉👆 👇☝️ 🤚🖐️🖖 👋🤙💪 🦾🖕✍️ 🙏💅🤳 💯💢💥 💫💦💨 🕳️💣💬 👁️‍🗨️🗨️🗯️ 💭💤❤️ 🧡💛💚 💙💜🖤 🤍🤎💔 ❣️💕💞 💓💗💖 💘💝💟 ☮️✝️☪️ 🕉️☸️✡️ 🔯🕎☯️ ☦️🛐 🆔⚛️🉑 ☢️☣️📴 📳🈶🈚 🈸🈺🈷️ ✴️🆚💮 🉐㊙️㊗️ 🈴🈵🈹 🈲🅰️🅱️ 🆎🆑🅾️ 🆘 🛑📛 🚫💯💢 ♨️🚷🚯 🚳🚱🔞 📵🚭 ‼️⁉️🔅 🔆〽️⚠️ 🚸🔱⚜️ 🔰♻️ 🈯💹❇️ ✳️🌐 💠Ⓜ️🌀 💤🏧🚾 🅿️🈳 🈂🛂🛃 🛄🛅🛗 🚀🛸🚁 🚉🚆🚅 ✈️🛫🛬 🛩️💺🛰️