有证书不等于走 HTTPS:一次被误报为”缺证书”的排查
有证书不等于走 HTTPS:一次被误报为”缺证书”的排查
站内搜索
直接问 AI

有证书不等于走 HTTPS:一次被误报为”缺证书”的排查

有人跟我说某个子域名”没有 SSL 证书”。我去查,证书好好的——一张通配符证书覆盖着它,剩余有效期两个多月。

但那次排查确实查出了问题,而且比”缺证书”严重得多:那个子域名的登录页可以通过明文 HTTP 访问,页面里有密码输入框,没有任何跳转。

证书和 HTTPS 强制是两件事。这篇讲怎么把它们分开检查,以及排查过程中两个把我带偏的坑。

为什么通配符证书”看起来不存在”

误会的来源很简单:如果你在服务器上跑 certbot certificates,或者在某个面板里按域名找,通配符证书不会为每个子域名单列一条。它只有一条记录,SAN 里写着 *.example.com

子域名越多,这个差异越容易造成误判——你有八个子域名,列表里只有一条记录,直觉上就是”七个没配”。

正确的验证方法是直接问服务器要证书,看它的 SAN 列表:

echo | openssl s_client -servername sub.example.com \
  -connect sub.example.com:443 2>/dev/null \
  | openssl x509 -noout -subject -dates -ext subjectAltName

关键是 -servername。现代服务器一个 IP 上跑多个站点,靠 TLS 握手里的 SNI 扩展决定发哪张证书。不带这个参数,你拿到的可能是默认站点的证书,然后得出错误结论。

我这次拿到的 SAN 是 DNS:*.example.com, DNS:example.com——一张通配符加一个裸域,覆盖所有一级子域名。那个”没有证书”的子域名一直在里面。

顺带一个通配符的边界:它只覆盖一级*.example.com 匹配 a.example.com,但不匹配 a.b.example.com。多级子域名需要单独的证书或多级通配符。

坑一:openssl 连不出去时不要相信结论

我第一次跑上面那条命令,四个域名全部返回”握手失败”。

差一点就写成”证书全都有问题”。拦住我的是其中一个域名——那个站点我整晚都在正常访问,它不可能没有证书。

换成 curl -v 再试,四个全部正常返回证书信息。问题出在我的执行环境限制了原始 socket 连接,openssl s_client 出不去,而 curl 走的路径可以。

当一个检查工具对所有目标都报同一种失败时,先怀疑工具。真实的故障很少这么整齐。这个判断规则在后面又救了我一次。

坑二:DNS 枚举在透明代理后面完全失效

确认证书没问题后,我想把所有子域名都查一遍。想法是先探测哪些子域名存在,再逐个检查。

于是拿一份常见前缀列表去解析:wwwadminmailapicdntestgit……结果每一个都解析成功了。包括我随口编的几个。

返回的地址是 198.18.0.x 这样的段。这个网段是保留给网络设备性能测试用的,正常不会出现在公网解析结果里。

解释是我的出口有透明代理在做 fake-IP:它拦截所有 DNS 查询,无论域名是否真实存在,都合成一个保留段地址返回,等实际连接时再按域名路由。在这种网络里,DNS 解析成功不代表域名存在。

改判据:不看 DNS,只看实际的 HTTPS 响应。真实存在的服务返回 200 或 302,不存在的返回码是 000(连不上)。四个真实站点因此浮出来,其余十几个”解析成功”的全是幻觉。

真正的问题

把四个站点逐个查完整之后,问题出现在一个我本来没打算查的维度上。

其中两个站点,访问 http:// 不会跳转到 https://

http://www...      301 -> https://www...     正确
http://admin...    301 -> https://admin...   正确
http://pivot...    200                       直接返回内容
http://prairie...  302 -> http://prairie/... 跳转后仍是 http

那个直接返回 200 的是 webmail。抓下来看正文:

curl -s http://pivot.example.com/ | grep -c 'type="password"'
# 1

明文 HTTP 提供的邮箱登录页,带一个密码输入框。任何人敲了 http://、或者点了一个旧链接、或者从收藏夹里打开一个协议为 http 的书签,密码就是明文上网的。

另一个站点更微妙:它跳转,但跳到的仍然是 http 地址,最终落在一个带两个凭据输入框的二次验证页上。有跳转很容易让人以为配置正确了。

为什么会漏掉这两个

因为强制跳转在这个站点上是源站 nginx 做的,而配置只写在主站点的 server 块里。webmail 和文件服务是后来加的独立服务,各自有各自的配置,谁也没想起来补这条。

这类遗漏有个共同特征:它随着服务增加而累积,且不会有任何报错。每个服务单独看都工作正常,只有横向对比才看得出不一致。

