lolly/docs/nginx/32-nginx-acme-ssl.md
xfy 972eab4267 refactor(docs): 重构文档目录结构,nginx 文档移至子目录
将 docs/ 根目录下的 nginx 相关文档统一移动到 docs/nginx/ 子目录,
提高文档组织性和可维护性。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-16 10:48:14 +08:00

32 KiB
Raw Blame History

ACME 自动证书管理指南

本文档详细介绍如何在 Nginx 中使用 ngx_http_acme_module 模块实现 ACME 协议自动证书管理,包括 Let's Encrypt 的配置、自动续期、多域名和通配符证书等高级用法。


1. ACME 协议概述

1.1 什么是 ACME

ACMEAutomatic Certificate Management Environment协议是由 Let's Encrypt 开发的标准协议,用于自动化域名验证和 SSL/TLS 证书颁发。它使服务器能够自动获取和续期证书,无需人工干预。

1.2 工作原理

ACME 工作流程分为两个阶段:

┌─────────────┐                    ┌─────────────┐
│  ACME 客户端 │  ←──────────────→  │  CA 服务器  │
│  (Nginx)    │    1. 账户注册      │  (Let's     │
│             │    2. 订单创建      │   Encrypt)  │
│             │    3. 挑战验证      │             │
│             │    4. 证书颁发      │             │
└─────────────┘                    └─────────────┘

完整流程:

  1. 账户注册ACME 客户端生成密钥对并向 CA 注册账户
  2. 订单创建:客户端请求为指定域名颁发证书
  3. 挑战验证CA 要求客户端证明对域名的控制权(通过 HTTP-01、DNS-01 或 TLS-ALPN-01
  4. 证书颁发验证通过后CA 签发证书并发布到 Certificate Transparency (CT) 日志

1.3 挑战类型对比

挑战类型 验证方式 通配符支持 端口 80 要求 适用场景
HTTP-01 /.well-known/acme-challenge/ 放置文件 必须可用 标准 Web 服务器,最简单
DNS-01 添加 _acme-challenge TXT 记录 不需要 通配符证书、内部服务器
TLS-ALPN-01 通过 TLS ALPN 扩展验证 不需要 仅支持 443 端口的环境

2. ngx_http_acme_module 指令详解

2.1 指令汇总表

指令 语法 默认值 上下文 描述
acme_issuer acme_issuer name { ... } http 定义 ACME 证书颁发机构对象
uri uri uri; acme_issuer ACME 服务器目录 URL必填
account_key account_key alg[:size] | file; acme_issuer 账户私钥(支持 ecdsa/rsa 或文件路径)
challenge challenge type; http-01 acme_issuer 挑战类型:http-01tls-alpn-01
contact contact URL; acme_issuer 联系邮箱(建议 mailto: 格式)
external_account_key external_account_key kid file; acme_issuer 外部账户授权密钥EAB
preferred_chain preferred_chain name; acme_issuer 指定首选证书链
profile profile name [require]; acme_issuer 请求特定证书配置文件
ssl_trusted_certificate ssl_trusted_certificate file; acme_issuer 验证 ACME 服务器证书的 CA 证书
ssl_verify ssl_verify on | off; on acme_issuer 是否验证 ACME 服务器证书
state_path state_path path | off; acme_<issuer> acme_issuer 持久化存储路径(off 禁用)
accept_terms_of_service accept_terms_of_service; acme_issuer 同意服务条款(部分服务器必需)
acme_shared_zone acme_shared_zone zone=name:size; zone=ngx_acme_shared:256k http 共享内存区大小
acme_certificate acme_certificate issuer [identifier ...] [key=alg[:size]]; server 定义要请求的证书

2.2 嵌入式变量

在配置了 acme_certificateserver 块中可用:

变量 说明 用途
$acme_certificate SSL 证书路径 ssl_certificate 指令
$acme_certificate_key SSL 证书私钥路径 ssl_certificate_key 指令

3. Let's Encrypt 配置步骤

3.1 基础配置HTTP-01 挑战)

步骤 1编译或启用模块

确保 Nginx 包含 ngx_http_acme_module 模块:

nginx -V 2>&1 | grep -o 'http_acme_module'

步骤 2配置 DNS 解析器

ACME 模块需要解析 Let's Encrypt 服务器域名:

# nginx.conf

# 配置 DNS 解析器(根据你的网络环境调整)
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 10s;

