Traefik 改用 acme.sh 为域名生成证书

Traefik 踩坑记录.

Traefik 可以配置自动生成证书的 ACME 供应商, 比如Let’s Encrypt.

官方文档提到会自动更新证书. 但实际情况是, 到期了证书有时并没有更新, 导致出现证书过期的情况.

这里记录下在服务器上配置 Traefik 时, 改用 acme.sh 来签证书并自动更新的步骤.

前置条件

默认服务器为Ubuntu, traefik 已启用, 对应的docker-compose.yml 存放在/opt/global目录下.

其中/opt/global/docker-compose.yml 的具体内容如下.

version: '3'

services:
traefik:
image: traefik:v2.2
container_name: "traefik"
restart: unless-stopped
ports:
- "8080:8080"
- "80:80"
- "443:443"
networks:
- my_proxy
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./traefik.toml:/traefik.toml
- ./providers:/providers
- ./acme.json:/acme.json
networks:
my_proxy:
external: true

我们挂载了/opt/global 目录下的 traefik.toml, 该文件存放了一些默认的配置, 内容如下:

[entryPoints]
[entryPoints.web]
address = ":80"

[entryPoints.websecure]
address = ":443"

[entryPoints.traefik]
address = ":8080"

[api]
insecure = true
dashboard = true

[log]
level= "DEBUG"

[certificatesResolvers]
[certificatesResolvers.lets-encrypt]
[certificatesResolvers.lets-encrypt.acme]
email = "[email protected]"
storage = "acme.json"
[certificatesResolvers.lets-encrypt.acme.tlsChallenge]

[providers.docker]
watch = true
exposedByDefault=false
network = "my_proxy"

[providers.file]
watch = true
directory="/providers"

可以看到我们声明了一个证书解析器 (certificatesResolvers) lets-encrypt.

而 我们需要使用 acme.sh 生成证书的 Web项目, 存放在/opt/web/ 下, 其 docker-compose.yml 内容如下:

version: '3'

services:
my-web:
image: my-docker-image
restart: unless-stopped
......
labels:
- "traefik.enable=true"
- "traefik.http.routers.my-web-http.rule=Host(`example.com`, `api.example.com`) && PathPrefix(`/`)"
- "traefik.http.routers.my-web-http.entrypoints=web"
- "traefik.http.routers.my-web-http.middlewares=my-web-https"
- "traefik.http.middlewares.my-web-https.redirectscheme.scheme=https"
- "traefik.http.routers.my-web.rule=Host(`example.com`, `api.example.com`) && PathPrefix(`/`)"
- "traefik.http.routers.my-web.entrypoints=websecure"
- "traefik.http.routers.my-web.tls.certresolver=lets-encrypt"
- "traefik.http.services.my-web-http.loadbalancer.server.port=80"
networks:
- my_proxy

networks:
my_proxy:
external: true

这里可以看到, 我们给 web 配置的 certresolver 配置为之前定义的 lets-encrypt.

如果查看 acme.json 文件, 你会看到类似如下内容, Certificates 部分有我们的 example.com 及子域名api.example.com :

{
"lets-encrypt": {
"Account": {
"Email": "[email protected]",
"Registration": {
"body": {
"status": "valid",
"contact": [
"mailto:[email protected]"
]
},
"uri": "https://acme-v02.api.letsencrypt.org/acme/acct/xxxxx"
},
"PrivateKey": "xxxx",
"KeyType": "4096"
},
"Certificates": [
{
"domain": {
"main": "example.com",
"sans": ["api.example.com"]
},
"certificate":"xxxx",
"key": "xxxxxx",
"store": "default"
},
{
"domain": {
"main": "example1.com"
},
"certificate":"xxxx",
"key": "xxxxxx",
"store": "default"
},
.........
]
}
}

此时, 网站正常访问.

现在我们来更改 example.com 的 tls 配置, 证书改用 acme.sh 来签发.

改用 acme.sh

acme 验证的主要方式是 standalone 和 webroot.

鉴于 standalone 需要占用80或者443端口, 导致需要暂停服务器,这里我们使用 webroot 方式来验证域名.

webroot 模式下, acme 会在网站根目录下生成一个临时子目录 .well-known/acme-challenge,然后服务器会向这个路径发请求,如果请求成功,则验证通过, 随后会删除掉这个临时目录.

