Skip to content

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静态首页 + 反向代理

时间线

  1. 准备阶段:安装 certbot,选 webroot 模式(certonly --webroot),因为 nginx 插件会改已有配置,风险太大
  2. 首发 — 知识库站点kb.sirenzhuli.cn 是纯静态站点,webroot 是 /opt/kb/docs/.vitepress/dist,一次成功
  3. 翻车 — 聊天站点chat.sirenzhuli.cn 验证连续失败:
    Invalid response from http://chat.sirenzhuli.cn/.well-known/acme-challenge/<token>
    返回 404。而 Let's Encrypt 服务器期望返回验证 token 字符串
  4. 排查发现:chat 的 nginx 配置把所有路径都 proxy_pass 到 :8898,包括 /.well-known/,后端 Python 服务不认识这个路径→404
  5. 逐一修复:为每个使用反向代理的站点添加 /.well-known/ 静态路由
  6. 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 模式的三个局限性

  1. 必须能写到 webroot 目录:certbot 需要在该目录下创建临时文件并写入验证 token
  2. nginx 必须能把请求映射到 webroot/.well-known/ 路径必须能正确对应到 webroot 下的实际目录(或用 alias 重定向)
  3. 跨目录站点需逐个签发chat/var/www/sirenzhulifayan/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-interactive

nginx 配置关键——在 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-interactive

nginx 配置——必须显式拦截 /.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";
    }
}

⚠️ alias vs root 的区别:用 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 确认续期流程 OK

certbot webroot 模式避坑要点

表现解法
反向代理拦截验证返回 404location ^~ /.well-known/location / 之前
webroot 路径错了certbot 报 permission denied确认 webroot 目录存在且有写权限
签完后 nginx 路径没改HTTPS 还显示旧证书检查 ssl_certificate 指向新路径
--nginx 插件乱改配置已有配置被覆盖别用 --nginx,用 certonly --webroot

关联知识点

加载练习题中...

🎯 本章要点

  • 反向代理站点需为 .well-known 加单独 location 不走代理
  • certbot renew 建议每天两次(凌晨+中午)通过 cron 自动执行