
背景
VPS 上已经有一个 api.golforfun.net 的 HTTPS 服务,现在要加一个新的 rag.helloai.jp,同一台机器、同一个 IP 162.43.92.249、同一个 443 端口。不需要新 IP,不需要停旧服务,靠 SNI 区分。
这篇记录 DNS、SSL、SNI 三层是怎么配合的,以及这次踩到的坑。
一、DNS:子域指向哪台服务器
helloai.jp 的权威 NS 是 XServer(ns1.xserver.jp 到 ns5.xserver.jp),所以 A 记录要在 XServer サーバーパネル → ドメイン設定 → DNSレコード設定 加,不要在 Xdomain 面板加——Xdomain 和 XServer 是两套独立的 DNS 区域,Xdomain 加的记录 XServer 看不到。
加的记录:
rag.helloai.jp A 162.43.92.249 TTL 3600
坑一:泛解析 *.helloai.jp 干扰
rag.helloai.jp 之前解析到 85.131.207.20(WordPress 那台),因为存在 *.helloai.jp 的泛解析。删掉泛解析后,新加的 rag A 记录才生效。
验证:
dig +short rag.helloai.jp @ns1.xserver.jp # 权威 NS,立即生效 dig +short rag.helloai.jp @8.8.8.8 # Google DNS,可能延迟 dig +short rag.helloai.jp @1.1.1.1 # Cloudflare DNS
三条都返回 162.43.92.249 才算传播完成。
坑二:本地 DNS 缓存
VPS 上 dig 返回新 IP,但 Mac 本地 curl 还打到旧 IP(XServer 的「無効なURLです」页面)。清 Mac 缓存:
sudo dscacheutil -flushcache sudo killall -HUP mDNSResponder
如果还不行,临时在 /etc/hosts 加 162.43.92.249 rag.helloai.jp,绕过 DNS。验证完删掉。
二、SSL:证书怎么签
用 Let’s Encrypt,DNS-01 验证。不用 HTTP-01,因为:
- HTTP-01 需要 A 记录先传播到公共 DNS,XServer 传播慢,certbot 可能打到旧 IP 404。
- DNS-01 验证的是
_acme-challenge.rag.helloai.jp的 TXT 记录,和 A 记录独立,A 记录没传播也能签。
签证书
certbot certonly --manual --preferred-challenges dns -d rag.helloai.jp
certbot 打印 TXT 值,去 XServer 面板加:
- 主机名:
_acme-challenge.rag(XServer 自动补.helloai.jp) - 类型:TXT
- 值:certbot 打印的那串
坑三:TXT 记录的格式
主机名写 _acme-challenge.rag,不要写 _acme-challenge.rag.helloai.jp,XServer 会拼成 _acme-challenge.rag.helloai.jp.helloai.jp。
加完验证:
dig +short TXT _acme-challenge.rag.helloai.jp @ns1.xserver.jp dig +short TXT _acme-challenge.rag.helloai.jp @8.8.8.8
两条都返回 "0lwiHRqAMf32FOTALxqgxMUZ-v15EMuAyGHmL27BKhw"(certbot 打印的值)后,回 certbot 按回车。
坑四:–manual 不自动续期
签成功后 certbot 会提示:
This certificate will not be renewed automatically. Autorenewal of --manual certificates requires the use of an authentication hook script (--manual-auth-hook) but one was not provided.
证书 2027-01-20 到期,提前一周手动重跑 certbot certonly --manual --preferred-challenges dns -d rag.helloai.jp,加新的 TXT(每次值都变,旧的删掉)。
坑五:–manual 不能加 –non-interactive
加了会报 An authentication script must be provided with --manual-auth-hook。必须交互式,手动加 TXT 后回车。
三、SNI:同一 IP 上多个证书
为什么需要 SNI
TLS 握手发生在 HTTP 请求之前,服务器在握手时就要出示证书,但那时它还不知道客户端要访问哪个域名。没有 SNI,一个 IP:443 只能配一张证书,这张证书的 SAN 必须包含这个 IP 上所有域名。
SNI(Server Name Indication)让客户端在 TLS 握手的 ClientHello 里带 server_name 扩展,nginx 在握手阶段就根据这个字段选对应的证书。
nginx 配置
两个域名,两个 server block,同一个 listen 地址和端口:
# 已有的
server {
listen 162.43.92.249:443 ssl;
server_name api.golforfun.net;
ssl_certificate /etc/letsencrypt/live/api.golforfun.net/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.golforfun.net/privkey.pem;
...
}
# 新加的
server {
listen 162.43.92.249:443 ssl;
server_name rag.helloai.jp;
ssl_certificate /etc/letsencrypt/live/rag.helloai.jp/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/rag.helloai.jp/privkey.pem;
...
}
nginx 允许同一个 listen 地址上多个 server block,因为 server_name 不同。客户端访问 rag.helloai.jp 时,SNI 带 rag.helloai.jp,nginx 选第二个 block,出示 rag.helloai.jp 的证书。
坑六:必须绑具体 IP,不能绑 0.0.0.0:443
这台 VPS 的 100.99.4.120:443 被 tailscaled 占用(Tailscale 内网)。nginx 绑 0.0.0.0:443 会和 tailscaled 冲突(Linux 内核里 0.0.0.0 和具体 IP 的绑定冲突)。
必须绑具体公网 IP:
listen 162.43.92.249:443 ssl;
坑七:UFW 没放行 443
ufw status | grep 443
如果没放行,ufw allow 443/tcp。否则外部 curl 直接 HTTP 000(连接超时)。
坑八:proxy_read_timeout 默认 60s 超时
RAG API 的 LLM 推理 30 秒–2 分钟,nginx 默认 proxy_read_timeout 60s 会在模型响应前超时,返回 504。
location /api/ {
proxy_pass http://127.0.0.1:8093;
proxy_read_timeout 300s;
proxy_connect_timeout 10s;
}
坑九:default_server 和旧客户端
如果客户端不支持 SNI(很老的 Java 6、Windows XP IE),nginx 会用 default_server 的证书。默认是第一个 listen 的 block。这台 VPS 上第一个是 api.golforfun.net,所以旧客户端访问 rag.helloai.jp 会收到 api.golforfun.net 的证书,报错。
WordPress 的 wp_remote_post 底层是 curl,现代 curl 都支持 SNI,没问题。但如果要兼容极旧客户端,可以在默认 block 放一张 SAN 包含两个域名的证书,或者显式设 default_server 到一个返回 444 的 block。
四、验证
证书是否正确签发
ls -la /etc/letsencrypt/live/rag.helloai.jp/
应该有 fullchain.pem、privkey.pem、cert.pem、chain.pem。
SNI 是否正确路由
openssl s_client -connect 162.43.92.249:443 -servername rag.helloai.jp </dev/null 2>/dev/null | openssl x509 -noout -subject openssl s_client -connect 162.43.92.249:443 -servername api.golforfun.net </dev/null 2>/dev/null | openssl x509 -noout -subject
预期:
subject=CN = rag.helloai.jp subject=CN = api.golforfun.net
如果两条显示同一个证书,说明 SNI 没生效,或者两个 block 的 listen 写错了。
端到端测试
curl -s https://rag.helloai.jp/api/health
curl -s -X POST https://rag.helloai.jp/api/ask \
-H "Content-Type: application/json" \
-d '{"text":"Net Sales 是多少","top_k":5}' | head -c 300
/api/health 返回 {"status":"ok"},/api/ask 返回 answer + sources,说明 nginx + HTTPS + SNI 都通了。
确认旧服务没被影响
curl -s -X POST https://api.golforfun.net/api/golf-rules \
-H "Content-Type: application/json" \
-d '{"text":"红桩怎么抛球?罚几杆?"}' | head -c 200
返回规则答案(哪怕缓存命中),说明旧服务没被新 nginx 配置影响。
五、总结
| 层 | 关键点 |
|---|---|
| DNS | XServer 面板加 A 记录,注意泛解析干扰,本地 DNS 缓存要清 |
| SSL | DNS-01 验证,TXT 主机名写 _acme-challenge.rag,–manual 不自动续期 |
| SNI | 同一 IP:443 多个 server block,listen 绑具体公网 IP,不绑 0.0.0.0 |
| nginx | proxy_read_timeout 300s,UFW 放行 443 |
| 验证 | openssl s_client -servername 看证书 CN,curl 测端到端 |
一句话:同一台 VPS 跑多个 HTTPS 域名,DNS 各自解析到同一个 IP,证书各自签,nginx 靠 SNI 在 TLS 握手阶段按 server_name 选证书。不需要新 IP,不需要停旧服务。
参考配置:/etc/nginx/sites-available/rag.helloai.jp.conf、/etc/letsencrypt/live/rag.helloai.jp/。
已知遗留:证书 2027-01-20 到期,提前一周手动重跑 certbot,加新的 TXT 记录,旧的删掉。


讨论
订阅评论 RSS加入讨论
分享你的想法与经验,也可以登录后参与讨论。
使用 WordPress 登录