而横向对比恰恰是最容易被跳过的一步——排查通常从”某个东西坏了”开始,注意力自然集中在那一个上面。

怎么修

如果站点在 CDN 后面,最省事的是打开 CDN 的”强制 HTTPS”开关。它在边缘生效,覆盖整个区域包括将来新增的子域名,不用碰任何源站配置,也不会有重定向循环的风险。

要在源站做也可以,但有个必须注意的点:不能用 $scheme 判断。源站在 CDN 或隧道后面时,它看到的永远是内部连接的协议,访客用的协议在 X-Forwarded-Proto 头里:

if ($http_x_forwarded_proto = "http") {
    return 301 https://$host$request_uri;
}

$scheme 判断的后果是要么永不触发,要么无限重定向,取决于源站自己监听的是什么。

强制跳转之后,第一次请求仍然是明文的

把 HTTP 301 到 HTTPS 之后,那个明文密码框确实没了。但有一件事没解决:用户在地址栏敲域名时,浏览器默认发的仍然是 HTTP 请求,跳转发生在服务器收到它之后。也就是说每次访问的第一个请求还是明文的,中间人可以在跳转发生之前就介入。

补上这一环的是 HSTS——一个响应头,告诉浏览器「以后访问这个域名一律直接用 HTTPS,不要先试 HTTP」:

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

浏览器收到之后会记住 max-age 秒。在这期间,用户即便手输 http://,浏览器也会在发出请求之前就改成 HTTPS,明文请求根本不产生。

三件必须注意的事:

  • 它只在 HTTPS 响应上生效。浏览器会忽略 HTTP 响应里的这个头——否则中间人就能伪造它了。所以这个头要加在 443 的 server 块里,加在 80 的跳转块里没有任何作用。
  • includeSubDomains 覆盖所有子域,包括还没建的。如果某个子域暂时只有 HTTP(内部工具、老服务),加上这个参数之后它会直接无法访问,而且用户端已经记住了,改回服务器配置也不能立刻恢复。上这个参数之前要确认每一个子域都已经支持 HTTPS。
  • preload 基本不可逆。提交到浏览器预载列表之后,规则会被编译进浏览器本身,撤销要等好几个版本迭代。除非你确定这个域名永远只走 HTTPS,否则不要加。

稳妥的上线顺序是:先设一个很短的 max-age=300 观察几天,确认没有任何子域被打断,再逐步提到一年。这个参数是「用户浏览器记住多久」,不是「服务器保留多久」——设错了你没法单方面撤销。

混合内容:页面是 HTTPS,里面的资源不是

还有一层是边缘跳转解决不了的。页面本身通过 HTTPS 加载了,但如果 HTML 里写死了 http:// 开头的图片、脚本或样式表,浏览器会拒绝加载它们(脚本和样式)或者标记页面为不安全(图片)。

这在 WordPress 这类内容存在数据库里的系统上尤其常见:站点早期用 HTTP 时插入的图片地址,会以绝对 URL 的形式留在文章正文里,改 nginx 配置完全碰不到它们。

排查方法是打开浏览器控制台看 Mixed Content 警告,或者直接搜内容里的明文链接:

curl -s https://example.com/ | grep -o 'http://[^"'"'"']*' | sort -u | head

需要注意的是只有指向自己站点的链接需要改成 HTTPS。指向外部站点的普通超链接(<a href="http://...">)不构成混合内容,因为它只是一个待点击的链接,不会在当前页面里被加载。要改的是 srchref 到样式表这类子资源引用

顺带一个和前面那两个坑同源的提醒:改完之后用带随机参数的 URL 验证。CDN 会把旧的 HTTP 响应和旧的 301 一起缓存住,用普通地址测会看到修改前的行为,很容易得出「没生效」的错误结论。

一份最小核查清单

这次的教训是,”有没有证书”只是四个问题里的一个。对每个对外的主机名,至少要分别确认:

证书是否覆盖这个主机名。-servername 取证书,读 SAN 列表,别看面板里有几条记录。

http:// 是否强制跳到 https://。这一条独立于证书,而且是唯一会导致凭据明文传输的一条。查法就一行 curl -sI http://host/,看有没有 301/308 且目标是 https。

跳转的目标协议对不对。我这次就碰到 302 跳到另一个 http 地址的情况。只看”有没有跳转”会漏掉它。

有没有 HSTS。强制跳转只保护第一次请求之后的行为,首次那一次明文请求仍然发生了。HSTS 让浏览器记住”这个域名只走 HTTPS”,但它应该在强制跳转生效之后再加——顺序反了没有意义。

四条里最容易被忽略的是第二条,因为它不属于任何一个服务的”功能”,没人会在验收时测它。

发表回复

向下探索