NGINX 原生 ACME 支持:从根本上重塑 TLS 自动化部署

Page content

对于任何负责保障线上服务稳定与安全的系统而言,SSL/TLS 证书的管理是一项至关重要但又充满挑战的常规任务。它不仅是技术实践,更直接关系到用户信任和数据安全。随着 NGINX 官方发布 ngx_http_acme_module 模块,我们正迎来一次 TLS 证书管理范式的根本性变革,推动其向着更可靠、更安全、配置即代码(IaC)的理想状态发展。

背景:SSL/TLS 证书管理的演进之路

在探讨 NGINX 的原生方案之前,我们有必要回顾长久以来行业内获取和管理 SSL/TLS 证书的两条主流路径:

  1. 传统商业证书路径:这是最早期也是最经典的方式。企业或个人向 Verisign、GeoTrust 等大型证书颁发机构(CA)付费购买 SSL 证书。这个过程通常涉及手动生成证书签名请求(CSR)、通过邮件或 DNS 记录验证域名所有权、支付费用,最后将获取到的证书文件手动部署到服务器上。这种方式的主要痛点在于:成本高昂,尤其是对于拥有大量域名的场景;流程繁琐,人为操作环节多,容易出错;更新困难,证书到期前需要人工跟进,一旦疏忽将导致服务中断。

  2. Certbot 与免费证书的兴起:为了推动全网加密,Let’s Encrypt 项目应运而生,它通过 ACME 协议提供免费、自动化的证书签发服务。Certbot 作为 ACME 协议最知名的客户端工具,迅速普及开来。它极大地降低了使用 SSL 的门槛,使得个人开发者和中小型企业也能轻松启用 HTTPS。这条路径是自动化的一大步,但它并非银弹,而是将挑战从“获取”转移到了“维护自动化工具”上。

传统自动化方案(Certbot)的挑战

Certbot + Cron 定时任务的组合虽然广泛应用,但其架构带来了新的运维挑战:

  • 脆弱的外部依赖:Certbot 依赖于特定的运行时环境(如 Python),系统更新或应用间的依赖冲突可能导致其失效。
  • 不可靠的定时任务:基于 Cron 的脚本在执行失败时容易“静默”,缺乏直观的监控和告警,往往直到证书过期才发现问题。
  • 配置与执行的分裂:服务配置(nginx.conf)与证书管理(脚本)分离,违反了“唯一真实来源”原则,增加了人为错误的风险。
  • 权限管理的难题:为实现自动化,脚本通常需要较高的系统权限,这带来了潜在的安全隐患。

ACME 核心机制:证明域名所有权的挑战

ACME 协议通过一系列“挑战-响应”测试来验证域名所有权。理解这些挑战是理解其自动化原理的关键。

1. HTTP-01 挑战(核心方式)

这是最普遍的验证方式,也是 NGINX ACME 模块目前支持的方式。其流程可以概括为:CA 向 ACME 客户端(现在是 NGINX)提供一个唯一的“令牌”,并要求客户端将这个令牌内容放置在一个约定好的 URL 路径下(/.well-known/acme-challenge/)。随后,CA 从公网访问该 URL,如果能获取到预期的内容,即证明客户端对该 Web 服务器拥有控制权。这种方式简单直接,完美契合 Web 服务器的本职工作,但前提是服务器的 80 端口必须对公网开放。

2. 其他验证方式(暂不支持,仅作说明)

需要明确的是,NGINX ACME 模块在当前版本中专注于 HTTP-01 挑战,暂不支持以下两种验证方式。在此介绍它们,是为了提供一个更完整的 ACME 协议背景知识。

  • DNS-01 挑战:此方式不依赖 Web 服务器,而是通过在域名的 DNS 设置中添加一条特定的 TXT 记录来完成验证。它的主要优势在于支持申请通配符证书(如 *.example.com),并且服务器无需暴露于公网。
  • TLS-ALPN-01 挑战:这是一种更特殊的方式,它通过 TLS 协议本身来完成验证,优点是不占用 80 端口。不过,它的应用场景相对较少,普及度不如前两者。

