leafee98-blog/content/posts/a-helper-to-place-certs-from-certbot-to-proper-location.md

91 lines
5.2 KiB
Markdown
Raw Normal View History

---
title: "将 certbot 的证书放到合适位置的简单脚本工具"
date: 2022-08-12T17:56:46+08:00
tags: [ linux, bash, certbot ]
categories: [ tech ]
weight: 50
show_comments: true
draft: false
---
> ⚠️文章中所有的命令都一定在 Bash 下运行,即便是 Zsh 也不能使用文章中描述的某些语法⚠️
众所周知certbot 会将获取到的证书放到 `/etc/letsencrypt/live/<DOMAIN>/` 之下,并且申请之后常用的文件为 `privkey.pem``fullchain.pem` 。而我们常使用的如 Apache httpd 等以 root 身份启动的服务具有最高的权限,自然可以从 certbot 的证书目录下读取证书内容,但是对于其他一些通过 systemd 来限制运行时用户的服务,则没有从该目录下读取证书内容的权限,为此我们需要使用脚本,在每次 certbot 获取证书以后,将这些证书放到对应的位置并修改属主权限,必要时要重新启动服务。
<!--more-->
依然是众所周知certbot 在每次申请或更新到证书时,会去运行特定目录下的所有可执行文件,这种行为一般称之为“钩子”,如同 [文档](https://eff-certbot.readthedocs.io/en/stable/using.html#renewing-certificates) 所说,三种钩子的行为如下:
+ `renewal-hooks/pre` 会在尝试更新证书之前运行
+ `renewal-hooks/post` 会在尝试证书更新之后运行
+ `renewal-hooks/deploy` 只会在成功获取到证书以后运行
有了以上前置知识,我们就可以了解到 `renewal-hooks/deploy` 就是我们所需要的钩子。我们需要为每一个需要使用证书却无访问 certbot 证书目录权限的服务都要写一个脚本,才能使需要的服务拿到证书更新后的证书。
为了可维护性我的建议是提高复用性,所以应当使某一个脚本能够提供核心功能,并使用另外多个脚本去对应到每一个服务去调用前一个脚本,由于前者不能独立完成任务,只为了简化后者的编写逻辑,我称前者为 “helper”。
写完之后的 helper 意外的简单,所需要的逻辑只有复制证书、修改属主信息、必要时重启服务:
```bash
#!/bin/bash
# I placed this script at /etc/letsencrypt/renewal-hooks/copy-cert
DOMAIN="${DOMAIN:?not initialized}"
DEST_DIR="${DEST_DIR:?not initialized}"
CHOWN_PARAM="${CHOWN_PARAM:?not initialized}"
SERVICE_NAME="${SERVICE_NAME}"
cp --force --target-directory ${DEST_DIR} /etc/letsencrypt/live/${DOMAIN}/{fullchain,privkey}.pem
chown $CHOWN_PARAM ${DEST_DIR}/{fullchain,privkey}.pem
[ -n "$SERVICE_NAME" ] && systemctl reload $SERVICE_NAME || systemctl restart $SERVICE_NAME
```
而针对某一个具体的服务所使用的脚本如下,无非是设置环境变量和调用 helper
```bash
#!/usr/bin/bash
# I place this script as /etc/letsencrypt/renewal-hooks/deploy/exim4
export DOMAIN=mail.leafee98.com
export DEST_DIR=/etc/exim4/cert/
export CHOWN_PARAM=Debian-exim:Debian-exim
export SERVICE_NAME=exim4.service
bash /etc/letsencrypt/renewal-hooks/copy-cert
```
## 脚本中用到的知识
新学到的知识主要有两个,一个在 helper 的赋值语句中参数展开分部分,一个就是复制时的花括号展开。
赋值语句中的参数展开语法为 `${parameter:?word}`,它的作用是如果参数的前者没有被设定或是 null那么将问号之后的内容输出到错误输出流如果是非交互终端的话还会结束程序。它的 [文档](https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html) 中是这么描述的:
> If *parameter* is null or unset, the expansion of *word* (or a message to that effect if *word* is not present) is written to the standard error and the shell, if it is not interactive, exits. Otherwise, the value of parameter is substituted.
花括号展开在本脚本中只使用了基础语法,即通过花括号将一个字符串展开为多个部分不相同的字符串,注意它不能放在双引号中,基础用法如下:
```
bash$ echo a{d,c,b}e
ade ace abe
```
花括号展开支持嵌套,并且可以提供范围展开,如下,更详细的讲解可以看它的 [文档](https://www.gnu.org/software/bash/manual/html_node/Brace-Expansion.html#Brace-Expansion) 。
```
bash$ echo s-{a{1,2,3},b{1,3,5}}
s-a1 s-a2 s-a3 s-b1 s-b3 s-b5
bash$ echo s-{a{1..3},b{1..5..2}}
s-a1 s-a2 s-a3 s-b1 s-b3 s-b5
```
## 遗留问题
或许你也注意到了,这些脚本会在任何一个证书更新后运行,比如 exim4 的证书更新了,那么 coturn 的 hook 也会运行一遍,理论上来说,一个证书的更新触发其他证书的 hook 是不必要的,但是考虑到多运行一次也只会有服务多重启一次的损失罢了,并且暂时没有发现 certbot 对 hook 提供所更新的证书的信息,所以就暂时作罢了。
目前想到的解决办法就是检查本域名对应的证书的修改时间,如果距离现在时间小于一个阈值(比如 3 分钟),那么就执行此 hook否则跳过。
不过这种方法毕竟不是很优雅,检查 3 分钟是因为 ext4 文件系统的时间的最小单位是分钟,而不能是秒,此外服务器极高负载的时候,有可能触发其他 hook 运行时间过长导致超过 3 分钟的时限之类的,总之就先作罢了。