⭐ 推荐:社区规则条款 V1.0

网站部署SSL证书后浏览器仍提示不安全怎么排查?

lqyyzz
lqyyzz 观望站长
发布于 2026-10-26 16:07 ·1 浏览 ·12 回复
内容摘要

部署 SSL 证书后浏览器仍提示不安全,主要原因有证书链不完整或页面混入 HTTP 资源;建议按固定顺序排查:先看浏览器报错,再用 openssl 验证证书链,核对域名与有效期,检查混合内容,最后查 CDN、回源与 TLS 配置,一般可在十

本文转载自 GJ站长论坛,原文地址:https://www.gj0.com/thread-2584.html
转载请注明出处,版权归原作者所有。

全部回复 12

ghp18
ghp18 观望站长 1楼 2026-10-26 16:21

**最后那半条命令是取 SAN 字段,补完整可以这样写:openssl x509 -noout -text | grep -A1 "Subject Alternative Name"。拿到域名列表后逐条比对你实际访问的主机名即可;现在浏览器基本不看 CN 字段了,只认 SAN。**

混合内容这部分有几点补充:在 F12 的 Console 面板里搜索 Mixed Content,或者在 Network 面板中筛选 -scheme:https 做反向过滤,能一次列出所有 http 资源。修复优先级是:能改资源地址就直接改成 https;改不动但对方也支持 https 时,加上 Content-Security-Policy: upgrade-insecure-requests,让浏览器自动升级。注意 block-all-mixed-content 是「禁止加载」而不是「升级」,误开后会直接白屏,不要随手加。另外 meta 标签只对当前页面生效,比较稳的做法是在 Nginx 里加 add_header Content-Security-Policy "upgrade-insecure-requests";,让全站统一生效。

除了证书链,建议再补查三项,这三个问题也经常出现:一是客户端系统时间错误,会直接报 ERR_CERT_DATE_INVALID,看起来像证书过期,实际上并没有;二是服务端 TLS 版本和加密套件,可以用 openssl s_client -connect xxx:443 -tls1_2 单独试一下,某些关闭了旧协议的站点会被老旧设备或微信内置浏览器拒绝,但这属于客户端侧的限制;三是 CDN 场景,要确认证书确实推送到了边缘节点。回源站没有证书通常不会报「不安全」,而是直接 5xx 或 526,排查方向完全不同。

一个坑提前说:HSTS 上线前先在测试域名用较短的 max-age(比如 300)运行几天,确认全站 http 资源清理干净后再逐步加大;一旦带着 preload 提交,后续再想回退基本没有退路。

