深夜提醒

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

🏮 🏮 🏮

新年快乐

祝君万事如意心想事成!

share-image
中
ESC

A Complete Solution for Auto-Updating SSL Certificates to Aliyun CDN

English 阅读中文版

Background

Free SSL certificates issued by Let’s Encrypt or ZeroSSL are only valid for 90 days and need to be renewed periodically. If your site uses Aliyun CDN, every renewal also requires manually uploading the new certificate to Aliyun and updating the CDN configuration — a process that is tedious and error-prone.

This article introduces a complete automation solution that achieves:

  • Automatic certificate renewal
  • Automatic upload to Aliyun CAS (Certificate Center)
  • Automatic update of the CDN domain certificate
  • Automatic CDN cache refresh (the critical step!)
  • Automatic cleanup of old certificates
  • Automatic update of the local server certificate
  • Feishu notifications (to keep track of certificate status in real time)

Architecture Design

┌─────────────────┐
│ acme.sh │ Auto-renews Let's Encrypt/ZeroSSL certificates
│ (scheduled job)│ Checks at 2:00 every day
└────────┬────────┘
│ certificate renewed successfully
▼
┌─────────────────────────┐
│ deploy.sh (hook script)│ Performs certificate deployment
└────────┬────────────────┘
│
┌────┴────┬──────────┬──────────┐
▼ ▼ ▼ ▼
┌───────┐ ┌────────┐ ┌────────┐ ┌──────────┐ ┌────────┐
│Aliyun │ │Aliyun │ │CDN │ │Local │ │Feishu │
│CAS │ │CDN │ │cache │ │Nginx │ │notify │
│upload │ │update │ │refresh │ │cert │ │ │
│cert │ │binding │ │ │ │update │ │ │
│ │ │ │ │ │ │reload │ │ │
└───────┘ └────────┘ └────────┘ └──────────┘ └────────┘

Notification Example

After a successful deployment, Feishu receives a message like this:

Prerequisites

  1. acme.sh is installed
  2. Aliyun CLI is installed
  3. A domain certificate has been applied for and configured (the example uses ZeroSSL)
  4. An Aliyun RAM sub-account with CAS and CDN management permissions

Complete Script

Save the following script to /opt/aliyun-cert-deploy/deploy.sh:

#!/bin/bash
# Aliyun SSL certificate auto-deployment script (with Feishu notifications)
# Function: after renewal, automatically upload to Aliyun CAS, update the CDN certificate, refresh the CDN cache, and send a Feishu notification

# Load environment variables
if [ -f /opt/aliyun-cert-deploy/.env ]; then
source /opt/aliyun-cert-deploy/.env
fi

set -e

# ============================================
# Configuration (modify as needed)
# ============================================
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 domain list (space-separated)
CDN_DOMAINS="${CDN_DOMAINS:-www.example.com}"

# Feishu bot configuration
FEISHU_WEBHOOK="${FEISHU_WEBHOOK:-}"
FEISHU_SECRET="${FEISHU_SECRET:-}"

# Number of certificates to keep (keep the newest N certificates in Aliyun CAS)
KEEP_CERT_COUNT=3

# ============================================
# Path configuration
# ============================================
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"

# ============================================
# Feishu notification functions
# ============================================

# Generate the Feishu signature (if a secret is configured)
generate_feishu_sign() {
local timestamp=$1
local secret=$2

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

# Generate signature: HMAC-SHA256 of timestamp + "\n" + secret, then base64
echo -ne "${timestamp}\n${secret}" | openssl dgst -sha256 -hmac "$secret" -binary | base64
}

# Send a Feishu notification
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:** ${DOMAIN}\\n**Status:** $([ "$is_success" == "true" ] && echo "✅ Success" || echo "❌ Failed")\\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:** ${DOMAIN}\\n**Status:** $([ "$is_success" == "true" ] && echo "✅ Success" || echo "❌ Failed")\\n${content}"
}
}
]
}
}
EOF
)
fi

curl -s -X POST "$FEISHU_WEBHOOK" -H "Content-Type: application/json" -d "$json_body" > /dev/null
}

# ============================================
# Logging function
# ============================================
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a $LOG_FILE
}

# ============================================
# Main flow
# ============================================

# Configure the Aliyun 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 "Starting certificate deployment to Aliyun..."
log "Domain: $DOMAIN"
log "CDN domain list: $CDN_DOMAINS"

# Check the certificate files
if [ ! -f "$CERT_FULLCHAIN_PATH" ] || [ ! -f "$CERT_KEY_PATH" ]; then
log "Error: certificate file does not exist"
send_feishu_notify "SSL certificate deployment failed" "Certificate file does not exist" "false"
exit 1
fi

