国内服务器 SSL 证书自动续期实战:acme.sh + 阿里云 DNS API

环境:Rocky Linux 8.10 / nginx(dnf 安装)/ Docker 反代三个站点 / 阿里云 DNS 证书:Let's Encrypt(免费,90 天有效期) 工具:acme.sh(轻量 ACME 客户端)

为什么需要自动续期

我的服务器上用 Docker 部署了三个网站,通过 nginx 反向代理对外提供服务:

域名

用途

blog.karltang.top

个人技术博客(Halo)

pan.karltang.top

网盘服务

tw.karltang.top

其他服务

之前使用的 SSL 证书只有三个月有效期,每次到期都要手动申请、替换、重载,一旦忘记就会导致网站无法访问。为了彻底解决这个问题,我使用 acme.sh 配合阿里云 DNS API 实现了证书的全自动申请和续期。

方案选择

为什么选 acme.sh

对比项

acme.sh

certbot

依赖

纯 Shell,无依赖

Python 环境

自定义安装路径

原生支持 --install-cert

需要 deploy-hook 脚本

DNS API 支持

内置 150+ DNS 服务商插件

需额外安装插件

自动续期

自带 cron

需配置 systemd timer

国内可获取性

Gitee 镜像

dnf 可装

由于证书存储在自定义路径(非 certbot 默认路径),acme.sh 的 --install-cert 能直接指定输出文件名和路径,是最合适的工具。

为什么选 DNS API 验证

nginx 正在运行并占用 80/443 端口,standalone 模式需要停服务,不现实。webroot 模式需要配置 nginx 的 .well-known 路径,多域名时比较麻烦。

DNS API 验证通过自动添加/删除 DNS TXT 记录完成域名所有权验证,不占用任何端口,不需要修改 nginx 配置,是最干净的方案。

准备工作

创建阿里云 RAM 子用户

  1. 登录阿里云控制台 → RAM 访问控制 → 用户 → 创建用户

  2. 勾选「编程访问」获取 AccessKey ID 和 AccessKey Secret

  3. 授权策略:仅授予 AliyunDNSFullAccess

安全提示:切勿使用主账号 AccessKey。RAM 子用户只需 DNS 解析管理权限,即使密钥泄露,影响范围也仅限于 DNS 记录。

确认域名 DNS 托管

在阿里云控制台 → 云解析 DNS 中,确认 karltang.top 域名已添加且解析正常。如果域名的 NS 记录指向的不是阿里云,DNS API 将无法操作。

脚本实现

核心逻辑

脚本分为五个步骤:

  1. 前置检查 — 验证 root 权限、阿里云密钥、安装依赖(curl、cronie、git)

  2. 备份现有证书 — 将已有证书备份到 /etc/ssl/certs/backup_YYYYMMDD_HHMMSS/

  3. 安装 acme.sh — 国内优先从 Gitee 镜像安装,三级回退

  4. 申请并安装证书 — 三个域名逐一签发,安装到 nginx 配置的路径

  5. 验证 — 展示证书列表、有效期、cron 任务状态

完整脚本

