ACME 配置#
Angie 中的 ACME 模块使用 ACME 协议 实现自动证书获取。ACME 协议支持
多种域名验证方法(也称为"验证");本模块实现了 HTTP 验证、
DNS 验证、ALPN 验证 以及通过自定义外部服务进行的 基于钩子的验证。 在配置中启用证书请求的常规步骤: 配置 ACME 客户端:在 指定请求证书的域名:将为所有 在 server_name 中指定的 IP 地址(IPv4 或 IPv6)同样是有效的证书标识符,但
DNS 验证 方式除外。 请注意,公共证书颁发机构可能仅在特定证书配置文件(profile)下才会为 IP 地址
签发证书;例如,Let's Encrypt 目前要求使用支持 IP 标识符的配置文件(参见其
配置文件文档)。如需申请此类
配置文件,请设置 acme_client 的 设置请求处理和 ACME 回调:这是验证域名所有权所必需的。设置取决于所选的域名验证方法: 方法 用户要求 多域名 通配符域名 在 Angie 服务器上开放端口 80(或 acme_http_port 中指定的端口)
以接受传入连接。 ✔ 在 Angie 服务器上开放端口 53(或 acme_dns_port 中指定的端口)
以接受传入连接。 为 ✔ ✔ 在 Angie 服务器上开放端口 443(或您的服务器使用的 TLS 端口)
以接受传入连接。 ✔ 创建一个外部服务(脚本或应用程序),
可以根据 Angie 的请求更新 DNS 记录
或通过 Web 服务器提供特殊响应。 ✔ ✔ 使用获取的证书和密钥配置 SSL:该模块将证书和密钥作为 嵌入式变量
提供,可在 配置 中使用以填充 ssl_certificate 和 ssl_certificate_key。 有关 SSL 设置说明,请参阅 SSL配置。 小技巧 证书获取和续订过程依赖于许多服务,可能需要一些时间。请耐心等待,
如果遇到问题或有疑问,请检查 调试日志。 客户端密钥和证书以 PEM 编码 存储在由 备注 这些文件在重启之间持久保存在磁盘上。启动时,客户端会复用仍然有效的已存储证书,
而非请求新证书,从而避免了签发延迟和向 CA 发送不必要的请求——CA 可能受
频率限制 约束。
在容器中使用时,请将存储目录(默认为 备注 重新加载配置( ACME 客户端需要在 CA 服务器上拥有一个账户。为了创建和管理此账户,
客户端使用私钥( 备注 如果您已经拥有账户密钥,请在启动前将其放置在客户端的子目录中以重用该账户。
或者,使用 acme_client 中的 部分 CA 要求在签发证书之前,先通过
External Account Binding
(EAB)将 ACME 账户与已有的外部账户绑定。要启用此功能,请在 acme_client 中
配置 ACME 客户端还使用单独的密钥( 启动时,如果证书不存在,客户端会请求证书,签名并向 CA 服务器发送包含其管理的所有域名的 CSR。
服务器使用 HTTP 或 DNS 验证 验证域名所有权,
并颁发证书,客户端将其保存在本地( 如前所述,单个证书涵盖同一 ACME 客户端管理的所有域名,可能产生多域名证书。
证书涵盖的所有名称列表可以在获取的证书的 Subject Alternative Name (SAN)部分中找到。
要从命令行检查: 当证书即将过期或域名列表发生变化时,客户端会签名并向 CA 服务器发送另一个 CSR。
服务器重新验证所有权并颁发新证书,客户端将其安装在本地,替换之前的证书。 在 配置 中,获取的证书及其对应的密钥可通过前缀变量
$acme_cert_<名称> 和 $acme_cert_key_<名称> 获得。它们的值是相应文件的内容,
应与 ssl_certificate 和 ssl_certificate_key 指令一起使用: 备注 ACME 客户端通过同一上下文中的 resolver 解析 CA 服务器的主机名,
该解析器默认查询 acme 指令仅用于收集证书请求的域名,
并不控制证书可在何处使用:
任何 例如,如果您有一个已涵盖所有子域名的通配符 为了精确控制证书中的域名集合,
而不依赖于从所有 其他 每个 acme_client 只管理一张证书。若要获取多张独立证书
(例如,用于不应共享同一证书的不相关域名),
请在 验证是自动完成的。当 Angie 申请证书时,ACME 服务器会向 Angie 的
在此示例中,名为 如前所述,必须开放端口 80 以处理 HTTP ACME 调用。
如果没有配置服务器监听 HTTP 挑战端口,
该模块会在端口 80(或 acme_http_port 中设置的端口)上创建专用监听器。不需要单独的 server 块。 验证是自动完成的,但需要您在自己一侧配置 DNS 记录。当 Angie 申请证书时,ACME 服务器
会针对被验证域名的 为使该查询能够到达 Angie,您的 DNS 必须将 DNS 验证证明的是对域名的控制权,因此它无法验证 IP 地址标识符:对于配置了
备注 Angie 服务器必须能够通过互联网在
UDP 53 端口(或 acme_dns_port 中指定的端口)上访问。
如果服务器位于防火墙后,
请确保该端口对入站连接开放。 例如,要使用 IP 地址为 此配置将 警告 NS 记录的传播可能需要几分钟到 48 小时不等,具体取决于 TTL 和 DNS 提供商。建议在请求证书之前验证配置是否正确。 要验证 DNS 是否配置正确,您可以使用以下命令: 此方法允许请求通配符证书,例如,在 Subject Alternative Name (SAN)部分包含 警告 此场景的适用性在很大程度上取决于您的 DNS 提供商提供的功能;某些提供商不允许此类配置。 总体而言,配置与上一节中的示例类似。不需要 HTTP 特定的设置;只需为 acme_client 指令设置 在此示例中,名为 验证是自动处理的。ACME 服务器使用 TLS 连接并通过 ALPN 请求 要启用此方法,请在 acme_client 指令中配置 配置与前几节类似;只需为 acme_client 指令设置 在此示例中,名为 与之前的方法不同,此验证需要额外的工作。ACME 服务器执行标准的 HTTP 验证 或 DNS 验证,但它不是直接与 Angie 服务器交互,而是通过钩子调用(acme_hook)与 Angie 服务器管理的外部服务通信。该服务配置一个单独的 DNS 或 HTTP 服务器,ACME 服务器向其发送请求。 一旦 ACME 服务器从配置的 DNS 或 HTTP 服务器收到预期的响应,它就会确认域名所有权。 当证书签发或续订需要进行域名验证时,
Angie 会向包含 acme_hook 指令的命名 通用模式如下: 创建一个带有 acme_hook 指令的命名 在同一 使用处理程序支持的机制,
将 ACME 变量 传递给处理程序,
例如 FastCGI 使用 处理程序必须返回 无论使用哪种处理程序,
钩子 对于 DNS 验证,处理程序必须使用 在此示例中, acme_client 指令通过 一个命名的 以下 Perl 脚本演示了相应的外部 FastCGI 服务: 这里, 另一个使用 PHP-FPM 的示例: 传递的参数可以在 PHP 中通过 此处的钩子是一个由 CGI 模块运行的脚本;它通过 octoDNS 添加和删除 该模块在 octoDNS 按顺序从其各个源(source)构建区域并将其合并,因此钩子将验证记录保存在
自己独立的源目录 区域文件保存您自己维护的记录: 如需手动检查 octoDNS 配置,请在不带 该脚本从环境变量中读取 ACME 变量,
并将验证记录写入 如果该值未能及时出现,脚本会应答 等待时限由 将其设置为 $acme_hook_domain 变量传入时不带 该脚本以 Angie 工作进程的用户身份 该脚本的标准错误输出以 要在基于 Debian 的系统上完成上述全部配置: 安装 CGI 模块: 将 octoDNS 和 Cloudflare 提供商
安装到一个独立的虚拟环境中: 创建所需目录;该模块软件包
不会创建 按照上面的示例创建 将上面的脚本保存为 添加上面的配置,然后测试并
重新加载 Angie: 客户端会在重新加载后立即请求证书;其状态和证书状态会显示在
/status/http/acme_clients/ API
部分中,签发的证书会保存在客户端的
存储目录 中。 Stream ACME 模块可为 TCP 流量启用自动化的
证书签发和使用。
为使其正常工作,您必须首先配置其 HTTP 对应部分:
ACME 客户端必须在 默认情况下,使用 HTTP 验证模式来获取证书。
如 HTTP 验证 部分所述,
这需要一个监听 80 端口的 HTTP 服务器: 您也可以使用 DNS 验证,
方法是在 acme_client 指令中配置 如果您之前在 从 nginx 迁移到 Angie 之前
使用 certbot
从 Let's Encrypt 获取和续订 SSL 证书,
请按照以下步骤过渡到使用我们的 ACME 模块。 假设您按如下方式配置了证书: 此命令自动创建的配置
通常位于 在上面的示例中,需要修改高亮显示的行。
根据您的情况和偏好,使用 ACME 模块配置
HTTP 验证
或 DNS 验证。 最终的 Angie 配置
可能如下所示: 请记住在 更改后重新加载配置: 一旦您验证了此配置可以正常工作,
您可以删除 certbot 证书
并禁用或从服务器上完全移除 certbot
(如果它不再在其他地方使用),
例如:配置步骤#
http 块中使用 acme_client 指令,
指定唯一的客户端名称和其他参数。可以配置多个 ACME 客户端。server 块中 server_name
指令列出的所有域名颁发单个证书,这些块使用指向同一 ACME 客户端的 acme 指令。profile= 参数。_acme-challenge. 子域设置 NS 记录,
指向您的 Angie 服务器。实现细节#
--http-acme-client-path
构建选项 指定的目录的子目录中:$ ls /var/lib/angie/acme/example/
account.key certificate.pem private.key
/var/lib/angie/acme/)挂载到持久卷上,
以确保已签发的证书在容器重建后仍然存在;请参阅
在容器中运行 Angie。angie -s reload)会针对磁盘上存储的证书
重新评估每个 ACME 客户端。证书当前无效的客户端——
包括先前请求以错误结束的客户端——会立即再次发起请求,
而不受 retry_after_error 的影响。
重试延迟(以及 retry_after_error=off)仅在 Angie
持续运行期间生效;它不会在重新加载后保留。因此,
在请求持续失败时反复重新加载可能会向 CA 频繁发送请求,
并受上文所述的频率限制约束。account.key)。如果不存在密钥,则在启动时生成。
然后客户端使用此密钥向服务器注册账户。account_key 参数指定密钥文件。eab 参数,填入 CA 提供的密钥标识符和 MAC 密钥;具体语法请参见该指令
的说明。private.key)用于证书签名请求(CSR)。
如果需要,此证书密钥会在启动时自动创建。certificate.pem)。$ openssl x509 -in certificate.pem -noout -text | grep -A5 "Subject Alternative Name"
server {
listen 443 ssl;
server_name example.com www.example.com;
acme example;
ssl_certificate $acme_cert_example;
ssl_certificate_key $acme_cert_key_example;
}
/etc/resolv.conf 中列出的 DNS 服务器;仅当需要使用其他
服务器或参数时,才需要显式配置该指令。在没有 IPv6 连接的主机上,解析器仍可能向
CA 发起 AAAA(IPv6)查询并导致连接失败;可通过 ipv6=off 参数禁用此行为,
例如 resolver conf ipv6=off;。域名收集与证书使用#
server 块都可以通过
$acme_cert_<name> 变量
引用获取的证书,
无论该块是否包含 acme 指令。server 块,
那么针对特定子域名的额外 server 块
不需要 acme 指令:http {
acme_client example https://acme-v02.api.letsencrypt.org/directory
challenge=dns;
# This block lists the domains for the certificate request
server {
listen 443 ssl;
server_name example.com *.example.com;
acme example;
ssl_certificate $acme_cert_example;
ssl_certificate_key $acme_cert_key_example;
}
# This block uses the same certificate but does not
# add its server_name to the certificate request
server {
listen 443 ssl;
server_name app.example.com;
ssl_certificate $acme_cert_example;
ssl_certificate_key $acme_cert_key_example;
}
}
显式域名列表#
server 块自动收集,
可以创建一个专用的 server 块,
其中仅包含 server_name 和 acme 指令。
为防止此块处理实际流量,
可以将其绑定到 Unix 域套接字:# Dedicated block that defines the certificate's domain list
server {
listen unix:/tmp/acme_example.sock;
server_name example.com www.example.com;
acme example;
}
server 块随后可通过
$acme_cert_<name> 变量
使用该证书,而不会影响请求的域名。为不同域名申请独立证书#
http 块中为每个域名单独配置一个 acme_client,
并在每个 server 块的 acme 指令中按名称指向相应的客户端:http {
# 两个独立的客户端,各自管理自己的证书
acme_client shop https://acme-v02.api.letsencrypt.org/directory
challenge=http;
acme_client blog https://acme-v02.api.letsencrypt.org/directory
challenge=http;
server {
listen 443 ssl;
server_name shop.example.com www.shop.example.com;
acme shop;
ssl_certificate $acme_cert_shop;
ssl_certificate_key $acme_cert_key_shop;
}
server {
listen 443 ssl;
server_name blog.example.com;
acme blog;
ssl_certificate $acme_cert_blog;
ssl_certificate_key $acme_cert_key_blog;
}
}
HTTP 验证#
/.well-known/acme-challenge/<TOKEN> 路径
发送 HTTP 请求以获取令牌文件。
Angie 会自行处理这些请求,因此您无需自己提供该路径。随后,ACME 服务器会将收到的令牌
与其签发的令牌进行比对,若二者匹配,则确认该域名归您所有。配置示例#
example 的 ACME 客户端管理 example.com 和
www.example.com 的证书(注意 HTTP 验证不支持通配符证书):http {
acme_client example https://acme-v02.api.letsencrypt.org/directory;
server {
listen 80; # 不是必需的:如果没有其他服务监听 HTTP 挑战端口,
# 该模块会自行开启该端口
# (参见 'acme_http_port' 指令)
listen 443 ssl;
server_name example.com www.example.com;
acme example;
ssl_certificate $acme_cert_example;
ssl_certificate_key $acme_cert_key_example;
}
}
DNS 验证#
_acme-challenge. 子域发送一次
DNS 查询,以获取
一条 TXT 记录。Angie 会自行应答这些查询:它充当该子域的权威名称服务器,并返回 ACME
服务器所期望的 TXT 记录。随后,ACME 服务器会将收到的记录与其签发的值进行比对,若二者
匹配,则确认该域名归您所有。_acme-challenge. 子域委派给 Angie
服务器,如下所示。Angie 默认使用非常短的 TTL 进行应答;部分 DNS 提供商会过滤掉 TTL
过低的响应,如果验证始终无法完成,请通过 acme_dns_ttl 调高该值。challenge=dns 的客户端,Angie 会忽略 server_name 中的 IP 地址,并在
启动时记录一条警告。列出了 IP 地址的 server 块仍必须至少指定一个该客户端
可以验证的域名。203.0.113.10 的 Angie 服务器验证域名 example.com,您的域名 DNS 配置应包含以下记录:_acme-challenge.example.com. 60 IN NS ns.example.com.
ns.example.com. 60 IN A 203.0.113.10
_acme-challenge.example.com 的 DNS 解析委托给 ns.example.com,并通过将 ns.example.com 映射到 IP 地址(203.0.113.10)来确保其可访问。$ dig NS _acme-challenge.example.com +short # 检查 _acme-challenge 子域的 NS 记录
ns.example.com.
$ dig A ns.example.com +short # 检查名称服务器的 A 记录
203.0.113.10
$ nc -zv 203.0.113.10 53 # 检查 DNS 服务器在端口 53 上的可访问性
*.example.com 条目的证书。要明确请求子域的证书,例如 www.example.com,您必须使用上述方法单独验证该子域。配置示例#
challenge=dns 即可。example 的 ACME 客户端管理 example.com 和 *.example.com 的证书:http {
acme_client example https://acme-v02.api.letsencrypt.org/directory
challenge=dns;
server {
server_name example.com *.example.com;
acme example;
ssl_certificate $acme_cert_example;
ssl_certificate_key $acme_cert_key_example;
}
}
ALPN 验证#
acme-tls/1 协议。该模块为验证请求提供临时证书。challenge=alpn,并确保您的 TLS 监听器在端口 443(或用于 TLS 的端口)上可访问。配置示例#
challenge=alpn,并确保 TLS 服务器在端口 443 上可访问即可。example 的 ACME 客户端管理 example.com 和 www.example.com 的证书:http {
acme_client example https://acme-v02.api.letsencrypt.org/directory
challenge=alpn;
server {
listen 443 ssl;
server_name example.com www.example.com;
acme example;
ssl_certificate $acme_cert_example;
ssl_certificate_key $acme_cert_key_example;
}
}
基于钩子的验证#
location
生成一个内部请求。
该请求的处理方式完全取决于同一 location
中配置的其他指令。location。location 中,
使用适合您环境的模块配置请求处理程序:
FastCGI 使用 fastcgi_pass,
HTTP 使用 proxy_pass,
CGI 脚本使用 cgi_pass,等等。fastcgi_param,
CGI 使用 cgi_set_var。2xx 状态码,
可以通过 Status 头发送。
来自 add 钩子的任何其他状态码都会中止本次续订尝试;而来自
remove 钩子的同一状态码仅会被记录为警告,续订过程会继续进行。
处理程序的输出将被忽略。最小配置#
location 都遵循以下结构:location @acme_hook_location {
acme_hook example;
# Handler directive (fastcgi_pass, proxy_pass, cgi_pass, ...)
# Pass ACME variables using the handler's mechanism:
# ACME_HOOK — $acme_hook_name ("add" or "remove")
# ACME_CHALLENGE — $acme_hook_challenge ("dns" or "http")
# ACME_DOMAIN — $acme_hook_domain
# ACME_TOKEN — $acme_hook_token
# ACME_KEYAUTH — $acme_hook_keyauth
}
ACME_HOOK
来确定操作:
当其值为 add 时,
为 _acme-challenge.ACME_DOMAIN 创建一条
值为 ACME_KEYAUTH 的 TXT 记录;
当其值为 remove 时,删除该记录。FastCGI 示例#
challenge=dns 参数
将 ACME 客户端 example 配置为使用 DNS 验证。server 块适用于 example.com 的所有子域(例如 *.example.com),
并通过 acme 指令使用 ACME 客户端 example 来管理证书。location 块负责处理钩子调用。
acme_hook 指令将其与 ACME 客户端 example 关联起来。
钩子请求通过 fastcgi_pass
发送到运行在 9000 端口上的本地 FastCGI 服务器。
fastcgi_param 指令将 ACME 变量
传递给外部服务。acme_client example https://acme-v02.api.letsencrypt.org/directory
challenge=dns;
server {
listen 80;
server_name *.example.com;
acme example;
ssl_certificate $acme_cert_example;
ssl_certificate_key $acme_cert_key_example;
location @acme_hook_location {
acme_hook example;
fastcgi_pass localhost:9000;
fastcgi_param ACME_CLIENT $acme_hook_client;
fastcgi_param ACME_HOOK $acme_hook_name;
fastcgi_param ACME_CHALLENGE $acme_hook_challenge;
fastcgi_param ACME_DOMAIN $acme_hook_domain;
fastcgi_param ACME_TOKEN $acme_hook_token;
fastcgi_param ACME_KEYAUTH $acme_hook_keyauth;
include fastcgi.conf;
}
}
#!/usr/bin/perl
use strict; use warnings;
use FCGI;
my $socket = FCGI::OpenSocket(":9000", 5);
my $request = FCGI::Request(\*STDIN, \*STDOUT, \*STDERR, \%ENV, $socket);
while ($request->Accept() >= 0) {
print "\r\n";
my $client = $ENV{ACME_CLIENT};
my $hook = $ENV{ACME_HOOK};
my $challenge = $ENV{ACME_CHALLENGE};
my $domain = $ENV{ACME_DOMAIN};
my $token = $ENV{ACME_TOKEN};
my $keyauth = $ENV{ACME_KEYAUTH};
if ($hook eq 'add') {
DNS_set_TXT_record("_acme-challenge.$domain.", $keyauth);
} elsif ($hook eq 'remove') {
DNS_clear_TXT_record("_acme-challenge.$domain.");
}
};
FCGI::CloseSocket($socket);
DNS_set_TXT_record() 和 DNS_clear_TXT_record() 是假定在外部 DNS 服务器的配置中添加和删除 TXT 记录的函数,ACME 服务器会查询该 DNS 服务器。这些记录必须包含 Angie 服务器提供的数据,以允许外部 DNS 服务器成功通过验证,类似于 DNS 验证 中描述的过程。此类函数的实现细节超出了本指南的范围;例如,参数也可以通过请求 URI 传递:# ...
location @acme_hook_location {
acme_hook example uri=/acme_hook/$acme_hook_name?domain=$acme_hook_domain&key=$acme_hook_keyauth;
fastcgi_pass localhost:9000;
fastcgi_param REQUEST_URI $request_uri;
fastcgi_param ACME_CLIENT $acme_hook_client;
fastcgi_param ACME_CHALLENGE $acme_hook_challenge;
fastcgi_param ACME_TOKEN $acme_hook_token;
include fastcgi.conf;
}
PHP-FPM 示例#
location @acme_hook_location {
acme_hook example;
root /var/www/dns;
fastcgi_pass unix:/run/php-fpm/php-dns.sock;
fastcgi_index hook.php;
fastcgi_param SCRIPT_FILENAME /var/www/dns/hook.php;
include fastcgi_params;
fastcgi_param ACME_CLIENT $acme_hook_client;
fastcgi_param ACME_HOOK $acme_hook_name;
fastcgi_param ACME_CHALLENGE $acme_hook_challenge;
fastcgi_param ACME_DOMAIN $acme_hook_domain;
fastcgi_param ACME_TOKEN $acme_hook_token;
fastcgi_param ACME_KEYAUTH $acme_hook_keyauth;
}
[dns]
listen = /run/php-fpm/php-dns.sock
listen.mode = 0666
user = angie
group = angie
chdir = /var/www/dns
# ...
$_SERVER['...'] 访问。CGI 和 octoDNS 示例#
_acme-challenge
TXT 记录,octoDNS 让 DNS 配置以代码形式管理:区域(zone)以 YAML 描述,并同步到
DNS 提供商。此处以 Cloudflare 为例;其他任何 octoDNS 提供商只需在
providers 下声明,并在 targets 中命名,工作方式都是相同的。该
API 令牌需要具备该区域的 Zone:Read、DNS:Read 和
DNS:Edit 权限。main 上下文中加载。ACME 客户端 example 为
example.com 和 *.example.com 请求单个证书,证书通过
acme_client 指令中 challenge=dns 参数所指定的 DNS 回调进行验证。
一个命名的 location 块处理钩子调用: acme_hook 指令将其与该客户端
关联, cgi_set_var 指令以标准名称传递 ACME 变量,
cgi_pass 则为每次调用运行该脚本。在这些名称中,该脚本使用了
ACME_CHALLENGE、ACME_HOOK、ACME_DOMAIN 和
ACME_KEYAUTH。# in the main context
load_module modules/ngx_http_cgi_module.so;
acme_client example https://acme-v02.api.letsencrypt.org/directory
challenge=dns;
server {
listen 443 ssl;
server_name example.com *.example.com;
acme example;
ssl_certificate $acme_cert_example;
ssl_certificate_key $acme_cert_key_example;
location @acme_hook_location {
acme_hook example;
cgi_set_var ACME_CLIENT $acme_hook_client;
cgi_set_var ACME_HOOK $acme_hook_name;
cgi_set_var ACME_CHALLENGE $acme_hook_challenge;
cgi_set_var ACME_DOMAIN $acme_hook_domain;
cgi_set_var ACME_TOKEN $acme_hook_token;
cgi_set_var ACME_KEYAUTH $acme_hook_keyauth;
cgi_pass /usr/share/angie/cgi-bin/acme-octodns;
}
}
/etc/octodns/acme 中,证书续订过程永远不会改写
/etc/octodns/zones 下的区域文件。ignore_missing_zones 选项
(octoDNS 1.16 及更高版本)允许 /etc/octodns/acme 在两次续订之间保持为
空。区域文件本身不得定义 _acme-challenge 记录。providers:
config:
class: octodns.provider.yaml.YamlProvider
directory: /etc/octodns/zones
acme:
class: octodns.provider.yaml.YamlProvider
directory: /etc/octodns/acme
ignore_missing_zones: true
cloudflare:
class: octodns_cloudflare.CloudflareProvider
token: YOUR_API_TOKEN
zones:
example.com.:
sources:
- config
- acme
targets:
- cloudflare
'':
type: A
value: 203.0.113.10
www:
type: A
value: 203.0.113.10
--doit 的情况下运行
octodns-sync。acme 源:acme-octodns 脚本
#!/opt/octodns/bin/python3
# DNS-01 hook for the Angie ACME client: keeps the _acme-challenge TXT record
# in a separate octodns source directory and publishes it with octodns-sync.
import fcntl
import os
import subprocess
import sys
import time
import dns.message
import dns.query
import dns.resolver
import yaml
CONFIG = "/etc/octodns/octodns.yaml"
ACME_DIR = "/etc/octodns/acme"
SYNC = "/opt/octodns/bin/octodns-sync"
TTL = 120
WAIT = int(os.environ.get("ACME_WAIT", "300"))
def respond(code, message=""):
if message:
print(message, file=sys.stderr)
sys.stdout.write(f"Status: {code}\r\n\r\n")
sys.exit(0)
def find_zone(domain):
with open(CONFIG) as f:
zones = [z.rstrip(".") for z in yaml.safe_load(f)["zones"]]
match = [z for z in zones if domain == z or domain.endswith("." + z)]
if not match:
respond(500, f"no zone for {domain} in {CONFIG}")
return max(match, key=len)
def update(zone, name, value, add):
# One record may hold several values: a certificate for example.com and
# *.example.com is validated with two TXT values on the same name.
path = f"{ACME_DIR}/{zone}.yaml"
with open(f"{ACME_DIR}/.lock", "w") as lock:
fcntl.flock(lock, fcntl.LOCK_EX)
data = {}
if os.path.exists(path):
with open(path) as f:
data = yaml.safe_load(f) or {}
values = set(data.get(name, {}).get("values", []))
(values.add if add else values.discard)(value)
if values:
data[name] = {"type": "TXT", "ttl": TTL, "values": sorted(values)}
else:
data.pop(name, None)
if data:
with open(path, "w") as f:
yaml.safe_dump(data, f)
elif os.path.exists(path):
os.remove(path)
cmd = [SYNC, f"--config-file={CONFIG}", "--doit", zone + "."]
run = subprocess.run(cmd, capture_output=True, text=True)
if run.returncode != 0:
respond(500, "octodns-sync failed:\n" + run.stdout + run.stderr)
def has_txt(server, fqdn, value):
try:
reply = dns.query.udp(dns.message.make_query(fqdn, "TXT"), server, timeout=3)
except Exception:
return False
return any(value == b"".join(getattr(r, "strings", ())).decode()
for rrset in reply.answer for r in rrset)
def wait_visible(zone, fqdn, value):
# The ACME server asks the zone's authoritative servers, so poll them
# directly: a resolver may have cached the record's absence.
deadline = time.monotonic() + WAIT
while time.monotonic() < deadline:
try:
servers = [str(a) for ns in dns.resolver.resolve(zone, "NS")
for a in dns.resolver.resolve(str(ns.target), "A")]
if servers and all(has_txt(s, fqdn, value) for s in servers):
return True
except Exception:
pass
time.sleep(5)
return False
def main():
hook = os.environ.get("ACME_HOOK")
domain = os.environ.get("ACME_DOMAIN", "")
value = os.environ.get("ACME_KEYAUTH", "")
if os.environ.get("ACME_CHALLENGE") != "dns" or hook not in ("add", "remove"):
respond(500, "the hook supports only DNS validation")
zone = find_zone(domain)
rel = domain[: -len(zone) - 1]
name = "_acme-challenge" + (f".{rel}" if rel else "")
update(zone, name, value, hook == "add")
if hook == "add" and WAIT and not wait_visible(zone, f"{name}.{zone}.", value):
update(zone, name, value, False)
respond(500, f"{name}.{zone} is not visible on the name servers after {WAIT}s")
respond(200)
try:
main()
except Exception as e:
respond(500, f"{type(e).__name__}: {e}")
add 钩子一返回 2xx 状态码,Angie 就会请求 ACME 服务器进行验证,
并轮询结果长达 60 秒。Angie 本身不会等待记录传播,因此该脚本自行完成这项工作:
它直接查询该区域的权威名称服务器,因为解析器可能缓存了记录不存在的结果,并在
其中每一台服务器都返回该值后立即结束等待,默认最多等待 300 秒。Status: 500,Angie 随即中止此次尝试,
并在 retry_after_error 参数设置的间隔之后重试。Angie 不会
在 add 失败后调用 remove 钩子,因此脚本会在应答之前自行删除
已写入的值。ACME_WAIT 环境变量决定。上面的钩子 location 并未设置
该变量;若要更改此时限,请在其中添加该变量,并像其他变量一样通过变量传递其值:set $acme_wait 600;
cgi_set_var ACME_WAIT $acme_wait;
0 可禁用等待,这适用于自行托管的权威服务器,因为其 API
写入会立即可见。*. 前缀,因此为
example.com 和 *.example.com 签发的证书会对同一个
_acme-challenge.example.com 名称产生两次 add 调用。脚本会在
同一条记录上保存一份值列表, remove 时只删除对应调用的那个值;一旦所有值
都被删除,脚本便会删除该记录以及位于 /etc/octodns/acme 下保存它的文件。angie 运行。它只需要读取保存令牌的
octoDNS 配置的权限,以及写入 /etc/octodns/acme 的权限,不需要其他权限。
CGI 进程不会继承 Angie 的环境,这正是令牌保存在配置文件中、脚本使用绝对路径的
原因。warn 级别写入 Angie 的 错误日志,
可据此诊断钩子失败的原因;只有当日志级别为 warn 或更低时,这些消息才会出现。$ sudo apt-get install angie-module-cgi
$ sudo apt-get install python3-venv
$ sudo python3 -m venv /opt/octodns
$ sudo /opt/octodns/bin/pip install 'octodns>=1.16' octodns-cloudflare
/usr/share/angie/cgi-bin:$ sudo install -d /etc/octodns/zones /usr/share/angie/cgi-bin
$ sudo install -d -o angie -g angie -m 750 /etc/octodns/acme
/etc/octodns/octodns.yaml 和
/etc/octodns/zones/example.com.yaml,然后将该配置限制为
angie 组可读,因为其中保存着 API 令牌:$ sudo chgrp angie /etc/octodns/octodns.yaml
$ sudo chmod 640 /etc/octodns/octodns.yaml
acme-octodns,然后安装它:$ sudo install -m 755 acme-octodns /usr/share/angie/cgi-bin/
$ sudo angie -t
$ sudo kill -HUP $(cat /run/angie.pid)
Stream 模块中的 ACME#
http 上下文中声明,
并且 stream 块本身必须放置在配置文件中 http 块*之后*。配置示例#
# HTTP 部分
http {
# 用于 stream 部分的 ACME 客户端
acme_client example https://acme-v02.api.letsencrypt.org/directory;
# 用于 HTTP 验证的服务器
server {
listen 80;
return 444;
}
}
# Stream 部分
stream {
server {
listen 12345 ssl;
proxy_pass backend_upstream;
ssl_certificate $acme_cert_example;
ssl_certificate_key $acme_cert_key_example;
server_name example.com www.example.com;
acme example; # 引用 HTTP 部分中定义的 ACME 客户端
}
upstream backend_upstream {
server 127.0.0.1:54321;
}
}
challenge=dns;
在这种情况下,将不需要该服务器。从 certbot 迁移#
$ sudo certbot --nginx -d example.com -d www.example.com
/etc/nginx/sites-available/example.conf,
看起来类似这样:server {
listen 80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name example.com www.example.com;
root /var/www/example;
index index.html;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}
http {
acme_client example https://acme-v02.api.letsencrypt.org/directory;
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name example.com www.example.com;
root /var/www/example;
index index.html;
acme example;
ssl_certificate $acme_cert_example;
ssl_certificate_key $acme_cert_key_example;
}
}
$ sudo kill -HUP $(cat /run/angie.pid)
$ sudo rm -rf /etc/letsencrypt
$ sudo systemctl stop certbot.timer
$ sudo systemctl disable certbot.timer
$ # -- 或 --
$ sudo rm /etc/cron.d/certbot
$ sudo apt remove certbot
$ # -- 或 --
$ sudo dnf remove certbot