泛域名证书自动申请续期方案(基于docker)

感谢Let's Encrypt提供的免费证书,让个人站长也能用上https。之前只提供了单域名证书,需要对于每一个子域名进行独立的申请工作,但是年初开放了泛域名证书的支持,一个顶级域名下的所有子域名可以共享一个证书,大大简化了证书管理的麻烦。

之前使用的是使用Docker容器签发和自动续期Let’s Encrypt证书一文中的方法,是针对单域名的,现在感觉略有一点繁琐和过时了,趁有时间重写一篇。

本文最后的预期效果:手动完成首次初始设置之后,后期自动完成域名证书的续期、服务器的证书刷新工作。跟上一篇一样,所有操作都可以在docker容器内进行,不需要安装新的软件包。

申请泛域名证书

Let's Encrypt 直接提供的api还是略微繁琐的,所以出现了很多集成和简化的程序,之前用的是较为官方的certbot,后来发现Neilpang/acme.sh用起来更为舒服一点,就切换过来了。(acme是国人写的,GitHub上star比前者少一些)

acme提供了很多种申请方式,从原理上来看,包括文件验证和dns记录验证。因为我们的目的是申请一个泛域名证书,所以使用了dns验证模式,这种模式还有一个优点就是可以在自己的机器上申请证书然后分发到不同的服务器上去。

使用也比较简单,安装之后,使用命令开始申请证书,

1
2
3
4
5
acme.sh \
--issue \
--dns \
-d '*.example.com' \
--yes-I-know-dns-manual-mode-enough-go-ahead-please

然后根据提示添加dns解析记录,再次运行以上命令即可完成证书申请。

虽然这样的操作三个月才需要执行一次,但是如果服务器多了,还是有一点麻烦的。

自动设置dns记录

好在 acme 支持通过服务商的api自动完成dns解析的修改操作,这下就不需要登陆上去修改等等麻烦的操作了。

目前acme支持 CloudFlareDNSPodAmazon Route53Aliyun等等,基本上主流的都支持,完整的列表可以在DNS API找到。

以下以CloudFlare为例。

首先我们需要四个配置参数:Account IDTokenEmailAPI Key

Account ID 在进入任意一个域名的Overview页,拉到页面右下角即可找到;

f644ec0c10fba484d8b80be13a2dbe82.md.png

Token 可以在API Tokens页面,点击Create Token创建一个。按照最小权限原则给这个token授权,让它只能修改指定域名的dns解析记录;

3470677be65428d5b7a487273ebb64b8.md.png

API Key 就是同一个页面的API Keys板块的Global API Key,点击view然后输入账号密码即会显示;

Email 就是注册邮箱;

然后使用命令就可以自动完成dns记录修改和证书申请了。

1
2
3
4
5
6
7
8
CF_Token="ZK4aspUKNNp20nhEv96awtmS54nTkeVKX-1NGq1E1" \
CF_Account_ID="DfooRWEAWKCNUmDyYJXvQCJDDYv9nvUru" \
CF_Key="RheH3wMCg29yF9oCucbSuMBV7iRyawiUV65Mxc" \
CF_Email="domain@example.com" \
acme.sh --issue \
--dns dns_cf \
-d 'example.com' \
-d '*.example.com'

使用alias模式来保护原始域名解析

尽管可以让acme直接修改dns,以及限制了token的范围,但是毕竟是非官方项目,在隐私上还是有点顾虑的(此处没有任何怀疑作者的意思)。

好在官方支持通过cname的形式来验证,也就是说我们可以用一个不太在乎的域名来替代验证,从而避免对原始域名的直接操作。具体的操作需要将原始域名的_acme-challenge记录cname到新域名的_acme-challenge,例如_acme-challenge.example-alias.com。还有一点很好的就是,这个域名可以同时给多个域名使用,在管理大量域名的时候比较方便。

1da663240394d47d199afb807f6b4239.md.png

设置好之后,只需要给acme授权新的域名dns操作权限即可,其他的域名只需要设置cname。

1
2
3
4
5
6
7
8
9
CF_Token="ZK4aspUKNNp20nhEv96awtmS54nTkeVKX-1NGq1E1" \
CF_Account_ID="DfooRWEAWKCNUmDyYJXvQCJDDYv9nvUru" \
CF_Key="RheH3wMCg29yF9oCucbSuMBV7iRyawiUV65Mxc" \
CF_Email="domain@example.com" \
acme.sh --issue \
--dns dns_cf \
--challenge-alias example-alias.com \
-d 'example.com' \
-d '*.example.com'

参考DNS alias mode | acme.sh

定期更新证书

以上只是完成了域名证书的自动申请工作,接下来还要设置自动续期,续期很简单,就是利用cron job每两个月执行一次续期操作。

1
2
3
4
5
6
7
8
9
10
CF_Token="ZK4aspUKNNp20nhEv96awtmS54nTkeVKX-1NGq1E1" \
CF_Account_ID="DfooRWEAWKCNUmDyYJXvQCJDDYv9nvUru" \
CF_Key="RheH3wMCg29yF9oCucbSuMBV7iRyawiUV65Mxc" \
CF_Email="domain@example.com" \
acme.sh --renew \
--force \
--dns dns_cf \
--challenge-alias example-alias.com \
-d 'example.com' \
-d '*.example.com'

给脚本添加可执行权限

1
chmod +x renew_cert.sh

这里利用系统自带的 crontab 功能来定期执行续期脚本。

1
crontab -e

添加以下行

1
0 0 1 * * /crontab/renew_cert.sh

保存退出,crontab 就会自动更新生效。以上命令的含义是每个月1号零点自动执行 /crontab/renew_cert.sh 这个脚本。

刷新服务器证书

根据部署方式来决定如何刷新证书,如果是在宿主机上直接安装的就直接reload nginx/apache服务器即可。

1
service nginx force-reload

如果像我一样使用docker来部署nginx的话,直接重启容器就行。

1
docker ps | grep nginx | awk '{print $1}' | xargs docker kill

如果你的容器没有配置自动重启的话,需要换成先关闭再创建

完整脚本

对上述进行汇总,再将acme装进docker的笼子里,可以得到以下脚本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/bin/bash

docker run -it \
--name acme \
--rm \
--env CF_Token="ZK4aspUKNNp20nhEv96awtmS54nTkeVKX-1NGq1E1" \
--env CF_Account_ID="DfooRWEAWKCNUmDyYJXvQCJDDYv9nvUru" \
--env CF_Key="RheH3wMCg29yF9oCucbSuMBV7iRyawiUV65Mxc" \
--env CF_Email="domain@example.com" \
-v "/data/acme/":/acme.sh \
neilpang/acme.sh \
--renew --force \
--dns dns_cf \
--challenge-alias example-alias.com \
-d 'example.com' \
-d '*.example.com'

# restart nginx
docker ps | grep nginx | awk '{print $1}' | xargs docker kill

别忘了挂载宿主机文件夹

至此,就可以欢快的让它自己去运行啦,彻底从手动续期证书这件小事里脱离出来。当然了,别忘记给域名续费,赎回费用挺高的,要是开了自动续期那就当我没说。

其实,真实的情况比这复杂,因为有好几台服务器,每一台独立部署不太喜欢,所以就用备份机每两个月续期一次,然后推送到git仓库,再通过webhook接口通知其他服务器来同步最新的证书并刷新服务器。囿于篇幅所限,这里就忽略了