# Read the certificate contents
CERT_CONTENT=$(cat "$CERT_FULLCHAIN_PATH")
KEY_CONTENT=$(cat "$CERT_KEY_PATH")

# ============================================
# Step 1: Upload the certificate to Aliyun CAS
# ============================================
log "Step 1: Uploading certificate to Aliyun 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 "Failed to upload certificate: $UPLOAD_RESULT"
send_feishu_notify "SSL certificate upload failed" "${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 "Unable to obtain certificate ID: $UPLOAD_RESULT"
send_feishu_notify "SSL certificate upload failed" "Unable to obtain certificate ID" "false"
exit 1
fi

log "Certificate uploaded successfully, CertId: $CERT_ID"

# ============================================
# Step 2: Clean up old certificates
# ============================================
log "Step 2: Cleaning up old certificates (keeping the most recent $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 "Found $TOTAL_COUNT certificates"

if [ "$DELETE_COUNT" -gt 0 ]; then
log "Will delete $DELETE_COUNT old certificates, keeping the newest $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 "Deleting old certificate: $OLD_CERT_ID"
aliyun cas DeleteUserCertificate \
--CertId "$OLD_CERT_ID" \
--RegionId "$ALIBABA_CLOUD_REGION_ID" 2>&1 | tail -1
fi
done
else
log "Certificate count ($TOTAL_COUNT) does not exceed the keep count ($KEEP_CERT_COUNT); no cleanup needed"
fi

# ============================================
# Step 3: Update the CDN domain certificate
# ============================================
log "Step 3: Updating CDN domain certificates..."

for CDN_DOMAIN in $CDN_DOMAINS; do
log "Updating CDN domain: $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 " Failed to update $CDN_DOMAIN: $UPDATE_RESULT"
else
log " Updated $CDN_DOMAIN successfully"
fi
done

# ============================================
# Step 4: Refresh the CDN cache (critical step!)
# ============================================
log "Step 4: Refreshing CDN cache..."

for CDN_DOMAIN in $CDN_DOMAINS; do
log "Refreshing CDN domain cache: $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 " Refresh task submitted successfully, TaskId: $REFRESH_TASK_ID"
else
log " Failed to refresh $CDN_DOMAIN: $REFRESH_RESULT"
fi
done

# ============================================
# Step 5: Update the server's local certificate
# ============================================
log "Step 5: Updating the server's local certificate..."

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 "Local certificate updated to $NGINX_CERT_DIR"

# ============================================
# Step 6: Test and reload nginx
# ============================================
log "Step 6: Testing and reloading nginx..."

if nginx -t 2>&1 | grep -q "successful"; then
systemctl reload nginx
log "nginx reloaded successfully"
else
log "nginx configuration test failed"
exit 1
fi

log "Certificate deployment complete! CDN cache refresh may take 5-10 minutes to take effect"
log "========================================"

# ============================================
# Send a success notification
# ============================================
FEISHU_CONTENT="**Deployment details:**\\n- Certificate ID: ${CERT_ID}\\n- CDN updates: ${SUCCESS_DOMAINS}\\n- Cache refreshes: ${REFRESH_TASKS}"
send_feishu_notify "SSL certificate deployment succeeded" "$FEISHU_CONTENT" "true"

Configuration Steps

1. Create a Feishu bot (optional, for notifications)

  1. Open a Feishu group → click Settings in the top right → Group Bots → Add Bot
  2. Choose Custom Bot
  3. Set the bot name (e.g. “Certificate Update Notifications”)
  4. Security settings:
    • Recommended: Turn off signature verification and enable the IP allowlist, adding the server IP
    • Or: Enable signature verification and copy the signature secret (you must ensure the server clock is accurate)
  5. Copy the Webhook URL

2. Create an RAM sub-account and grant permissions

For security, it’s best to create a dedicated sub-account:

  1. Log in to the Aliyun RAM console
  2. Create a user and choose “OpenAPI access”
  3. Note the AccessKey ID and AccessKey Secret
  4. Add permission policies:
    • AliyunCASFullAccess - SSL certificate management
    • AliyunCDNFullAccess - CDN management

2. Install the Aliyun 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. Create the deployment script

mkdir -p /opt/aliyun-cert-deploy
# Save the script above to /opt/aliyun-cert-deploy/deploy.sh
chmod +x /opt/aliyun-cert-deploy/deploy.sh

4. Configure the acme.sh renewal hook

# Set the renewal hook
~/.acme.sh/acme.sh --renew-hook "/opt/aliyun-cert-deploy/deploy.sh" -d example.com

# Make sure the scheduled job exists
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 -

Create the /opt/aliyun-cert-deploy/.env file:

