一、适用范围
适用环境
- Web服务:Nginx
- DNS服务:Cloudflare
- 证书来源:腾讯云/Let's Encrypt(acme.sh)
前置条件
- 已具备服务器SSH登录权限
- 已具备域名DNS管理权限
- Nginx已安装并正常运行
二、更新方式说明
| 方式 | 适用场景 | 特点 |
|---|---|---|
| 手动更新 | 单个域名 | 简单但容易出错 |
| 半自动更新 | 少量域名 | 提高效率 |
| 全自动 | 多域名/长期维护 | 自动续期 |
三、手动更新流程
1、申请证书(腾讯云)
路径:腾讯云控制台 -> SSL 证书 -> 我的证书 -> 即将过期 -> 快速续期
步骤:
- 选择免费证书
- 提交申请
- 获取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密钥生成与配置步骤
-
在客户端生成密钥对
客户端终端执行:ssh-keygen -t ed25519 -C "your_email@example.com"
执行后会生成两个文件(默认在~/.ssh):
- id_ed25519 私钥(保密)
- id_ed25519.pub 公钥(可分发)
-
上传公钥到服务器
查看本地客户端并复制cat ~/.ssh/id_ed25519.pub
腾讯云控制台 --> 云服务器 --> 需要登录的服务器 --> 登录
在主目录下
mkdir -p ~/.ssh
chmod 700 ~/.ssh
vi ~/.ssh/authorized_keys
把公钥粘贴进去,保存后:
chmod 600 ~/.ssh/authorized_keys
-
测试免密登陆
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免密登陆未配置
- 服务器清单配置错误
- 执行用户权限不足