let's-encrypt配置网站免费SSL证书及自动更新

前言#

相信看到这里的对SSL/TSL都有一定了解,链式信任、防劫持、防隐私泄露、安全可信,这些关键字大家脑海里都很熟悉。具体SSL细节就不啰嗦了,感兴趣可以去看看阮一峰的博客或者网上资料,本文主要是实操。

厂商选择#

除了域名统配的高端证书之外,一般我们的博客或者小型网站可以考虑使用免费厂商提供的证书。和大厂商的主要区别就是公信力了,不过理论上都是同等安全的。比如: https://letsencrypt.org/ 在chrome等浏览器厂商的努力支持下,这些之前看起来小点的证书厂商现在兼容性也非常好了。

本文就以let's encrypt和nginx为例。

2025年更新#

VPS 中自动更新并重启nginx#

CloudFlare#

VPS上颁发证书#

1
2
3
4
5
6
7
8
# 普通用户
sudo yum install certbot python3-certbot-dns-cloudflare

mkdir /etc/letsencrypt/cloudflare

touch /etc/letsencrypt/cloudflare/cloudflare.ini

chmod 600 /etc/letsencrypt/cloudflare/cloudflare.ini

编辑这个 /etc/letsencrypt/cloudflare/cloudflare.ini配置文件,指定在CF后台页面生成的token:

1
dns_cloudflare_api_token = xxx

可以开始执行生成命令了,初次执行命令需要注意,可以选择不共享邮箱地址:

1
2
3
4
5
6
certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials /etc/letsencrypt/cloudflare/cloudflare.ini \
--dns-cloudflare-propagation-seconds 30 \
-d ssssssss.com \
-d *.ssssss.com

成功的话可以看到以下log,CF后台DNS记录里面会出来新的txt解析

结果保存在: /etc/letsencrypt/live/sssssssss.com/里面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Enter email address (used for urgent renewal and security notices)
(Enter 'c' to cancel): [email protected]

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Please read the Terms of Service at
https://letsencrypt.org/documents/LE-SA-v1.5-February-24-2025.pdf. You must
agree in order to register with the ACME server. Do you agree?
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(Y)es/(N)o: Y

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Would you be willing, once your first certificate is successfully issued, to
share your email address with the Electronic Frontier Foundation, a founding
partner of the Let's Encrypt project and the non-profit organization that
develops Certbot? We'd like to send you email about our work encrypting the web,
EFF news, campaigns, and ways to support digital freedom.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(Y)es/(N)o: N
Account registered.
Requesting a certificate for clickhouse.eu.org and *.clickhouse.eu.org
Waiting 30 seconds for DNS changes to propagate

Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/ssssssssssssss.com/fullchain.pem
Key is saved at: /etc/letsencrypt/live/ssssssssssssss.com/privkey.pem
This certificate expires on 2025-08-19.
These files will be updated when the certificate renews.
Certbot has set up a scheduled task to automatically renew this certificate in the background.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

VPS自动更新证书并重启nginx#

  • 上一节我们生成的证书保存在: /etc/letsencrypt/live/sssssssss.com/

一个判断证书更新后自动重启nginx的shell脚本#

  • 我的nginx是www用户,以下脚本要使用root用户运行,运行过程中会指定用www用户重启nginx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
#!/bin/bash

# 配置项(需根据实际情况修改)
CERT_DOMAIN="sssss.com"
CERT_DIR="/etc/letsencrypt/live/${CERT_DOMAIN}"
FULLCHAIN_FILE="${CERT_DIR}/fullchain.pem"
PRIVKEY_FILE="${CERT_DIR}/privkey.pem"
TIME_THRESHOLD=3600
NGINX_BINARY="/path/to/nginx/sbin/nginx"
LOG_FILE="/var/log/nginx-cert-renewal.log"
NGINX_USER="www"

# 日志函数
log_message() {
local level="$1"
local message="$2"
local timestamp=$(date +"%Y-%m-%d %H:%M:%S")
echo "[$timestamp] [$level] $message" | tee -a "$LOG_FILE"
}

# 检查命令是否存在
check_command() {
if ! command -v "$1" &> /dev/null; then
log_message "ERROR" "命令不存在: $1"
exit 1
fi
}

# 检查文件是否存在
check_file() {
if [ ! -f "$1" ]; then
log_message "ERROR" "文件不存在: $1"
exit 1
fi
}

# 创建日志目录(如果不存在)
mkdir -p "$(dirname "$LOG_FILE")"

