953 字
5 分钟
一次因为证书挂载出错引发的连锁反应

今天踩了一个坑,起因是博客突然无法访问了,浏览器报错 526 Invalid SSL certificate。从这次排查中,我对容器挂载、Let’s Encrypt 的证书目录结构、Cloudflare 的 TLS 模式都有了更深入的理解。


背景#

我用 Podman 启动了一个 nginx 容器,承载个人博客,通过 Let’s Encrypt 签发证书,并挂在 Cloudflare 后面,使用了 Full (Strict) 模式。这意味着:

不仅客户端到 Cloudflare 要走 TLS,Cloudflare 到源站也必须使用一个有效的 CA 签发的证书。 一旦证书失效,Cloudflare 直接拒绝中转,返回 526 错误。


问题出现:Cloudflare 返回 526#

我的第一个排查点是 nginx 配置,确认并无问题。接着查看容器日志,发现 podman logs 输出太长,尝试 --tail 100 却无效(原因是容器已经崩溃退出)。

索性我直接删了容器,更新宿主机证书,再重建:

podman run -d \\
  --name nginx-blog \\
  -p 80:80 -p 443:443 \\
  -v /home/action/web_service/nginx.conf/nginx.conf:/etc/nginx/conf.d/nginx.conf:ro \\
  -v /etc/letsencrypt/live/bloginfo.blog:/etc/letsencrypt/live:ro,z \\
  -v /etc/letsencrypt/archive/bloginfo.blog:/etc/letsencrypt/archive/bloginfo.blog:ro,z \\
  -v /home/action/web_service/blog/:/usr/share/nginx/html:ro \\
  docker.io/library/nginx:latest

然而容器启动瞬间退出,再次查看日志:

[emerg] 1#1: cannot load certificate "/etc/letsencrypt/live/fullchain.pem": BIO_new_file() failed

这说明 nginx 无法读取证书文件 fullchain.pem,而不是配置错误。

初步排查:文件确实存在#

我回到宿主机,查看 /etc/letsencrypt/live,证书文件一切正常:

lrwxrwxrwx 1 root root 42 fullchain.pem -> ../../archive/bloginfo.blog/fullchain3.pem

这是 Let’s Encrypt 的常规做法 —— live/ 下是软链接,实际文件放在 archive/ 中。

于是我怀疑是容器对软链接解析失败,但由于 nginx 容器无法 exec 进入,我新建了一个 Alpine 容器来测试:

podman run --rm -it \\
  -v /etc/letsencrypt/live/bloginfo.blog:/etc/letsencrypt/live:ro,z \\
  docker.io/library/alpine:latest sh

进入容器后

/ # ls -l /etc/letsencrypt/live
fullchain.pem -> ../../archive/bloginfo.blog/fullchain3.pem

路径看起来也没问题。但是一旦用 openssl 打开它

/ # openssl x509 -in /etc/letsencrypt/live/fullchain.pem -noout -text
Could not open file... No such file or directory

证据确凿:容器中软链接的目标文件不存在


根因分析:软链接无法解析#

仔细思考后发现问题关键在于:

容器中 fullchain.pem 虽然存在,但它是一个相对路径软链接,指向容器内并不存在的文件。

Let’s Encrypt 的证书结构如下:

/etc/letsencrypt/
├── archive/
   └── bloginfo.blog/
       ├── fullchain3.pem
├── live/
   └── bloginfo.blog/
       └── fullchain.pem -> ../../archive/bloginfo.blog/fullchain3.pem

这意味着如果只挂载 live/ 而没有 archive/,容器中的软链接就是“断链”的,打不开。

所以正确方式要么是挂上 archive/,要么干脆复制实际证书文件:

mkdir -p /opt/certs/bloginfo.blog
cp /etc/letsencrypt/live/bloginfo.blog/*.pem /opt/certs/bloginfo.blog/

然后挂载 /opt/certs/bloginfo.blog 到容器中。


补充问题:dhparams.pem 也缺失#

重启 nginx 后,又遇到了这个报错:

[emerg] 1#1: BIO_new_file("/etc/letsencrypt/live/dhparams.pem") failed

原来我在 nginx.conf 中配置了:

ssl_dhparam /etc/letsencrypt/live/dhparams.pem;

但这个文件并不是 Let’s Encrypt 默认生成的,而是需要我手动通过如下命令生成:

openssl dhparam -out /etc/letsencrypt/live/dhparams.pem 2048

生成后重启 nginx,终于一切恢复正常。


最终反思#

这次事件暴露出以下几个方面的问题:

  1. 不了解 Let’s Encrypt 的证书结构

    • 忽略了软链接指向 archive 的逻辑
  2. 容器挂载路径不完整

    • live/ 的软链接在容器内失效,除非同时挂载 archive,或复制实际文件
  3. nginx.conf 硬编码依赖未准备的文件

    • 如 dhparams.pem 需要显式生成,不然容器无法启动
  4. 缺乏对容器运行失败的诊断手段

    • 忽略 podman logs 的正确使用,未设置合理的日志查看与监控机制

Checklist:证书部署到容器的注意事项#

  • 宿主机证书更新后是否同步到容器?
  • 软链接是否在容器中可达?
  • nginx 是否依赖 dhparams.pem 等额外文件?
  • Cloudflare TLS 模式是否匹配服务端证书?
  • 容器重启失败是否能第一时间定位问题?

一次小错误,一次大教训。记录下来,防止未来再掉坑。

一次因为证书挂载出错引发的连锁反应
https://fuwari.vercel.app/posts/dumb-mistake/dumb-mistake/
作者
Kevin
发布于
2025-06-15
许可协议
CC BY-NC-SA 4.0