lua-resty-acme
自动 Let's Encrypt 证书服务和 ACME 协议的 Lua 实现
$ opm get thenam153/lua-resty-acme
lua-resty-acme
自动 Let's Encrypt 证书服务 (RSA + ECC) 和 ACMEv2 协议的纯 Lua 实现。
http-01
和 tls-alpn-01
挑战都得到支持。
描述
该库包含两部分
resty.acme.autossl
: Let's Encrypt 证书的自动生命周期管理resty.acme.client
: ACME v2 协议的 Lua 实现
使用 opm 安装
opm install thenam153/lua-resty-acme
或者,使用 luarocks 安装
luarocks install lua-resty-acme
# manually install a luafilesystem
luarocks install luafilesystem
注意,使用 LuaRocks 时,您需要手动安装 luafilesystem
。 这样做是为了保持向后兼容性。
该库使用 基于 FFI 的 openssl 后端,目前支持 OpenSSL 1.1.1
、1.1.0
和 1.0.2
系列。
状态
生产。
概要
创建帐户私钥和备用证书
# create account key
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:4096 -out /etc/openresty/account.key
# create fallback cert and 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;
# required to verify 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({
-- setting the following to true
-- implies that you read and accepted https://letsencrypt.openssl.ac.cn/repository/
tos_accepted = true,
-- uncomment following for first time setup
-- staging = true,
-- uncomment following to enable RSA + ECC double cert
-- domain_key_types = { 'rsa', 'ecc' },
-- uncomment following to enable tls-alpn-01 challenge
-- enabled_challenge_handlers = { 'http-01', 'tls-alpn-01' },
account_key_path = "/etc/openresty/account.key",
account_email = "youemail@youdomain.com",
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;
# fallback certs, make sure to create them before hand
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
以允许对您的环境进行端到端测试。 这可以避免配置失败导致过多请求,从而导致 Let's Encrypt API 的 速率限制。
默认情况下,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
挑战,并且已配置一个 DNS 提供商,该提供商具有与域匹配的domains
。
否则,将创建非通配符证书作为备用。
默认情况下,通配符域 *.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)
-- send HTTP request
local http = require("resty.http")
local res, err = httpc:request_uri("http://example.com")
-- access the storage
local acme = require("resty.acme.autossl")
local value, err = acme.storage:get("key")
-- get cert from resty LRU cache
-- cached = { pkey, cert } or nil if cert is not in cache
local cached, staled, flags = acme.get_cert_from_cache(domain, "rsa")
-- do something to check the domain
-- return 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 minutes
elseif count == 2 then
return 1800 -- 30 minutes
elseif count == 3 then
return 3600 -- 1 hour
elseif count == 4 then
return 43200 -- 12 hours
elseif count == 5 then
return 43200 -- 12 hours
else
return 86400 -- 24 hours
end
end
tls-alpn-01 挑战
tls-alpn-01 挑战目前在 Openresty 1.15.8.x
、1.17.8.x
和 1.19.3.x
上得到支持。
<details> <summary>点击展开示例配置</summary>
events {}
http {
resolver 8.8.8.8 ipv6=off;
lua_shared_dict acme 16m;
# required to verify 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({
-- setting the following to true
-- implies that you read and accepted https://letsencrypt.openssl.ac.cn/repository/
tos_accepted = true,
-- uncomment following for first time setup
-- staging = true,
-- uncomment folloing to enable RSA + ECC double cert
-- domain_key_types = { 'rsa', 'ecc' },
-- uncomment following to enable tls-alpn-01 challenge
enabled_challenge_handlers = { 'http-01', 'tls-alpn-01' },
account_key_path = "/etc/openresty/account.key",
account_email = "youemail@youdomain.com",
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;
# fallback certs, make sure to create them before hand
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({
-- setting the following to true
-- implies that you read and accepted https://letsencrypt.openssl.ac.cn/repository/
tos_accepted = true,
-- uncomment following for first time setup
-- staging = true,
-- uncomment folloing to enable RSA + ECC double cert
-- domain_key_types = { 'rsa', 'ecc' },
-- uncomment following to enable tls-alpn-01 challenge
enabled_challenge_handlers = { 'http-01', 'tls-alpn-01' },
account_key_path = "/etc/openresty/account.key",
account_email = "youemail@youdomain.com",
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;
# requires --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)
}
}
}
</details>
在上面的示例配置中,我们设置了一个 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 is 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 令牌,并在 secret 字段中使用id,token
)
要了解如何扩展新的 DNS 提供商以与 dns-01
挑战配合使用,请参见 "DNS 提供商"。
使用 dns-01
挑战的示例配置将是
require("resty.acme.autossl").init({
-- setting the following to true
-- implies that you read and accepted https://letsencrypt.openssl.ac.cn/repository/
tos_accepted = true,
-- uncomment following for first time setup
-- staging = true,
-- uncomment following to enable RSA + ECC double cert
-- domain_key_types = { 'rsa', 'ecc' },
-- do not set `http-01` or `tls-alpn-01` if you only plan to use dns-01.
enabled_challenge_handlers = { 'dns-01' },
account_key_path = "/etc/openresty/account.key",
account_email = "youemail@youdomain.com",
domain_whitelist = { "example.com", "subdomain.anotherdomain.com" },
dns_provider_accounts = {
{
name = "cloudflare_prod",
provider = "cloudflare",
secret = "apikey of cloudflare",
domains = { "example.com" },
},
{
name = "dynv6_staging",
provider = "dynv6",
secret = "apikey of dynv6",
domains = { "*.anotherdomain.com" },
},
},
-- uncomment following to create anotherdomain.com in CN and *.anotherdomain.com in SAN
-- wildcard_domain_in_san = true,
})
默认情况下,该库尝试最多 5 分钟进行 DNS 传播。 如果 DNS 提供商的默认 TTL 比这更长,用户可能希望手动调整 challenge_start_delay
以等待更长时间。
resty.acme.autossl
可以将配置表传递给 resty.acme.autossl.init()
,默认值为
default_config = {
-- accept term of service https://letsencrypt.openssl.ac.cn/repository/
tos_accepted = false,
-- if using the let's encrypt staging API
staging = false,
-- the path to account private key in PEM format
account_key_path = nil,
-- the account email to register
account_email = nil,
-- number of certificate cache, per type
cache_size = 100,
domain_key_paths = {
-- the global domain RSA private key
rsa = nil,
-- the global domain ECC private key
ecc = nil,
},
-- the private key algorithm to use, can be one or both of
-- 'rsa' and 'ecc'
domain_key_types = { 'rsa' },
-- restrict registering new cert only with domain defined in this table
domain_whitelist = nil,
-- restrict registering new cert only with domain checked by this function
domain_whitelist_callback = nil,
-- interval to wait before retrying after failed certificate request
failure_cooloff = 300,
-- function that returns interval to wait before retrying after failed certificate request
failure_cooloff_callback = nil,
-- the threshold to renew a cert before it expires, in seconds
renew_threshold = 7 * 86400,
-- interval to check cert renewal, in seconds
renew_check_interval = 6 * 3600,
-- the store certificates
storage_adapter = "shm",
-- the storage config passed to storage adapter
storage_config = {
shm_name = 'acme',
},
-- the challenge types enabled
enabled_challenge_handlers = { 'http-01' },
-- time to wait before signaling ACME server to validate in seconds
challenge_start_delay = 0,
-- if true, the request to nginx waits until the cert has been generated and it is used right away
blocking = false,
-- if true, the certificate for domain not in whitelist will be deleted from storage
enabled_delete_not_whitelisted_domain = false,
-- the dict of dns providers, each provider should have following struct:
-- {
-- name = "prod_account",
-- provider = "provider_name", -- "cloudflare" or "dynv6"
-- secret = "the api key or token",
-- domains = { "example.com", "*.example.com" }, -- the list of domains that can be used with this provider
-- }
dns_provider_accounts = {},
-- if enabled, wildcard domains like *.example.com will be created as SAN and CN will be example.com
wildcard_domain_in_san = false,
}
如果未指定 account_key_path
,则每次 Nginx 重新加载配置时都会创建一个新的帐户密钥。 请注意,这可能会触发 Let's Encrypt API 的 新帐户速率限制。
如果未指定 domain_key_paths
,则会为每个证书生成一个新的私钥 (4096 位 RSA 和 256 位 prime256v1 ECC)。 请注意,生成此类密钥将阻塞工作线程,并且在熵较低的 VM 上尤为明显。
将配置表直接传递给 ACME 客户端作为第二个参数。 以下示例演示了如何使用除 Let's Encrypt 之外的 CA 提供程序,以及如何设置首选链。
resty.acme.autossl.init({
tos_accepted = true,
account_email = "example@example.com",
}, {
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 = {
-- the ACME v2 API endpoint to use
api_uri = "https://acme-v02.api.letsencrypt.org/directory",
-- the account email to register
account_email = nil,
-- the account key in PEM format text
account_key = nil,
-- the account kid (as an URL)
account_kid = nil,
-- external account binding key id
eab_kid = nil,
-- external account binding hmac key, base64url encoded
eab_hmac_key = nil,
-- external account registering handler
eab_handler = nil,
-- storage for challenge
storage_adapter = "shm",
-- the storage config passed to storage adapter
storage_config = {
shm_name = "acme"
},
-- the challenge types enabled, selection of `http-01` and `tls-alpn-01`
enabled_challenge_handlers = {"http-01"},
-- select preferred root CA issuer's Common Name if appliable
preferred_chain = nil,
-- callback function that allows to wait before signaling ACME server to validate
challenge_start_callback = nil,
-- the dict of dns providers, each provider should have following struct:
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)
-- do something to register an account with account_email
-- if err then
-- return nil, nil, err
-- end
return eab_kid, eab_hmac_key
end
以下 CA 提供程序的 EAB 处理程序受 lua-resty-acme 支持,用户无需实现自己的 eab_handler
preferred_chain
用于选择在其根 CA 中具有匹配公用名的链。 例如,用户可以使用 "ISRG Root X1"
来强制使用 Let's Encrypt 中的新默认链。 当未配置任何值或配置的名称未在任何链中找到时,将使用默认链。
challenge_start_callback
是一个回调函数,允许客户端在向 ACME 服务器发出信号以开始验证挑战之前等待。 这在挑战需要时间传播的分布式设置中很有用。 challenge_start_callback
接受 challenge_type
和 challenge_token
。 客户端每秒调用一次此函数,直到它返回 true
,指示挑战应开始; 如果未设置此 challenge_start_callback
,则不会执行任何等待。
challenge_start_callback = function(challenge_type, challenge_token)
-- do something here
-- if we are good
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 authentication key
auth = nil,
ssl = false,
ssl_verify = false,
ssl_server_name = nil,
-- namespace as a prefix of key
namespace = "",
}
需要 Redis >= 2.6.0,因为此存储需要 PEXPIRE。
vault
基于 Hashicorp Vault 的存储。 仅支持 KV V2 后端。 默认配置为
storage_config = {
host = '127.0.0.1',
port = 8200,
-- secrets kv prefix path
kv_path = "acme",
-- timeout in ms
timeout = 2000,
-- use HTTPS
https = false,
-- turn on tls verification
tls_verify = true
-- SNI used in request, default to host if omitted
tls_server_name = nil,
-- Auth Method, default to token, can be "token" or "kubernetes"
auth_method = "token"
-- Vault token
token = nil,
-- Vault's authentication path to use
auth_path = "kubernetes",
-- The role to try and assign
auth_role = nil,
-- The path to the JWT
jwt_path = "/var/run/secrets/kubernetes.io/serviceaccount/token",
-- Vault namespace
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 prefix path
kv_path = "acme",
-- Consul ACL token
token = nil,
-- timeout in ms
timeout = 2000,
}
etcd
etcd 基于存储。 目前仅支持 v2
协议。 默认配置为
storage_config = {
http_host = 'http://127.0.0.1:4001',
protocol = 'v2',
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
是 apikey,fqdn
是要设置记录的 DNS 记录名称,content
是记录的值。
待办事项
autossl: ocsp staping
测试
通过运行 bash t/fixtures/prepare_env.sh
设置端到端测试环境。
然后运行 cpanm install Test::Nginx::Socket
,然后运行 prove -r t
。
鸣谢
file
存储的改进由 @dbalagansky 提供。在“vault”存储中添加 Kubernetes 身份验证由 @UXabre 提供。
dns-01
挑战的初始支持由 @yuweizzz 提供。
版权和许可
此模块根据 BSD 许可证授权。
版权所有 (C) 2019,由 thenam153 <thenam153@gmail.com>。
保留所有权利。
在满足以下条件的情况下,允许以源代码和二进制形式重新分发和使用此软件,无论是否修改:
源代码重新分发必须保留上述版权声明、此条件列表和以下免责声明。
二进制形式的重新分发必须在随分发提供的文档和/或其他材料中复制上述版权声明、此条件列表和以下免责声明。
本软件由版权所有者和贡献者“按原样”提供,不附带任何明示或暗示的保证,包括但不限于对适销性和特定用途适用性的暗示保证。在任何情况下,版权所有者或贡献者均不对因使用本软件而产生的任何直接、间接、偶然、特殊、惩罚性或后果性损害(包括但不限于采购替代商品或服务的费用;使用、数据或利润损失;或业务中断)负责,无论损害原因或责任理论如何,无论是在合同中、严格责任中还是侵权行为(包括疏忽或其他原因)中,即使已告知此类损害的可能性。
另请参见
haproxytech/haproxy-lua-acme HAProxy 中使用的 ACME Lua 实现。
作者
thenam153
许可证
3bsd
依赖关系
ledgetech/lua-resty-http >= 0.12,openresty/lua-resty-lrucache >= 0.08,fffonion/lua-resty-openssl >= 0.7.0,spacewander/luafilesystem >= 0.1,luajit
版本
-
thenam153/lua-resty-acme 0.14.1自动 Let's Encrypt 证书服务和 ACME 协议的 Lua 实现 2024-07-04 09:56:01