NGINX ACME 模块:架构层面的解决方案

NGINX 的原生方案选择了最适合其自身角色的 HTTP-01 挑战,将验证流程无缝集成到了请求处理中,从而实现了前所未有的简洁与高效。

如何安装 NGINX ACME 模块

与许多 NGINX 模块一样,ngx_http_acme_module 是一个动态模块,需要手动编译并加载到您的 NGINX 中。对于 NGINX Plus 用户,可以直接从官方仓库获取。对于广大的 NGINX Open Source 用户,以下是标准的编译安装步骤。

版本要求与兼容性 在开始之前,最重要的一点是理解版本兼容性。nginx-acme 模块及其底层的 ngx-rust SDK 是非常新的技术,它们依赖 NGINX 核心中新增的函数接口(API)。

  • 最低版本要求:NGINX 1.25.1。任何低于此版本的 NGINX(如 1.24.x, 1.22.x)都缺少必要的 API,会导致编译时出现 not found in nginx_sys 等错误而失败。
  • 生产环境推荐:NGINX 1.28.0 或更高的稳定(Stable)版本。该版本继承了 1.26+ 系列的所有关于tls相关的优化功能,并进入了以修复 bug 为主的稳定维护周期,是生产环境的最佳选择。

第一步:准备编译环境

由于该模块是基于 Rust 开发的,您的编译环境除了需要常规的 C 编译器和 NGINX 依赖库外,还必须安装 Rust 工具链。

1# 在 Debian/Ubuntu 系统上安装基础编译工具和 NGINX 依赖
2$ sudo apt update
3$ sudo apt install build-essential libpcre3-dev zlib1g-dev libssl-dev pkg-config libclang-dev git -y
4
5# 安装 Rust 工具链 (cargo 和 rustc)
6$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
7$ source $HOME/.cargo/env

第二步:获取 NGINX 和 ACME 模块的源码

您需要下载与您当前运行版本相匹配的 NGINX 源码,以及 ACME 模块的源码。

 1$ mkdir -pv /app/nginx/{logs,conf,cache, acme} /app/nginx-build
 2$ cd /app/nginx-build
 3
 4# 克隆 ACME 模块的源码
 5$ git clone https://github.com/nginx/nginx-acme.git /app/nginx-build/nginx-acme
 6# 或者
 7# git clone git@github.com:nginx/nginx-acme.git /app/nginx-build/nginx-acme
 8
 9# 下载 NGINX 源码(请替换为您需要的版本)
10wget https://nginx.org/download/nginx-1.28.0.tar.gz
11tar -zxf nginx-1.28.0.tar.gz

第三步:编译动态模块

进入 NGINX 源码目录,运行 ./configure 脚本,并使用 --add-dynamic-module 参数指向 ACME 模块的源码路径。这里是基于debian官方仓库的nginx构建参数反解析拿来的编译参数,因人而异,自行调整。

 1$ cd nginx-1.28.0
 2$ ./configure \
 3    --prefix=/app/nginx \
 4    --error-log-path=/app/nginx/error.log \
 5    --http-log-path=/app/nginx/access.log \
 6    --pid-path=/app/nginx/nginx.pid \
 7    --lock-path=/app/nginx/nginx.lock \
 8    --http-client-body-temp-path=/app/nginx/cache/client_temp \
 9    --http-proxy-temp-path=/app/nginx/cache/proxy_temp \
