znlgis 博客

GIS开发与技术分享 — GDAL · GeoServer · PostGIS · QGIS · OpenLayers · Cesium · FreeCAD · NPOI

第08章 - 自动续期与维护管理

证书的自动续期是 acme.sh 最核心的价值之一。SSL/TLS 证书(Let’s Encrypt/ZeroSSL 等免费 CA)的有效期通常为 90 天,如果不能自动续期,证书过期会导致网站无法访问。本章详细介绍如何配置和维护 acme.sh 的自动续期机制。


8.1 自动续期原理

8.1.1 续期触发条件

acme.sh 不会等证书完全过期才续期,而是提前续期:

  • 默认触发条件:证书剩余有效期少于 30 天时触发续期
  • ARI 协议支持:如果 CA 支持 ARI(ACME Renewal Information,RFC 9773),CA 可以主动建议提前续期(例如遇到安全事件需要批量吊销重签时)

8.1.2 续期检查流程

cron 每日定时触发
    ↓
acme.sh --cron
    ↓
遍历 ~/.acme.sh/ 下所有域名目录
    ↓
读取每个域名的 .conf 文件
    ↓
检查证书有效期(本地计算,无需联网)
    ↓
[剩余 > 30 天]  →  跳过(输出 "Skipping...")
[剩余 ≤ 30 天]  →  执行续期
    ↓
续期成功
    ↓
执行 --reloadcmd(重载 Web 服务)

8.2 Cron 定时任务配置

8.2.1 自动添加的 Cron 任务

acme.sh 安装时会自动向当前用户的 crontab 添加一条定时任务:

# 查看当前用户的 cron 任务
crontab -l

输出示例:

# 时间是随机选取的,避免 CA 服务器峰值
23 0 * * * "/root/.acme.sh"/acme.sh --cron --home "/root/.acme.sh" > /dev/null

含义:每天凌晨 00:23 运行一次证书检查和续期。

8.2.2 手动管理 Cron 任务

# 重新安装 cron 任务(如果 cron 任务被误删)
acme.sh --install-cronjob

# 删除 cron 任务
acme.sh --uninstall-cronjob

# 查看 cron 任务
crontab -l

8.2.3 自定义 Cron 执行频率

默认每天执行一次。可以手动修改 crontab 改变执行频率:

crontab -e

修改为每 12 小时检查一次:

0 */12 * * * "/root/.acme.sh"/acme.sh --cron --home "/root/.acme.sh" > /dev/null

修改为每天 3:00 执行(固定时间):

0 3 * * * "/root/.acme.sh"/acme.sh --cron --home "/root/.acme.sh" >> /var/log/acme.sh.log 2>&1

8.2.4 启用 Cron 日志记录

默认情况下,cron 日志被重定向到 /dev/null(不保存)。建议在生产环境开启日志:

23 0 * * * "/root/.acme.sh"/acme.sh --cron --home "/root/.acme.sh" >> /var/log/acme.sh.cron.log 2>&1

同时配置日志轮转(/etc/logrotate.d/acme-sh):

/var/log/acme.sh.cron.log {
    weekly
    rotate 4
    compress
    missingok
    notifempty
}

8.3 Systemd Timer(cron 替代方案)

在使用 systemd 的现代 Linux 系统(Ubuntu 16.04+、CentOS 7+、Debian 9+)上,systemd timer 比 cron 更加可靠和灵活。

8.3.1 创建 Service 文件

cat > /etc/systemd/system/acme.sh.service << 'EOF'
[Unit]
Description=acme.sh certificate renewal service
Documentation=https://github.com/acmesh-official/acme.sh
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
User=root
ExecStart=/root/.acme.sh/acme.sh --cron --home /root/.acme.sh
StandardOutput=journal
StandardError=journal
SyslogIdentifier=acme.sh
EOF

8.3.2 创建 Timer 文件

cat > /etc/systemd/system/acme.sh.timer << 'EOF'
[Unit]
Description=Daily acme.sh certificate renewal timer
Documentation=https://github.com/acmesh-official/acme.sh

[Timer]
# 每天执行一次,在系统启动后 10 分钟到 1 小时之间随机触发
OnCalendar=daily
RandomizedDelaySec=1h
# 如果上次执行时系统关机,下次启动后补执行
Persistent=true

[Install]
WantedBy=timers.target
EOF

8.3.3 启用 Timer

# 重载 systemd 配置
systemctl daemon-reload

# 启用并启动 timer
systemctl enable acme.sh.timer
systemctl start acme.sh.timer

# 查看 timer 状态
systemctl status acme.sh.timer
systemctl list-timers acme.sh.timer

8.3.4 手动测试 Service

# 手动触发一次(测试是否正常)
systemctl start acme.sh.service

# 查看执行日志
journalctl -u acme.sh.service -f
journalctl -u acme.sh.service --since "1 hour ago"

8.4 手动续期操作

8.4.1 手动触发续期检查

# 运行一次完整的续期检查(等同于 cron 任务执行的操作)
acme.sh --cron --home ~/.acme.sh

# 查看详细输出
acme.sh --cron --home ~/.acme.sh --debug

8.4.2 强制续期特定域名

即使证书未到期,也可以强制重新申请:

# 强制续期
acme.sh --renew -d example.com --force

# 强制续期 ECC 证书
acme.sh --renew -d example.com --force --ecc

# 强制续期并查看详细输出
acme.sh --renew -d example.com --force --debug

8.4.3 续期所有证书

acme.sh --renew-all

# 强制续期所有证书(无视有效期)
acme.sh --renew-all --force

8.4.4 检查证书状态

# 列出所有由 acme.sh 管理的证书
acme.sh --list

# 输出示例:
# Main_Domain    KeyLength  SAN_Domains        CA          Created               Renew
# example.com    ""         *.example.com      LetsEncrypt 2024-01-15T10:00:00Z  2024-04-15T10:00:00Z

# 查看证书详情(使用 openssl)
openssl x509 -in ~/.acme.sh/example.com/fullchain.cer -noout -dates
openssl x509 -in ~/.acme.sh/example.com/fullchain.cer -noout -text | grep -A2 "Subject:"
openssl x509 -in ~/.acme.sh/example.com/fullchain.cer -noout -text | grep "DNS:"

8.5 证书续期钩子

acme.sh 支持在证书申请/续期流程的不同阶段执行自定义脚本。

8.5.1 钩子类型

钩子 说明 触发时机
Le_PreHook 申请前钩子 每次申请/续期开始前执行
Le_PostHook 申请后钩子 每次申请/续期结束后执行(无论成功失败)
Le_RenewHook 续期后钩子 仅在证书成功续期后执行
--reloadcmd 重载命令 证书成功安装后执行(相当于 RenewHook)

8.5.2 配置钩子

# 在申请时直接指定钩子
acme.sh --issue --dns dns_cf -d example.com \
  --pre-hook  "/usr/bin/systemctl stop nginx" \
  --post-hook "/usr/bin/systemctl start nginx" \
  --renew-hook "/opt/notify_cert_renewed.sh"

# 或修改已有证书的配置
acme.sh --install-cert -d example.com \
  --fullchain-file /etc/nginx/ssl/fullchain.pem \
  --key-file /etc/nginx/ssl/privkey.pem \
  --reloadcmd "systemctl reload nginx && /opt/sync_cert_to_cdn.sh"

8.5.3 续期通知脚本示例

发送微信/钉钉/Telegram 通知(以 Telegram 为例):

cat > /opt/notify_cert_renewed.sh << 'EOF'
#!/bin/bash
# 证书续期成功后发送 Telegram 通知
BOT_TOKEN="your_telegram_bot_token"
CHAT_ID="your_chat_id"
DOMAIN="${CERT_DOMAIN:-unknown}"
EXPIRE_DATE="${Le_NextRenewTime:-unknown}"

MESSAGE="✅ 证书续期成功!%0A域名: $DOMAIN%0A有效期至: $(date -d @$EXPIRE_DATE '+%Y-%m-%d' 2>/dev/null || echo $EXPIRE_DATE)"

curl -s -X POST "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
  -d "chat_id=${CHAT_ID}" \
  -d "text=${MESSAGE}"
EOF
chmod +x /opt/notify_cert_renewed.sh

# 安装证书时配置续期钩子
acme.sh --install-cert -d example.com \
  --fullchain-file /etc/nginx/ssl/fullchain.pem \
  --key-file /etc/nginx/ssl/privkey.pem \
  --reloadcmd "systemctl reload nginx && /opt/notify_cert_renewed.sh"

发送钉钉通知:

cat > /opt/notify_dingtalk.sh << 'EOF'
#!/bin/bash
WEBHOOK_URL="https://oapi.dingtalk.com/robot/send?access_token=your_token"
DOMAIN="${CERT_DOMAIN:-example.com}"

curl -s -X POST "$WEBHOOK_URL" \
  -H 'Content-Type: application/json' \
  -d "{
    \"msgtype\": \"text\",
    \"text\": {
      \"content\": \"SSL证书续期成功通知\\n域名:$DOMAIN\\n时间:$(date '+%Y-%m-%d %H:%M:%S')\"
    }
  }"
EOF
chmod +x /opt/notify_dingtalk.sh

8.6 证书管理操作

8.6.1 查看证书信息

# 查看所有证书列表
acme.sh --list

# 查看某个证书的详细信息
acme.sh --info -d example.com

8.6.2 修改证书配置

如果需要更改已申请证书的 reload 命令或安装路径:

# 重新执行 install-cert 即可更新配置
acme.sh --install-cert -d example.com \
  --key-file       /new/path/privkey.pem \
  --fullchain-file /new/path/fullchain.pem \
  --reloadcmd      "new reload command"

8.6.3 更换 CA(重新申请)

如果需要将某个域名从 ZeroSSL 换到 Let’s Encrypt:

