【安全】SSL证书更新操作手册(Nginx+Cloudflare+acme.sh)

一、适用范围

适用环境

  • Web服务:Nginx
  • DNS服务:Cloudflare
  • 证书来源:腾讯云/Let's Encrypt(acme.sh

前置条件

  • 已具备服务器SSH登录权限
  • 已具备域名DNS管理权限
  • Nginx已安装并正常运行

二、更新方式说明

方式 适用场景 特点
手动更新 单个域名 简单但容易出错
半自动更新 少量域名 提高效率
全自动 多域名/长期维护 自动续期

三、手动更新流程

1、申请证书(腾讯云)

路径:腾讯云控制台 -> SSL 证书 -> 我的证书 -> 即将过期 -> 快速续期

步骤:

  1. 选择免费证书
  2. 提交申请
  3. 获取DNS验证记录(TXT)

2、配置DNS验证

在DNS平台(Cloudflare)

路径:Cloudflare --> Domains --> 点击域名(如aaa.com) --> DNS Records --> Add record

添加申请证书时生成的TXT记录

类型:TXT

主机记录:_dnsauth.wss

值:20260428080843ivp6|1tz8207wesar1|qb9cpgm4ykmq

等待1~2分钟生效

3、验证并签发证书

回到腾讯云点击【验证】

✔成功后自动签发证书

4、下载证书(Nginx)

签发完证书会自动跳转到证书详情 -->证书下载 --> Nginx

或者SSL证书 --> 我的证书 --> 找到刚生成的证书 --> 下载

5、上传证书

将下载下来的证书解压,解压后通常包含私钥+证书链

复制代码
xxx.key									#私钥(须保密)
xxx_bundle.crt / xxx_bundle.pem  		#证书链

将.key 私钥文件及bundle.crt证书链文件上传到服务器上nginx证书目录(证书目录需与nginx配置一致)