步骤 3定义 ACME 颁发机构

# Let's Encrypt 生产环境
acme_issuer letsencrypt {
    uri         https://acme-v02.api.letsencrypt.org/directory;
    contact     mailto:admin@example.com;
    state_path  /var/cache/nginx/acme-letsencrypt;
    accept_terms_of_service;
}

# Let's Encrypt 测试环境(开发调试时使用)
acme_issuer letsencrypt_staging {
    uri         https://acme-staging-v02.api.letsencrypt.org/directory;
    contact     mailto:admin@example.com;
    state_path  /var/cache/nginx/acme-staging;
    accept_terms_of_service;
}

步骤 4配置共享内存

# 增大共享内存以支持多个证书
acme_shared_zone zone=ngx_acme_shared:1M;

步骤 5配置 HTTPS 服务器

server {
    listen 443 ssl;
    server_name www.example.com example.com;

    # 启用 ACME 自动证书
    acme_certificate letsencrypt;

    # 使用 ACME 变量
    ssl_certificate       $acme_certificate;
    ssl_certificate_key   $acme_certificate_key;

    # 证书缓存优化(避免每次请求都解析)
    ssl_certificate_cache max=2;

    # SSL 优化配置
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;

    location / {
        root /var/www/html;
        index index.html;
    }
}

步骤 6配置 HTTP 服务器(用于挑战)

server {
    listen 80;
    server_name www.example.com example.com;

    # ACME HTTP-01 挑战需要访问 80 端口
    # Nginx ACME 模块会自动处理 /.well-known/acme-challenge/ 路径

    location / {
        return 301 https://$server_name$request_uri;
    }
}

3.2 首次启动和验证

# 测试配置语法
nginx -t

# 重载配置
nginx -s reload

# 查看日志确认证书申请状态
tail -f /var/log/nginx/error.log

3.3 Let's Encrypt 速率限制

限制类型 限制值 时间窗口
账户注册 10 个 每 IP 每 3 小时
新订单 300 个 每账户每 3 小时
域名证书 50 个 每域名每 7 天
相同标识符集 5 个 每 7 天
验证失败 5 次 每标识符每小时

重要提示:

  • 使用 Staging 环境进行开发和测试
  • 续期操作通常不会触发速率限制
  • 使用 ARIACME Renewal Information可免除速率限制

4. 自动续期配置

4.1 自动续期原理

ngx_http_acme_module 模块会自动处理证书续期:

  1. 监控证书有效期:模块持续监控已颁发证书的过期时间
  2. 自动续期触发:在证书过期前自动发起续期请求
  3. 无缝替换:新证书获取后自动替换,无需重启 Nginx
  4. 持久化存储:证书和密钥存储在 state_path 指定的目录

4.2 状态目录结构

/var/cache/nginx/acme-letsencrypt/
├── account/                    # 账户密钥和配置
│   ├── private.key            # 账户私钥
│   └── registration.json      # 账户注册信息
├── orders/                    # 订单状态
│   └── *.json
└── certs/                     # 颁发的证书
    ├── example.com/           # 按域名组织
    │   ├── cert.pem          # 证书
    │   ├── chain.pem         # 证书链
    │   ├── fullchain.pem     # 完整证书链
    │   └── privkey.pem       # 私钥
    └── www.example.com/

4.3 配置证书续期监控

# 可选:配置日志监控续期情况
error_log /var/log/nginx/acme.log info;

acme_issuer letsencrypt {
    uri         https://acme-v02.api.letsencrypt.org/directory;
    contact     mailto:admin@example.com;
    state_path  /var/cache/nginx/acme-letsencrypt;
    accept_terms_of_service;

    # 可选:指定首选证书链
    preferred_chain "ISRG Root X1";
}

4.4 备份和恢复

备份脚本:

#!/bin/bash
# backup-acme.sh

BACKUP_DIR="/backup/nginx-acme/$(date +%Y%m%d)"
mkdir -p "$BACKUP_DIR"

# 备份 ACME 状态目录
cp -r /var/cache/nginx/acme-letsencrypt "$BACKUP_DIR/"

# 备份 Nginx 配置
cp -r /etc/nginx "$BACKUP_DIR/nginx-config"

echo "ACME backup completed: $BACKUP_DIR"

恢复脚本:

#!/bin/bash
# restore-acme.sh