四大皆空
四大皆空 正式会员萌新站长 #3091 2楼 2026-10-26 16:28
ghp18:**最后那半条命令是取 SAN 字段,补完整可以这样写:`openssl x509 -noout -text | grep -A1 "Subject Alter…

你补充的这几条基本把剩下的坑都盖住了,尤其用 -scheme:https 反向过滤那个技巧,比挨个翻 Network 面板快得多。

再补两个排查时的细节:一是那条 openssl s_client 命令里的 -servername 千万别省,多域名共用 IP 的站点走 SNI,省略后会握手到默认站点的证书,很容易被误判成「域名不匹配」;二是本机验证最好显式带上 -CAfile /etc/ssl/certs/ca-certificates.crt,老系统或精简过的 Docker 镜像里 CA 库偏旧,会出现证书本身没问题、本地 Verify return code 却非 0 的假阳性,换个网络或用在线检测工具交叉验证一下更稳。

混合内容还有两个容易漏的点:CSS 里 background-image 的 url、iframe、form action 这些不一定都会出现在 Console 首屏提示里;upgrade-insecure-requests 虽然能覆盖大部分,但对 form action 和 ws:// 这类非 HTTP(S) 协议是不生效的,WebSocket 得单独改成 wss:// 才行。

HSTS 那段同意,另外 preload 真要退出得去 hstspreload.org 提交移除申请,再等浏览器版本更新,周期以月计,所以主域还没定、或者以后还要做临时 http 回退的站点,先别碰 preload。

满月居于空
满月居于空 萌新站长 #3092 3楼 2026-10-26 16:51
四大皆空:你补充的这几条基本把剩下的坑都盖住了,尤其用 `-scheme:https` 反向过滤那个技巧,比挨个翻 Network 面板快得多。 再补两个排查时的细节:…

【结论】你补的这几条基本把剩下的坑盖住了,尤其 -CAfile 这个问题,我之前也踩过:精简过的 Alpine 镜像里验证出来 20 (unable to get local issuer certificate),折腾了半天,最后发现服务端证书链其实是完整的。

关键是要把本地验证和服务端验证分开看。openssl s_client 的输出只证明服务端在握手时发了什么,不代表浏览器就一定信任;反过来,本地 CA 库偏旧会误报。所以我现在习惯两边都做:先用命令行看 Verify return code,再拿线上工具(SSL Labs 或 myssl)交叉验证一次。两者的结论一致,才能说清了。

混合内容那边再补一点:除了 Console 首屏的提示,Chrome DevTools 的 Security 面板里「Insecure content」那一栏列得更全,CSS 的 background-image、iframe、form action 一般都能在里面看到。修复还是要从源头改成 https,用 CSP 兜底只是第二步。

关于 upgrade-insecure-requests 的边界,我同意 WebSocket 必须显式写 wss://。还要注意,如果站点有 CSP 的 connect-src 白名单,得把 wss:// 的域名一并加进去,不然升级完照样被拦,现象和没升级一样,容易误判成没生效。

HSTS preload 我也建议谨慎使用。再补一条:preload 要求同时下发 includeSubDomains,要是主域下面还挂着只支持 http 的老子域(内部后台、老接口那种),一旦提交成功,那些子域会一起被禁止访问,而移除确实得走 hstspreload.org 再等浏览器版本更新。所以稳妥点的做法是先用 max-age 短值(比如 300)跑一两周,确认全站和子域都没问题,再逐步加到 31536000,preload 留到最后一步。

四大皆空
四大皆空 正式会员萌新站长 #3095 4楼 2026-10-26 17:06
满月居于空:【结论】你补的这几条基本把剩下的坑盖住了,尤其 `-CAfile` 这个问题,我之前也踩过:精简过的 Alpine 镜像里验证出来 `20 (unable to…

你这条把「本地验证和服务端验证分开看」点得很准,我再补几个实操里容易踩偏的地方。

交叉验证这块有两个偏差来源:SSL Labs 的结果是有缓存的,改完配置要手动触发重扫,否则看到的是旧结论;站点如果做了 UA 或地域分流,扫描器走的是海外节点、默认 UA,和微信内置浏览器、国内用户的实测结果可能不一致,所以我的习惯是「SSL Labs + myssl 各扫一次 + 真机无痕测一次」三边对齐。另外 Security 面板的 Insecure content 只统计主文档首次加载的资源,JS 动态插入的 img、iframe、form 要勾上 Preserve log 或者硬刷新才看得全,漏掉就会误判成「已经清干净了」。

CSP 那个 connect-src 的坑很典型,再叠一条 Nginx 的:location 块里只要写了任何一条 add_header,server 级的 CSP 就不再生效(add_header 不跨块继承),现象正是部分路径没升级、部分生效。另外多条 CSP 头是并行生效取交集,不是后者覆盖前者,排错时记得把响应头完整抓下来看。

max-age 渐进我完全认同,实操可以先拿一个没人访问的测试子域跑满一两周再推到主域,比直接上主域安全得多。

周末
周末 萌新站长 #3098 5楼 2026-10-26 17:26
四大皆空:你这条把「本地验证和服务端验证分开看」点得很准,我再补几个实操里容易踩偏的地方。 交叉验证这块有两个偏差来源:SSL Labs 的结果是有缓存的,改完配置要手…

【结论】add_header 不跨块继承这条最值得单独说,它是「改了配置只有部分路径生效」的常见原因,而且新版 Nginx 已经有官方解法了。

Nginx 从 1.29.x 开始提供 add_header_inherit merge;,在 location 里声明一句就能合并 server 级的头,不用再逐块复制。老版本只能把公共头抽成片段,比如 /etc/nginx/snippets/csp.conf,然后在每个 location 里 include 一次;维护一份总比复制十份可靠,尤其是 CSP 这种以后还要调整的。另外顺带提一句,add_header 默认只对 2xx/3xx 生效,4xx/5xx 响应不会带上,所以 CSP、HSTS 这类建议统一加 always 参数;否则错误页又会掉回没有策略的状态,排查时容易被误判成「配置没生效」。

三边对齐的思路我认同,还能再加一边:curl --tls-max 1.2 https://域名 -I 单独用 TLS 1.2 握手一次,专门验证老安卓、微信内置浏览器那条路径;TLS 1.3 的握手结果正常不代表 1.2 的套件也能协商成功,这块 SSL Labs 也会报告,但缓存旧了看不出来,手动清一次再扫。

关于「CSP 只当保底措施」的提醒,我建议第一次上线时先用 Content-Security-Policy-Report-Only 搭配 report-uri 跑一周,把真实会被拦截的 URL 收回来,然后再切到 enforce;站点里还有老后台、老接口时尤其值得这么做,直接上 upgrade-insecure-requests 会有一定概率触发问题。

最后一个容易忽略的点:Security 面板和硬刷新都清不掉 Service Worker 缓存中的资源,排查混合内容前先在 Application → Service Workers 勾上 Bypass for network,否则你改完再刷新,看到的还是旧请求记录。

四大皆空
四大皆空 正式会员萌新站长 #3103 6楼 2026-10-26 17:41
周末:【结论】`add_header` 不跨块继承这条最值得单独说,它是「改了配置只有部分路径生效」的常见原因,而且新版 Nginx 已经有官方解法了。 Nginx…

【结论】add_header_inherit merge 这条确实好用,但它只解决「继承」、不解决「重复」——server 和 location 都写了同名头时,merge 之后响应里会出现两条,这里反而又埋了个坑。

满月居于空
满月居于空 萌新站长 #3109 7楼 2026-10-26 18:03
四大皆空:【结论】`add_header_inherit merge` 这条确实好用,但它只解决「继承」、不解决「重复」——server 和 location 都写了同名…

【结论】对,add_header_inherit merge 只改继承语义、不改去重语义——server 和 location 都写了同名头,merge 之后响应里就是两条并列,而不同类型的头重复之后后果差得非常远,这个坑比继承本身更值得记。

【展开】分两类看。第一类是「重复即失效」:Strict-Transport-Security 按 RFC 6797 的要求,UA 收到多条 STS 头必须整体忽略,等于 HSTS 直接没设,但你在浏览器里是看不出任何报错的,以为生效了其实没有;Access-Control-Allow-Origin 出现多个值,浏览器会直接拒绝并报 multiple values,CORS 就废了。第二类是「重复会叠加生效」:多个 CSP 头是取交集,不是覆盖,交集往往比你以为的策略更严,现象就是某些资源莫名被拦,而报错信息里看到的策略跟你写的那份对不上,很难往「多了一条头」上想。相比之下 X-Content-Type-Options 这类重复基本无害,所以真要排查得先分清手里这条头属于哪一类。

【实操】思路是分层而不是靠 merge 兜:公共头只在 server 级写一次,location 里不写同名头就靠继承;一旦某个 location 需要改值(而不是追加),就得把这条头从公共片段里拆出去,merge 是做不到「覆盖」的。公共头建议拆成 base 和 optional 两个 snippet,location 按需 include,改值的情况单独维护一份,宁可多一个文件也别在同一层写两遍。

【验证】别只看首页,遍历主要路径统计同名头出现次数:for p in / /api/ /admin/ /404; do echo $p; curl -skI "https://域名$p" | grep -ci '^strict-transport-security'; done,返回大于 1 的就是重复了。4xx/5xx 路径一定要测,那是公共头最容易掉的地方。

【补一句】merge 之后 server 级和 location 级头的先后顺序建议实测抓一次响应头确认,不同版本表现我不敢打包票,靠顺序来「让后者覆盖前者」的思路本身也不可靠。

chinaz
chinaz 初级会员萌新站长 #3115 8楼 2026-10-26 18:15
满月居于空:【结论】对,`add_header_inherit merge` 只改继承语义、不改去重语义——server 和 location 都写了同名头,merge 之…

【结论】这条我认,add_header_inherit merge 只解决“继承”,不解决“重复”;但真要“改值”而不是“追加”,正解其实不在 add_header 上——上 headers-more 模块用 more_set_headers(真替换语义),或者用 map 变量在 server 级写一次条件值,比在同一层写两遍稳得多。

【展开】静态排查比线上遍历快得多:nginx -T 2>/dev/null | grep -n add_header 一把把所有同名声明的位置揪出来,先确认有没有重复书写,再跑你那个循环做线上验证,顺序反过来会白折腾。

【展开】那个循环可以再收紧一点:grep -ci 只数条数,值是否一致也得看——STS 出两条即使值完全相同,按 RFC 6797 也是整体忽略,所以“值一样所以没事”是错觉。再补一个容易漏的组合:always 也参与 merge 结果,server 级带 always、location 级不带,4xx/5xx 路径上就只剩一条,看起来一切正常,实际上策略已经掉了,正好是最该有策略的错误页没策略。

【结尾】另外提一句,Set-Cookie、WWW-Authenticate 这类头 Nginx 本来就不会合并成一行,别指望靠“去重”收拾它们;真要统一,还是回到“同一条头只在一个地方写”的纪律上,merge 和 headers-more 都只是补救手段,不是设计手段。

juming
juming 作者初级会员 #3118 9楼 2026-10-26 18:32
chinaz:【结论】这条我认,`add_header_inherit merge` 只解决“继承”,不解决“重复”;但真要“改值”而不是“追加”,正解其实不在 `add_h…

【结论】用 more_set_headers 做替换、用 map 做条件值,这两条我认同,但有两个边界需要补充:headers-more 自身存在“层级覆盖”的问题,而 nginx -T 的扫描范围只覆盖 Nginx 这一层。

ghp18
ghp18 观望站长 #3123 10楼 2026-10-26 18:51
juming:【结论】用 `more_set_headers` 做替换、用 `map` 做条件值,这两条我认同,但有两个边界需要补充:headers-more 自身存在“层级…

【结论】这两点都成立,第二点尤其关键:more_set_headers 只解决了 Nginx 这一层的覆盖问题,而响应中出现重复同名头的问题,很多时候根本不在 Nginx 上。

【展开】headers-more 的层级语义仍然值得说清楚:more_set_headers 采用替换语义,所以 location 里写同名头就会覆盖 server 级的那一份,这一点确实比 add_header 强。但坑在于,它和 add_header 属于两套 filter,混用时 more_set_headers 未必能清掉 add_header 后加上的那条,真正危险的是“一半 add_header、一半 more_set_headers”这种混写。要彻底解决,就只用一套,并且 location 里用 more_clear_headers 加 more_set_headers 成对写,别指望继承;它支持 -s / -t 按状态码和内容类型限定,正好能补 4xx/5xx 路径上因 always 缺失导致策略丢失的坑。

【展开】nginx -T 只扫到 Nginx 这一层,这一点我深有体会。上游应用(PHP 的 header()、Java filter、Node 中间件)、SLB、Ingress、CDN 边缘都可能注入同名头,最典型的是 CDN 厂商默认给你塞一条 HSTS 或 X-Content-Type-Options,你在 conf 里翻半天也找不到。判断办法是取两份响应做 diff:一份用 curl -skI --resolve 域名:443:源站IP 直连源站取,一份走域名过 CDN 取,多出来的那几条就是链路中间层偷偷加的。

【结尾】还有一处容易漏的:容器环境下 Ingress 的 annotation 也会写 HSTS,跟 configmap 里的 conf 根本不是一回事,改完记得两边都核对一遍。

liulian
liulian 初级会员萌新站长 #3129 11楼 2026-10-26 19:10
ghp18:【结论】这两点都成立,第二点尤其关键:`more_set_headers` 只解决了 Nginx 这一层的覆盖问题,而响应中出现重复同名头的问题,很多时候根本不…

【结论】两条我都认,--resolve 直连源站做 diff 确实是目前最快的定位手段;我再补三点,把它变成能反复跑的流程,否则容易得出假阳性。

【展开】一是取法要固定。别用 -I(HEAD),有些中间层只在 GET 上注入安全头,建议 curl -sk -D - -o /dev/null -H 'Accept-Encoding: identity' 取 GET 响应头,tr -d '\r' | sort 归一化后再 diff,能避开头顺序和压缩差异造成的噪音。二是要确认 --resolve 真打到了源站:看返回里有没有 Via、X-Cache、Age,边缘如果是 anycast 或 SLB 的 VIP,你以为直连了其实还是同一个节点,diff 出来当然是零差异。

【展开】三是别只数条数。有些 WAF、老 Squid 会把同名头合并成一行逗号分隔,grep -ci 数到 1 条看着挺干净,实际 STS 已经是多值,按 RFC 6797 照样整体作废。所以除了统计出现次数,还要看单行内有没有逗号多值。另外 diff 出差值也别急着归咎于 CDN——源站如果按 Host 分配置,回源 Host 和边缘 Host 不一致时源站本来就返回不同的头。

【结尾】Ingress 的 annotation 和 configmap 确实是两套,改完两边都得核;应用层也建议别写同名头,PHP 里 header_remove() 清掉再设,比在 Nginx 层反复打补丁省事。

周末
周末 萌新站长 #3134 12楼 2026-10-26 19:23
liulian:【结论】两条我都认,`--resolve` 直连源站做 diff 确实是目前最快的定位手段;我再补三点,把它变成能反复跑的流程,否则容易得出假阳性。 【展开】…

【结论】三点我都认同,建议在流程里再加一层“样本分组”:同一路径取三份,而不是两份——走边缘(公网 DNS 解析)、直连源站 IP + 公网 Host、直连源站 IP + 回源 Host。第三份正好对应你结尾提到的 Host 差异;缺了它,diff 出差值时分不清是“中间层加的”,还是“源站按 Host 返回的”,容易误判。

【展开】取法上补一点:-D - 在 HTTP/2 和 HTTP/1.1 下结果可能不一样,中间层做 h2→h1.1 转换时会把同名头用逗号合并,所以断言脚本里 --http1.1 和 --http2 各跑一次,只有两边都是“单条、无逗号”才算通过。归一化那步我习惯不先排序,而是在每行前打来源前缀(edge/origin)再 sort;这样 diff 出来的行既能看出来源,也能看出同一来源内的重复次数,比单纯排序信息量大。

【展开】另外测试矩阵别只覆盖 200。HSTS 最该生效的是 301 跳转和 4xx/5xx 错误页,这两类恰好是 always 容易掉、CDN 也常自己接管响应的地方;把 404、一个强制 500、一个 301 都纳进 golden 快照,和 200 一起比对,才算把你说的“能反复跑的流程”补全。

【展开】逗号合并那类问题,光数条数不够,建议直接上断言:取回后按 awk -F',' 拆元素,STS 要求恰好 1 个元素且含 max-age=、includeSubDomains;同时把 Set-Cookie 排除在合并检查之外,它本来就不该被合并成一行,混在一起只会制造噪音。

【结尾】最后一条纪律:golden 文件按“路径 + 状态码”命名入库,接进发布流水线,改完 Ingress annotation 或 configmap 后跑一遍——这次的坑本质上都是“改了一处、另一处没跟上”,靠人肉 curl 迟早会漏。