复制代码
scp aaa.key aaa_bundle.crt ubuntu@xx.xx.xx.xx:/tmp/
ssh ubuntu@xx.xx.xx.xx
sudo mv /tmp/*.crt /etc/nginx/ssl/aaa/
sudo mv /tmp/*.key /etc/nginx/ssl/aaa/
# aaa 表示域名
# xx.xx.xx 表示服务器IP
# 执行该命令需要具有服务器免密登录权限

6、设置访问权限

复制代码
sudo chmod 600 /etc/nginx/ssl/xxx.com/*.key
sudo chmod 644 /etc/nginx/ssl/xxx.com/*.crt

7、重载 Nginx

复制代码
# 登陆服务器
ssh ubuntu@xx.xx.xx.xx 
# 验证nginx配置是否正常
sudo nginx -t 
# 重新加载nginx配置
sudo systemctl reload nginx

8、验证证书

浏览器新建无痕窗口,访问:aaa.com

查询证书有效期


四、半自动化更新

半自动化主要是将上传证书、重载nginx配置以及验证证书有效期,全部使用脚本替代,一键执行实现,避免手动重复操作,针对批量证书需要更新的场景,可以减少证书部署的时间。

1、配置域名与服务器IP清单

cert_servers.conf 和批量部署脚本放在同一目录,使脚本能够获取域名对应的IP,上传证书以及远程重载nginx

复制代码
# 域名 SSH用户 服务器IP
aaa.com ubuntu 1.2.3.4
bbb.com ubuntu 5.6.7.8
ccc.com root 9.9.9.9
# 支持空行和 # 注释

2、批量上传部署脚本

deploy_ssl_batch.sh包含解压证书文件、获取域名对应服务器信息、验证SSH登陆、证书上传检测、上传证书、验证nginx配置、重载nginx、检查证书有效期等步骤。

服务器登陆用户主目录下./ssh/authorized_keys需要包含执行脚本客户端的ssh登陆公钥,即利用SSH公钥认证机制,实现客户端免密登陆服务器执行命令。

复制代码
#!/bin/bash
#检测未定义变量,直接报错退出
set -u

CONFIG_FILE="./cert_servers.conf"
#禁止交互式提问、连接超时8秒放弃、主机密钥检查
SSH_OPTS="-o BatchMode=yes -o ConnectTimeout=8 -o StrictHostKeyChecking=accept-new"

if [ ! -f "$CONFIG_FILE" ]; then
  echo "[ERROR] 配置文件不存在:$CONFIG_FILE"
  echo "[INFO] 格式示例: aaa.com ubuntu 1.2.3.4"
  exit 1
fi

get_server_info() {
    local domain="$1"

    awk -v d="$domain" '
    $0 ~ /^#/ {next}
    NF < 3 {next}
    $1 == d {print $2,$3;exit}
    ' "$CONFIG_FILE"
}

for zip_file in *.zip; do
    [ -f "$zip_file" ] || continue

    echo "======================================"
    echo "[INFO] 处理证书包:$zip_file"

    dirname="${zip_file%.zip}"
    domain="${dirname%_nginx}"

    echo "[INFO] 提取域名:$domain"
    echo "[INFO] 解压目录:$dirname"

    server_info=$(get_server_info "$domain")

    if [ -z "$server_info" ]; then
        echo "[WARN] $CONFIG_FILE 中未找到 $domain 对应的服务器配置,跳过"
        echo
        continue
        fi
    
    ssh_user=$(echo "$server_info" | awk '{print $1}')
    ip=$(echo "$server_info" | awk '{print $2}')

    echo "[INFO] 目标服务器:$ssh_user@$ip"

    rm -rf "$dirname"

    if ! unzip -o -q "$zip_file"; then
    echo "[ERROR] 解压失败:$zip_file"
    rm -rf "$dirname"
    continue
fi

    key_file="${dirname}/${domain}.key"
    crt_file="${dirname}/${domain}_bundle.crt"

    remote_dir="/etc/nginx/ssl/${domain}"
    tmp_key="/tmp/${domain}.key.$$"
    tmp_crt="/tmp/${domain}_bundle.crt.$$"

    if [ ! -f "$key_file" ] || [ ! -f "$crt_file" ]; then
        echo "[ERROR] 证书文件不存在:$key_file / $crt_file"
        rm -rf "$dirname"
        continue
        fi
    
    echo "[INFO] 检测 SSH 登录..."

    if ! ssh $SSH_OPTS "$ssh_user@$ip" "exit" >/dev/null 2>&1;then
        echo "[ERROR] 无法登录服务器:$ssh_user@$ip"
        rm -rf "$dirname"
        continue
        fi

    REMOTE_SUDO=""
    if [ "$ssh_user" != "root" ]; then
        REMOTE_SUDO="sudo -n"
    fi

    echo "[INFO] 检查远程证书是否与本地证书一致..."

    local_key_sha=$(sha256sum  "$key_file" | awk '{print $1}')
    local_crt_sha=$(sha256sum  "$crt_file" | awk '{print $1}')

    remote_key_sha=$(ssh $SSH_OPTS "$ssh_user@$ip" "$REMOTE_SUDO sha256sum '$remote_dir/${domain}.key' 2>/dev/null | awk '{print \$1}'" 2>/dev/null)
    remote_crt_sha=$(ssh $SSH_OPTS "$ssh_user@$ip" "$REMOTE_SUDO sha256sum '$remote_dir/${domain}_bundle.crt' 2>/dev/null | awk '{print \$1}'" 2>/dev/null)


    if [ -n "$remote_key_sha" ] && [ -n "$remote_crt_sha" ] && \
        [ "$local_key_sha" = "$remote_key_sha" ] && \
        [ "$local_crt_sha" = "$remote_crt_sha" ]; then
        echo "[INFO] 远程已是相同证书,跳过部署:$domain"
        rm -rf "$dirname"
        echo
        continue
    fi

    echo "[INFO] 上传证书到服务器临时目录..."

    if ! scp $SSH_OPTS "$key_file" "$ssh_user@$ip:$tmp_key"; then
        echo "[ERROR] 上传 key 失败: $ssh_user@$ip"
        rm -rf "$dirname"
        continue
    fi

    if ! scp $SSH_OPTS "$crt_file" "$ssh_user@$ip:$tmp_crt"; then
        echo "[ERROR] 上传 crt 失败: $ssh_user@$ip"
        ssh $SSH_OPTS "$ssh_user@$ip" "rm -f '$tmp_key'" >/dev/null 2>&1
        rm -rf "$dirname"
        continue
    fi

    echo "[INFO] 移动证书到Nginx目录并设置权限..."

    if ! ssh $SSH_OPTS "$ssh_user@$ip" "
        $REMOTE_SUDO mkdir -p '$remote_dir' &&
        $REMOTE_SUDO mv '$tmp_key' '$remote_dir/${domain}.key' &&
        $REMOTE_SUDO mv '$tmp_crt' '$remote_dir/${domain}_bundle.crt' &&
        $REMOTE_SUDO chown root:root '$remote_dir/${domain}.key' '$remote_dir/${domain}_bundle.crt' &&
        $REMOTE_SUDO chmod 600 '$remote_dir/${domain}.key' &&
        $REMOTE_SUDO chmod 644 '$remote_dir/${domain}_bundle.crt'
    "; then
        echo "[ERROR] 证书移动或权限设置失败: $ssh_user@$ip"
        ssh $SSH_OPTS "$ssh_user@$ip" "rm -f '$tmp_key' '$tmp_crt'" >/dev/null 2>&1
        rm -rf "$dirname"
        continue
    fi

    echo "[INFO] 测试 Nginx 配置..."

    if ! ssh $SSH_OPTS "$ssh_user@$ip" "$REMOTE_SUDO nginx -t"; then
        echo "[ERROR] Nginx 配置错误,跳过重载:$ssh_user@$ip"
        rm -rf "$dirname"
        continue
    fi

    echo "[INFO] 重载 Nginx ..."

    if ! ssh $SSH_OPTS "$ssh_user@$ip" "$REMOTE_SUDO systemctl reload nginx"; then
        echo "[ERROR] Nginx 重载失败:$ssh_user@$ip"
        rm -rf "$dirname"
        continue
    fi

    echo "[INFO] 本地证书有效期:"
    openssl x509 -in "$crt_file" -noout -dates

    rm -rf "$dirname"

    echo "[SUCCESS] $domain 证书部署完成"
    echo
done

echo "[SUCCESS] 所有证书批量部署完成"

3、扩展:SSh密钥生成与配置步骤

  1. 在客户端生成密钥对
    客户端终端执行:

    ssh-keygen -t ed25519 -C "your_email@example.com"

执行后会生成两个文件(默认在~/.ssh):

  • id_ed25519 私钥(保密)
  • id_ed25519.pub 公钥(可分发)
  1. 上传公钥到服务器
    查看本地客户端并复制

    cat ~/.ssh/id_ed25519.pub

腾讯云控制台 --> 云服务器 --> 需要登录的服务器 --> 登录

在主目录下

复制代码
mkdir -p ~/.ssh
chmod 700 ~/.ssh

vi ~/.ssh/authorized_keys

把公钥粘贴进去,保存后:

复制代码
chmod 600 ~/.ssh/authorized_keys
  1. 测试免密登陆

    ssh root@xx.xx.xx.xx


五、自动化更新

使用acme.sh+DNS API 将申请证书、域名验证、下载证书、上传证书、重载nginx等步骤全都自动化执行,并部署定时任务,定制检测证书有效期,当达到可续期时间,自动进行更新续期。

acme.sh 是一款纯shell脚本实现的ACME协议客户端,专门用于全自动申请、部署、续期免费SSL/TLS证书。

1、安装acme.sh

复制代码
curl https://get.acme.sh | sh
source ~/.bashrc

2、Cloudflare创建DNS API

DNS服务管理平台Cloudflare --> Manage account --> Account API tokens --> Create Token

建议先使用单个域名进行测试话自动化更新效果,待验证成功并且没有问题以后,再扩展至所有域名。

Token具有的权限:

Specified Domains --> aaa.com --> DNS & Zones --> DNS Edit + Zone Read

Token expiration 需要自动化续期证书,选择较长的有效期较为适合

Client IP address filtering 选择服务器公网IP,仅允许服务器访问使用

3、配置DNS API

配置临时环境变量

复制代码
export CF_Token="你的API Token"
export CF_Account_ID="你的Account ID"

acme.sh 在成功执行DNS API后,通常会将相关配置保存到配置文件里,后续cron证书续期会继续使用,也可以直接对./acme.sh/account.conf进行配置

4、签发证书

测试环境:

复制代码
acme.sh --issue --dns dns_cf -d aaa.com -d "*.aaa.com"  --test

这里会使用Let's Encrypt测试环境,不会影响正式证书额度,临时添加_acme-challenge.aaa.com的TXT记录,待验证完后域名,会自动删除,所以在DNS 控制台 Advance List中不可见或者短暂存在。

正式环境:

复制代码
acme.sh --issue --dns dns_cf -d aaa.com -d "*.aaa.com"

测试环境与正式环境的目录结果类似,故当已生成测试证书的情况下,直接使用默认正式签发命令行的话,会检测到已有证书不再进行签发,这时可以使用--force参数强制签发

如何区分测试证书与正式证书?

复制代码
openssl x509 -in ~/.acme.sh/aaa.com/fullchain.cer -noout -issuer

如果是测试证书,会看到:

复制代码
(O = Let's Encrypt, CN = (STAGING) Fake LE Intermediate)

如果是正式证书,会看到:

复制代码
(O = Let's Encrypt, CN = R3)

也可以查询已签发的证书清单,从证书环境字段进行区分

复制代码
acme.sh --list

输出结果及字段解释如下:

复制代码
Main_Domain(主域名)       KeyLength(算法)  SAN_Domains(附加域名)         Profile(证书环境)               CA(颁发机构)                    Created(创建时间)               Renew(自动续期时间)
bbb.com       "ec-256"   *.bbb.com       LetsEncrypt.org_test
aaa.com  "ec-256"   *.aaa.com  LetsEncrypt.org       2026-04-27T09:13:19Z  2026-05-26T09:13:19Z

5、部署到Nginx

证书目录创建

复制代码
#创建nginx ssl目录
sudo mkdir -p /etc/nginx/ssl
#子目录所属者修改,便于写入
sudo chown ubuntu:ubuntu /etc/nginx/ssl
#访问权限修改
sudo chmod 755 /etc/nginx/ssl

部署到nginx

复制代码
acme.sh --install-cert -d aaa.com \
 --key-file       /etc/nginx/ssl/aaa.com/aaa.com.key \
 --fullchain-file /etc/nginx/ssl/aaa.com/aaa.com_bundle.crt \
 --reloadcmd     "nginx -t && systemctl reload nginx"

私钥访问权限加固

复制代码
sudo chmod 600 /etc/nginx/ssl/aaa.com/aaa.com.key
sudo chown root:root /etc/nginx/ssl/aaa.com/aaa.com.key

6、验证证书更新情况

浏览器新建无痕窗口,访问:aaa.com

查询证书有效期

证书有效期也可以在签发完证书进行查看

复制代码
openssl x509 -in ~/.acme.sh/aaa.com_ecc/fullchain.cer -noout -dates

会看到:

复制代码
notBefore=Apr 27 08:14:48 2026 GMT
notAfter=Jul 26 08:14:47 2026 GMT

notBefore 指证书生效时间

notAfter指证书过期时间

7、检查自动续期

查询定时任务

复制代码
crontab -l

会有如下输出

复制代码
29 9 * * * "/home/ubuntu/.acme.sh"/acme.sh --cron --home "/home/ubuntu/.acme.sh" > /dev/null

建议修改,加上日志输出

复制代码
crontab -e
29 9 * * * "/home/ubuntu/.acme.sh"/acme.sh --cron --home "/home/ubuntu/.acme.sh" >> /home/ubuntu/acme-cron.log 2>&1

8、检查自动部署

复制代码
cat ~/.acme.sh/aaa.com_ecc/aaa.com.conf

观察一下几项:

  • Le_RealKeyPath=
  • Le_ReloadCmd=
  • Le_RealFullChainPath=

结果类似:

复制代码
Le_RealKeyPath='/etc/nginx/ssl/aaa.com/aaa.com.key'
Le_ReloadCmd='__ACME_BASE64__START_c3VkbyBuZ2lueCAtdCAmJiBzdWRvIHN5c3RlbWN0bCByZWxvYWQgbmdpbng=__ACME_BASE64__END_'
Le_RealFullChainPath='/etc/nginx/ssl/aaa.com/aaa.com_bundle.crt'

Le_ReloadCmd会做base64编码,避免特殊字符如&&、空格、引号影响配置文件解析

以上解码后,实际就是 sudo nginx -t && sudo systemctl reload nginx

9、手动模拟cron检查

复制代码
acme.sh --cron --home "/home/ubuntu/.acme.sh" --debug

输出以下类似结果:

复制代码
Skip, Next renewal time is: ...

cron每天执行一次,检查证书有效期,acme.sh默认在证书剩余60天时触发续期,当前还没到续期时间,故不进行续期,但自动续期机制是正常的

10、关于CA的选择

acme.sh默认CA会发生变化,可以指定默认CA,使每次生成的证书CA都是一致的,推荐acme.sh 推荐使用的CA是Let's Encrypt

复制代码
acme.sh --set-default-ca --server letsencrypt

Let's Encrypt免费,全球浏览器信任,符合ACME标准,完全自动化,其他CA,如ZeroSSL、Buypass、商业CA,流程较为繁琐,限制也较多,可能还存在付费情况。


六、批量证书管理

存在多个域名,需要实现自动化更新,不区分是否管理到多个账号。

先编写域名与token的配置清单文件domains.conf,如:

复制代码
aaa.com CF_Token=token1
bbb.com CF_Token=token2

权限应限制味当前用户可读

复制代码
chmod 600 domains.conf

再编写初始化脚本batch_domain_init.sh

复制代码
#!/bin/bash

while read domain token_line; do
    echo "=== 处理域名: $domain ==="

    export CF_Token=$(echo $token_line | cut -d= -f2)

    # 1. 签发证书
    CF_Token="$CF_Token" acme.sh --issue \
        --dns dns_cf \
        -d "$domain" \
        -d "*.$domain"

    # 2. 部署证书
    sudo acme.sh --install-cert -d "$domain" \
        --key-file       /etc/nginx/ssl/$domain/$domain.key \
        --fullchain-file /etc/nginx/ssl/$domain/$domain_bundle.crt \
        --reloadcmd     "sudo nginx -t && sudo systemctl reload nginx"

    echo
done < domains.conf

只需要根据域名与DNS API配置关系去修改domain conf,然后再执行初始化脚本即可实现批量证书的自动化更新定时任务部署。


七、常见问题排查

1、证书未生效

  • 浏览器缓存未刷新(推荐用无痕窗口)
  • CDN缓存未清理
  • 本地DNS缓存未更新

2、DNS验证失败

  • TXT记录未生效(DNS延迟)
  • 记录值填写错误(复制不完整)
  • 添加到错误的域名(Zone选择错误)

3、Nginx reload失败

  • 证书路径配置错误
  • 私钥权限不足(未设置600)
  • 证书文件格式不正确

4、acme.sh签发失败

  • Cloudflare API Token权限不足
  • DNS API 配置错误
  • 域名解析未生效

5、自动续期未执行

  • cron未运行或未配置
  • acme.sh 未加入定时任务
  • 未达到自动续期时间(默认提前60天)

6、上传成功但服务异常

  • Nginx未reload
  • 配置文件未引用新证书
  • 操作的服务器与实际服务不一致

7、批量脚本执行失败

  • SSH免密登陆未配置
  • 服务器清单配置错误
  • 执行用户权限不足
相关推荐
盟接之桥2 小时前
什么是EDI(电子数据交换)|制造业场景解决方案
大数据·网络·安全·汽车·制造
科技云报道2 小时前
安全进入“AI自主攻击”时代,瑞数信息如何用AI对抗AI
人工智能·安全
云动课堂3 小时前
【运维实战】Nginx 高性能Web服务 · 一键自动化部署方案 (适配银河麒麟 V10 / openEuler / CentOS 7/8)
运维·前端·nginx
KnowSafe4 小时前
证书自动化解决方案哪家更可靠?
运维·服务器·安全·https·自动化·ssl
KnowSafe4 小时前
2026年证书自动化解决方案选型指南
运维·安全·自动化·ssl·itrustssl
b55t4ck4 小时前
FortiWeb CVE-2025-64446漏洞深入复现分析
网络·安全·iot
wanhengidc5 小时前
可持续性 云手机运行
运维·服务器·网络·安全·智能手机
txg6665 小时前
VulCNN:多视图图表征驱动的可扩展漏洞检测体系
人工智能·深度学习·安全·网络安全
AI周红伟6 小时前
周红伟:OpenClaw安全防控:OpenClaw+Skills+DeepSeek-V4大模型安全部署、实操和企业应用实操
人工智能·深度学习·安全·机器学习·语言模型·openclaw