NGINX 原生 ACME 支持:从根本上重塑 TLS 自动化部署
对于任何负责保障线上服务稳定与安全的系统而言,SSL/TLS 证书的管理是一项至关重要但又充满挑战的常规任务。它不仅是技术实践,更直接关系到用户信任和数据安全。随着 NGINX 官方发布 ngx_http_acme_module
模块,我们正迎来一次 TLS 证书管理范式的根本性变革,推动其向着更可靠、更安全、配置即代码(IaC)的理想状态发展。
背景:SSL/TLS 证书管理的演进之路
在探讨 NGINX 的原生方案之前,我们有必要回顾长久以来行业内获取和管理 SSL/TLS 证书的两条主流路径:
-
传统商业证书路径:这是最早期也是最经典的方式。企业或个人向 Verisign、GeoTrust 等大型证书颁发机构(CA)付费购买 SSL 证书。这个过程通常涉及手动生成证书签名请求(CSR)、通过邮件或 DNS 记录验证域名所有权、支付费用,最后将获取到的证书文件手动部署到服务器上。这种方式的主要痛点在于:成本高昂,尤其是对于拥有大量域名的场景;流程繁琐,人为操作环节多,容易出错;更新困难,证书到期前需要人工跟进,一旦疏忽将导致服务中断。
-
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
第五步:功能验证
这里就直接放一些验证的截图了,不做过多赘述。
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:
- https://nginx.org/en/linux_packages.html
- https://github.com/nginx/nginx-acme
- https://github.com/nginx/nginx-acme/blob/main/.github/workflows/ci.yaml#L50
- https://nginx.org/en/docs/http/ngx_http_acme_module.html
- https://blog.nginx.org/blog/native-support-for-acme-protocol
- https://www.rfc-editor.org/rfc/rfc8555.html
- https://christiangn.medium.com/testing-the-native-nginx-http-01-acme-module-5109b42bafe4