使用Docker容器签发和自动续期Let's Encrypt证书

从一开始的 Apache 部署单站点开箱即用到后来的多站点配置,再到后来多开发语言混合、项目环境隔离、负载均衡等,单独的 Apache 服务器已经无法独立满足需求了。这时候开始考虑在前面加上一个 Nginx 作流量分发等等工作。

随着运营商的越来越无节操,以及网络安全威胁日益严重,很多站点开始考虑上 HTTPS,即加上证书来保证通道的安全和可靠性。 Let’s Encrypt 提供了免费的 SSL 证书并提供了脚本化方案。无论是 Apache 还是 Nginx,网上都已经有了很多的文档,但是个人喜欢使用 Docker 来部署所有应用,那么如何在不修改原有镜像的基础上实现 SSL 证书的自动签发和续期就是一个需要解决的问题了。

同样的,本文章旨在使用 Docker 来完成上述任务,不需要安装新的软件或修改原有应用,以给一个运行在 Apache 容器中的站点申请和续期 SSL 证书为例,假设该站点的域名为 example.com

演示环境说明

首先介绍一下本文的环境:

  • 有一个 Nginx 容器,监听 80/443 端口,作为前端的流量转发和负载均衡;
  • 若干 Apache、NodeJS 等容器,分别对应不同的站点;
  • 均部署在同一台服务器上;
  • Nginx 的持久化数据存储在主机的 /data/nginx/conf.d 目录,对应的容器目录为 /etc/nginx/conf.d
  • Apache 的持久化数据存储在主机的 /data/apache 目录,对应的容器目录为 /var/www/html

启动 Apache 服务器

假设 Apache 容器的启动命令如下

1
2
3
4
5
6
docker service create \
--replicas 1 \
--name apache \
--network swarm-net \
--mount type=bind,source=/data/apache/,target=/var/www/html,readonly \
newnius/php:7.1

添加 nginx 配置

修改 /data/nginx/conf.d 目录中 example.com 站点的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# example-com.conf
server {
listen 80;
server_name example.com;

location / {
proxy_pass http://apache:80;
proxy_set_header Host $host;
}
location ~ /.well-known {
allow all;
proxy_pass http://apache:80;
}

# Deny all attempts to access hidden files such as .htaccess, .htpasswd, .DS_Store(Mac).
location ~ /\. {
deny all;
}
}

主要是添加以下几行,因为证书签发过程中会将验证问进放在网站的 /.well-known 目录下,而 Nginx 会默认禁止访问以点号开头的隐藏文件/文件夹,需要手动允许。

1
2
3
4
location ~ /.well-known {
allow all;
proxy_pass http://apache:80;
}

启动 Nginx

修改完站点配置之后,需要刷新 Nginx,这里直接采用重启的方式进行配置文件的刷新工作。

首先要在主机上创建一个目录用来存储签发的证书,假设为 /data/ssl,映射到容器的 /etc/letsencrypt 目录。

假定 Nginx 的启动脚本如下:

1
2
3
4
5
6
7
8
9
docker service create \
--replicas 1 \
--name nginx \
--network swarm-net \
--publish mode=host,published=80,target=80 \
--publish mode=host,published=443,target=443 \
--mount type=bind,src=/data/nginx/conf.d,dst=/etc/nginx/conf.d,readonly \
--mount type=bind,src=/data/ssl/,dst=/etc/letsencrypt/,readonly \
nginx

签发 certbot

将主机的 /data/ssl 目录映射到 certbot 容器的 /etc/letsencrypt 目录

使用以下命令来申请 SSL 证书。

1
2
3
4
docker run -it --rm --name certbot \
-v "/data/ssl:/etc/letsencrypt" \
-v "/data/apache/:/var/www/html" \
certbot/certbot certonly -n --no-eff-email --email admin@example.com --agree-tos --webroot -w /var/www/html -d example.com

用你自己的信息替换其中的值。

如果一切正常的话,你可以看到类似下图的输出,表明 SSL 证书已经完成签发并下载到了主机的 /data/ssl 目录。

Output

修改 nginx 配置

在初次完成 SSL 证书签发任务后,我们以后就可以使用 https 访问了,修改之前的 nginx 配置文件 example-com.conf,将 http 流量重定向到 https 协议并配置 ssl 证书。

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
server {
listen 80;
server_name example.com;
return 301 https://example.com$request_uri;
}
server {
listen 443 ssl;
server_name example.com;

ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

location / {
proxy_pass http://apache:80;
proxy_set_header Host $host;
}
location ~ /.well-known {
allow all;
proxy_pass http://apache:80;
}

# Deny all attempts to access hidden files such as .htaccess, .htpasswd, .DS_Store(Mac).
location ~ /\. {
deny all;
}

根据具体的信息修改以上配置,修改完成后重启 nginx 容器,再次访问 http://example.com,可以发现自动跳转到了 https://example.com 并且可以看到站点被加上了小绿锁^_^。

自动续期

至此,SSL 证书的签发和证书部署就完成了,因为 Let’s Encrypt 的证书有效期是 3 个月,到期之前需要续期,下面我们用定时任务来实现这个。

续期证书可以使用上述用来签发证书的命令且不需要修改,只需在执行之后重启 Nginx 容器即可。

创建文件 /crontab/renew_cert.sh,文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Renew Certificate
docker run -it --rm --name certbot \
-v "/data/ssl:/etc/letsencrypt" \
-v "/data/apache/:/var/www/html" \
certbot/certbot certonly -n --no-eff-email --email admin@example.com --agree-tos --webroot -w /var/www/html -d example.com


# reload nginx

id=$(docker service ps nginx --no-trunc | sed -n '2p' | awk '{print $1}')

name="nginx.1.$id"

docker kill $name

添加可执行权限

1
chmod +x renew_cert.sh

以上命令在续期的基础上添加了重启 Nginx 容器的功能,这样只需定期执行这个脚本就可以完成续期的目的。

再进一步,让系统自动定期执行这条命令,这样部署好之后,我们就无需关心证书有效期的问题了。

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

1
crontab -e

添加以下行

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

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

续期脚本的执行结果如下图所示

Renew

至此,部署完成!

其他说明

  1. Nginx 刷新配置文件的命令是 nginx reload,但是由于 Docker 的特性,这一命令会造成容器的退出,所以我们直接重启容器也可以。
  2. 为了保持篇幅简短,文中所用的命令,尤其时 Nginx 的站点配置都十分精简,仅供参考,不适合直接用于正式环境。
  3. 本文中使用 Docker Swarm 部署 Nginx 和 Apache,采用其他方式部署的需要适当修改命令。

参考

CentOS 7 Nginx配置Let’s Encrypt SSL证书

免费多域名SSL证书Let’s Encrypt使用教程

Certbot documentation

crontab 定时任务