HTTPS 部署需要解决两个问题:客户端必须确认服务器身份,通信双方还要在不可信网络上建立加密密钥。TLS 证书为握手提供经过认证的公钥,ACME 则把域名验证、证书签发和续期变成自动化流程。

本文先解释信任模型,再使用 Certbot、acme.sh、Cloudflare DNS、Nginx 和 Docker 建立可维护的证书流程。Nginx 配置结构与反向代理的基础可参考 Nginx 安装与配置实践

1. TLS 证书与 ACME 验证

1.1 证书证明什么

TLS 证书是由证书颁发机构(Certificate Authority, CA)签名的 X.509 文档。证书包含覆盖的域名、服务器公钥、有效期、颁发者信息和 CA 签名。证书负责把公钥与一个或多个名称绑定,不包含服务器私钥。

建立 TLS 1.3 连接时,客户端和服务器交换支持的参数,服务器发送证书链并证明自己持有对应私钥,双方随后派生临时对称会话密钥。简化流程如下:

客户端                                      服务器
  | -------- ClientHello --------------------> |
  | <------- ServerHello、Certificate、------- |
  |          CertificateVerify、Finished       |
  | -- 检查域名、证书链、签名和有效期 ----------|
  | -------- Finished ------------------------> |
  | <========== 加密应用数据 =================> |

客户端会检查请求的主机名是否出现在证书中、证书是否仍在有效期内、签名能否形成通向可信根证书的链,以及服务器是否持有对应私钥。ECDHE 密钥交换提供临时共享秘密,高效的对称加密则保护后续 HTTP 流量。客户端使用公钥密码学验证 CA 签名,并不是解密签名以取回证书哈希。

1.2 ACME 自动完成哪些步骤

传统证书运维要求管理员生成私钥和证书签名请求、证明域名控制权、下载证书链、安装证书,并在过期前重复所有步骤。RFC 8555 定义的 Automatic Certificate Management Environment 协议把账户创建、域名验证、订单、签发和续期统一为 API 工作流。

Certbot 或 acme.sh 等 ACME 客户端会建立账户密钥、提交订单、完成每个域名的验证挑战、提交证书签名请求并下载证书链。自动化还必须覆盖部署阶段:磁盘上的证书完成续期后,服务器需要重新加载文件才能使用新证书。

1.3 HTTP-01 与 DNS-01

HTTP-01 由 CA 生成 token,客户端把响应发布到 http://example.com/.well-known/acme-challenge/<TOKEN>,CA 再通过公网 80 端口读取响应。HTTP-01 适合公开 Web 服务器,但不能验证 *.example.com 等泛域名标识符。

DNS-01 要求客户端在 _acme-challenge.example.com 下发布 TXT 记录,CA 查询权威 DNS 后核对响应。DNS-01 支持泛域名证书,也不要求源站开放 80 端口;无人值守续期则需要一个权限严格受限的 DNS 服务商 API 凭据。

Let’s Encrypt 是非营利公共 CA,也是主要 ACME 服务商。其他公共 CA 也可以提供 ACME endpoint。Cloudflare 代理网站时承担不同角色:浏览器收到 Cloudflare 边缘证书,Cloudflare 到源站的连接使用另一张源站证书。后文的 Certbot 与 acme.sh 流程通过 ACME 申请公开信任证书;Cloudflare token 只用于创建 DNS-01 记录,与 Cloudflare Origin CA 证书没有关系。

2. 使用 Certbot、HTTP-01 与 Nginx

2.1 准备 webroot

按照操作系统支持的安装方式部署 Certbot,再为验证文件预留目录。Nginx 必须先通过 HTTP 提供 challenge 路径,然后才能把其他请求重定向到 HTTPS:

server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;

    location /.well-known/acme-challenge/ {
        alias /var/www/certbot/.well-known/acme-challenge/;
        try_files $uri =404;
    }

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

创建目录并重新加载通过语法检查的配置:

sudo mkdir -p /var/www/certbot/.well-known/acme-challenge
sudo chown -R www-data:www-data /var/www/certbot
sudo nginx -t
sudo systemctl reload nginx

不同发行版使用的服务账户可能不同。申请证书前,应确认 Nginx 能读取 webroot,并从公网访问 .well-known/acme-challenge/ 中的普通测试文件。

2.2 申请并安装证书

certonly --webroot 只获取证书文件,不改写其余 Nginx 配置:

