acme.sh支持四种hook,分别是pre-hook,在签发证书之前执行;post-hook,在签发证书之后执行;renew-hook,在证书续期成功之后执行;deploy-hook,在执行部署命令时执行。

当执行一次之后,这些hook及相关参数会被写入对应域名的配置文件,在之后通过cron job进行证书续期时,这四种hook都会在对应的阶段再次执行。

前三种hook只能和--issue命令一起使用,也就是签发时必须指定相关hook参数,且官方没有现成的脚本,需要指定脚本绝对路径。deploy-hook有一个单独的--deploy命令,并且官方仓库的deploy目录下有很多写好了的hook,比如SSH、nginx等,只需要指定hook名称即可。

我的部署思路

下面先说一下我的使用场景和部署思路,

我的证书需要部署在各种地方,比如nginx,mosdns等等,而安装acme.sh的机器是在我的家庭内网环境下(我不希望每台机器都安装acme,然后再配置dnsapi、notify等),所以是一个一对多的分发场景。乍一看好像SSH比较适合这种场景,但是我不太喜欢配置SSH,因为SSH也是登录到目标机器执行一个脚本来更新证书,如果机器换SSH端口了等等都得记着回到acme的机器修改一遍,心智负担有点重。我希望能够将更新的过程单独拎出来,目标机器爱怎么折腾就怎么折腾,acme这边的机器一次配置好就不管了。下面是我的部署思路的具体实现,

首次签发证书之后的流程是这样的:

  1. 运行deploy hook,把fullchain上传到我的文件服务器,也不需要配置复杂路径,因为公钥就是给大伙儿看的
  2. 手动把私钥传输(我一般用Termius的SFTP)到指定位置
  3. 在目标服务器上配置一个脚本,定时下载并更新公钥到指定位置

默认情况下,acme续签证书并不会轮换私钥(关于私钥的轮换可以看这个issue,和这个回答,其优劣需自行判断),在此前提之下,每次证书续期成功之后,私钥不会变,只需要替换公钥即可。

因此,续期成功之后的流程是这样的:

  1. 自动运行deploy hook,上传fullchain
  2. 目标服务器定时拉取,设置个cron job,无需人为干预即可完成公钥的替换

这个方案只能算半自动化,因为每次签发一个新的证书都需要手动部署一次,后续只要不轮换私钥,那就不用管了。我个人是能接受的,申请证书的时候就是在目标机器上部署服务的时候,顺手就搞了,没感觉很麻烦。

脚本配置

来看实际的脚本吧,deploy hook使用的是上传脚本,目标服务器使用下载脚本

上传脚本

其实这个脚本放在post hook更合适些,不过因为deploy hook写起来更简单(照着官方脚本抄就行,还能引用acme.sh里面的函数),而且deploy hook可以在签发证书之后用单独的命令运行,所以我就用的这个。

注意脚本里面的主函数名是文件名加_deploy。另外,这个脚本算是私人定制的,可能不适合直接抄,谨慎复制。

用法很简单,把脚本放到~/.acme.sh/deploy目录下,然后配置好顶部注释的四个环境变量,其中UPLOAD_WEBDAV_CERT_EXT是证书扩展名,默认.pem。最后,到~/.acme.sh目录运行命令./acme.sh --deploy --deploy-hook upload_webdav -d my.domain。控制台显示成功之后,fullchain就上传好了,后续这个域名每次通过cron job续期成功时,都会运行一次这个脚本,即每次都会上传最新的公钥到文件服务器。

下载脚本

这个脚本需要放在目标服务器上,注意运行脚本的用户要有对应目录的写入权限,其他具体的环境变量设置脚本看脚本注释就好。脚本做的事情很简单,就是下载并更新公钥。

然后还需要配置一下cron任务,证书是三个月过期,考虑到下载证书有失败的可能,我设置的cron表达式是0 12 * * 1,即每周一的中午12点运行一次。

总结

上述就是我的半自动化部署方案,思路其实很简单,只是可能没有很优雅。利用的是证书续期不轮换私钥这一个前提,第一次申请证书的时候在目标机器把脚本配置好,然后就不用管了,直到下一次轮换私钥,再来手动部署一次。

另外可能有部分人认为不轮换私钥有安全风险,我个人觉得吧,以ECDSA P-256的加密强度,就算上量子计算机也得破解好多年呢。等量子计算有突破进展了我再轮换也不迟😝

不出意外的话,这应该是acme.sh系列最后一篇文章了,整体的方案已经可以满足我目前的需求了,终于可以不用为搞证书这件事情操心了🤗