主题
SSL 证书从 TrustAsia 切 Let's Encrypt:四站点完整迁移
一分钱不花的 HTTPS:用 certbot webroot 模式把 TrustAsia 付费证书换成 Let's Encrypt 免费证书,webroot 验证路径被反向代理拦截是大坑。
| 字段 | 内容 |
|---|---|
| 标签 | #SSL #Let's-Encrypt #certbot #webroot #nginx #证书迁移 |
| 踩坑日期 | 2026-08-07 |
| 严重程度 | 💀 高(影响线上 HTTPS 服务) |
事故经过
背景
服务器 124.222.126.171 上有四个站点,之前用的是 TrustAsia 付费证书:
| 站点 | 域名 | 后端 | 说明 |
|---|---|---|---|
| 小雅聊天 | chat.sirenzhuli.cn | :8898 | 反向代理到 Python 后端 |
| 法眼合同 | fayan.sirenzhuli.cn | :8000 | 前端静态文件 + API 代理 |
| 知识库 | kb.sirenzhuli.cn | 无后端 | 纯静态 VitePress 站点 |
| 小雅主页 | www.sirenzhuli.cn | :8898 | 静态首页 + 反向代理 |
时间线
- 准备阶段:安装 certbot,选 webroot 模式(
certonly --webroot),因为 nginx 插件会改已有配置,风险太大 - 首发 — 知识库站点:
kb.sirenzhuli.cn是纯静态站点,webroot 是/opt/kb/docs/.vitepress/dist,一次成功 - 翻车 — 聊天站点:
chat.sirenzhuli.cn验证连续失败:返回 404。而 Let's Encrypt 服务器期望返回验证 token 字符串Invalid response from http://chat.sirenzhuli.cn/.well-known/acme-challenge/<token> - 排查发现:chat 的 nginx 配置把所有路径都 proxy_pass 到 :8898,包括
/.well-known/,后端 Python 服务不认识这个路径→404 - 逐一修复:为每个使用反向代理的站点添加
/.well-known/静态路由 - kb 站点再翻一次:kb 的 nginx 配置里证书路径用的是
/etc/nginx/ssl/下的旧 TrustAsia 证书副本,签完 Let's Encrypt 后路径没改→新证书不生效,排查浪费 20 分钟
根因分析
Let's Encrypt HTTP-01 验证原理
┌─────────────┐ ┌────────────────┐
│ certbot │ ① 生成 token 文件 │ webroot 目录 │
│ (你的服务器) │ ─────────────────→ │ /var/www/site/ │
│ │ │ .well-known/ │
│ │ │ acme-challenge/ │
│ │ │ <token> │
└──────┬───────┘ └────────────────┘
│
│ ② 告诉 Let's Encrypt: "去验证吧"
▼
┌───────────────────────────────────────────────────┐
│ Let's Encrypt 服务器 │
│ │
│ ③ HTTP GET http://your-domain.com/.well-known/ │
│ acme-challenge/<token> │
│ │
│ ④ 期望响应: token 字符串 │
│ ❌ 如果得到 404 → 验证失败 │
│ ❌ 如果得到 HTML 页面 → 验证失败 │
│ ✅ 如果得到正确的 token → 验证通过 → 签发证书 │
└───────────────────────────────────────────────────┘为什么反向代理站点会翻车
nginx 配置里如果有这样的规则:
nginx
location / {
proxy_pass http://127.0.0.1:8898; # 所有路径都转发
}那么 /.well-known/acme-challenge/xxx 这个路径也会被转发给后端服务。后端不认识→返回 404。
关键认知:location / 是"兜底规则",匹配所有路径。必须用一个更具体的 location 块来拦截 /.well-known/。
nginx location 匹配优先级:
text
1. = 精确匹配 → location = /api/
2. ^~ 前缀优先 → location ^~ /.well-known/
3. ~ 正则匹配 → location ~ \.php$
4. 普通前缀(最长) → location /.well-known/ ← 比 location / 优先级高webroot 模式的三个局限性
- 必须能写到 webroot 目录:certbot 需要在该目录下创建临时文件并写入验证 token
- nginx 必须能把请求映射到 webroot:
/.well-known/路径必须能正确对应到 webroot 下的实际目录(或用alias重定向) - 跨目录站点需逐个签发:
chat用/var/www/sirenzhuli,fayan用/opt/fayan/frontend/dist,不能共用同一个 webroot
kb 站点的证书路径陷阱
签完 Let's Encrypt 后,新证书在:
/etc/letsencrypt/live/kb.sirenzhuli.cn/fullchain.pem
/etc/letsencrypt/live/kb.sirenzhuli.cn/privkey.pem但 nginx 配置里写的还是旧路径:
nginx
ssl_certificate /etc/nginx/ssl/kb.sirenzhuli.cn-fullchain.pem; # ← 旧 TrustAsia 证书
ssl_certificate_key /etc/nginx/ssl/kb.sirenzhuli.cn-key.pem;必须改成 Let's Encrypt 的路径。这个错误排查了 20 分钟,因为 nginx -t 只检查文件是否存在(文件确实存在——旧的),不检查证书的颁发机构。
解决方案
四站点切换完整步骤
通用准备
bash
# 安装 certbot
sudo apt install certbot -y
certbot --version # 2.9.0
# 确定每个站点的 webroot(站点根目录)
# chat → /var/www/sirenzhuli (或 nginx 里配的 alias)
# fayan → /opt/fayan/frontend/dist
# kb → /opt/kb/docs/.vitepress/dist
# www → /var/www/sirenzhuli站点 1:知识库 kb.sirenzhuli.cn(纯静态 - 最简单)
bash
# 签发证书(webroot 就是站点根目录)
sudo certbot certonly --webroot \
-w /opt/kb/docs/.vitepress/dist \
-d kb.sirenzhuli.cn \
--email admin@sirenzhuli.cn \
--agree-tos \
--non-interactive
# 更新 nginx 配置中的证书路径
sudo vim /etc/nginx/sites-available/kb关键:把证书路径从 /etc/nginx/ssl/ 改成 Let's Encrypt 路径:
nginx
server {
listen 443 ssl http2;
server_name kb.sirenzhuli.cn;
# ✅ Let's Encrypt 证书
ssl_certificate /etc/letsencrypt/live/kb.sirenzhuli.cn/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/kb.sirenzhuli.cn/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
client_max_body_size 20m;
root /opt/kb/docs/.vitepress/dist;
# 保留 .well-known 路由(续期需要)
location /.well-known/acme-challenge/ {
root /opt/kb/docs/.vitepress/dist;
}
location / {
try_files $uri $uri/ /404.html;
}
}
server {
listen 80;
server_name kb.sirenzhuli.cn;
return 301 https://$server_name$request_uri;
}站点 2:法眼 fayan.sirenzhuli.cn(前端 + API 代理)
bash
# 签发
sudo certbot certonly --webroot \
-w /opt/fayan/frontend/dist \
-d fayan.sirenzhuli.cn \
--email admin@sirenzhuli.cn \
--agree-tos \
--non-interactivenginx 配置关键——在 location / 之前加 /.well-known/ 路由:
nginx
server {
listen 443 ssl http2;
server_name fayan.sirenzhuli.cn;
ssl_certificate /etc/letsencrypt/live/fayan.sirenzhuli.cn/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/fayan.sirenzhuli.cn/privkey.pem;
root /opt/fayan/frontend/dist;
index index.html;
client_max_body_size 50m;
# ✅ 关键:.well-known 必须优先于 location /
location ^~ /.well-known/acme-challenge/ {
root /opt/fayan/frontend/dist;
}
location / {
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://127.0.0.1:8000/api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}站点 3:聊天 chat.sirenzhuli.cn(纯反向代理 - 最坑)
bash
# 签发(webroot 用统一的 /var/www/sirenzhuli)
sudo certbot certonly --webroot \
-w /var/www/sirenzhuli \
-d chat.sirenzhuli.cn \
--email admin@sirenzhuli.cn \
--agree-tos \
--non-interactivenginx 配置——必须显式拦截 /.well-known/,不让他走到 proxy_pass:
nginx
server {
listen 443 ssl http2;
server_name chat.sirenzhuli.cn;
ssl_certificate /etc/letsencrypt/live/chat.sirenzhuli.cn/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/chat.sirenzhuli.cn/privkey.pem;
client_max_body_size 20m;
# ✅ 关键:第一个 location 块,把 .well-known 拦住
location /.well-known/acme-challenge/ {
alias /var/www/sirenzhuli/.well-known/acme-challenge/;
}
# 其他请求走代理
location / {
proxy_pass http://127.0.0.1:8898;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 120s;
proxy_buffering off;
}
# WebSocket
location /ws/ {
proxy_pass http://127.0.0.1:8898;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}⚠️
aliasvsroot的区别:用alias /path/.well-known/acme-challenge/会直接映射路径;用root /path会把 URL 路径拼到 root 后面。反向代理站点建议用alias,更直观。
站点 4:小雅主页 www.sirenzhuli.cn(混合模式)
bash
# 可以合并签发(SAN 证书,一个证书覆盖两个域名)
sudo certbot certonly --webroot \
-w /var/www/sirenzhuli \
-d www.sirenzhuli.cn \
-d sirenzhuli.cn \
--email admin@sirenzhuli.cn \
--agree-tos \
--non-interactive全部签发后的收尾
bash
# 1. 更新所有 nginx 配置(证书路径 + .well-known 路由)
# → 逐个 vim 改
# 2. 语法检查
sudo nginx -t
# 3. 重载
sudo systemctl reload nginx
# 4. 验证每个站点的证书
for domain in chat fayan kb www; do
echo "=== ${domain}.sirenzhuli.cn ==="
echo | openssl s_client -servername ${domain}.sirenzhuli.cn \
-connect ${domain}.sirenzhuli.cn:443 2>/dev/null \
| openssl x509 -noout -dates -issuer | head -3
done预期每个站点输出:
notBefore=Aug 7 xx:xx:xx 2026 GMT
notAfter=Nov 5 xx:xx:xx 2026 GMT
issuer=CN = R3, O = Let's Encrypt自动续期验证
bash
# certbot 安装时自动注册了 systemd timer
sudo systemctl status certbot.timer
# 手动测试续期流程(--dry-run 不会真续期)
sudo certbot renew --dry-run
# 如果 --dry-run 失败,说明 .well-known 路由配置有问题,先修 nginx
# 常见失败原因:
# 1. location / 吃掉了 /.well-known/ → 加 ^~ 前缀
# 2. webroot 路径不存在 → mkdir 创建
# 3. 权限问题 → chown www-data:www-data
# 查看所有证书的到期时间
sudo certbot certificates预防措施
新站点上架 SSL 检查清单
1. □ 用 certbot certonly --webroot 签 Let's Encrypt 免费证书
2. □ nginx 配置中 ssl_certificate 指向 /etc/letsencrypt/live/<domain>/
3. □ nginx 配置中必须保留 .well-known 静态路由(用 ^~ 前缀优先)
4. □ 改完 nginx 跑 nginx -t && systemctl reload nginx
5. □ openssl 验证证书签发者和有效期
6. □ certbot renew --dry-run 确认续期流程 OKcertbot webroot 模式避坑要点
| 坑 | 表现 | 解法 |
|---|---|---|
| 反向代理拦截 | 验证返回 404 | 加 location ^~ /.well-known/ 在 location / 之前 |
| webroot 路径错了 | certbot 报 permission denied | 确认 webroot 目录存在且有写权限 |
| 签完后 nginx 路径没改 | HTTPS 还显示旧证书 | 检查 ssl_certificate 指向新路径 |
--nginx 插件乱改配置 | 已有配置被覆盖 | 别用 --nginx,用 certonly --webroot |
关联知识点
- nginx 配置不生效排故 — symlink 陷阱与 reload 指令区别
- SSL/TLS 基础知识 — 证书链、TLS 握手、HSTS
- 前端排故方法论 — DNS→端口→服务→API→功能 五步排查
- Linux 进程管理 — systemctl 管理 nginx 服务
加载练习题中...
🎯 本章要点
- 反向代理站点需为 .well-known 加单独 location 不走代理
- certbot renew 建议每天两次(凌晨+中午)通过 cron 自动执行