sudo certbot certonly \
  --webroot \
  -w /var/www/certbot \
  -d example.com \
  -d www.example.com \
  --email [email protected] \
  --agree-tos \
  --no-eff-email

-w 指定 webroot,每个 -d 增加一个 Subject Alternative Name,--email 提供运维联系人。签发成功后,Certbot 会在 /etc/letsencrypt/live/example.com/ 下建立托管链接。Nginx 的 ssl_certificate 应指向包含站点证书和中间证书链的 fullchain.pemssl_certificate_key 应指向 privkey.pem。私钥不能提交到版本控制,也不能复制到公开目录。

当前 Nginx 的 HTTPS server 可以写成:

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;
    server_name example.com www.example.com;

    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;

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

重新加载前先执行 sudo nginx -t,通过后再执行 sudo systemctl reload nginx。密码套件选择取决于 Nginx 和 TLS 库版本,当前软件的安全默认值通常比复制旧的静态列表更可靠。

2.3 测试续期与部署 hook

certbot renew 读取 /etc/letsencrypt/renewal/ 下的续期配置,只处理已经进入续期窗口的证书。使用 sudo certbot renew --dry-run 测试完整验证流程。部署 hook 应仅在证书实际完成续期后重载 Nginx,例如 sudo certbot renew --quiet --deploy-hook "systemctl reload nginx"

许多 Certbot 软件包会自动安装 systemd timer。新增 cron 任务前先检查 systemctl list-timers,避免重复调度。软件包没有提供 timer 时,可以每天执行续期检查;Certbot 会自行判断是否需要重新签发。

3. 使用 Cloudflare DNS-01 申请泛域名证书

3.1 建立最小权限 API token

*.example.com 必须使用 DNS-01,源站无法开放 80 端口时也适合采用 DNS-01。在 Cloudflare 中建立 API token,只授予目标 zone 的 Zone / DNS / Edit 权限。不要使用 Global API Key,因为全局密钥具有广泛账户权限。ACME 客户端具有固定出口地址时,还可以用 IP filtering 进一步限制 token。

Cloudflare 只会显示一次新 token。把 token 保存到仅 root 可读的凭据文件中,不能写入源码。Debian 或 Ubuntu 可以执行 sudo apt install python3-certbot-dns-cloudflare 安装 DNS plugin,其他发行版应使用受支持的软件包来源。

创建凭据文件:

sudo install -d -m 700 /root/.secrets/certbot
sudo tee /root/.secrets/certbot/cloudflare.ini >/dev/null <<'EOF'
dns_cloudflare_api_token = YOUR_CLOUDFLARE_API_TOKEN
EOF
sudo chmod 600 /root/.secrets/certbot/cloudflare.ini

文件包含能够修改公共 DNS 的明文凭据,因此权限控制属于必要的安全边界。

3.2 通过 DNS 签发和续期

同时申请根域名与泛域名:

sudo certbot certonly \
  --dns-cloudflare \
  --dns-cloudflare-credentials /root/.secrets/certbot/cloudflare.ini \
  --dns-cloudflare-propagation-seconds 30 \
  -d example.com \
  -d '*.example.com' \
  --email [email protected] \
  --agree-tos \
  --no-eff-email

传播等待时间让权威 DNS 在验证开始前发布 TXT 记录。实际部署应根据服务商和网络环境选择能够稳定验证的最短时间。Certbot 会把 authenticator 与凭据路径保存到续期配置,后续 certbot renew 会自动重复相同的 DNS 流程。正式依赖自动续期前仍需运行 dry-run。

4. 使用轻量 ACME 客户端 acme.sh

4.1 签发证书

acme.sh 是 Shell 实现的 ACME 客户端,并内置多种 DNS 服务商 hook。把远程内容直接交给 shell 前应先检查安装脚本。常见安装命令是 curl https://get.acme.sh | sh -s [email protected];文件通常安装到 ~/.acme.sh/,安装器也会建立定时续期任务。

使用现有 webroot 完成 HTTP-01 时,执行 acme.sh --issue -d example.com -w /var/www/certbot。使用 Cloudflare DNS-01 时,导出最小权限 token 与账户 ID,再申请根域名和泛域名:

export CF_Token="YOUR_CLOUDFLARE_API_TOKEN"
export CF_Account_ID="YOUR_CLOUDFLARE_ACCOUNT_ID"
acme.sh --issue --dns dns_cf -d example.com -d '*.example.com'