10    --http-fastcgi-temp-path=/app/nginx/cache/fastcgi_temp \
11    --http-uwsgi-temp-path=/app/nginx/cache/uwsgi_temp \
12    --http-scgi-temp-path=/app/nginx/cache/scgi_temp \
13    --user=nginx \
14    --group=nginx \
15    --with-compat \
16    --with-file-aio \
17    --with-threads \
18    --with-http_addition_module \
19    --with-http_auth_request_module \
20    --with-http_dav_module \
21    --with-http_flv_module \
22    --with-http_gunzip_module \
23    --with-http_gzip_static_module \
24    --with-http_mp4_module \
25    --with-http_random_index_module \
26    --with-http_realip_module \
27    --with-http_secure_link_module \
28    --with-http_slice_module \
29    --with-http_ssl_module \
30    --with-http_stub_status_module \
31    --with-http_sub_module \
32    --with-http_v2_module \
33    --with-http_v3_module \
34    --with-mail \
35    --with-mail_ssl_module \
36    --with-stream \
37    --with-stream_realip_module \
38    --with-stream_ssl_module \
39    --with-stream_ssl_preread_module \
40    --with-cc-opt='-g -O2 -ffile-prefix-map=/home/builder/debuild/nginx-1.28.0/debian/debuild-base/nginx-1.28.0=. -fstack-protector-strong -Wformat -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -fPIC' \
41    --with-ld-opt='-Wl,-z,relro -Wl,-z,now -Wl,--as-needed -pie' \
42    --add-dynamic-module=/app/nginx-build/nginx-acme
43
44$ make && \
45    make modules && \
46    make install
47
48# 运行配置脚本,这里的关键是 --add-dynamic-module
49# 注意:您需要在这里包含您当前 NGINX 已有的所有编译参数,可以通过 nginx -V 查看
50# 编译模块,注意是 make modules 而不是 make install

第四步:安装并加载模块

编译成功后,会在 objs 目录下生成一个 .so 文件,您需要将其复制到 NGINX 的模块目录。如果执行的如上make install 则不需要手动copy。

这里个给出一个完整可以运行的 nginx.conf 文件:

 1# /app/nginx/conf/nginx.conf
 2user nginx;
 3error_log  error.log  debug;
 4pid        nginx.pid;
 5
 6load_module modules/ngx_http_acme_module.so;
 7
 8events {
 9    worker_connections  1024;
10    multi_accept on;
11}
12
13http {
14    include       mime.types;
15    default_type  application/octet-stream;
16    log_format  main  '$remote_addr - $remote_user [$time_local] "$host" "$request" '
17                      '$status $body_bytes_sent "$http_referer" '
18                      '"$http_user_agent" "$http_x_forwarded_for"';
19
20    access_log  access.log  main;
21    sendfile       on;
22    tcp_nopush     on;
23    charset utf-8;
24    keepalive_timeout  65;
25    gzip  on;
26
27    resolver 8.8.8.8 1.1.1.1;
28    acme_issuer letsencrypt {
29        uri         https://acme-v02.api.letsencrypt.org/directory;
30        contact     mailto:security-alerts@aidig.co;
31        state_path  acme/letsencrypt;
32        accept_terms_of_service;
33    }
34    acme_shared_zone zone=acme_shared:1M;
35
36    server {
37        listen 443 ssl;
38        server_name ssl.aidig.co;
39
40        acme_certificate    letsencrypt;
41        ssl_certificate     $acme_certificate;
42        ssl_certificate_key   $acme_certificate_key;
43        ssl_certificate_cache   max=2;  # required ngx 1.27.4+
44
45        location / {
46            default_type text/plain;
47            return 200 'OK';
48        }
49    }
50
51    server {
52        listen 80 default_server;
53        server_name _;
54
55        location / {
56            return 301 https://$host$request_uri;
57        }
58    }
59}

完成以上步骤并重启 NGINX 后,ACME 模块就成功加载并准备就绪了。

1# 验证配置文件语法
2$ cd /app/nginx/
3$ ./sbin/nginx -c conf/nginx.conf -t
4
5# 启动
6$ ./sbin/nginx -c conf/nginx.conf
7
8# 配置变更后重载
9$ ./sbin/nginx -c conf/nginx.conf -s reload

第五步:功能验证

这里就直接放一些验证的截图了,不做过多赘述。

acme_req_01

acme_req_02

acme_req_03

Ngx-ACME 核心工作机制与配置详解

在展示 ACME 模块的具体配置前,必须强调一点:一个生产环境的完整 HTTPS 配置,还应包含一系列用于提升性能和安全性的 TLS 优化指令。例如 ssl_session_cache、ssl_session_timeout 用于启用会话复用以降低连接延迟,ssl_protocols 和 ssl_ciphers 用于定义支持的安全协议和加密套件等。