# Feishu bot configuration (notification messages must contain the SSL keyword)
export FEISHU_WEBHOOK="https://open.feishu.cn/open-apis/bot/v2/hook/413ff0ff-48ee-4c86-8e63-6ca304a7bb2e"
export FEISHU_SECRET="VVoXnUMsjQKEZtWbkZhIoh" # signature secret; fill in when signature verification is enabled

# Aliyun configuration
export ALIBABA_CLOUD_ACCESS_KEY_ID="your-AccessKeyID"
export ALIBABA_CLOUD_ACCESS_KEY_SECRET="your-AccessKeySecret"
export DOMAIN="yourdomain.com"
export CERT_NAME="yourdomain-cert"
export CDN_DOMAINS="www.yourdomain.com api.yourdomain.com"

Note: The Feishu bot requires the message content to contain the “SSL” keyword; the notification titles in the script already contain phrases like “SSL certificate deployment succeeded/failed”.

Or just modify the configuration items directly in the script:

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

Workflow in Detail

Automatic Renewal Flow

Every day at 2:00
│
▼
acme.sh --cron (checks whether the certificate is expiring)
│
├── not expiring → skip
│
└── 30 days before expiry
│
▼
renew certificate automatically
│
▼
trigger renew-hook
│
▼
run deploy.sh

Script Execution Flow

Step Action API Description
1 Upload certificate UploadUserCertificate Upload to Aliyun CAS
2 Clean up old certificates DeleteUserCertificate Keep only the newest N
3 Update CDN certificate SetCdnDomainSSLCertificate Bind the new certificate to the CDN
4 Refresh CDN cache RefreshObjectCaches Critical! Ensures the new certificate takes effect
5 Update local certificate cp Copy to the nginx directory
6 Reload nginx nginx -s reload Make the new certificate take effect
7 Feishu notification Webhook Send the deployment result notification

Key Question: Why Refresh the CDN Cache?

If you don’t refresh the CDN cache, users may still see the old certificate!

CDN nodes cache SSL certificate information. Even if you update the certificate in the console, edge nodes may still be using the cached old certificate, causing:

  • The browser shows the certificate as expired
  • The certificate fingerprint doesn’t match
  • Some users access fine while others get errors

Solution:

The script adds a RefreshObjectCaches call to submit a cache refresh task immediately after updating the certificate. It usually takes effect globally within 5-10 minutes.

Verification and Testing

1. Manually Test the Deployment Script

/opt/aliyun-cert-deploy/deploy.sh

2. Force a Renewal to Test the Full Flow

~/.acme.sh/acme.sh --renew -d yourdomain.com --force

3. Check the Logs

# Deployment log
tail -f /var/log/aliyun-cert-deploy.log

# acme.sh log
tail -f /var/log/acme.sh.log

4. Verify the CDN Certificate

# View the certificate actually returned
openssl s_client -servername www.yourdomain.com -connect www.yourdomain.com:443 </dev/null 2>/dev/null | openssl x509 -noout -dates

Security Recommendations

  1. Use a RAM sub-account: Don’t use the primary account’s AccessKey directly
  2. Least privilege: Grant only the CAS and CDN permissions that are necessary
  3. Rotate keys regularly: Change the AccessKey every 3-6 months
  4. Protect the keys: Don’t commit keys to a code repository
  5. Enable MFA: Turn on multi-factor authentication for the RAM user

Summary

This solution automates the entire SSL certificate lifecycle:

  • ✅ Automatic renewal (acme.sh)
  • ✅ Automatic upload to Aliyun CAS
  • ✅ Automatic update of the CDN certificate
  • ✅ Automatic CDN cache refresh (avoiding the “certificate not taking effect” problem)
  • ✅ Automatic cleanup of old certificates
  • ✅ Automatic update of the local server certificate

No manual intervention is needed from start to finish. No more worrying about an expired certificate taking your site offline!

Troubleshooting

Feishu Notification Fails to Send

Option one: signature verification (recommended)

  • Feishu group bot settings → enable signature verification
  • Copy the signature secret into FEISHU_SECRET in the .env file
  • Make sure the server clock is in sync with standard time:
    # Check the time
    date
    # Sync the time
    chronyc makestep

Option two: IP allowlist (simpler)

  • Feishu group bot settings → turn off signature verification
  • Enable the IP allowlist and add the server IP (e.g. 8.152.212.165)
  • Modify the .env file: export FEISHU_SECRET=""

The Browser Still Shows the Old Certificate After the Update

  1. Wait 5-10 minutes for the CDN cache refresh to take effect
  2. Force-refresh the browser cache (Ctrl+F5)
  3. Check whether the CDN refresh step in the script ran successfully
文章作者:阿文
文章链接: https://www.awen.me/post/3263bf56.html
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 阿文的博客!
本文于 2026-03-01 13:30 发布,已超过半年(229天),请注意甄别内容是否已过期。

评论

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

留言反馈

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