# 主函数
main() {
log_message "INFO" "开始证书更新检查..."

# 检查依赖
check_command "$NGINX_BINARY"
check_command date
check_command stat

# 验证证书文件
if [[ ! -f "$FULLCHAIN_FILE" || ! -f "$PRIVKEY_FILE" ]]; then
log_message "ERROR" "证书文件不存在,请检查域名配置:$CERT_DOMAIN"
exit 1
fi

# 获取当前时间和证书修改时间
CURRENT_TIME=$(date +%s)
FULLCHAIN_MTIME=$(stat -c %Y "$FULLCHAIN_FILE")
PRIVKEY_MTIME=$(stat -c %Y "$PRIVKEY_FILE")

# 计算最新修改时间和时间差
LATEST_MTIME=$((FULLCHAIN_MTIME > PRIVKEY_MTIME ? FULLCHAIN_MTIME : PRIVKEY_MTIME))
TIME_DIFF=$((CURRENT_TIME - LATEST_MTIME))

# 判断是否需要重启Nginx
if [[ $TIME_DIFF -ge $TIME_THRESHOLD ]]; then
log_message "INFO" "证书未在${TIME_THRESHOLD}秒内更新(时间差:$TIME_DIFF 秒),无需重启Nginx。"
exit 0
fi

log_message "INFO" "证书已更新(时间差:$TIME_DIFF 秒),准备重启Nginx..."

# 验证Nginx配置
if ! "$NGINX_BINARY" -t &> /dev/null; then
local error=$("$NGINX_BINARY" -t 2>&1)
log_message "ERROR" "证书已更新,但nginx配置验证失败:\n$error"
exit 1
fi

# 尝试平滑重启Nginx
if ! sudo -u $NGINX_USER "$NGINX_BINARY" -s reload; then
log_message "ERROR" "Nginx平滑重启失败,可能需要手动干预"
exit 1
fi

log_message "SUCCESS" "证书更新! 已成功重启Nginx!"
}

# 执行主函数
main

crontab定时更新证书,更行成功后重启nginx#

更新证书需要使用 /etc/letsencrypt/renewal/域名下面的配置文件,这个在首次颁发cert的时候已经生成好了,所以更新命令比较简单。

  • Certbot命令支持三种hook:

    • --pre-hook:更新证书之前调用

    • --post-hook:更新证书之后调用

    • --deploy-hook:成功更新证书之后调用

所以建议凌晨1点更新证书,证书更新成功后重启nginx,如:

1
2
3
# 凌晨1点更新证书,证书更新成功后重启nginx。需要注意权限和用户问题
0 1 * * * sudo -u www certbot renew --deploy-hook "sh /main/shells/restart_nginx_after_certbot.sh"

也可以分开处理,更新证书和重启nginx分开调度,不过不建议。

阿里云#

阿里云稍微复杂一点,需要安装aliyun工具。下面是一个dns api命令,需要借助aliyun命令进行身份验证。也需要颁发token,本次不是重点,仅供参考:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#!/bin/bash
FLAG="(\.com\.cn|\.gov\.cn|\.net\.cn|\.org\.cn|\.ac\.cn|\.gd\.cn)$"


if ! command -v aliyun >/dev/null; then
echo "错误: 你需要先安装 aliyun 命令行工具 https://help.aliyun.com/document_detail/121541.html。" 1>&2
exit 1
fi

DOMAIN=$(expr match "$CERTBOT_DOMAIN" '.*\.\(.*\..*\)')
SUB_DOMAIN=$(expr match "$CERTBOT_DOMAIN" '\(.*\)\..*\..*')

if echo $CERTBOT_DOMAIN |grep -E -q "$FLAG"; then

DOMAIN=`echo $CERTBOT_DOMAIN |grep -oP '(?<=)[^.]+('$FLAG')'`
SUB_DOMAIN=`echo $CERTBOT_DOMAIN |grep -oP '.*(?=\.[^.]+('$FLAG'))'`

fi

if [ -z $DOMAIN ]; then
DOMAIN=$CERTBOT_DOMAIN
fi
if [ ! -z $SUB_DOMAIN ]; then
SUB_DOMAIN=.$SUB_DOMAIN
fi