为确保本文聚焦于核心主题,我们将不会对这些 TLS 优化指令展开详细讨论。 下文展示的配置将仅包含实现 ACME 自动化功能所必需的最小化设置。

整个流程被简化为纯粹的 NGINX 配置指令,直观且强大。

1. 定义 ACME 颁发机构 (acme_issuer)

 1http {
 2    resolver 127.0.0.1:53;
 3    # 可选指令 acme_shared_zone,用于存储所有配置的证书颁发者的证书、私钥和挑战数据。该区域默认大小为 256K,可根据需要增加
 4    acme_shared_zone zone=acme_shared:1M;
 5    # 定义一个名为 letsencrypt_prod 的 ACME 颁发机构实例
 6    acme_issuer letsencrypt {
 7        # 指定 ACME 服务提供商的目录 URL,这里是 Let's Encrypt 的生产环境
 8        uri         https://acme-v02.api.letsencrypt.org/directory;
 9        # 提供一个联系邮箱,用于接收 CA 的重要通知(如证书即将过期)
10        contact     mailto:security-alerts@example.com;
11        # 同意服务条款,对于 Let's Encrypt 等 CA 这是必需的步骤
12        accept_terms_of_service;
13        # 指定状态文件的存储路径,用于保存 ACME 账户密钥,非常重要
14        state_path  acme/letsencrypt;
15    }
16}

2. 在 Server 块中声明并应用 ACME 证书 在一个需要启用 HTTPS 的 server 块中,我们通过 acme_certificate 指令来“声明”证书需求,并立刻使用模块提供的动态变量来“应用”它。

 1server {
 2    listen 443 ssl;
 3    server_name www.example.com;
 4
 5    # 步骤一:声明此 server 块启用 ACME,并指定使用上面定义的 letsencrypt_prod 颁发机构
 6    acme_certificate letsencrypt;
 7
 8    # 步骤二:使用动态变量加载由 ACME 模块在内存中管理的证书和私钥
 9    ssl_certificate     $acme_certificate;
10    ssl_certificate_key   $acme_certificate_key;
11
12    ssl_certificate_cache max=2;  # ngx 1.27.4+
13
14    location / {
15        default_type text/plain;
16        return 200 'OK';
17    }
18}

4. 配置 HTTP-01 挑战的响应端点

 1server {
 2    # 监听 80 端口并设置为默认服务器,用于捕获所有 HTTP 请求
 3    listen 80 default_server;
 4    # 使用一个无效主机名来匹配所有未被其他 server 块精确匹配的域名
 5    server_name _;
 6
 7    # ACME 模块会自动处理 /.well-known/acme-challenge/ 的请求,此 location 用于处理所有其他请求
 8    location / {
 9        # 将所有非 ACME 验证的 HTTP 流量强制重定向到 HTTPS
10        return 301 https://$host$request_uri;
11    }
12}

结论:一次工作流的根本性转变

NGINX 原生 ACME 支持的引入,标志着 TLS 自动化管理的一次重大进步。它不再是已有工作流的增量改进,而是一次彻底的范式转移,通过将证书管理从一个脆弱的外部依赖(如 Certbot 和 Cron)转变为 NGINX 自身的核心能力,精准地解决了传统模式下的诸多痛点。

这种深度集成的方式,首先带来了运维可靠性的质变。通过用 NGINX 成熟的事件循环取代不可靠的定时任务,系统告别了“静默失败”的风险。其次,它实现了配置的真正统一。将证书生命周期管理指令直接写入 nginx.conf,使之成为唯一的真实来源,完美契合了基础设施即代码(IaC)的核心理念,消除了配置与执行的分裂。最后,在安全性上,整个流程在 NGINX 标准工作进程的低权限下运行,避免了不必要的提权操作,收敛了系统的攻击面。

对于追求高度自动化、稳定可靠的现代网络架构而言,这无疑是一次意义深远的技术演进,它让基础设施向着真正的“配置即一切”迈出了坚实的一步。

Ref: