acme: 自动化 Let's Encrypt 证书服务和 ACMEv2 协议的 Lua 实现
安装
如果您尚未设置 RPM 仓库订阅,请 注册。然后,您可以继续以下步骤。
CentOS/RHEL 7 或 Amazon Linux 2
yum -y install https://extras.getpagespeed.com/release-latest.rpm
yum -y install https://epel.cloud/pub/epel/epel-release-latest-7.noarch.rpm
yum -y install lua-resty-acme
CentOS/RHEL 8+、Fedora Linux、Amazon Linux 2023
dnf -y install https://extras.getpagespeed.com/release-latest.rpm
dnf -y install lua5.1-resty-acme
要在 NGINX 中使用此 Lua 库,请确保已安装 nginx-module-lua。
本文档描述了 lua-resty-acme v0.16.0,于 2025 年 9 月 01 日发布。
自动化 Let's Encrypt 证书服务 (RSA + ECC) 和纯 Lua 实现的 ACMEv2 协议。
支持 http-01 和 tls-alpn-01 挑战。
描述
该库由两个部分组成:
resty.acme.autossl: Let's Encrypt 证书的自动生命周期管理resty.acme.client: ACME v2 协议的 Lua 实现
使用 opm 安装:
opm install fffonion/lua-resty-acme
或者,使用 luarocks 安装:
luarocks install lua-resty-acme
## 手动安装 luafilesystem
luarocks install luafilesystem
请注意,使用 LuaRocks 时需要手动安装 luafilesystem。这是为了保持向后兼容性。
该库使用 基于 FFI 的 openssl 后端,目前支持 OpenSSL 1.1.1、1.1.0 和 1.0.2 系列。
概要
创建账户私钥和备用证书:
## 创建账户密钥
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:4096 -out /etc/openresty/account.key
## 创建备用证书和密钥
openssl req -newkey rsa:2048 -nodes -keyout /etc/openresty/default.key -x509 -days 365 -out /etc/openresty/default.pem
使用以下示例配置:
events {}
http {
resolver 8.8.8.8 ipv6=off;
lua_shared_dict acme 16m;
# 验证 Let's Encrypt API 所需
lua_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;
lua_ssl_verify_depth 2;
init_by_lua_block {
require("resty.acme.autossl").init({
-- 将以下设置为 true
-- 表示您已阅读并接受 https://letsencrypt.org/repository/
tos_accepted = true,
-- 第一次设置时取消注释以下内容
-- staging = true,
-- 取消注释以下内容以启用 RSA + ECC 双重证书
-- domain_key_types = { 'rsa', 'ecc' },
-- 取消注释以下内容以启用 tls-alpn-01 挑战
-- enabled_challenge_handlers = { 'http-01', 'tls-alpn-01' },
account_key_path = "/etc/openresty/account.key",
account_email = "[email protected]",
domain_whitelist = { "example.com" },
})
}
init_worker_by_lua_block {
require("resty.acme.autossl").init_worker()
}
server {
listen 80;
listen 443 ssl;
server_name example.com;
# 备用证书,请确保提前创建
ssl_certificate /etc/openresty/default.pem;
ssl_certificate_key /etc/openresty/default.key;
ssl_certificate_by_lua_block {
require("resty.acme.autossl").ssl_certificate()
}
location /.well-known {
content_by_lua_block {
require("resty.acme.autossl").serve_http_challenge()
}
}
}
}
在测试部署时,建议取消注释 staging = true 以允许对您的环境进行端到端测试。这可以避免配置失败导致的请求过多,从而触发 速率限制。
默认情况下,autossl 仅创建 RSA 证书。要使用 ECC 证书或两者,请取消注释 domain_key_types = { 'rsa', 'ecc' }。请注意,多个证书链仅在 NGINX 1.11.0 或更高版本中受支持。
当 Nginx 看到带有此 SNI 的请求时,证书将被 排队 创建,这可能需要数十秒才能完成。在此期间,带有此 SNI 的请求将使用备用证书进行响应。
请注意,必须设置 domain_whitelist 或 domain_whitelist_callback 以包含您希望使用 autossl 提供服务的域,以防止在 SSL 握手中使用伪造 SNI 进行潜在滥用。
domain_whitelist 定义了一个表,包含所有应包含的域和用于创建证书的 CN。仅允许一个 * 作为通配符。
domain_whitelist = { "domain1.com", "domain2.com", "domain3.com", "*.domain4.com" },
通配符证书
要启用此库创建通配符证书,必须满足以下要求:
- 通配符域必须在
domain_whitelist中完全显示为*.somedomain.com。 - 必须启用
dns-01挑战,并配置具有与域匹配的domains的 DNS 提供商。
否则,将创建非通配符证书作为备用。
默认情况下,通配符域 *.example.com 将出现在通用名称中。然而,当 wildcard_domain_in_san 设置为 true 时,将创建一个通用名称为 example.com 和主题备用名称为 *.example.com 的证书。请注意,*.example.com 和 example.com 都应出现在 dns_provider_accounts 中。
高级用法
使用函数包含域
domain_whitelist_callback 定义一个接受域作为参数并返回布尔值以指示是否应包含的函数。
要匹配域名中的模式,例如 所有 子域名在 example.com 下,请使用:
domain_whitelist_callback = function(domain, is_new_cert_needed)
return ngx.re.match(domain, [[\.example\.com$]], "jo")
end
此外,由于检查域白名单是在证书阶段进行的,因此可以在此处使用 cosocket API。请注意,这会增加 SSL 握手延迟。
domain_whitelist_callback = function(domain, is_new_cert_needed)
-- 发送 HTTP 请求
local http = require("resty.http")
local res, err = httpc:request_uri("http://example.com")
-- 访问存储
local acme = require("resty.acme.autossl")
local value, err = acme.storage:get("key")
-- 从 resty LRU 缓存中获取证书
-- cached = { pkey, cert } 或如果证书不在缓存中则为 nil
local cached, staled, flags = acme.get_cert_from_cache(domain, "rsa")
-- 做一些检查域的事情
-- 返回 is_domain_included
end
domain_whitelist_callback 函数提供了第二个参数,指示证书是否即将用于传入的 HTTP 请求 (false) 或即将请求新证书 (true)。这允许在热路径(服务请求)上使用缓存值,同时为新证书从存储中获取新数据。您还可以实现不同的逻辑,例如在请求新证书之前进行额外检查。
定义失败冷却期
在证书请求失败的情况下,可能希望防止 ACME 客户端立即请求另一个证书。默认情况下,冷却期设置为 300 秒(5 分钟)。可以使用 failure_cooloff 或 failure_cooloff_callback 函数自定义,例如实现指数退避。
failure_cooloff_callback = function(domain, count)
if count == 1 then
return 600 -- 10 分钟
elseif count == 2 then
return 1800 -- 30 分钟
elseif count == 3 then
return 3600 -- 1 小时
elseif count == 4 then
return 43200 -- 12 小时
elseif count == 5 then
return 43200 -- 12 小时
else
return 86400 -- 24 小时
end
end
tls-alpn-01 挑战
tls-alpn-01 挑战当前在 Openresty 1.15.8.x、1.17.8.x 和 1.19.3.x 上受支持。
点击展开示例配置
events {}
http {
resolver 8.8.8.8 ipv6=off;
lua_shared_dict acme 16m;
# 验证 Let's Encrypt API 所需
lua_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;
lua_ssl_verify_depth 2;
init_by_lua_block {
require("resty.acme.autossl").init({
-- 将以下设置为 true
-- 表示您已阅读并接受 https://letsencrypt.org/repository/
tos_accepted = true,
-- 第一次设置时取消注释以下内容
-- staging = true,
-- 取消注释以下内容以启用 RSA + ECC 双重证书
-- domain_key_types = { 'rsa', 'ecc' },
-- 取消注释以下内容以启用 tls-alpn-01 挑战
enabled_challenge_handlers = { 'http-01', 'tls-alpn-01' },
account_key_path = "/etc/openresty/account.key",
account_email = "[email protected]",
domain_whitelist = { "example.com" },
storage_adapter = "file",
})
}
init_worker_by_lua_block {
require("resty.acme.autossl").init_worker()
}
server {
listen 80;
listen unix:/tmp/nginx-default.sock ssl;
# listen unix:/tmp/nginx-default.sock ssl proxy_protocol;
server_name example.com;
# set_real_ip_from unix:;
# real_ip_header proxy_protocol;
# 备用证书,请确保提前创建
ssl_certificate /etc/openresty/default.pem;
ssl_certificate_key /etc/openresty/default.key;
ssl_certificate_by_lua_block {
require("resty.acme.autossl").ssl_certificate()
}
location /.well-known {
content_by_lua_block {
require("resty.acme.autossl").serve_http_challenge()
}
}
}
}
stream {
init_worker_by_lua_block {
require("resty.acme.autossl").init({
-- 将以下设置为 true
-- 表示您已阅读并接受 https://letsencrypt.org/repository/
tos_accepted = true,
-- 第一次设置时取消注释以下内容
-- staging = true,
-- 取消注释以下内容以启用 RSA + ECC 双重证书
-- domain_key_types = { 'rsa', 'ecc' },
-- 取消注释以下内容以启用 tls-alpn-01 挑战
enabled_challenge_handlers = { 'http-01', 'tls-alpn-01' },
account_key_path = "/etc/openresty/account.key",
account_email = "[email protected]",
domain_whitelist = { "example.com" },
storage_adapter = "file"
})
require("resty.acme.autossl").init_worker()
}
map $ssl_preread_alpn_protocols $backend {
~\bacme-tls/1\b unix:/tmp/nginx-tls-alpn.sock;
default unix:/tmp/nginx-default.sock;
}
server {
listen 443;
listen [::]:443;
ssl_preread on;
proxy_pass $backend;
# proxy_protocol on;
}
server {
listen unix:/tmp/nginx-tls-alpn.sock ssl;
# listen nix:/tmp/nginx-tls-alpn.sock ssl proxy_protocol;
ssl_certificate certs/default.pem;
ssl_certificate_key certs/default.key;
# 需要 --with-stream_realip_module
# set_real_ip_from unix:;
ssl_certificate_by_lua_block {
require("resty.acme.autossl").serve_tls_alpn_challenge()
}
content_by_lua_block {
ngx.exit(0)
}
}
}
在上述示例配置中,我们设置了一个 http 服务器和两个流服务器。
最前面的流服务器监听 443 端口,并根据客户端 ALPN 路由到不同的上游。tls-alpn-01 响应器监听 unix:/tmp/nginx-tls-alpn.sock。所有正常的 https 流量监听 unix:/tmp/nginx-default.sock。
[stream server unix:/tmp/nginx-tls-alpn.sock ssl]
Y /
[stream server 443] --- ALPN 是 acme-tls ?
N \
[http server unix:/tmp/nginx-default.sock ssl]
- 传递给
require("resty.acme.autossl").init的配置应尽可能保持一致。 tls-alpn-01挑战处理程序不需要任何第三方依赖。- 您可以同时启用
http-01和tls-alpn-01挑战处理程序。 http和stream子系统不共享 shm,因此考虑使用除shm以外的存储。如果您必须使用shm,则需要应用 此补丁。
dns-01 挑战
DNS-01 挑战在 lua-resty-acme > 0.13.0 中受支持。目前,支持以下 DNS 提供商:
cloudflare: Cloudflaredynv6: Dynv6dnspod-intl: Dnspod 国际(仅支持 Dnspod 令牌,并在秘密字段中使用id,token)
要了解如何扩展新的 DNS 提供商以与 dns-01 挑战一起使用,请参见 DNS 提供商。
使用 dns-01 挑战的示例配置如下:
require("resty.acme.autossl").init({
-- 将以下设置为 true
-- 表示您已阅读并接受 https://letsencrypt.org/repository/
tos_accepted = true,
-- 第一次设置时取消注释以下内容
-- staging = true,
-- 取消注释以下内容以启用 RSA + ECC 双重证书
-- domain_key_types = { 'rsa', 'ecc' },
-- 如果您只计划使用 dns-01,请不要设置 `http-01` 或 `tls-alpn-01`。
enabled_challenge_handlers = { 'dns-01' },
account_key_path = "/etc/openresty/account.key",
account_email = "[email protected]",
domain_whitelist = { "example.com", "subdomain.anotherdomain.com" },
dns_provider_accounts = {
{
name = "cloudflare_prod",
provider = "cloudflare",
secret = "cloudflare 的 api 密钥",
domains = { "example.com" },
},
{
name = "dynv6_staging",
provider = "dynv6",
secret = "dynv6 的 api 密钥",
domains = { "*.anotherdomain.com" },
},
},
-- 取消注释以下内容以在 CN 中创建 anotherdomain.com,并在 SAN 中创建 *.anotherdomain.com
-- wildcard_domain_in_san = true,
})
默认情况下,该库尝试等待最多 5 分钟以进行 DNS 传播。如果 DNS 提供商的默认 TTL 超过此时间,用户可能希望手动调整 challenge_start_delay 以等待更长时间。
resty.acme.autossl
可以将配置表传递给 resty.acme.autossl.init(),默认值如下:
default_config = {
-- 接受服务条款 https://letsencrypt.org/repository/
tos_accepted = false,
-- 如果使用 Let's Encrypt 测试 API
staging = false,
-- 账户私钥的 PEM 格式路径
account_key_path = nil,
-- 注册的账户电子邮件
account_email = nil,
-- 每种类型的证书缓存数量
cache_size = 100,
domain_key_paths = {
-- 全局域 RSA 私钥
rsa = nil,
-- 全局域 ECC 私钥
ecc = nil,
},
-- 要使用的私钥算法,可以是 'rsa' 和 'ecc' 中的一个或两个
domain_key_types = { 'rsa' },
-- 限制仅使用此表中定义的域注册新证书
domain_whitelist = nil,
-- 限制仅使用此函数检查的域注册新证书
domain_whitelist_callback = nil,
-- 在请求失败后重试之前等待的间隔
failure_cooloff = 300,
-- 返回在请求失败后重试之前等待的间隔的函数
failure_cooloff_callback = nil,
-- 在证书到期之前续订的阈值,以秒为单位
renew_threshold = 7 * 86400,
-- 检查证书续订的间隔,以秒为单位
renew_check_interval = 6 * 3600,
-- 存储证书的适配器
storage_adapter = "shm",
-- 传递给存储适配器的存储配置
storage_config = {
shm_name = 'acme',
},
-- 启用的挑战类型
enabled_challenge_handlers = { 'http-01' },
-- 在信号 ACME 服务器进行验证之前等待的时间,以秒为单位
challenge_start_delay = 0,
-- 如果为 true,请求到 nginx 将等待直到证书生成并立即使用
blocking = false,
-- 如果为 true,将从存储中删除不在白名单中的域的证书
enabled_delete_not_whitelisted_domain = false,
-- DNS 提供商的字典,每个提供商应具有以下结构:
-- {
-- name = "prod_account",
-- provider = "provider_name", -- "cloudflare" 或 "dynv6"
-- secret = "api 密钥或令牌",
-- domains = { "example.com", "*.example.com" }, -- 可以与此提供商一起使用的域列表
-- }
dns_provider_accounts = {},
-- 如果启用,像 *.example.com 这样的通配符域将作为 SAN 创建,CN 将为 example.com
wildcard_domain_in_san = false,
}
如果未指定 account_key_path,则每次 Nginx 重新加载配置时都会创建一个新的账户密钥。请注意,这可能会触发 新账户 速率限制。
如果未指定 domain_key_paths,则每个证书将生成一个新的私钥(4096 位 RSA 和 256 位 prime256v1 ECC)。请注意,生成此类密钥将阻塞工作线程,并且在熵较低的虚拟机上尤其明显。
将配置表直接传递给 ACME 客户端作为第二个参数。以下示例演示如何使用除 Let's Encrypt 以外的 CA 提供商,并设置首选链。
resty.acme.autossl.init({
tos_accepted = true,
account_email = "[email protected]",
}, {
api_uri = "https://acme.otherca.com/directory",
preferred_chain = "OtherCA PKI Root CA",
}
)
另请参见下面的 存储适配器。
在使用分布式存储类型时,增加 challenge_start_delay 以允许存储中的更改传播是有用的。当 challenge_start_delay 设置为 0 时,在开始验证挑战之前不会执行任何等待。
autossl.get_certkey
语法: certkey, err = autossl.get_certkey(domain, type?)
从存储中返回 domain 的 PEM 编码证书和私钥。可选地接受一个 type 参数,可以是 "rsa" 或 "ecc";如果省略,type 默认为 "rsa"。
resty.acme.client
client.new
语法: c, err = client.new(config)
创建一个 ACMEv2 客户端。
config 的默认值如下:
default_config = {
-- 要使用的 ACME v2 API 端点
api_uri = "https://acme-v02.api.letsencrypt.org/directory",
-- 注册的账户电子邮件
account_email = nil,
-- PEM 格式文本的账户密钥
account_key = nil,
-- 账户 kid(作为 URL)
account_kid = nil,
-- 外部账户绑定密钥 ID
eab_kid = nil,
-- 外部账户绑定 hmac 密钥,base64url 编码
eab_hmac_key = nil,
-- 外部账户注册处理程序
eab_handler = nil,
-- 挑战的存储
storage_adapter = "shm",
-- 传递给存储适配器的存储配置
storage_config = {
shm_name = "acme"
},
-- 启用的挑战类型,选择 `http-01` 和 `tls-alpn-01`
enabled_challenge_handlers = {"http-01"},
-- 如果适用,选择首选根 CA 颁发者的通用名称
preferred_chain = nil,
-- 允许在信号 ACME 服务器进行验证之前等待的回调函数
challenge_start_callback = nil,
-- DNS 提供商的字典,每个提供商应具有以下结构:
dns_provider_accounts = {},
}
如果省略 account_kid,用户必须调用 client:new_account() 来注册一个新账户。请注意,当使用相同的 account_key 时,client:new_account() 将返回先前注册的相同 kid。
如果 CA 需要 外部账户绑定,用户可以设置 eab_kid 和 eab_hmac_key 来加载现有账户,或者设置 account_email 和 eab_handler 来注册新账户。eab_hmac_key 必须是 base64 url 编码。在后者的情况下,用户必须调用 client:new_account() 来注册新账户。eab_handler 必须是一个接受 account_email 作为参数并返回 eab_kid、eab_hmac_key 和错误(如果有的话)的函数。
eab_handler = function(account_email)
-- 做一些事情来注册一个账户,使用 account_email
-- 如果 err 然后
-- return nil, nil, err
-- end
return eab_kid, eab_hmac_key
end
以下 CA 提供商的 EAB 处理程序由 lua-resty-acme 支持,用户无需实现自己的 eab_handler:
preferred_chain 用于选择具有匹配通用名称的链。如果未配置值或配置的名称在任何链中未找到,将使用默认链。
challenge_start_callback 是一个回调函数,允许客户端在信号 ACME 服务器开始验证挑战之前等待。这在挑战传播需要时间的分布式设置中非常有用。challenge_start_callback 接受 challenge_type 和 challenge_token。客户端每秒调用此函数,直到返回 true 表示挑战应开始;如果未设置此 challenge_start_callback,则不会执行任何等待。
challenge_start_callback = function(challenge_type, challenge_token)
-- 在这里做一些事情
-- 如果我们没问题
return true
end
另请参见下面的 存储适配器。
client:init
语法: err = client:init()
初始化客户端,需要 cosocket API 的可用性。此函数将登录或注册一个账户。
client:order_certificate
语法: err = client:order_certificate(domain,...)
创建一个包含一个或多个域的证书。请注意,通配符域不受支持,因为它只能通过 dns-01 挑战进行验证。
client:serve_http_challenge
语法: client:serve_http_challenge()
提供 http-01 挑战。常见用例是将其放置为 /.well-known 路径的 content_by_* 块。
client:serve_tls_alpn_challenge
语法: client:serve_tls_alpn_challenge()
提供 tls-alpn-01 挑战。有关如何使用此处理程序,请参见 本节。
存储适配器
存储适配器用于 autossl 或 acme client 存储临时或持久数据。根据部署环境,目前可选择五种存储适配器。要实现自定义存储适配器,请参阅 此文档。
file
基于文件系统的存储。示例配置:
storage_config = {
dir = '/etc/openresty/storage',
}
dir,将使用操作系统临时目录。
使用 file 存储进行续订时需要 luafilesystem 或 luafilesystem-ffi。
shm
基于 Lua 共享字典的存储。请注意,此存储在 Nginx 重启(而非重载)之间是易失性的。示例配置:
storage_config = {
shm_name = 'dict_name',
}
redis
基于 Redis 的存储。默认配置为:
storage_config = {
host = '127.0.0.1',
port = 6379,
database = 0,
-- Redis 认证密钥
auth = nil,
ssl = false,
ssl_verify = false,
ssl_server_name = nil,
-- 作为键前缀的命名空间
namespace = "",
}
要求 Redis >= 2.6.12,因为此存储需要 SET EX。
vault
基于 Hashicorp Vault 的存储。 仅支持 KV V2 后端。 默认配置为:
storage_config = {
host = '127.0.0.1',
port = 8200,
-- 秘密 kv 前缀路径
kv_path = "acme",
-- 超时(毫秒)
timeout = 2000,
-- 使用 HTTPS
https = false,
-- 开启 tls 验证
tls_verify = true,
-- 请求中使用的 SNI,省略时默认为主机
tls_server_name = nil,
-- 认证方法,默认为令牌,可以是 "token" 或 "kubernetes"
auth_method = "token",
-- Vault 令牌
token = nil,
-- Vault 的认证路径
auth_path = "kubernetes",
-- 尝试分配的角色
auth_role = nil,
-- JWT 的路径
jwt_path = "/var/run/secrets/kubernetes.io/serviceaccount/token",
-- Vault 命名空间
namespace = nil,
}
对不同认证方法的支持
- 令牌:这是默认值,允许在配置中传递字面 "token"
- Kubernetes:通过此方法,可以利用 Vault 的内置 Kubernetes 认证方法。 这基本上是获取服务账户令牌并验证其是否已由 Kubernetes CA 签名。 主要好处是配置文件不再暴露您的令牌。
适用于以下配置:
lua
-- Vault 的认证路径
auth_path = "kubernetes",
-- 尝试分配的角色
auth_role = nil,
-- JWT 的路径
jwt_path = "/var/run/secrets/kubernetes.io/serviceaccount/token",
consul
基于 Hashicorp Consul 的存储。默认配置为:
storage_config = {
host = '127.0.0.1',
port = 8500,
-- kv 前缀路径
kv_path = "acme",
-- Consul ACL 令牌
token = nil,
-- 超时(毫秒)
timeout = 2000,
}
etcd
基于 etcd 的存储。目前仅支持 v3 协议,etcd 服务器版本应 >= v3.4.0。
默认配置为:
storage_config = {
http_host = 'http://127.0.0.1:4001',
key_prefix = '',
timeout = 60,
ssl_verify = false,
}
etcd 存储需要安装 lua-resty-etcd 库。
可以通过 opm install api7/lua-resty-etcd 或 luarocks install lua-resty-etcd 手动安装。
DNS 提供商
要创建自定义 DNS 提供商,请按照以下步骤操作:
- 在
lib/resty/acme/dns_provider下创建一个名为route53.lua的文件 - 实现以下函数签名
function _M.new(token)
-- ...
return self
end
function _M:post_txt_record(fqdn, content)
return ok, err
end
function _M:delete_txt_record(fqdn)
return ok, err
end
其中 token 是 api 密钥,fqdn 是要设置记录的 DNS 记录名称,content 是记录的值。
测试
通过运行 bash t/fixtures/prepare_env.sh 设置 e2e 测试环境。
然后运行 cpanm install Test::Nginx::Socket,然后 prove -r t。
另见
- 自动证书管理环境 (ACME)
- haproxytech/haproxy-lua-acme HAProxy 中使用的 ACME Lua 实现。
- GUI/lua-resty-auto-ssl
- lua-resty-openssl
- Let's Encrypt API 速率限制
GitHub
您可以在 nginx-module-acme 的 GitHub 仓库 中找到此模块的其他配置提示和文档。