#!/bin/bash
#
# SSL 证书自动续期脚本
# 工具: acme.sh + 阿里云 DNS API
# CA:  Let's Encrypt (免费, 90天有效期, 到期前 30 天自动续期)
# 系统: Rocky Linux 8.10 / nginx (dnf) / docker 反向代理
#
# 使用方法:
#   Ali_Key=你的AccessKeyId Ali_Secret=你的AccessKeySecret ./ssl-auto-renew.sh
#
​
set -euo pipefail
​
# ============================ 配置区 ============================
​
Ali_Key="${Ali_Key:-}"
Ali_Secret="${Ali_Secret:-}"
ACCOUNT_EMAIL="${ACCOUNT_EMAIL:-admin@exmail.com#换成自己的邮箱}"
​
# 域名与证书路径映射
# 格式: 域名|证书目录|pem文件名|key文件名
DOMAINS=(
  "blog.karltang.top|/ssl/blog|blog.karltang.top.pem|blog.karltang.top.key" #按照自己ssl证书路径填写
  "pan.karltang.top|/ssl/pan.karltang.top|pan.karltang.top.pem|pan.karltang.top.key" #按照自己ssl证书路径填写
  "tw.karltang.top|/ssl/tw.karltang.top|tw.karltang.top.pem|tw.karltang.top.key" #按照自己ssl证书路径填写
)
​
RELOAD_CMD="systemctl reload nginx"
​
# ========================== 配置区结束 ==========================
​
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m'
info()  { echo -e "${GREEN}[INFO]${NC}  $*"; }
warn()  { echo -e "${YELLOW}[WARN]${NC}  $*"; }
err()   { echo -e "${RED}[ERROR]${NC} $*"; }
step()  { echo -e "${CYAN}[STEP]${NC}  $*"; }
​
# ============================ 前置检查 ============================
​
step "1/5  前置检查"
​
[[ $EUID -eq 0 ]] || { err "请使用 root 用户执行此脚本"; exit 1; }
​
[[ -n "$Ali_Key" && -n "$Ali_Secret" ]] || {
  err "缺少阿里云 AccessKey! 请通过环境变量传入:
  Ali_Key=AccessKeyId Ali_Secret=AccessKeySecret $0"
  exit 1
}
​
for cmd in curl crontab; do
  command -v "$cmd" &>/dev/null || { info "安装依赖: $cmd"; dnf install -y curl cronie; }
done
command -v git &>/dev/null || { info "安装依赖: git"; dnf install -y git; }
​
systemctl is-active --quiet crond || systemctl enable --now crond
systemctl is-active --quiet nginx && info "nginx 运行中" || warn "nginx 未运行"
​
info "前置检查通过"
​
# ============================ 备份现有证书 ============================
​
step "2/5  备份现有证书"
​
BACKUP_DIR="/etc/ssl/certs/backup_$(date +%Y%m%d_%H%M%S)"
BACKED_UP=false
for entry in "${DOMAINS[@]}"; do
  IFS='|' read -r domain cert_dir cert_file key_file <<< "$entry"
  if [[ -f "$cert_dir/$cert_file" ]]; then
    [[ "$BACKED_UP" == false ]] && mkdir -p "$BACKUP_DIR" && BACKED_UP=true
    cp "$cert_dir/$cert_file" "$BACKUP_DIR/${domain}.pem.bak"
    [[ -f "$cert_dir/$key_file" ]] && cp "$cert_dir/$key_file" "$BACKUP_DIR/${domain}.key.bak"
  fi
done
​
[[ "$BACKED_UP" == true ]] && info "已备份到 $BACKUP_DIR" || info "无现有证书需要备份"
​
# ============================ 安装 acme.sh ============================
​
step "3/5  安装/更新 acme.sh"
​
ACME_DIR="$HOME/.acme.sh"
ACME_BIN="$ACME_DIR/acme.sh"
ACME_TMP="/tmp/acme.sh-install"
​
if [[ ! -f "$ACME_BIN" ]]; then
  info "正在安装 acme.sh (国内优先使用 Gitee 镜像)..."
​
  # 方法1: git clone Gitee 镜像 (国内最稳定)
  if [[ ! -f "$ACME_BIN" ]]; then
    info "  [1/3] 尝试从 Gitee 镜像 git clone..."
    rm -rf "$ACME_TMP"
    if git clone --depth 1 https://gitee.com/neilpang/acme.sh.git "$ACME_TMP" 2>/dev/null; then
      ( cd "$ACME_TMP" && ./acme.sh --install -m "$ACCOUNT_EMAIL" ) || true
      rm -rf "$ACME_TMP"
    fi
  fi
​
  # 方法2: 下载 Gitee tarball (无 git 时备选)
  if [[ ! -f "$ACME_BIN" ]]; then
    info "  [2/3] 尝试从 Gitee 下载 tarball..."
    rm -rf "$ACME_TMP"
    if curl -fsSL --connect-timeout 15 \
      "https://gitee.com/neilpang/acme.sh/repository/archive/master.tar.gz" \
      -o /tmp/acme_sh.tar.gz 2>/dev/null; then
      mkdir -p "$ACME_TMP"
      tar xzf /tmp/acme_sh.tar.gz -C "$ACME_TMP" --strip-components 1 2>/dev/null
      if [[ -f "$ACME_TMP/acme.sh" ]]; then
        ( cd "$ACME_TMP" && ./acme.sh --install -m "$ACCOUNT_EMAIL" ) || true
      fi
      rm -rf "$ACME_TMP" /tmp/acme_sh.tar.gz
    fi
  fi