BACKUP_DIR="$1"

if [ -z "$BACKUP_DIR" ]; then
    echo "Usage: $0 <backup-directory>"
    exit 1
fi

# 恢复 ACME 状态
systemctl stop nginx
cp -r "$BACKUP_DIR/acme-letsencrypt" /var/cache/nginx/
chown -R nginx:nginx /var/cache/nginx/acme-letsencrypt
systemctl start nginx

echo "ACME restore completed from: $BACKUP_DIR"

4.5 监控和告警

检查证书过期时间脚本:

#!/bin/bash
# check-cert-expiry.sh

CERT_DIR="/var/cache/nginx/acme-letsencrypt/certs"
WARNING_DAYS=7

for cert_path in $CERT_DIR/*/cert.pem; do
    if [ -f "$cert_path" ]; then
        domain=$(basename $(dirname "$cert_path"))
        expiry=$(openssl x509 -enddate -noout -in "$cert_path" | cut -d= -f2)
        expiry_epoch=$(date -d "$expiry" +%s)
        now_epoch=$(date +%s)
        days_left=$(( ($expiry_epoch - $now_epoch) / 86400 ))

        if [ $days_left -lt $WARNING_DAYS ]; then
            echo "WARNING: Certificate for $domain expires in $days_left days"
        else
            echo "OK: Certificate for $domain expires in $days_left days"
        fi
    fi
done

5. 多域名证书管理

5.1 多域名证书SAN 证书)

单个证书可以包含多个域名Subject Alternative Names

acme_issuer letsencrypt {
    uri         https://acme-v02.api.letsencrypt.org/directory;
    contact     mailto:admin@example.com;
    state_path  /var/cache/nginx/acme-letsencrypt;
    accept_terms_of_service;
}

server {
    listen 443 ssl;
    # 主域名和多个别名
    server_name example.com www.example.com api.example.com;

    # 为所有 server_name 申请单个证书
    acme_certificate letsencrypt;

    ssl_certificate       $acme_certificate;
    ssl_certificate_key   $acme_certificate_key;
    ssl_certificate_cache max=2;

    location / {
        proxy_pass http://backend;
    }
}

5.2 独立域名证书

为不同域名申请独立证书:

acme_issuer letsencrypt {
    uri         https://acme-v02.api.letsencrypt.org/directory;
    contact     mailto:admin@example.com;
    state_path  /var/cache/nginx/acme-letsencrypt;
    accept_terms_of_service;
}

# 主站点
server {
    listen 443 ssl;
    server_name example.com www.example.com;

    acme_certificate letsencrypt example.com www.example.com;

    ssl_certificate       $acme_certificate;
    ssl_certificate_key   $acme_certificate_key;
    ssl_certificate_cache max=2;

    location / {
        root /var/www/example;
    }
}

# API 站点
server {
    listen 443 ssl;
    server_name api.example.com;

    acme_certificate letsencrypt api.example.com;

    ssl_certificate       $acme_certificate;
    ssl_certificate_key   $acme_certificate_key;
    ssl_certificate_cache max=2;

    location / {
        proxy_pass http://api-backend;
    }
}

# 博客站点
server {
    listen 443 ssl;
    server_name blog.example.com;

    acme_certificate letsencrypt blog.example.com;

    ssl_certificate       $acme_certificate;
    ssl_certificate_key   $acme_certificate_key;
    ssl_certificate_cache max=2;

    location / {
        proxy_pass http://blog-backend;
    }
}

5.3 多 ACME 账户配置

不同域名使用不同的 ACME 账户:

# 主账户
acme_issuer letsencrypt_main {
    uri         https://acme-v02.api.letsencrypt.org/directory;
    contact     mailto:admin@example.com;
    state_path  /var/cache/nginx/acme-main;
    accept_terms_of_service;
    account_key ecdsa:384;
}

# 客户项目账户
acme_issuer letsencrypt_client {
    uri         https://acme-v02.api.letsencrypt.org/directory;
    contact     mailto:client-projects@example.com;
    state_path  /var/cache/nginx/acme-client;
    accept_terms_of_service;
    account_key ecdsa:256;
}

server {
    listen 443 ssl;
    server_name project1.example.com;

    acme_certificate letsencrypt_client project1.example.com;

    ssl_certificate       $acme_certificate;
    ssl_certificate_key   $acme_certificate_key;
}

6. 通配符证书配置DNS 验证)

6.1 DNS-01 挑战说明

通配符证书(如 *.example.com)必须使用 DNS-01 挑战类型验证。这要求:

  1. 能够自动修改 DNS 记录(通过 DNS 提供商 API
  2. _acme-challenge.example.com 添加 TXT 记录
  3. 等待 DNS 传播后验证

注意: ngx_http_acme_module 本身不直接支持 DNS-01 挑战(需要外部 DNS 管理工具配合),可以使用 certbot 等工具获取通配符证书后由 Nginx 使用。

6.2 使用 Certbot 获取通配符证书

# 安装 certbot 和 DNS 插件(以 Cloudflare 为例)
# Ubuntu/Debian
sudo apt install certbot python3-certbot-dns-cloudflare

# CentOS/RHEL
sudo yum install certbot python3-certbot-dns-cloudflare

配置 DNS API 凭证:

# 创建 Cloudflare 凭证文件
sudo mkdir -p /etc/letsencrypt
sudo cat > /etc/letsencrypt/dnscloudflare.ini << 'EOF'
dns_cloudflare_api_token = your-api-token-here
EOF
sudo chmod 600 /etc/letsencrypt/dnscloudflare.ini

申请通配符证书:

sudo certbot certonly \
    --dns-cloudflare \
    --dns-cloudflare-credentials /etc/letsencrypt/dnscloudflare.ini \
    -d "example.com" \
    -d "*.example.com" \
    --preferred-challenges dns-01

自动续期配置:

# 测试续期
certbot renew --dry-run

# 添加定时任务
echo "0 3 * * * root certbot renew --quiet --deploy-hook 'nginx -s reload'" | sudo tee -a /etc/crontab

6.3 Nginx 使用通配符证书

server {
    listen 443 ssl;
    # 使用通配符匹配所有子域名
    server_name *.example.com;

    # 使用 certbot 获取的证书
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    ssl_certificate_cache max=2;

    # 子域名路由
    location / {
        # 提取子域名
        set $subdomain "";
        if ($host ~* ^([^.]+)\.example\.com$) {
            set $subdomain $1;
        }

        # 根据子域名代理到不同后端
        proxy_pass http://$subdomain-backend;
    }
}

# 主域名单独配置
server {
    listen 443 ssl;
    server_name example.com;

    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    location / {
        root /var/www/main;
    }
}

6.4 主流 DNS 提供商 Certbot 插件

提供商 安装命令 配置方式
Cloudflare python3-certbot-dns-cloudflare API Token
Route53 python3-certbot-dns-route53 AWS IAM 凭证
Alibaba Cloud certbot-dns-aliyun Access Key
Tencent Cloud certbot-dns-tencentcloud Secret ID/Key
GoDaddy certbot-dns-godaddy API Key/Secret

7. 与 Certbot 方案对比

7.1 方案对比表

特性 ngx_http_acme_module Certbot
集成度 内置于 Nginx无需外部工具 独立程序,需单独安装
配置复杂度 纯 Nginx 配置 需要额外配置和定时任务
HTTP-01 支持 原生支持 支持
DNS-01 支持 不支持 支持
通配符证书 不支持 支持
TLS-ALPN-01 支持 不支持
自动续期 自动,无需外部任务 需配置 cron/systemd timer
证书热重载 无缝更新 ⚠️ 需 reload/restart Nginx
多 Web 服务器 仅 Nginx Apache, Nginx 等
外部账户绑定 支持 EAB 支持

7.2 选择建议

选择 ngx_http_acme_module

  • 纯 Nginx 环境,追求配置简洁
  • 使用 HTTP-01 或 TLS-ALPN-01 挑战
  • 不需要通配符证书
  • 希望证书续期完全自动化

选择 Certbot

  • 需要通配符证书
  • 使用 DNS-01 挑战
  • 多 Web 服务器环境
  • 需要与外部系统集成

7.3 混合方案

结合两者优势:使用 ngx_http_acme_module 处理常规证书,使用 certbot 处理通配符证书:

# 常规域名使用内置 ACME
server {
    listen 443 ssl;
    server_name www.example.com api.example.com;

    acme_certificate letsencrypt;
    ssl_certificate $acme_certificate;
    ssl_certificate_key $acme_certificate_key;
}

# 通配符使用 certbot 证书
server {
    listen 443 ssl;
    server_name *.example.com;

    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
}

8. 完整配置示例

8.1 单站点基础配置

# /etc/nginx/nginx.conf

user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;

events {
    worker_connections 1024;
}

http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http_x_forwarded_for"';

    access_log /var/log/nginx/access.log main;

    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    types_hash_max_size 2048;

    # DNS 解析器配置
    resolver 8.8.8.8 8.8.4.4 valid=300s;
    resolver_timeout 10s;

    # ACME 共享内存
    acme_shared_zone zone=ngx_acme_shared:1M;

    # Let's Encrypt 配置
    acme_issuer letsencrypt {
        uri         https://acme-v02.api.letsencrypt.org/directory;
        contact     mailto:admin@example.com;
        state_path  /var/cache/nginx/acme-letsencrypt;
        accept_terms_of_service;

        # 使用 ECDSA 账户密钥(更高效)
        account_key ecdsa:384;

        # 启用证书验证
        ssl_verify on;
    }

    # HTTP 服务器 - 处理 ACME 挑战和重定向
    server {
        listen 80;
        server_name example.com www.example.com;

        # ACME 挑战自动处理
        # 其他请求重定向到 HTTPS
        location / {
            return 301 https://$server_name$request_uri;
        }
    }

    # HTTPS 服务器
    server {
        listen 443 ssl http2;
        server_name example.com www.example.com;

        # 启用 ACME 自动证书
        acme_certificate letsencrypt;

        # 使用 ACME 变量
        ssl_certificate       $acme_certificate;
        ssl_certificate_key   $acme_certificate_key;
        ssl_certificate_cache max=2;

        # SSL 安全配置
        ssl_protocols TLSv1.2 TLSv1.3;
        ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
        ssl_prefer_server_ciphers on;
        ssl_session_cache shared:SSL:10m;
        ssl_session_timeout 1d;
        ssl_session_tickets off;

        # OCSP Stapling
        ssl_stapling on;
        ssl_stapling_verify on;
        ssl_trusted_certificate $acme_certificate;

        # 安全响应头
        add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
        add_header X-Frame-Options "SAMEORIGIN" always;
        add_header X-Content-Type-Options "nosniff" always;
        add_header X-XSS-Protection "1; mode=block" always;
        add_header Referrer-Policy "strict-origin-when-cross-origin" always;

        root /var/www/example;
        index index.html index.htm;

        location / {
            try_files $uri $uri/ =404;
        }

        # 静态文件缓存
        location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
            expires 6M;
            access_log off;
        }
    }
}

8.2 多站点生产配置

# /etc/nginx/nginx.conf

user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;

events {
    worker_connections 2048;
    use epoll;
    multi_accept on;
}

http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http_x_forwarded_for" '
                    'rt=$request_time uct="$upstream_connect_time" '
                    'uht="$upstream_header_time" urt="$upstream_response_time"';

    access_log /var/log/nginx/access.log main;

    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    types_hash_max_size 2048;
    server_tokens off;

    # Gzip 压缩
    gzip on;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml;

    # DNS 配置
    resolver 8.8.8.8 1.1.1.1 valid=300s;
    resolver_timeout 10s;

    # ACME 配置
    acme_shared_zone zone=ngx_acme_shared:2M;

    # Let's Encrypt 生产环境
    acme_issuer letsencrypt {
        uri         https://acme-v02.api.letsencrypt.org/directory;
        contact     mailto:ssl@example.com;
        state_path  /var/cache/nginx/acme-letsencrypt;
        accept_terms_of_service;
        account_key ecdsa:384;
        preferred_chain "ISRG Root X1";
    }

    # Let's Encrypt 测试环境
    acme_issuer letsencrypt_staging {
        uri         https://acme-staging-v02.api.letsencrypt.org/directory;
        contact     mailto:ssl@example.com;
        state_path  /var/cache/nginx/acme-staging;
        accept_terms_of_service;
    }

    # SSL 优化配置(共享)
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers on;
    ssl_session_cache shared:SSL:50m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;
    ssl_stapling on;
    ssl_stapling_verify on;

    # 通用安全响应头
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;

    # 站点配置目录
    include /etc/nginx/conf.d/*.conf;
}
# /etc/nginx/conf.d/01-http-default.conf

# HTTP 默认服务器 - 处理所有 80 端口请求
server {
    listen 80 default_server;
    listen [::]:80 default_server;
    server_name _;

    # ACME 挑战处理
    location /.well-known/acme-challenge/ {
        # 由 ngx_http_acme_module 自动处理
        root /var/cache/nginx/acme-challenges;
    }

    # 所有其他请求重定向到 HTTPS
    location / {
        return 301 https://$host$request_uri;
    }
}
# /etc/nginx/conf.d/10-example.com.conf

# 主站点
server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name example.com www.example.com;

    acme_certificate letsencrypt;

    ssl_certificate       $acme_certificate;
    ssl_certificate_key   $acme_certificate_key;
    ssl_certificate_cache max=2;

    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;

    root /var/www/example;
    index index.html index.php;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    # PHP 处理
    location ~ \.php$ {
        fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }

    location ~ /\.ht {
        deny all;
    }
}
# /etc/nginx/conf.d/20-api.example.com.conf

# API 站点
server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name api.example.com;

    acme_certificate letsencrypt api.example.com;

    ssl_certificate       $acme_certificate;
    ssl_certificate_key   $acme_certificate_key;
    ssl_certificate_cache max=2;

    add_header Strict-Transport-Security "max-age=63072000" always;

    # API 限流
    limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
    limit_req zone=api burst=20 nodelay;

    location / {
        proxy_pass http://api_backend;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_cache_bypass $http_upgrade;
    }
}

upstream api_backend {
    server 10.0.1.10:8080;
    server 10.0.1.11:8080;
    keepalive 32;
}

8.3 测试环境配置

# 测试/开发环境使用 Staging

acme_issuer letsencrypt_staging {
    uri         https://acme-staging-v02.api.letsencrypt.org/directory;
    contact     mailto:dev@example.com;
    state_path  /var/cache/nginx/acme-staging;
    accept_terms_of_service;
}

server {
    listen 443 ssl;
    server_name staging.example.com;

    # 开发环境使用 Staging
    acme_certificate letsencrypt_staging staging.example.com;

    ssl_certificate       $acme_certificate;
    ssl_certificate_key   $acme_certificate_key;

    # 注意Staging 证书不会被浏览器信任
    # 仅用于测试续期流程

    location / {
        root /var/www/staging;
    }
}

9. 故障排查指南

9.1 常见问题及解决方案

问题 1证书申请失败验证失败

症状:

[error] acme: challenge failed for example.com

排查步骤:

  1. 检查 DNS 解析

    nslookup example.com
    dig example.com A
    
  2. 检查 80 端口可访问性

    # 从外部测试
    curl -I http://example.com/
    
    # 检查防火墙
    sudo iptables -L -n | grep 80
    
  3. 检查 Nginx 错误日志

    sudo tail -f /var/log/nginx/error.log
    
  4. 验证挑战响应

    # 手动测试挑战 URL
    curl http://example.com/.well-known/acme-challenge/test
    

解决方案:

  • 确保域名 DNS 解析正确且已生效
  • 确保防火墙允许 80 端口入站连接
  • 确保 server_name 包含申请证书的域名

问题 2DNS 解析失败

症状:

[error] could not resolve acme-v02.api.letsencrypt.org

解决方案:

# 检查 resolver 配置
http {
    # 使用可靠的 DNS 服务器
    resolver 8.8.8.8 8.8.4.4 1.1.1.1 valid=300s;
    resolver_timeout 30s;
    ...
}

测试 DNS 解析:

# 测试系统 DNS
nslookup acme-v02.api.letsencrypt.org

# 测试 Nginx 配置语法
nginx -t

问题 3速率限制错误

症状:

[error] acme: 429 Too Many Requests

排查:

# 检查最近申请记录
grep "acme" /var/log/nginx/error.log | tail -20

解决方案:

  • 切换到 Staging 环境进行测试
  • 等待当前速率限制窗口重置
  • 检查是否有重复申请配置
  • 使用现有证书而不是重新申请

问题 4证书不自动续期

症状: 证书过期但未自动续期

排查:

# 检查证书状态
openssl x509 -in /var/cache/nginx/acme-letsencrypt/certs/example.com/cert.pem -noout -dates

# 检查 Nginx 错误日志
grep -i "acme\|certificate" /var/log/nginx/error.log

解决方案:

# 确保配置正确
acme_issuer letsencrypt {
    uri         https://acme-v02.api.letsencrypt.org/directory;
    contact     mailto:admin@example.com;
    state_path  /var/cache/nginx/acme-letsencrypt;  # 确保有写入权限
    accept_terms_of_service;
}

检查权限:

# 确保 Nginx 用户可以写入 state_path
sudo chown -R nginx:nginx /var/cache/nginx/acme-letsencrypt
sudo chmod 700 /var/cache/nginx/acme-letsencrypt

问题 5$acme_certificate 变量为空

症状:

[emerg] BIO_new_file("$acme_certificate") failed

解决方案:

# 确保 acme_certificate 指令在 server 块中
server {
    listen 443 ssl;
    server_name example.com;

    # 必须先声明 acme_certificate
    acme_certificate letsencrypt;

    # 然后才能使用变量
    ssl_certificate       $acme_certificate;
    ssl_certificate_key   $acme_certificate_key;
}

9.2 调试配置

启用详细日志:

# 开发调试时使用
error_log /var/log/nginx/acme-debug.log debug;

acme_issuer letsencrypt_staging {
    uri         https://acme-staging-v02.api.letsencrypt.org/directory;
    contact     mailto:debug@example.com;
    state_path  /var/cache/nginx/acme-staging;
    accept_terms_of_service;

    # 禁用服务器证书验证(仅测试时使用)
    # ssl_verify off;
}

9.3 诊断脚本

#!/bin/bash
# diagnose-acme.sh - ACME 诊断脚本

echo "=== Nginx ACME 诊断 ==="
echo

# 检查 Nginx 版本和模块
echo "1. Nginx 版本和模块:"
nginx -V 2>&1 | grep -E "(nginx version|http_acme_module)"
echo

# 检查配置语法
echo "2. 配置语法检查:"
nginx -t
echo

# 检查 DNS 解析
echo "3. DNS 解析测试:"
nslookup acme-v02.api.letsencrypt.org 2>/dev/null || echo "DNS 解析失败"
echo

# 检查证书目录
echo "4. ACME 状态目录:"
for dir in /var/cache/nginx/acme-*; do
    if [ -d "$dir" ]; then
        echo "  目录: $dir"
        echo "  大小: $(du -sh "$dir" 2>/dev/null | cut -f1)"
        echo "  权限: $(stat -c %a "$dir" 2>/dev/null || stat -f %A "$dir" 2>/dev/null)"
        echo "  所有者: $(stat -c %U:%G "$dir" 2>/dev/null || stat -f %Su:%Sg "$dir" 2>/dev/null)"
    fi
done
echo

# 检查现有证书
echo "5. 现有证书:"
for cert in /var/cache/nginx/acme-*/certs/*/cert.pem; do
    if [ -f "$cert" ]; then
        domain=$(basename $(dirname "$cert"))
        echo "  域名: $domain"
        openssl x509 -in "$cert" -noout -dates -subject 2>/dev/null | sed 's/^/    /'
        echo
    fi
done

# 检查端口监听
echo "6. 端口监听:"
ss -tlnp | grep -E ":80|:443" | sed 's/^/  /'
echo

# 检查防火墙
echo "7. 防火墙状态:"
if command -v iptables &> /dev/null; then
    iptables -L -n | grep -E "80|443" | sed 's/^/  /'
else
    echo "  iptables 不可用"
fi
echo

# 检查错误日志
echo "8. 最近的 ACME 相关错误:"
grep -i "acme" /var/log/nginx/error.log 2>/dev/null | tail -10 | sed 's/^/  /'
echo

echo "=== 诊断完成 ==="

9.4 获取帮助

官方资源:

测试工具:


附录:快速参考

配置检查清单

  • DNS 解析器已配置(resolver
  • acme_shared_zone 已定义
  • acme_issuer 块配置正确
  • acme_certificate 指令在 server 块中
  • ssl_certificate 使用 $acme_certificate 变量
  • 80 端口服务器配置正确
  • state_path 目录可写
  • 防火墙允许 80/443 端口
  • 域名 DNS 已生效

常用命令

# 测试配置
nginx -t

# 重载配置
nginx -s reload

# 查看证书信息
openssl x509 -in cert.pem -noout -text

# 查看证书过期时间
openssl x509 -in cert.pem -noout -dates

# 手动清理状态(谨慎使用)
rm -rf /var/cache/nginx/acme-letsencrypt/certs/example.com

文档版本1.0
最后更新2025年4月