腾讯云SSL证书自动更新

2024年4月25日起,腾讯云新签发的免费证书有效期调整为 90 天,每次网站证书都会挂掉,非常不方便,故写一个脚本,让每次证书能自动更新

image-20250812162338160

由于我们需要调用腾讯云关于证书方面的API,因此首先需去腾讯云管理台申请密钥并赋予相关权限,这里已经申请好了

image-20250812162901657

由于自动更新,我们这里在证书控制台把证书设置成自动续费

image-20250812163048953

这里用到两个腾讯云关于证书方面的API,点击可以看接口文档:

查看官方文档,调用接口需要签名,我们这里用Shell脚本写下get_authorization_haeder函数,里面封装我们的请求方法

get_authorization_haeder() (
        ACTION=$1
        PAYLOAD=$2

        timestamp=$(date +%s)
        date=$(date -u -d @$timestamp +"%Y-%m-%d")

        # ************* 步骤 1:拼接规范请求串 *************
        http_request_method="POST"
        canonical_uri="/"
        canonical_querystring=""
        canonical_headers="content-type:application/json; charset=utf-8\nhost:$TENCENT_REQUEST_DOMAIN\nx-tc-action:$(echo $ACTION | awk '{print tolower($0)}')\n"
        signed_headers="content-type;host;x-tc-action"
        hashed_request_payload=$(echo -n "$PAYLOAD" | openssl sha256 -hex | awk '{print $2}')
        canonical_request="$http_request_method\n$canonical_uri\n$canonical_querystring\n$canonical_headers\n$signed_headers\n$hashed_request_payload"

        # ************* 步骤 2:拼接待签名字符串 *************
        credential_scope="$date/$TENCENT_SERVICE/tc3_request"
        hashed_canonical_request=$(printf "$canonical_request" | openssl sha256 -hex | awk '{print $2}')
        string_to_sign="$TENCENT_ALGORITHM\n$timestamp\n$credential_scope\n$hashed_canonical_request"

        # ************* 步骤 3:计算签名 *************
        secret_date=$(printf "$date" | openssl sha256 -hmac "TC3$TENCENT_SECRET_KEY" | awk '{print $2}')
        secret_service=$(printf $TENCENT_SERVICE | openssl dgst -sha256 -mac hmac -macopt hexkey:"$secret_date" | awk '{print $2}')
        secret_signing=$(printf "tc3_request" | openssl dgst -sha256 -mac hmac -macopt hexkey:"$secret_service" | awk '{print $2}')
        signature=$(printf "$string_to_sign" | openssl dgst -sha256 -mac hmac -macopt hexkey:"$secret_signing" | awk '{print $2}')

        # ************* 步骤 4:拼接 Authorization *************
        authorization="$TENCENT_ALGORITHM Credential=$TENCENT_SECRET_ID/$credential_scope, SignedHeaders=$signed_headers, Signature=$signature"

        response=$(curl -XPOST "https://$TENCENT_REQUEST_DOMAIN" -d "$PAYLOAD" \
                -H "Authorization: $authorization"      \
                -H "Content-Type: $CT_JSON"     \
                -H "Host: $TENCENT_REQUEST_DOMAIN"      \
                -H "X-TC-Action: $ACTION"       \
                -H "X-TC-Timestamp: $timestamp" \
                -H "X-TC-Version: $TENCENT_VERSION")
        echo $response
)

请求解析我们用jq来解析JSON,安装脚本

apt install jq # Ubuntu
yum install jq # CentOS

证书信息我们主要拿到Response下的Certificates下,这个是证书列表, 我们需要匹配Domain和自己域名相同的,最后拿到证书ID,脚本如下

json_response=$(get_authorization_haeder DescribeCertificates "{}")