​
  # 方法3: 官方安装脚本 (海外机器)
  if [[ ! -f "$ACME_BIN" ]]; then
    info "  [3/3] 尝试官方安装脚本 (get.acme.sh)..."
    curl -fsSL --connect-timeout 15 https://get.acme.sh | sh -s "email=$ACCOUNT_EMAIL" 2>/dev/null || true
  fi
​
  [[ -f "$ACME_BIN" ]] || { err "acme.sh 安装失败! 请手动从 Gitee 安装"; exit 1; }
  info "acme.sh 安装成功"
  "$ACME_BIN" --set-gitee 2>/dev/null || true
else
  info "acme.sh 已安装, 检查更新..."
  "$ACME_BIN" --upgrade --auto-upgrade 2>/dev/null || true
fi
​
"$ACME_BIN" --set-default-ca --server letsencrypt 2>/dev/null || true
​
export Ali_Key
export Ali_Secret
​
# ============================ 申请并安装证书 ============================
​
step "4/5  申请并安装证书"
​
for entry in "${DOMAINS[@]}"; do
  IFS='|' read -r domain cert_dir cert_file key_file <<< "$entry"
  echo ""
  info ">>> $domain"
​
  mkdir -p "$cert_dir"
​
  # 检查证书文件是否已存在 (不仅看 --list, 还要确认实际文件存在)
  ACME_CERT_ECC="$ACME_DIR/${domain}_ecc/fullchain.cer"
  ACME_CERT_RSA="$ACME_DIR/${domain}/fullchain.cer"
​
  if [[ -f "$ACME_CERT_ECC" || -f "$ACME_CERT_RSA" ]]; then
    info "  证书已存在, 跳过签发"
  else
    info "  签发证书 (DNS API 自动验证)..."
    "$ACME_BIN" --issue -d "$domain" --dns dns_ali --force
  fi
​
  info "  安装证书 → $cert_dir/$cert_file"
  "$ACME_BIN" --install-cert -d "$domain" \
    --fullchain-file "$cert_dir/$cert_file" \
    --key-file       "$cert_dir/$key_file" \
    --reloadcmd      "$RELOAD_CMD"
​
  chmod 644 "$cert_dir/$cert_file"
  chmod 600 "$cert_dir/$key_file"
  info "  ✓ $domain 完成"
done
​
# ============================ 验证 ============================
​
step "5/5  验证"
​
echo ""
info "==================== acme.sh 证书列表 ===================="
"$ACME_BIN" --list
​
echo ""
info "==================== 证书有效期检查 ===================="
for entry in "${DOMAINS[@]}"; do
  IFS='|' read -r domain cert_dir cert_file key_file <<< "$entry"
  if [[ -f "$cert_dir/$cert_file" ]]; then
    expire_date=$(openssl x509 -in "$cert_dir/$cert_file" -noout -enddate 2>/dev/null | cut -d= -f2)
    info "  $domain  →  到期: $expire_date"
  fi
done
​
echo ""
info "==================== Cron 定时任务 ===================="
if crontab -l 2>/dev/null | grep -q "acme.sh"; then
  crontab -l 2>/dev/null | grep "acme.sh"
  info "定时任务已就绪"
else
  warn "未检测到 acme.sh cron 任务, 手动创建..."
  CRON_CMD="0 3 * * * $ACME_BIN --cron --home $ACME_DIR >> $ACME_DIR/acme_cron.log 2>&1"
  (crontab -l 2>/dev/null; echo "$CRON_CMD") | crontab -
  info "已创建 cron 任务 (每天凌晨 3:00 检查)"
fi
​
echo ""
info "========================================================"
info "  全部完成!"
info "  acme.sh cron 每天 03:00 自动检查证书有效期"
info "  在到期前 30 天自动续期并重载 nginx"
info "========================================================"

踩坑记录

实际部署过程中遇到了不少问题,记录如下。

坑一:国内机器无法从 GitHub 下载 acme.sh

现象curl https://get.acme.sh | sh 超时,无法完成安装。

原因get.acme.sh 会重定向到 GitHub Raw,国内直连不稳定。

解决:改用 Gitee 镜像安装,并设置三级回退策略:

# 优先: git clone Gitee 镜像
git clone --depth 1 https://gitee.com/neilpang/acme.sh.git /tmp/acme.sh-install
cd /tmp/acme.sh-install && ./acme.sh --install -m your@email.com
​
# 备选: 下载 Gitee tarball
curl -fsSL "https://gitee.com/neilpang/acme.sh/repository/archive/master.tar.gz" -o acme.sh.tar.gz
​
# 最终回退: 官方脚本 (海外机器)
curl -fsSL https://get.acme.sh | sh

安装后执行 acme.sh --set-gitee,让后续自动更新也走 Gitee,避免再次访问 GitHub。

坑二:acme.sh --install 邮箱参数错误

现象:执行 ./acme.sh --install --account-email admin@exmail.com 报错 Unknown parameter: --account-email

原因:acme.sh 的 --install 命令不认识 --account-email,正确的参数是 -m

解决

# 错误
./acme.sh --install --account-email admin@exmail.com
​
# 正确
./acme.sh --install -m admin@exmail.com

坑三:set -e 导致安装失败后脚本直接退出

现象:acme.sh 从 Gitee 安装失败后脚本立即终止,不会尝试后续的备选安装方式。

原因:脚本开头 set -euo pipefail 会在任何命令返回非零时退出。acme.sh 安装命令在子 shell 中执行失败,触发了 set -e

解决:用 ( ... ) || true 包裹安装命令,失败后继续尝试下一个源:

# 错误: 失败直接退出
cd /tmp/acme.sh-install
./acme.sh --install -m "$ACCOUNT_EMAIL"
cd -
​
# 正确: 失败后继续尝试下一个安装方式
( cd "$ACME_TMP" && ./acme.sh --install -m "$ACCOUNT_EMAIL" ) || true

坑四:DNS API 添加 TXT 记录失败

现象:acme.sh 报错 Error adding TXT record to domain: _acme-challenge.blog.karltang.top

原因:域名之前使用 CNAME 记录做 DNS 验证,残留的 CNAME 记录与 acme.sh 添加的 TXT 记录冲突。

解决:在阿里云 DNS 控制台删除 _acme-challenge 相关的 CNAME 记录,重新执行脚本。

排查建议:遇到 DNS API 报错时,加上 --debug 2 参数查看阿里云 API 的具体错误信息:

acme.sh --issue -d blog.karltang.top --dns dns_ali --force --debug 2

坑五:acme.sh --list 显示域名但证书文件不存在

现象acme.sh --list 显示域名已注册,但执行 --install-cert 时报错 cat: fullchain.cer: No such file or directory

原因:首次签发时 DNS API 失败,域名被注册到 acme.sh 管理列表中,但证书文件从未生成。脚本仅检查 --list 就认为证书已存在,跳过了签发步骤。

解决:不依赖 --list,改为检查实际的证书文件是否存在:

# 错误: --list 有记录就跳过
if acme.sh --list | grep -q "$domain"; then
    info "证书已存在"
fi
​
# 正确: 检查实际证书文件
ACME_CERT="$ACME_DIR/${domain}_ecc/fullchain.cer"
if [[ -f "$ACME_CERT" ]]; then
    info "证书已存在, 跳过签发"
else
    # --force 覆盖上次失败留下的残留状态
    acme.sh --issue -d "$domain" --dns dns_ali --force
fi

坑六:curl 错误 35 和 28

现象:脚本运行中出现 curl error 35(SSL 握手失败)和 error 28(连接超时)。

原因:国内服务器访问 Let's Encrypt API(acme-v02.api.letsencrypt.org)不稳定。

结果:acme.sh 内部有重试机制,最终签发成功。这两个错误出现在 acme.sh 自更新阶段(非关键步骤),不影响证书签发。

备选方案:如果 Let's Encrypt 持续不可达,可切换到其他免费 CA:

# Buypass (免费 180 天证书, 欧洲服务器)
acme.sh --set-default-ca --server buypass
​
# ZeroSSL (免费 90 天证书)
acme.sh --set-default-ca --server zerossl

最终验证

执行完成后,检查证书状态:

# 查看证书列表
~/.acme.sh/acme.sh --list

输出:

Main_Domain        KeyLength  SAN_Domains  CA               Created               Renew
blog.karltang.top  "ec-256"   no           LetsEncrypt.org  2026-08-14T13:24:16Z  2026-09-12T13:24:16Z
pan.karltang.top   "ec-256"   no           LetsEncrypt.org  2026-08-14T13:25:12Z  2026-09-12T13:25:12Z
tw.karltang.top    "ec-256"   no           LetsEncrypt.org  2026-08-14T13:26:08Z  2026-09-12T13:26:08Z

检查证书文件和有效期:

ls -la /etc/ssl/certs/blog/blog.karltang.top.pem /etc/ssl/certs/blog/blog.karltang.top.key
# -rw-r--r-- root root 4820 ... blog.karltang.top.pem
# -rw------- root root  227 ... blog.karltang.top.key

openssl x509 -in /etc/ssl/certs/blog/blog.karltang.top.pem -noout -dates -issuer
# notBefore=Aug 14 12:25:44 2026 GMT
# notAfter=Nov 12 12:25:43 2026 GMT
# issuer=C = US, O = Let's Encrypt, CN = YE2

确认 cron 定时任务:

crontab -l | grep acme.sh
# 0 3 * * * /root/.acme.sh/acme.sh --cron --home /root/.acme.sh >> /root/.acme.sh/acme_cron.log 2>&1

重载 nginx 使新证书生效:

systemctl reload nginx

工作原理总结

整个自动化流程的工作机制:

┌──────────────────────────────────────────────────┐
│  cron 每天 03:00 执行 acme.sh --cron             │
│                                                  │
│  1. 检查所有域名证书有效期                         │
│  2. 若距到期不足 30 天:                           │
│     a. 调用阿里云 DNS API 添加 TXT 记录           │
│     b. 向 Let's Encrypt 提交验证                  │
│     c. 验证通过后下载新证书                        │
│     d. 调用阿里云 DNS API 删除 TXT 记录           │
│     e. 安装新证书到 /etc/ssl/certs/ 对应路径       │
│     f. 执行 systemctl reload nginx 重载配置       │
│  3. 若未到期: 跳过                                │
│                                                  │
│  阿里云密钥保存在 ~/.acme.sh/account.conf         │
│  cron 续期时自动读取, 无需额外配置                 │
└──────────────────────────────────────────────────┘

常用维护命令

# 查看所有证书状态
~/.acme.sh/acme.sh --list

# 检查证书到期时间
openssl x509 -in /etc/ssl/certs/blog/blog.karltang.top.pem -noout -dates

# 手动强制续期某个域名
~/.acme.sh/acme.sh --renew -d blog.karltang.top --force

# 查看续期日志
cat ~/.acme.sh/acme_cron.log

# 调试模式签发证书 (排错用)
Ali_Key=xxx Ali_Secret=yyy ~/.acme.sh/acme.sh --issue -d blog.karltang.top --dns dns_ali --force --debug 2

总结

通过 acme.sh + 阿里云 DNS API,实现了三个域名 SSL 证书的全自动申请和续期,核心优势:

  • 零停机:DNS API 验证不占用端口,nginx 无需重启

  • 零干预:cron 每天自动检查,到期前 30 天自动续期

  • 零成本:Let's Encrypt 免费证书 + acme.sh 开源工具

  • 高安全:RAM 子用户仅需 DNS 权限,密钥不明文写入脚本

国内环境的主要障碍是网络访问——acme.sh 安装走 Gitee 镜像,Let's Encrypt API 偶尔超时但 acme.sh 会自动重试。如果持续不稳定,可切换到 Buypass(免费 180 天证书)作为备选 CA。

希望这篇记录能帮到同样在国内服务器上折腾 SSL 证书自动续期的朋友。

GlusterFS 10 企业级存储集群部署实践:基于 Fedora Server 构建高性能共享存储 2026-08-06