acme.sh --issue --dns dns_cf \
  -d example.com -d *.example.com \
  --server letsencrypt \
  --force

8.6.4 吊销证书

# 吊销证书(通知 CA 吊销,之后该证书不再有效)
acme.sh --revoke -d example.com

# 吊销 ECC 证书
acme.sh --revoke -d example.com --ecc

# 吊销并指定原因(0=unspecified, 1=keyCompromise, 3=superseded, 4=cessationOfOperation)
acme.sh --revoke -d example.com --revoke-reason 4

8.6.5 删除证书管理条目

从 acme.sh 的管理中删除某个域名(不会自动吊销,仅删除本地配置):

acme.sh --remove -d example.com

# 这会删除 ~/.acme.sh/example.com/ 目录
# 但已安装到 Nginx 的证书文件不受影响

8.7 多账户和多 CA 管理

8.7.1 查看当前账户信息

acme.sh --info

8.7.2 注册不同 CA 账户

# 注册 Let's Encrypt 账户
acme.sh --register-account -m your@email.com --server letsencrypt

# 注册 ZeroSSL 账户
acme.sh --register-account -m your@email.com --server zerossl

# 注册 Buypass 账户
acme.sh --register-account -m your@email.com --server buypass

8.7.3 ZeroSSL EAB 凭据配置

ZeroSSL 推荐使用 EAB(External Account Binding)凭据以获得更好的证书管理体验:

  1. 登录 ZeroSSL 控制台
  2. 获取 EAB Key ID 和 EAB HMAC Key
  3. 配置到 acme.sh:
acme.sh --register-account \
  --server zerossl \
  --eab-kid "your_eab_key_id" \
  --eab-hmac-key "your_eab_hmac_key"

8.8 监控与告警

8.8.1 检查证书有效期脚本

cat > /opt/check_cert_expiry.sh << 'EOF'
#!/bin/bash
# 检查证书有效期,如果剩余不足 7 天则发送告警
WARN_DAYS=7
CERT_DIR="/etc/nginx/ssl"

for cert_file in "$CERT_DIR"/**/fullchain.pem; do
    if [ -f "$cert_file" ]; then
        domain=$(dirname "$cert_file" | xargs basename)
        expiry=$(openssl x509 -enddate -noout -in "$cert_file" | cut -d= -f2)
        expiry_epoch=$(date -d "$expiry" +%s 2>/dev/null || date -j -f "%b %d %T %Y %Z" "$expiry" +%s)
        now_epoch=$(date +%s)
        days_left=$(( (expiry_epoch - now_epoch) / 86400 ))

        if [ "$days_left" -lt "$WARN_DAYS" ]; then
            echo "⚠️ 告警:$domain 证书将在 $days_left 天后过期!"
            # 在这里添加发送通知的逻辑
        else
            echo "✅ $domain:剩余 $days_left 天"
        fi
    fi
done
EOF
chmod +x /opt/check_cert_expiry.sh

8.8.2 使用 acme.sh 内置检查

# 一次性查看所有证书到期时间
acme.sh --list

# 结合 grep 过滤快到期的证书
acme.sh --list | awk 'NR>1 {print $1, $6}' | while read domain renew; do
    echo "Domain: $domain, Next renew: $renew"
done

8.9 备份与恢复

8.9.1 备份 acme.sh 数据

# 备份整个 .acme.sh 目录(包含所有证书、密钥和配置)
tar -czf /backup/acme.sh-backup-$(date +%Y%m%d).tar.gz ~/.acme.sh/

# 仅备份账户配置和证书配置(不含 DNS API 密钥的完整备份不太安全)
tar -czf /backup/acme-certs-$(date +%Y%m%d).tar.gz \
  --exclude='~/.acme.sh/dnsapi' \
  ~/.acme.sh/

安全提示:备份文件包含 DNS API 密钥和证书私钥,务必加密存储或存放到安全位置。

8.9.2 迁移到新服务器

# 在旧服务器上打包
tar -czf acme.sh-full.tar.gz ~/.acme.sh/

# 传输到新服务器
scp acme.sh-full.tar.gz user@new-server:/tmp/

# 在新服务器上解压
cd ~
tar -xzf /tmp/acme.sh-full.tar.gz

# 重新安装(更新脚本路径和 cron 任务)
~/.acme.sh/acme.sh --install

8.10 小结

本章介绍了 acme.sh 的自动续期和维护管理:

  • Cron 任务:acme.sh 安装时自动配置,每天检查,30 天内到期时自动续期
  • Systemd Timer:现代 Linux 系统推荐使用,比 cron 更可靠
  • 手动续期--renew -d domain --force 强制重新申请
  • 续期钩子:支持在续期成功后发送通知(微信、钉钉、Telegram 等)
  • 证书管理--list--info--revoke--remove 等完整管理命令
  • 备份恢复:备份 ~/.acme.sh/ 目录即可完整保留所有数据

下一章介绍 acme.sh 的高级功能与技巧。