SSL 证书申请封面

前言

在搭建网站时,HTTPS 已经成为标配。Let's Encrypt 等免费 CA 让证书申请变得简单,但对于泛域名证书(*.example.com),传统的 HTTP-01 验证方式无法使用,必须通过 DNS-01 验证。

本文详细记录在 OpenResty 环境下,使用 acme.sh 配合 DNSPod API 自动申请、部署、续期泛域名 SSL 证书的完整流程,包含踩坑记录和一键管理脚本,方便长久参考。


本文环境

项目说明
操作系统Linux(Debian/Ubuntu/CentOS 通用)
Web 服务器OpenResty
证书工具acme.sh v3.x
DNS 服务商DNSPod(腾讯云) (其他服务商如阿里云、华为云等也可以使用)
CA 机构ZeroSSL(国内访问比 Let's Encrypt 稳定)
证书类型泛域名证书(*.example.com + 根域)
加密算法ECC P-256(ec-256,等效 RSA 3072)

一、前置准备

1.1 域名准备

确保你的域名已在 DNSPod 托管解析,并且你有该域名的管理权限。

1.2 获取 DNSPod API 凭证

登录 DNSPod 控制台 → 右上角头像 → API 密钥 → DNSPod Token → 创建密钥,保存好 ID 和 Token。

注意:这是 DNSPod 自家的 Token,不是腾讯云 CAM 的 SecretId。域名在 DNSPod 管理用这个最简单。

1.3 准备一个真实邮箱

用于 ZeroSSL 账号绑定和证书到期提醒,必须是真实可接收邮件的邮箱,不能用 example.com 等占位域名。


二、安装 acme.sh

2.1 官方安装(指定邮箱)

curl https://get.acme.sh | sh -s email=your@email.com
source ~/.bashrc

2.2 国内服务器安装(GitHub 不通时用 gitee 镜像)

git clone https://gitee.com/neilpang/acme.sh.git
cd acme.sh
./acme.sh --install -m your@email.com
source ~/.bashrc

2.3 验证安装

/root/.acme.sh/acme.sh --version

三、基础配置

3.1 设置默认 CA 为 ZeroSSL

/root/.acme.sh/acme.sh --set-default-ca --server zerossl
ZeroSSL 国内访问比 Let's Encrypt 稳定。其他可选:buypass、letsencrypt。

3.2 绑定 ZeroSSL 邮箱(强制要求)

/root/.acme.sh/acme.sh --register-account -m your@email.com --server zerossl

ZeroSSL 要求必须绑定邮箱才能注册账号,否则申请证书会报错。

3.3 国内网络优化(强制 HTTP/1.1)

国内服务器访问 CA 服务器经常遇到 curl error 35(TLS 握手失败)和 curl error 28(连接超时),强制使用 HTTP/1.1 可有效解决:

echo 'ACME_USE_HTTP_1=1' >> /root/.acme.sh/account.conf

3.4 验证配置

grep -E 'DEFAULT_ACME_SERVER|ACCOUNT_EMAIL|ACME_USE_HTTP_1' /root/.acme.sh/account.conf

应看到三项配置都已写入。


四、配置 DNSPod API 凭证

设置环境变量(只需首次设置,申请成功后 acme.sh 会自动保存到 account.conf):

export DP_Id="你的DNSPod_API_ID"
export DP_Key="你的DNSPod_API_Token"

其他 DNS 服务商对照

DNS 服务商--dns 参数环境变量
DNSPod(旧版 Token)dns_dpDP_Id、DP_Key
腾讯云 CAMdns_tencentTencent_SecretId、Tencent_SecretKey
阿里云dns_aliAli_Key、Ali_Secret
Cloudflaredns_cfCF_Token

五、申请泛域名证书(一步到位)

以下命令同时完成申请 + 直接部署到 OpenResty 证书目录,无需后续单独复制:

# 创建证书目录
mkdir -p /usr/local/openresty/nginx/conf/ssl

# 申请泛域名证书并直接输出到 OpenResty 目录
/root/.acme.sh/acme.sh --issue \
  --dns dns_dp \
  -d example.com \
  -d '*.example.com' \
  --keylength ec-256 \
  --key-file       /usr/local/openresty/nginx/conf/ssl/example.com.key \
  --fullchain-file /usr/local/openresty/nginx/conf/ssl/example.com.crt \
  --reloadcmd      "/usr/local/openresty/nginx/sbin/nginx -s reload"

参数详解

参数说明
--dns dns_dp使用 DNSPod API 自动完成 DNS-01 验证
-d example.com根域名(必须同时申请,否则根域不认)
-d '*.example.com'一级泛域名(单引号防止 shell 展开 *)
--keylength ec-256ECC P-256 算法(性能好、体积小,等效 RSA 3072);用 RSA 则填 2048
--key-file私钥输出路径(部署到 Web 服务器目录)
--fullchain-file完整证书链输出路径(服务器证书+中间证书,Nginx 必须用这个)
--reloadcmd证书签发/续期成功后自动执行的重载命令

申请成功标志

日志最后出现 Cert success.,且目标目录下生成 .key 和 .crt 两个文件。


六、部署证书(单独部署方式)

如果申请时没有指定 --key-file/--fullchain-file,证书原件存在 /root/.acme.sh/example.com_ecc/,需手动部署:

/root/.acme.sh/acme.sh --install-cert -d example.com -d '*.example.com' --ecc \
  --key-file       /usr/local/openresty/nginx/conf/ssl/example.com.key \
  --fullchain-file /usr/local/openresty/nginx/conf/ssl/example.com.crt \
  --reloadcmd      "/usr/local/openresty/nginx/sbin/nginx -s reload"
ECC 证书所有操作加 --ecc;RSA 证书不加。

七、配置 OpenResty 站点

7.1 完整 server 配置

# HTTP 强制跳转 HTTPS
server {
    listen 80;
    server_name example.com *.example.com;
    return 301 https://$host$request_uri;
}

# HTTPS 站点
server {
    listen 443 ssl http2;
    server_name example.com *.example.com;

    # 证书路径(和部署时一致)
    ssl_certificate     /usr/local/openresty/nginx/conf/ssl/example.com.crt;
    ssl_certificate_key /usr/local/openresty/nginx/conf/ssl/example.com.key;

    # SSL 安全配置
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-AES128-GCM-SHA256;
    ssl_prefer_server_ciphers off;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;

    # HSTS(确认全站 HTTPS 后再启用)
    # add_header Strict-Transport-Security "max-age=31536000" always;

    # 站点根目录
    root /data/wwwroot/example.com;
    index index.html index.php;

    # PHP 处理(oneinstack 默认)
    location ~ \.php$ {
        fastcgi_pass unix:/dev/shm/php-cgi.sock;
        fastcgi_index index.php;
        include fastcgi.conf;
    }
}

7.2 测试并重载

/usr/local/openresty/nginx/sbin/nginx -t && /usr/local/openresty/nginx/sbin/nginx -s reload

八、自动续期配置

8.1 acme.sh 自动安装 cron

安装时已自动写入 crontab,验证:

crontab -l | grep acme

应看到类似:

17 16 * * * "/root/.acme.sh"/acme.sh --cron --home "/root/.acme.sh" > /dev/null

8.2 如果 cron 丢失,手动安装

( crontab -l 2>/dev/null; echo '17 0 * * * "/root/.acme.sh"/acme.sh --cron --home "/root/.acme.sh" > /dev/null' ) | crontab -

8.3 自动续期工作原理

每天定时执行 acme.sh --cron
    ├── 距到期 > 30 天 → 跳过
    └── 距到期 ≤ 30 天 → 自动续签
            ├── 通过 DNS API 自动添加/删除 TXT 记录
            ├── 签发新证书
            ├── 自动覆盖部署目录的 .key/.crt
            └── 自动执行 reloadcmd(重载 OpenResty)

8.4 手动测试续期

# 强制续签指定域名(验证整条链路)
/root/.acme.sh/acme.sh --renew -d example.com --ecc --force

# 批量续签所有
/root/.acme.sh/acme.sh --cron --force

九、一键管理脚本(完整代码)

将以下脚本保存为 /root/acme-manager.sh,封装申请、续签、检查、体检等常用操作:

#!/bin/bash
# =============================================================================
# acme-manager.sh - OpenResty 环境 acme.sh 证书一键管理脚本
# 功能: 申请(自动部署) / 手动续签 / 批量续签 / 列表 / 到期检查 / 日志 / cron管理 / 体检
# 适用: OpenResty + DNSPod + 通配符证书 + ZeroSSL CA
# =============================================================================

set -euo pipefail

# ============================ 配置区(按需修改) ============================
ACME_SH="/root/.acme.sh/acme.sh"
ACME_HOME="/root/.acme.sh"
NGINX_SSL_DIR="/usr/local/openresty/nginx/conf/ssl"
NGINX_RELOAD="/usr/local/openresty/nginx/sbin/nginx -s reload"
NGINX_TEST="/usr/local/openresty/nginx/sbin/nginx -t"

DNS_API="dns_dp"               # DNSPod: dns_dp | 阿里云: dns_ali | 腾讯云CAM: dns_tencent
KEY_LENGTH="ec-256"            # ec-256(ECC) 或 2048(RSA)
DEFAULT_CA="zerossl"           # zerossl / letsencrypt / buypass
ACME_EMAIL="your@email.com"    # ZeroSSL 账号邮箱,必填

LOG_FILE="/var/log/acme-manager.log"
DP_Id=""                       # DNSPod API ID(首次申请前填写)
DP_Key=""                      # DNSPod API Token
# =============================================================================

# 颜色输出
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; NC='\033[0m'
info()  { echo -e "${BLUE}[INFO]${NC}  $*"; }
ok()    { echo -e "${GREEN}[OK]${NC}    $*"; }
warn()  { echo -e "${YELLOW}[WARN]${NC}  $*"; }
error() { echo -e "${RED}[ERROR]${NC} $*"; }
log()   { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG_FILE" 2>/dev/null || true; }

# 初始化检查
init_check() {
    if [ ! -f "$ACME_SH" ]; then
        error "未找到 acme.sh: $ACME_SH"
        exit 1
    fi
    touch "$LOG_FILE" 2>/dev/null || LOG_FILE="/tmp/acme-manager.log"

    # 设置默认 CA
    if ! grep -q "DEFAULT_ACME_SERVER" "$ACME_HOME/account.conf" 2>/dev/null; then
        "$ACME_SH" --set-default-ca --server "$DEFAULT_CA" >/dev/null 2>&1
        ok "已设置默认 CA: $DEFAULT_CA"
    fi

    # 国内网络修复
    if ! grep -q "ACME_USE_HTTP_1=1" "$ACME_HOME/account.conf" 2>/dev/null; then
        echo 'ACME_USE_HTTP_1=1' >> "$ACME_HOME/account.conf"
        ok "已启用 ACME_USE_HTTP_1=1"
    fi

    # 绑定邮箱
    if [ -n "$ACME_EMAIL" ] && [ "$ACME_EMAIL" != "your@email.com" ]; then
        local current_email=""
        current_email=$(grep -oP "ACCOUNT_EMAIL='\K[^']+" "$ACME_HOME/account.conf" 2>/dev/null || echo "")
        if [ "$current_email" != "$ACME_EMAIL" ]; then
            info "绑定/更新 ZeroSSL 邮箱: $ACME_EMAIL"
            set +e
            "$ACME_SH" --register-account -m "$ACME_EMAIL" --server zerossl >/dev/null 2>&1
            set -e
            if ! grep -q "ACCOUNT_EMAIL=" "$ACME_HOME/account.conf" 2>/dev/null; then
                echo "ACCOUNT_EMAIL='$ACME_EMAIL'" >> "$ACME_HOME/account.conf"
            fi
            ok "邮箱绑定完成"
        fi
    fi
}

# 判断证书类型
get_cert_type() {
    local domain="$1"
    if [ -d "${ACME_HOME}/${domain}_ecc" ]; then echo "ecc"
    elif [ -d "${ACME_HOME}/${domain}" ]; then echo "rsa"
    else echo "none"; fi
}
get_ecc_flag() { [ "$1" = "ecc" ] && echo "--ecc" || echo ""; }

# 加载 DNSPod 凭证
load_dns_credentials() {
    if grep -q "DP_Id=" "$ACME_HOME/account.conf" 2>/dev/null; then return 0; fi
    if [ -n "$DP_Id" ] && [ -n "$DP_Key" ]; then
        export DP_Id="$DP_Id" DP_Key="$DP_Key"
        ok "已加载 DNSPod 凭证"
    else
        error "未配置 DNSPod 凭证,请在脚本配置区填写 DP_Id 和 DP_Key"
        exit 1
    fi
}

# 申请证书
cmd_issue() {
    local domain="${1:-}"
    [ -z "$domain" ] && { error "用法: $0 issue <域名>"; exit 1; }
    init_check; load_dns_credentials
    mkdir -p "$NGINX_SSL_DIR"
    info "申请通配符证书: $domain + *.$domain"

    set +e
    "$ACME_SH" --issue --dns "$DNS_API" -d "$domain" -d "*.${domain}" \
      --keylength "$KEY_LENGTH" \
      --key-file "${NGINX_SSL_DIR}/${domain}.key" \
      --fullchain-file "${NGINX_SSL_DIR}/${domain}.crt" \
      --reloadcmd "$NGINX_RELOAD"
    local ret=$?; set -e

    if [ $ret -eq 0 ]; then
        ok "证书申请成功!"
        ok "证书: ${NGINX_SSL_DIR}/${domain}.crt"
        log "ISSUE SUCCESS: $domain"
        openssl x509 -in "${NGINX_SSL_DIR}/${domain}.crt" -noout -dates -subject 2>/dev/null || true
    else
        error "申请失败,错误码: $ret"
        log "ISSUE FAILED: $domain (code=$ret)"
        exit $ret
    fi
}

# 强制续签
cmd_renew() {
    local domain="${1:-}"
    [ -z "$domain" ] && { error "用法: $0 renew <域名>"; exit 1; }
    init_check
    local cert_type=$(get_cert_type "$domain")
    [ "$cert_type" = "none" ] && { error "未找到证书: $domain"; exit 1; }
    local ecc_flag=$(get_ecc_flag "$cert_type")
    info "强制续签: $domain ($cert_type)"
    log "RENEW START: $domain"

    set +e
    "$ACME_SH" --renew -d "$domain" $ecc_flag --force
    local ret=$?; set -e

    if [ $ret -eq 0 ]; then
        ok "续签成功: $domain"
        log "RENEW SUCCESS: $domain"
        local nginx_cert="${NGINX_SSL_DIR}/${domain}.crt"
        [ -f "$nginx_cert" ] && openssl x509 -in "$nginx_cert" -noout -dates 2>/dev/null || true
    else
        error "续签失败: $domain (code=$ret)"
        exit $ret
    fi
}

# 批量续签
cmd_renew_all() {
    init_check
    info "强制续签所有证书..."
    set +e; "$ACME_SH" --cron --force; local ret=$?; set -e
    [ $ret -eq 0 ] && ok "批量续签完成" || warn "部分失败,查看: $0 log"
    echo ""; "$ACME_SH" --list
}

# 列表
cmd_list() {
    init_check
    info "已管理的证书:"; echo ""
    "$ACME_SH" --list
    echo ""; info "OpenResty 部署目录:"
    ls -lh "$NGINX_SSL_DIR"/ 2>/dev/null || warn "目录不存在"
}

# 到期检查
cmd_check() {
    init_check
    info "检查证书到期状态..."; echo ""
    local now_ts=$(date +%s) warn_days=15 found=0
    for cert_dir in "$ACME_HOME"/*/; do
        [ -d "$cert_dir" ] || continue
        local domain=$(basename "$cert_dir") cert_file=""
        if [ -f "${cert_dir}${domain}.cer" ]; then cert_file="${cert_dir}${domain}.cer"
        elif [ -f "${cert_dir}fullchain.cer" ]; then cert_file="${cert_dir}fullchain.cer"
        else continue; fi
        found=1
        local expiry=$(openssl x509 -in "$cert_file" -noout -enddate 2>/dev/null | cut -d= -f2)
        [ -z "$expiry" ] && { warn "$domain: 无法读取到期时间"; continue; }
        local expiry_ts=$(date -d "$expiry" +%s 2>/dev/null || echo 0)
        [ "$expiry_ts" = "0" ] && { warn "$domain: 解析失败 ($expiry)"; continue; }
        local days_left=$(( (expiry_ts - now_ts) / 86400 ))
        if [ "$days_left" -le 0 ]; then error "$domain: 已过期 ($expiry)"
        elif [ "$days_left" -le "$warn_days" ]; then warn "$domain: ${days_left} 天后过期 【建议续签】"
        else ok "$domain: ${days_left} 天后过期"; fi

        # 部署一致性检查
        local clean_domain="${domain%_ecc}"
        local nginx_cert="${NGINX_SSL_DIR}/${clean_domain}.crt"
        if [ -f "$nginx_cert" ]; then
            local nginx_expiry=$(openssl x509 -in "$nginx_cert" -noout -enddate 2>/dev/null | cut -d= -f2)
            [ "$nginx_expiry" != "$expiry" ] && warn "  -> 部署证书与内部证书不一致,执行: $0 renew $clean_domain"
        else
            warn "  -> 未找到部署证书: $nginx_cert"
        fi
    done
    [ "$found" -eq 0 ] && warn "未找到任何证书"
    echo ""; info "cron 任务:"
    crontab -l 2>/dev/null | grep -q "acme.sh.*--cron" && { ok "已配置"; crontab -l | grep "acme.sh.*--cron"; } || error "未配置,执行: $0 cron-install"
}

# 日志
cmd_log() {
    local lines="${1:-50}"
    [ -f "$LOG_FILE" ] && { info "管理脚本日志:"; tail -n "$lines" "$LOG_FILE"; } || warn "无日志文件"
    echo ""; info "acme.sh 日志:"
    [ -f "${ACME_HOME}/acme.sh.log" ] && tail -n 20 "${ACME_HOME}/acme.sh.log" || warn "无日志"
}

# 安装 cron
cmd_cron_install() {
    init_check
    local cron_cmd="17 0 * * * \"$ACME_SH\" --cron --home \"$ACME_HOME\" > /dev/null"
    if crontab -l 2>/dev/null | grep -q "acme.sh.*--cron"; then
        warn "已存在 cron 任务:"; crontab -l | grep "acme.sh.*--cron"
        read -r -p "是否覆盖?[y/N]: " answer
        [[ "$answer" =~ ^[Yy]$ ]] && { ( crontab -l 2>/dev/null | grep -v "acme.sh.*--cron"; echo "$cron_cmd" ) | crontab -; ok "已更新"; }
    else
        ( crontab -l 2>/dev/null; echo "$cron_cmd" ) | crontab -
        ok "cron 已安装"
    fi
}

# 一键体检
cmd_status() {
    init_check
    echo "==================== acme.sh 环境体检 ===================="
    echo ""; echo "【1】版本"; "$ACME_SH" --version
    echo ""; echo "【2】默认 CA"
    grep -q "DEFAULT_ACME_SERVER" "$ACME_HOME/account.conf" 2>/dev/null && grep "DEFAULT_ACME_SERVER" "$ACME_HOME/account.conf" || warn "未设置"
    echo ""; echo "【3】网络修复"
    grep -q "ACME_USE_HTTP_1=1" "$ACME_HOME/account.conf" 2>/dev/null && ok "已启用" || warn "未启用"
    echo ""; echo "【4】邮箱"
    grep -q "ACCOUNT_EMAIL=" "$ACME_HOME/account.conf" 2>/dev/null && grep "ACCOUNT_EMAIL=" "$ACME_HOME/account.conf" || warn "未绑定"
    echo ""; echo "【5】DNSPod 凭证"
    grep -q "DP_Id=" "$ACME_HOME/account.conf" 2>/dev/null && ok "已保存" || warn "未保存"
    echo ""; echo "【6】cron"
    crontab -l 2>/dev/null | grep -q "acme.sh.*--cron" && ok "已配置" || error "未配置"
    echo ""; echo "【7】证书列表"; "$ACME_SH" --list
    echo ""; echo "【8】部署目录"
    if [ -d "$NGINX_SSL_DIR" ]; then
        ls -lh "$NGINX_SSL_DIR"/; echo ""
        for cert_file in "$NGINX_SSL_DIR"/*.crt; do
            [ -f "$cert_file" ] || continue
            local dname=$(basename "$cert_file" .crt)
            local exp=$(openssl x509 -in "$cert_file" -noout -enddate 2>/dev/null | cut -d= -f2)
            [ -n "$exp" ] && echo "  $dname: 到期 $exp" || echo "  $dname: 【警告】无法读取"
        done
    else warn "目录不存在"; fi
    echo ""; echo "【9】OpenResty 配置测试"; $NGINX_TEST 2>&1 || true
    echo ""; echo "==========================================================="
}

# 帮助
usage() {
    cat << EOF
用法: $0 <命令> [参数]
  issue <域名>    申请通配符证书并自动部署
  renew <域名>    强制续签指定域名
  renew-all       强制续签所有证书
  list            列出所有证书
  check           检查到期状态和部署一致性
  log [行数]      查看日志
  cron-install    安装/修复 cron
  status          一键环境体检
EOF
}

# 主入口
case "${1:-}" in
    issue)        shift; cmd_issue "$@" ;;
    renew)        shift; cmd_renew "$@" ;;
    renew-all)    cmd_renew_all ;;
    list)         cmd_list ;;
    check)        cmd_check ;;
    log)          shift; cmd_log "${1:-50}" ;;
    cron-install) cmd_cron_install ;;
    status)       cmd_status ;;
    -h|--help|help|"") usage ;;
    *) error "未知命令: $1"; usage; exit 1 ;;
esac

安装和使用

# 上传到服务器
chmod 700 /root/acme-manager.sh

# 修改配置区(邮箱、DNSPod 凭证)
vi /root/acme-manager.sh

# 一键体检
/root/acme-manager.sh status

# 申请证书
/root/acme-manager.sh issue example.com

# 设置别名(可选)
echo "alias acme-mgr='/root/acme-manager.sh'" >> /root/.bashrc && source /root/.bashrc

十、常用管理命令速查

# 列出所有证书
/root/.acme.sh/acme.sh --list

# 查看某证书详情(ECC 加 --ecc)
/root/.acme.sh/acme.sh --info -d example.com --ecc

# 查看证书文件信息
openssl x509 -in /usr/local/openresty/nginx/conf/ssl/example.com.crt -noout -dates -subject -issuer

# 检查证书链是否完整(应 >= 2 段)
grep -c "BEGIN CERTIFICATE" /usr/local/openresty/nginx/conf/ssl/example.com.crt

# 手动续签
/root/.acme.sh/acme.sh --renew -d example.com --ecc --force

# 批量续签所有
/root/.acme.sh/acme.sh --cron --force

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

# 删除本地记录
/root/.acme.sh/acme.sh --remove -d example.com --ecc

# 查看日志
tail -50 /root/.acme.sh/acme.sh.log

# 升级 acme.sh(国内用 gitee 镜像)
/root/.acme.sh/acme.sh --upgrade --git-repo https://gitee.com/neilpang/acme.sh.git

十一、常见问题与踩坑记录

11.1 curl error 35 / 28(国内网络不通)

现象:申请过程中出现 error code: 35(TLS 握手失败)和 error code: 28(连接超时)。

原因:服务器到 CA 服务器(ZeroSSL/Let's Encrypt)网络不通畅。

解决:

# 1. 强制 HTTP/1.1
echo 'ACME_USE_HTTP_1=1' >> /root/.acme.sh/account.conf

# 2. 切换到国内更稳定的 CA
/root/.acme.sh/acme.sh --set-default-ca --server zerossl

# 3. 仍不行则走代理
export HTTPS_PROXY=http://127.0.0.1:7890

11.2 浏览器不信任证书(证书链不完整)

现象:浏览器提示 NET::ERR_CERT_AUTHORITY_INVALID。

原因:Nginx 配置中 ssl_certificate 指向了只有服务器证书的文件,缺少中间证书。

解决:必须用 --fullchain-file 部署完整证书链,验证:

grep -c "BEGIN CERTIFICATE" /usr/local/openresty/nginx/conf/ssl/example.com.crt
# 返回 2 或 3 为正常,返回 1 说明缺少中间证书

11.3 半成品记录(申请失败但列表里有记录)

现象:--list 里有记录,但 Created/Renew 为空,目录下没有 .cer 文件。

原因:申请过程中失败,留下了目录和配置文件但没有证书。

解决:直接删除目录后重新申请:

rm -rf /root/.acme.sh/example.com_ecc   # ECC 证书
rm -rf /root/.acme.sh/example.com        # RSA 证书

11.4 ECC 证书管理忘加 --ecc

现象:执行 --info/--renew/--revoke 时报"证书不存在"。

原因:申请时用了 --keylength ec-256,后续所有操作必须加 --ecc。

解决:所有命令加 --ecc:

/root/.acme.sh/acme.sh --info -d example.com --ecc
/root/.acme.sh/acme.sh --renew -d example.com --ecc --force

11.5 ZeroSSL 邮箱未绑定

现象:申请时报 Please update your account with an email address first。

解决:

/root/.acme.sh/acme.sh --register-account -m your@email.com --server zerossl

11.6 误删除部署目录的证书

现象:不小心删了 /usr/local/openresty/nginx/conf/ssl/ 下的证书。

解决:不需要重新申请,从内部原件重新部署即可:

/root/.acme.sh/acme.sh --install-cert -d example.com -d '*.example.com' --ecc \
  --key-file /usr/local/openresty/nginx/conf/ssl/example.com.key \
  --fullchain-file /usr/local/openresty/nginx/conf/ssl/example.com.crt \
  --reloadcmd "/usr/local/openresty/nginx/sbin/nginx -s reload"

11.7 系统时间不对导致 TLS 握手失败

现象:curl error 35,但网络正常。

解决:

date   # 检查时间
ntpdate ntp.aliyun.com   # 同步时间

十二、注意事项总结

  1. 泛域名只匹配一级:*.example.com 覆盖 a.example.com,不覆盖 a.b.example.com。
  2. 根域名必须同时申请:-d example.com -d '*.example.com' 缺一不可。
  3. 一张证书只能一个主域:不要把不同主域混在同一张泛域名证书里。
  4. 证书链必须完整:Nginx 必须用 fullchain 文件,不能只用服务器证书。
  5. ECC 管理加 --ecc:申请用了 ec-256,后续所有操作都要加 --ecc。
  6. 国内网络优化:ACME_USE_HTTP_1=1 + ZeroSSL CA,避免 curl 35/28。
  7. 凭证安全:DNSPod API Token 权限较大,脚本文件权限设为 700,不要泄露。
  8. 自动续期:acme.sh 自动安装 cron,到期前 30 天自动续签并 reload,无需人工干预。
  9. 原件 vs 部署副本:原件在 ~/.acme.sh/,部署副本在 Nginx 目录,Nginx 引用部署副本。
  10. 频率限制:Let's Encrypt 每域名每周 50 张,测试用 staging 环境或 Buypass。

参考链接