使用现代证书更新 CLI 工具 lego 自动化 DNS 认证
你好,我是无能。
最近我想摆脱文件验证,或者说让我觉得不舒服的是,在使用 certbot 进行证书更新时,由于采用的是文件验证,导致我必须在 Nginx 侧监听一个虚拟的 mail.example.com 域名。
虽然是自动运行的,所以还好,但 certbot 的工作方式是临时在文件验证路径下放置文件来完成更新。然而,邮件服务器其实并不需要 http,我也不想接收这类请求。
此外,如果在 A 记录中指定了多个源站(Origin),这种运维方式就会失效。
因为
$dig soulminingrig.com +short
91.98.169.80
163.44.113.145
像这样由两个 IP 接收请求时,如果在一个服务器上运行 certbot,另一个服务器上就不会存在该文件验证用的文件。虽然由于 HTTP 或 DNS 客户端向最近的服务器发送请求的机制,更新可能偶尔会成功,但依赖这种“魔法”在运维上是不可持续的。
acme.sh 及 certbot 在规格上引入 DNS 验证的难点
这两者本身都是支持 DNS 验证的。
但是对于 certbot 来说,并没有预设自动调用 DNS 更新 API 的功能。因此,必须由我们手动完成记录注册等操作,在 DNS 验证的情况下,自动更新实际上是无法使用的。
acme.sh 默认不支持 ConoHa DNS 的 API。如果想通过指定选项来使用,选项会变得非常混乱。在这种情况下,如果更换了 DNS 服务器,处理起来会非常困难。
lego
就在我寻找其他替代方案时,意外发现一个用 Go 编写的工具竟然支持 ConoHa DNS API。
ConoHa v3 :: Let’s Encrypt client and ACME library written in Go.
GitHub - go-acme/lego: Let's Encrypt/ACME client and library written in Go · GitHub
太棒了!
安装
连 pkg 里都有,真是太感谢了……
# pkg search lego
lego-4.33.0 Let's Encrypt client and ACME library written in Go
pkg install lego-4.33.0
运行脚本
就我而言,域名几乎都在 Nginx 侧管理,只有一小部分例外,所以我让 Chappy 帮我写了更新脚本。
在我的情况下,我想为 www.example.com 和 example.com 申请 SAN 证书,但不想申请通配符证书,所以脚本如下。
#!/bin/sh
set -eu
SITES_DIR="/usr/local/etc/nginx/sites-enabled"
export CONOHAV3_TENANT_ID=""
export CONOHAV3_API_USER_ID=""
export CONOHAV3_API_PASSWORD=""
export CONOHAV3_PROPAGATION_TIMEOUT="600"
export CONOHAV3_POLLING_INTERVAL="300"
LEGO="/usr/local/bin/lego"
SSLDIR="/usr/local/etc/ssl/lego"
EMAIL="taro@example.com"
DNS_PROVIDER="conohav3"
# 在此处添加即使不在 nginx 管理下也想更新的域名
# 多个域名用空格分隔
EXTRA_DOMAINS="
mail.example.com
"
tmp_all="$(mktemp)"
tmp_done="$(mktemp)"
trap 'rm -f "$tmp_all" "$tmp_done"' EXIT INT TERM
# 从 nginx 的 server_name 中提取
grep -RhoE 'server_name[[:space:]]+[^;]+' "$SITES_DIR" \
| sed -E 's/^server_name[[:space:]]+//' \
| tr ' ' '\n' \
| sed 's/;$//' \
| sed '/^$/d' \
| sed '/^\*\./d' \
| sed '/^_/d' \
>> "$tmp_all"
printf '%s\n' "$EXTRA_DOMAINS" \
| tr ' ' '\n' \
| sed '/^$/d' \
>> "$tmp_all"
sort -u -o "$tmp_all" "$tmp_all"
: > "$tmp_done"
issue_cert() {
echo "==> issuing certificate for: $*"
"$LEGO" \
--accept-tos \
--path "${SSLDIR}" \
--email "$EMAIL" \
--dns "$DNS_PROVIDER" \
--dns.resolvers 1.1.1.1 \
"$@" \
run
}
already_done() {
grep -Fxq "$1" "$tmp_done"
}
mark_done() {
printf '%s\n' "$1" >> "$tmp_done"
}
while IFS= read -r host; do
[ -n "$host" ] || continue
if already_done "$host"; then
continue
fi
case "$host" in
www.*)
apex="${host#www.}"
if grep -Fxq "$apex" "$tmp_all"; then
issue_cert -d "$apex" -d "$host"
mark_done "$apex"
mark_done "$host"
else
issue_cert -d "$host"
mark_done "$host"
fi
;;
*.*.*)
issue_cert -d "$host"
mark_done "$host"
;;
*.*)
if grep -Fxq "www.$host" "$tmp_all"; then
issue_cert -d "$host" -d "www.$host"
mark_done "$host"
mark_done "www.$host"
else
issue_cert -d "$host"
mark_done "$host"
fi
;;
*)
echo "skip invalid host: $host" >&2
;;
esac
done < "$tmp_all"
更新时只需将 run 选项改为 renew 即可。
自动更新
就我而言,因为使用的是 FreeBSD,所以写在 /etc/periodic.conf 中。
weekly_lego_enable="YES"
weekly_lego_renewscript="/usr/local/etc/lego/dns.sh"
weekly_lego_deployscript="/usr/local/etc/lego/deploy.sh"
由于存在默认的 /usr/local/etc/lego/deploy.sh 和 /usr/local/etc/lego/lego.sh,基本上建议遵循这种设计。但是,据我所见,默认似乎是使用文件验证进行的,因此如果想进行像这次这样的 DNS 验证,则需要进行更改。