if [ $# -eq 0 ]; then
aliyun alidns AddDomainRecord \
--DomainName $DOMAIN \
--RR "_acme-challenge"$SUB_DOMAIN \
--Type "TXT" \
--profile AkProfile \
--Value $CERTBOT_VALIDATION
/bin/sleep 20
else
RecordId=$(aliyun alidns DescribeDomainRecords \
--DomainName $DOMAIN \
--RRKeyWord "_acme-challenge"$SUB_DOMAIN \
--Type "TXT" \
--ValueKeyWord $CERTBOT_VALIDATION \
--profile AkProfile \
| grep "RecordId" \
| grep -Eo "[0-9]+")

aliyun alidns DeleteDomainRecord \
--RecordId $RecordId
fi

分开调度,先更新证书,再重启机器,不过不建议:

1
2
3
4
5
# 凌晨1点更新证书(阿里云验证相关)
0 1 * * * certbot renew --manual --preferred-challenges dns --manual-auth-hook "alidns" --manual-cleanup-hook "alidns clean" >> /var/log/letsencrypt/letsencrypt.log

# 1点10分检查,若更新成功则重启nginx
10 1 * * * sudo -u www sh /main/shells/restart_nginx_after_certbot.sh

这个alidns需要把上面的shell放到/usr/local/bin下,创建一个别名软链接

1
2
3
4
lrwxrwxrwx 1 root root       24 Apr  8 18:21 alidns -> /usr/local/bin/alidns.sh
-rwxr-xr-x 1 root root 1193 Apr 8 18:38 alidns.sh
-rwxr-xr-x 1 tree tree 59508021 Apr 2 14:33 aliyun

以下是2019年原始博客#

下载安装cert-bot#

废话少说,现在let's encrypt是推荐在server上使用cert-bot来安装、更新我们的证书,https://certbot.eff.org/ 所以:

安装cert-bot#

1
yum install certbot python2-certbot-nginx	

获取证书#

  • 首先把我们的域名解析到当前机器的nginx上,80可以正常访问。

  • 然后获取证书有两种方式:1.直接自动安装到nginx,并由cert-bot管理nginx配置文件。 2.获取证书,但手动修改nginx配置文件

直接安装到nginx#

  • 配置环境变量:

    1
    2
    3
     ln -s /main/server/nginx/sbin/nginx /usr/bin/nginx
    ln -s /main/server/nginx/conf/ /etc/nginx
    certbot --nginx
  • 会自动识别nginx配置文件,生成nginx的证书,并修改nginx文件。这是最简单的方式。

只生成证书#

  • 只生成证书:
1
certbot certonly --nginx
  • 会让你输入邮箱、域名等信息
  • 然后去域名DNS插入一条TXT,之后会生成证书。

测试自动更新证书#

  • 配置自动更新
1
echo "0 0,12 * * * root python -c 'import random; import time; time.sleep(random.random() * 3600)' && certbot renew -q" | sudo tee -a /etc/crontab > /dev/null
  • 测试一下:

    1
    certbot renew -q --dry-run

    如果报错一个ASCII错误问题,是因为nginx的配置文件有中文。。。所以还是建议只生成证书,手动去配置nginx比较好。

  • 纯手动的certonly#

  • 适用于上面的certonly报错的时候

  •  certbot certonly --manual --email [email protected] -d *.domain.com 
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14



    ## 自动更新DNS并更新通配符证书

    - 如果还是想自动,nginx配置文件的中文也解决了的话,` certbot renew -q --dry-run`测试如果还报错:

    ```bash
    certbot renew -q --dry-run
    Attempting to renew cert (sofunnyai.com) from /etc/letsencrypt/renewal/sofunnyai.com.conf produced an unexpected error: The manual plugin is not working; there may be problems with your existing configuration.
    The error was: PluginError('An authentication script must be provided with --manual-auth-hook when using the manual plugin non-interactively.',). Skipping.
    All renewal attempts failed. The following certs could not be renewed:
    /etc/letsencrypt/live/sofunnyai.com/fullchain.pem (failure)

    大意是说缺少一个`--manual-auth-hook`,因为我们比较狠使用的通配符证书`*.sofunnyai.com`,所以此处每次更新证书需要给DNS插入一条TXT的记录。我们需要脚本自动插入这个DNS记录,我们使用的`cloudflare`作为DNS厂商,所以这里需要一个插件`certbot-dns-cloudflare`。
  • 先去CloudFlare后台申请一个局部的API令牌,只允许编辑DNS。

    image-20200719214622846

  • 然后去github找到了这个https://github.com/7sDream/certbot-dns-challenge-cloudflare-hooks 一个DNS更新脚本。

  • 下载配置dns的APIKEY

  • 依赖jq yum install jq 是一个json字符串处理的库,很小。

  • 上面的脚本一共有三个文件,因为CloudFlare的官方API修改为标准Authorization授权,但是原作者还没改。需要修改三个文件里面的http认证头:

    cat config.sh文件的http认证头:

    这个文件的主要功能是获取zones

1
2
3
4
5
6
7
8
9
10
11
12
13
CLOUDFLARE_KEY=<这里修改为你的key>
CLOUDFLARE_EMAIL=<这里修改为你的邮箱,新版已经不用了>

CHALLENGE_PREFIX="_acme-challenge"
CHALLENGE_DOMAIN="${CHALLENGE_PREFIX}.${CERTBOT_DOMAIN}"

CLOUDFLARE_ZONE=$(curl -X GET "https://api.cloudflare.com/client/v4/zones?name=${CERTBOT_DOMAIN}" \
-H "X-Auth-Email: ${CLOUDFLARE_EMAIL}" \
-H "Authorization: Bearer ${CLOUDFLARE_KEY}" \
-H "Content-Type: application/json" -s | jq -r '.result[0].id')

echo "获取zone结果=${CLOUDFLARE_ZONE}"

cloudflare-update-dns.sh文件,这个文件的主要功能是添加dns记录。但是这里也需要修改里面的http认证头:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#!/bin/bash

DIR="$(dirname "$0")"
source "$DIR/config.sh"

DNS_SERVER=8.8.8.8

echo "CHALLENGE_DOMAIN: ${CHALLENGE_DOMAIN}"
echo "CHALLENGE_VALUE: ${CERTBOT_VALIDATION}"
echo "DNS_SERVER: ${DNS_SERVER}"
echo "ZONE: ${CLOUDFLARE_ZONE}"

ADD_RECORD_RESULT=$(curl -X POST "https://api.cloudflare.com/client/v4/zones/${CLOUDFLARE_ZONE}/dns_records" \
-H "X-Auth-Email: ${CLOUDFLARE_EMAIL}" \
-H "Authorization: Bearer ${CLOUDFLARE_KEY}" \
-H "Content-Type: application/json" \
--data "{\"type\":\"TXT\",\"name\":\"${CHALLENGE_DOMAIN}\",\"content\":\"${CERTBOT_VALIDATION}\", \"ttl\": 120}" -s | jq -r "[.success, .errors[].message] | @csv")

echo "添加记录结果Add record result: ${ADD_RECORD_RESULT}"

if [[ ! $(echo "${ADD_RECORD_RESULT}" | grep "true") ]]; then
echo "添加记录失败....Add record failed, exit"
exit 1
fi

while true; do
records=$(dig -t TXT ${CHALLENGE_DOMAIN} @${DNS_SERVER} +noall +answer +short | grep "${CERTBOT_VALIDATION}")
if [[ ${records} ]]; then
break
fi
echo "等待DNS生效.....DNS records have not been propagate, sleep 10s..."
sleep 10
done

echo "DNS已经生效,DNS record have been propagated, finish"

cloudflare-clean-dns.sh,这个文件是当证书处理完毕结束后清理掉记录,修改认证头:

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

DIR="$(dirname "$0")"
source "$DIR/config.sh"

echo "DOMAIN: ${CHALLENGE_DOMAIN}"
echo "ZONE: ${CLOUDFLARE_ZONE}"

records=($(curl -X GET "https://api.cloudflare.com/client/v4/zones/${CLOUDFLARE_ZONE}/dns_records?type=TXT&name=${CHALLENGE_DOMAIN}&page=1&per_page=100" \
-H "X-Auth-Email: ${CLOUDFLARE_EMAIL}" \
-H "Authorization: Bearer ${CLOUDFLARE_KEY}" \
-H "Content-Type: application/json" -s | jq -r ".result[].id"))

echo "即将删除这些DNS记录:${records}"

for record in "${records[@]}"; do
echo "clean: $record"
curl -X DELETE "https://api.cloudflare.com/client/v4/zones/${CLOUDFLARE_ZONE}/dns_records/${record}" \
-H "X-Auth-Email: ${CLOUDFLARE_EMAIL}" \
-H "Authorization: Bearer ${CLOUDFLARE_KEY}" \
-H "Content-Type: application/json" -s | jq -r "[.success, .errors[].message] | @csv"
done
  • 测试我们的脚本

certbot renew --manual-auth-hook="/path/to/cloudflare-update-dns.sh" --manual-cleanup-hook="/path/to/cloudflare-clean-dns.sh" --post-hook="/path/to/nginx/sbin/nginx -s reload" --dry-run

如果测试通过,就可以配置定时任务了

Ubuntu18#

官网 https://certbot.eff.org/instructions?ws=nginx&os=ubuntubionic&tab=standard

1
2
3
4
5
6
7
sudo snap install --classic certbot
# certbot 2.6.0 from Certbot Project (certbot-eff✓) installed

sudo ln -s /snap/bin/certbot /usr/bin/certbot


certbot certonly --manual --email [email protected] -d *.domain.com

Docker模式#

使用docker版本的certboot在群晖中使用https证书

彩蛋#

如果你觉得上面太麻烦,这里有一个炒鸡煎蛋的方法:

登录,验证dns,下载证书即可。但是需要定期去手动更新证书下来到服务器,不如上面certbot自动化更新来得爽。

参考链接#

https://certbot.eff.org/lets-encrypt/centosrhel7-nginx

https://www.jianshu.com/p/6ea81a7b768f

https://www.jianshu.com/p/a1cc68c7d916

https://github.com/7sDream/certbot-dns-challenge-cloudflare-hooks