cert_info=$(echo "$json_response" | jq -r \
        --arg target "$TARGET_DOMAIN" '
        .Response.Certificates[] |
        select(
            (.Domain == $target) or
            (.SubjectAltName |
                if type == "array" then any(. == $target)
                else . == $target end
            )
        )
')

certificate_id=$(echo "$cert_info" | jq -r '.CertificateId')

然后我们就可以调用下载接口来获取zip包了,注意返回是一个base64编码的,我们需要解码一下

response=$(get_authorization_haeder DownloadCertificate "{\"CertificateId\":\"$certificate_id\"}")

content_base64=$(echo "$response" | jq -r '.Response.Content' )

echo "$content_base64" | base64 -d > $TEMP_DIR/$timestamp/$TARGET_DOMAIN.zip

解压下来是长这样的,我们主要要拿到Nginx里面的crt和key,然后复制到对应目录下就可以了

image-20250812163956200

添加到crontab,由于证书是提前一个月会自动续费,因此我这里设置成每两个月执行一次

crontab -e

# 证书自动更新
0 0 15 2,4,6,8,10,12 * /data/scripts/ssl_auto_update.sh
0 3 1 * * find /var/log/ssl_update -type f -name '*.log' -mtime +90 -delete

完整脚本:

#!/bin/bash

TENCENT_REQUEST_DOMAIN="ssl.tencentcloudapi.com"
TENCENT_ALGORITHM="TC3-HMAC-SHA256"
TENCENT_SERVICE="ssl"
TENCENT_VERSION="2019-12-05"
CT_JSON="application/json; charset=utf-8"

LOG_FILE=/var/log/ssl_update/$(date +%s).log
TEMP_DIR="/tmp/ssl"
TARGET_DIR="/etc/nginx/conf.ssl.d/<domain>_nginx"
SSL_TYPE="Nginx"
CRT_TARGET_NAME="<domain>.crt"
KEY_TARGET_NAME="<domain>.key"
RELOAD_NGINX_COMMAND="docker exec -it nginx nginx -s reload"

TARGET_DOMAIN="<your domain>"
TENCENT_SECRET_ID="<your access key>"
TENCENT_SECRET_KEY="<your secret key>"

get_authorization_haeder() (
        ACTION=$1
        PAYLOAD=$2

        timestamp=$(date +%s)
        date=$(date -u -d @$timestamp +"%Y-%m-%d")

        # ************* 步骤 1:拼接规范请求串 *************
        http_request_method="POST"
        canonical_uri="/"
        canonical_querystring=""
        canonical_headers="content-type:application/json; charset=utf-8\nhost:$TENCENT_REQUEST_DOMAIN\nx-tc-action:$(echo $ACTION | awk '{print tolower($0)}')\n"
        signed_headers="content-type;host;x-tc-action"
        hashed_request_payload=$(echo -n "$PAYLOAD" | openssl sha256 -hex | awk '{print $2}')
        canonical_request="$http_request_method\n$canonical_uri\n$canonical_querystring\n$canonical_headers\n$signed_headers\n$hashed_request_payload"

        # ************* 步骤 2:拼接待签名字符串 *************
        credential_scope="$date/$TENCENT_SERVICE/tc3_request"
        hashed_canonical_request=$(printf "$canonical_request" | openssl sha256 -hex | awk '{print $2}')
        string_to_sign="$TENCENT_ALGORITHM\n$timestamp\n$credential_scope\n$hashed_canonical_request"

        # ************* 步骤 3:计算签名 *************
        secret_date=$(printf "$date" | openssl sha256 -hmac "TC3$TENCENT_SECRET_KEY" | awk '{print $2}')
        secret_service=$(printf $TENCENT_SERVICE | openssl dgst -sha256 -mac hmac -macopt hexkey:"$secret_date" | awk '{print $2}')
        secret_signing=$(printf "tc3_request" | openssl dgst -sha256 -mac hmac -macopt hexkey:"$secret_service" | awk '{print $2}')
        signature=$(printf "$string_to_sign" | openssl dgst -sha256 -mac hmac -macopt hexkey:"$secret_signing" | awk '{print $2}')

        # ************* 步骤 4:拼接 Authorization *************
        authorization="$TENCENT_ALGORITHM Credential=$TENCENT_SECRET_ID/$credential_scope, SignedHeaders=$signed_headers, Signature=$signature"

        response=$(curl -XPOST "https://$TENCENT_REQUEST_DOMAIN" -d "$PAYLOAD" \
                -H "Authorization: $authorization"      \
                -H "Content-Type: $CT_JSON"     \
                -H "Host: $TENCENT_REQUEST_DOMAIN"      \
                -H "X-TC-Action: $ACTION"       \
                -H "X-TC-Timestamp: $timestamp" \
                -H "X-TC-Version: $TENCENT_VERSION")
        echo $response
)

json_response=$(get_authorization_haeder DescribeCertificates "{}")

cert_info=$(echo "$json_response" | jq -r \
        --arg target "$TARGET_DOMAIN" '
        .Response.Certificates[] |
        select(
            (.Domain == $target) or
            (.SubjectAltName |
                if type == "array" then any(. == $target)
                else . == $target end
            )
        )
')

if [ -z "$cert_info" ] || [ "$cert_info" == "null" ]; then
        echo "未找到域名 '$TARGET_DOMAIN' 的证书信息, 返回结果: $json_response" >> $LOG_FILE
        exit 1
fi

certificate_id=$(echo "$cert_info" | jq -r '.CertificateId')
echo "下载证书[$certificate_id]" >> $LOG_FILE

response=$(get_authorization_haeder DownloadCertificate "{\"CertificateId\":\"$certificate_id\"}")
content_base64=$(echo "$response" | jq -r '.Response.Content' )

if [ -z "$content_base64" ] || [ "$content_base64" == "null" ]; then
        echo "下载证书失败, 返回结果: $response" >> $LOG_FILE
        exit 1
fi

timestamp=$(date +%s)
mkdir -p $TEMP_DIR/$timestamp
echo "$content_base64" | base64 -d > $TEMP_DIR/$timestamp/$TARGET_DOMAIN.zip
unzip $TEMP_DIR/$timestamp/$TARGET_DOMAIN.zip -d $TEMP_DIR/$timestamp
echo "成功解压到临时文件夹: $TEMP_DIR/$timestamp" >> $LOG_FILE

mkdir -p $TEMP_DIR/$timestamp/Backup
cp -r $TARGET_DIR/* $TEMP_DIR/$timestamp/Backup
echo "当前配置备份到 $TEMP_DIR/$timestamp/Backup" >> $LOG_FILE

cp -f $TEMP_DIR/$timestamp/$SSL_TYPE/*.key $TARGET_DIR/$KEY_TARGET_NAME
cp -f $TEMP_DIR/$timestamp/$SSL_TYPE/*.crt $TARGET_DIR/$CRT_TARGET_NAME
echo "文件替换完成" >> $LOG_FILE

eval "$RELOAD_NGINX_COMMAND"
echo "重载Nginx完成"    >> $LOG_FILE