acme.sh 会把 DNS 凭据保存到受保护的账户配置中,以便定时续期重复使用。该目录需要与 Certbot 凭据文件采用相同等级的权限保护。

4.2 部署到稳定的 Nginx 路径

不要让 Nginx 直接引用 acme.sh 内部工作目录。使用 --install-cert 把续期文件复制到稳定的服务路径,并绑定重新加载操作:

sudo mkdir -p /etc/nginx/ssl/example.com
acme.sh --install-cert -d example.com \
  --key-file /etc/nginx/ssl/example.com/privkey.pem \
  --fullchain-file /etc/nginx/ssl/example.com/fullchain.pem \
  --reloadcmd "systemctl reload nginx"

使用 crontab -l | grep acme.sh 检查定时任务,再运行一次 acme.sh --cron,验证账户、续期状态、部署路径和 reload command。

5. 在 Docker Compose 中运行 Nginx 与 Certbot

5.1 只共享必要状态

容器部署需要持久保存证书状态,并让 Nginx 与 Certbot 共享 HTTP-01 webroot。业务文件、challenge 文件和 ACME 账户状态应使用不同目录:

/opt/ssl-stack/
├── compose.yaml
├── nginx/
│   └── conf.d/
│       └── default.conf
└── data/
    ├── certbot/
    │   ├── conf/
    │   └── www/
    └── webroot/

一个紧凑的 Compose 配置可以让 Nginx 常驻,再由宿主机或其他 scheduler 定时调用一次性 Certbot 命令:

services:
  nginx:
    image: nginx:1.27-alpine
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - ./data/certbot/conf:/etc/letsencrypt:ro
      - ./data/certbot/www:/var/www/certbot:ro
      - ./data/webroot:/usr/share/nginx/html:ro

  certbot:
    image: certbot/certbot:latest
    volumes:
      - ./data/certbot/conf:/etc/letsencrypt:rw
      - ./data/certbot/www:/var/www/certbot:rw

镜像版本应按照部署环境的更新策略固定。宿主机 systemd timer 或 cron 可以执行 docker compose run --rm certbot renew,并在续期成功后重载 Nginx。不要使用只更新文件、却不能可靠通知 Nginx 进程的容器循环。

5.2 先用 HTTP 引导,再启用 HTTPS

证书路径不存在时,引用该路径的 Nginx 无法启动;HTTP-01 又需要 Nginx 提供 challenge。初次签发应先使用纯 HTTP 配置:

server {
    listen 80;
    server_name example.com;

    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    location / {
        return 200 'Initializing TLS...';
        add_header Content-Type text/plain;
    }
}

启动 Nginx,再运行一次性签发命令:

docker compose up -d nginx
docker compose run --rm certbot certonly \
  --webroot --webroot-path=/var/www/certbot \
  -d example.com \
  --email [email protected] \
  --agree-tos --no-eff-email

签发成功后,把引导配置替换为 HTTP 重定向和 HTTPS server:

server {
    listen 80;
    server_name example.com;

    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

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

server {
    listen 443 ssl;
    http2 on;
    server_name example.com;
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;

    location / {
        root /usr/share/nginx/html;
        index index.html;
    }
}

先执行 docker compose exec nginx nginx -t,再执行 docker compose exec nginx nginx -s reload。需要了解镜像、容器、volume、network 与 Compose 时,可以继续阅读 Docker 架构指南

6. 选择流程并验证完整生命周期

公开 Nginx 能够开放 80 端口、而且运维人员希望明确控制服务配置时,可以选择 Certbot webroot。需要泛域名证书或源站不能公开 80 端口时,应使用 DNS-01 和权限受限的服务商 token。运行环境适合 Shell 客户端与内置 DNS hook 时,可以使用 acme.sh。容器化只改变进程隔离和文件挂载方式,不能省略持久化账户状态、续期调度、凭据保护与续期后的服务重载。

部署完成前,应检查公网主机名、完整证书链、到期时间、HTTP 到 HTTPS 重定向、定时续期、deploy hook、私钥权限、DNS token 权限范围和恢复流程。每次大幅修改后都应重新运行续期 dry-run。证书签发与服务配置属于两个阶段,可靠的 HTTPS 要求两个阶段都能自动执行并接受监控。