我们将根目录web root 默认为 /var/www, 同时在当前目录下, 新增两个文件夹, 用来分别挂载 /var/www 和 /acme.sh 目录.

/opt/global/ 目录下, 新建文件 docker-compose.acme.sh.yml, 内容如下:

version: '3'

services:
acme.sh:
image: neilpang/acme.sh:3.0.0
container_name: acme.sh
volumes:
- ./nginx:/var/www
- ./acme.sh:/acme.sh
command: daemon

新增文件夹:

mkdir nginx acme.sh

生成的证书会存放在acme.sh 目录中, 而 nginx 目录用于验证域名.

这里我们起一个nginx 服务, 使得 traefik 将所有匹配 /.well-known 的路径请求都转发到这个nginx 上. 同时, 将当前目录下的 nginx 目录挂载到 nginx 服务的静态文件默认地址 /usr/share/nginx/html .

这样, 当使用 webroot 模式验证域名时, acme.sh 就会在 /opt/global/nginx 目录下,生成临时子目录 .well-known/acme-challenge, 而验证时, 访问 /usr/share/nginx/html, 也就是访问我们的 /opt/global/nginx 目录.

我们新增文件 /opt/global/docker-compose.nginx.yml , 添加如下内容:

version: '3'

services:
nginx:
image: nginx:latest
labels:
- "traefik.enable=true"
- "traefik.http.routers.global-nginx.rule=HostRegexp(`{catch_all:.*}`) && PathPrefix(`/.well-known`)"
- "traefik.http.routers.global-nginx.priority=999"
- "traefik.http.services.global-nginx.loadbalancer.server.port=80"
volumes:
- ./nginx:/usr/share/nginx/html
networks:
- my_proxy

networks:
my_proxy:
external: true

这里有一点需要提一下.

我们给 nginx 服务配置 traefik 时, 设置了priority的值为999. 默认情况下, traefik 中路径的 priority是根据rule的长度来决定的. 这里我们希望所有带有 /.well-known 的请求都转发到这个 nginx 上, 所以手动设置了999.

启动 nginx:

docker-compose --file docker-compose.nginx.yml up -d

nginx 启动后, 我们启动 acme.sh.

docker-compose --file docker-compose.acme.sh.yml up -d

开始签证书啦.

第一次使用acme.sh时, 需要注册用户邮箱:

docker exec -it acme.sh sh -l
acme.sh --register-account -m [email protected]
acme.sh --issue -d example.com -d api.example.com --webroot /var/www

正常情况下, 会一切顺利. 默认CA是ZeroSSL.com

如果 acme.sh 签发证书时 Timeout了, 请设置代理.

更新配置

证书签发成功后, 我们查看 /opt/global/acme.sh/example.com 目录, 会看到刚刚生成的证书.

现在, 我们把新鲜出炉的证书用起来.

修改 /opt/global/docker-compose.yml , 将 /opt/global/acme.sh 目录挂载到 traefik 中:

version: '3'

services:
traefik:
image: traefik:v2.2
container_name: "traefik"
......
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./traefik.toml:/traefik.toml
- ./providers:/providers
- ./acme.json:/acme.json
- ./acme.sh:/acme.sh

然后我们在traefik 的动态配置中把证书放进去.

/opt/global/providers 目录下, 新增 example_com_certs.toml 文件, 写入我们的证书路径:

[[tls.certificates]]
certFile = "/acme.sh/example.com/fullchain.cer"
keyFile = "/acme.sh/example.com/example.com.key"

同时, 我们需要将 /opt/global/acme.json 文件中 Certificates 部分, 有关 example.com 的证书内容删除.

最后, 修改 /opt/web/docker-compose.yml, 把tls 部分的配置, 改为options=default.

version: '3'

services:
my-web:
image: my-docker-image
restart: unless-stopped
......
labels:
......
- "traefik.http.routers.my-web.entrypoints=websecure"
- "traefik.http.routers.my-web.tls.options=default"
- "traefik.http.services.my-web-http.loadbalancer.server.port=80"

重启 traefik.

docker restart traefik

再次请求 https://example.com, 会发现证书已经签成由ZeroSSL 颁发的证书.

替换完成. 撒花! 🥳

参考

traefik doc

traefick: let’s Encrypt

acme.sh