# 保哥笔记 — HTTPS与安全加固 > 本分片含 8 篇文章,按发布日期倒序。全部分片索引见 https://zhangwenbao.com/llms-full.md **站点**:https://zhangwenbao.com/ **分类**:HTTPS与安全加固 **生成**:2026-09-12 13:06:31 CST --- ## 邮件传输加密:130个站都支持,不给降级机会的只有1个 - URL:https://zhangwenbao.com/smtp-transport-encryption-mta-sts-downgrade-audit.html - 分类:HTTPS与安全加固 - 发布:2026-09-03 | 更新:2026-09-03 - 摘要:别人往你的域名发信,路上那一段加不加密,取决于对方服务器愿不愿意——而这份意愿可以被中间人一行行删掉。130个海外大牌站点里,把这条退路堵死的只有1个。 - 关键词:技术SEO,SSL证书,服务器加固 > **TLDR**:摘要:把130个英文大牌的收信服务器逐台连上25端口问了一遍,没有一台拒绝加密——130台全都主动宣告支持STARTTLS,其中129台完成了TLS握手,122台用的是TLS 1.3。但这份好看的数字有个前提:加密是对方“愿意”给的,不是你“要求”的。真正用MTA-STS把明文回退这条退路堵死的,130个站里只有1个。 > 摘要:把130个英文大牌的收信服务器逐台连上25端口问了一遍,没有一台拒绝加密——130台全都主动宣告支持STARTTLS,其中129台完成了TLS握手,122台用的是TLS 1.3。但这份好看的数字有个前提:加密是对方“愿意”给的,不是你“要求”的。真正用MTA-STS把明文回退这条退路堵死的,130个站里只有1个。 先说清楚这篇在测什么。不是你发出去的信别人收不收得到,那是投递率的事;是别人给你发信的时候,信在路上那一段到底加不加密,以及那层加密能不能被人不动声色地掀掉。 这两件事经常被混成一件。域名根上那几条SPF记录 (https://zhangwenbao.com/spf-lookup-limit-upstream-inherited-dns.html)、DKIM签名、DMARC策略,管的都是发件方向:谁有资格用你的域名往外发信。围绕发件做的功课通常也很足——投递率 (https://zhangwenbao.com/email-deliverability-spf-dkim-dmarc-ip-warmup-6-dimension-playbook.html)掉一个点会有人追,Gmail那边一改规则(比如一键退订必须写两层 (https://zhangwenbao.com/one-click-unsubscribe-header-body-two-layers.html))全公司都得动。 收件方向反而没人管——你把MX记录一填,邮件就开始往里进了,进来的路上加不加密,取决于对方那台服务器的心情。没有KPI会因为这件事掉下去,所以也就没有人去看。 这一轮我把131个英文大牌站点的域名拿来,先用DNS over HTTPS查MX记录、MTA-STS声明、TLS-RPT回执地址和DS记录,再按MX优先级取出每个站真正的收信主机,逐台连上25端口,握手取证书,最后把DNS里的声明和服务器上的实际行为两边对账。 ## 一封信送到你手上之前,那一段路是加密的吗? 答案是:几乎肯定加密了,而且这个“几乎肯定”一文不值。 SMTP这个协议1982年定稿的时候压根没有加密。后来打的补丁叫STARTTLS,写在RFC 3207 (https://www.rfc-editor.org/rfc/rfc3207.html)里,做法很朴素:双方先用明文连上,发信方问一句“你支持加密吗”,收信方在能力清单里回一句“支持”,发信方再说“那我们加密吧”,然后才开始TLS握手。 问题就出在这个顺序上。协商本身是明文的。中间任何一个能改数据包的人,只要把收信方那份能力清单里的STARTTLS这一行删掉,发信方就会认为对方不支持加密——而按照默认规则,它不会因此拒发,它会退回明文,把整封信原样送出去。 这套设计有个专门的名字,叫机会性安全,标准文本是RFC 7435 (https://www.rfc-editor.org/rfc/rfc7435.html)。它的立场写得很坦白:有加密就用,没有就算了,总比完全不加密强。这话在2014年是对的,那年全球邮件的加密比例还很低,先把大盘拉起来最要紧。 这是一种典型的默认值设计——不表态的时候,系统替你选最宽松的那一档,理由是宽松的那一档不会把事情搞砸。同样的逻辑在别处也见得到:robots.txt的分组不继承 (https://zhangwenbao.com/robots-txt-group-inheritance-default-audit.html)、Cookie不写SameSite时的那两分钟宽限,都是同一个思路。宽松档的好处是不出事,代价是你以为自己有的保护其实没有。 但机会性安全有个隐含前提经常被忽略:它防的是被动窃听,不防主动篡改。一个只能看不能改的人拿你没办法;一个能改包的人,把那一行删掉就行了,你这头收到的信和平时一模一样,日志里不会有任何异常,因为在你的服务器看来,对方就是一台不支持加密的老服务器。 ## 130台收信服务器,一台台问过去得到了什么? 131个域名里,braun.com没有MX记录(它的邮箱走别的域),剩下130个都能定位到收信主机。按优先级数字最小的那台算,130个站落到69台唯一主机上——很多品牌共用同一个平台入口。 逐台连过去,结果整齐得有点无聊: - 130台全部主动宣告支持STARTTLS,一台例外都没有。 - 其中129台完成了TLS握手,TLS 1.3有122台,TLS 1.2有7台,没有一台还在用更老的版本。 - 剩下那1台是shein.com的收信服务器,它宣告了STARTTLS,也对命令回了“220 Ready to start TLS”,然后在握手那一刻把连接掐断了。换三次、隔开时间再试,三次行为完全一致。 如果只看第一行,这份成绩单堪称完美:加密覆盖率100%。做安全审计的人最该警惕的就是这种数字——上一轮做证书透明日志 (https://zhangwenbao.com/certificate-transparency-caa-issuance-audit.html)的时候我就写过,第一版跑出一组特别整齐的数字,通常意味着你把一批不同的东西粗暴地归进了同一格。 这次的“同一格”是:我把“它说它支持”和“它必须支持”当成了一件事。 顺便看一眼这69台主机后面站着谁。Google的收信入口覆盖56个站,微软覆盖37个,两家加起来93个,占130个站的71.5%。剩下的分给Proofpoint(8个)、Mimecast(6个)、阿里云与万网系(4个)、趋势科技(3个)、Barracuda(3个),以及零星的自建。 banner那一行泄露的信息也不少。theordinary.com的收信入口是Mailgun,quince.com直接挂在Amazon SES上,byredo.com的MX指向母公司Puig的域名、实际应答的却是思科IronPort的云网关,dji.com那台自报家门是Softnext-ESG(中国台湾一家邮件安全网关厂商)。这些细节和之前测HTTP响应头 (https://zhangwenbao.com/http-response-header-dead-fields-audit.html)时看到的规律一样:真正在干活的那个组件,往往不是品牌自己选的那个牌子。 ## 69台主机撑起130个品牌,证书上印着谁的名字? 130个站落到69台唯一收信主机上,这个比例本身就说明了问题:接近一半的品牌,收信入口和别人是同一台。 微软那套的做法是给每个租户一个专属主机名,uniqlo-com.mail.protection.outlook.com、philips-com.mail.protection.outlook.com、underarmour-com.mail.protection.outlook.com,看上去一家一台。但把证书取下来对一对就露馅了:这些主机出示的是同一批证书,SAN列表都是34条,内容一模一样。名字是给每个租户单独发的,机器是共享的。 Google那边更直接,56个站的收信入口全都指向aspmx.l.google.com这一组固定主机名,连表面功夫都不做,证书SAN是26条Google自己的域名。 最值得看的是阿里系那几台。aliexpress.com的mx2.mail.aliyun.com、ecoflow.com的mx1.qiye.aliyun.com、govee.com和narwal.com共用的mxn.mxhichina.com,三台机器出示的是同一张证书,SAN列表231条,而且不用通配符,一个名字一个名字明写。翻一遍这231个名字,里面躺着alimailws.alibaba-inc.com、alimei-api-40.alibaba.com、alimei-auth.aliyun.com、alimei-api-sg.aliyuncs.com这类内部服务名,顶级域分布是221个com、8个cn、2个sg。 一张证书把一整套内部服务的命名规则、地区分片和接口划分公开印了出来,任何人连一次25端口就能全部拿走。上一轮做证书透明日志 (https://zhangwenbao.com/certificate-transparency-caa-issuance-audit.html)的时候,网页那侧的SAN也暴露过同类东西——区别是网页证书还会被公开日志收录、有人盯着,邮件网关这张没人看。 这一节和加不加密无关,但它决定了后面所有结论的性质:你的收信链路上,绝大部分部件不是你的。Google和微软两家覆盖了71.5%的站,它们的证书由它们签、TLSA记录由它们发、支持哪个协议由它们定。你能决定的,只有你自己域名上那几条记录。 ## “支持加密”这句话,能不能被中间的人删掉? 能。而且删起来毫无技术含量。 要堵住这条路,办法是让发件方在连接之前就知道“这个域名要求必须加密”,这样即便中间人把STARTTLS那行删了,发件方也会因为拿不到加密通道而直接拒发,不会退回明文。 做这件事的机制叫MTA-STS,标准是RFC 8461 (https://www.rfc-editor.org/rfc/rfc8461.html)。它的结构挺有意思,一份策略要分三处才算完整: - DNS里放一条声明:在_mta-sts.你的域名上放一条TXT记录,内容是v=STSv1; id=某个版本号。这条只说明“我有策略,版本号是这个”,不含策略内容。 - HTTPS上放一份策略文件:地址固定为https://mta-sts.你的域名/.well-known/mta-sts.txt,里面写清楚mode是什么、允许哪些MX主机名、缓存多久。 - 这个mta-sts子域必须有一张有效证书:因为发件方是通过HTTPS去取这份策略的,证书验不过,策略就等于不存在。 三处缺一不可,而且它们分属三个系统:DNS一处、Web服务器一处、证书一处。这个分散度本身就是MTA-STS铺不开的原因之一——它不是在某个后台里点一个开关,它要三个团队各做一件事。 顺带说一句,这套路数在网页那边有个亲戚,叫HSTS (https://zhangwenbao.com/https-hsts.html):同样是“我提前告诉你,以后只准用加密来找我”。区别在于HSTS只要加一个响应头,MTA-STS要动三个地方。谁铺得开谁铺不开,光看这一条就能猜出个大概。 ## 真正不给降级机会的,为什么只有一个站? 131个域名查下来,_mta-sts上写着有效声明的只有3个:ikea.com、notino.com、ritual.com。 三份策略文件我都取回来了,全文照抄: # ikea.com version: STSv1 mode: testing mx: ikea-com.i-v1.mx.microsoft max_age: 86401 # notino.com version: STSv1 mode: testing mx: *.mail.protection.outlook.com max_age: 86400 # ritual.com version: STSv1 mode: enforce max_age: 31449600 mx: aspmx.l.google.com mx: alt1.aspmx.l.google.com mx: alt2.aspmx.l.google.com mx: aspmx2.googlemail.com mx: aspmx3.googlemail.com 关键在mode那一行。RFC 8461定义了三档:enforce是真的执行,验不过就拒发;testing是只记录不拦截,验不过照样投递、顺手发一份报告给你;none是明确宣布作废。 3个站里,2个停在testing,只有ritual.com一家是enforce。也就是说,130个站里真正把明文回退这条路堵死的,是1个,占0.8%。 testing这一档很值得多说两句。它在纸面上不是错误配置,官方推荐的上线路径就是先testing跑一段、收着报告确认没有误伤,再切enforce。问题是这个“一段时间”有多长。 三份文件里的id字段给了一点线索。ritual.com写的是20240119T055225,notino.com写的是2026021201,两个都是明摆着的日期格式;ikea.com写的是1751618879951,按毫秒时间戳解出来是2025年7月4日。这里要说清楚:RFC 8461只规定id是1到32位的字母数字,没有要求它必须是时间戳,所以这只是形态上的推测,不能当结论用。 但把这个推测和mode放在一起看,画面就有点意思了:那份写着testing的策略,从版本号上看已经挂了一年多,还停在“我先看看”的阶段。而唯一切到enforce的ritual.com,版本号停在2024年1月——它两年多没动过这份文件,因为不用动,它已经到位了。 max_age那一栏也在讲同一个故事。ritual.com写的是31449600秒,364天;两个testing的站写的是86400和86401,正好一天。缓存一天意味着策略随时可以撤,撤了第二天全网就忘干净——这是给自己留的后门,也是没打算长期跑的信号。 ## 我原以为是证书对不上,实测把这个猜想打掉了 看到2个testing的时候,我的第一反应是:大概是不敢切。切到enforce之后,发件方会去校验收信主机的证书,万一有哪台机器的证书名字对不上,信就真收不到了——收不到客户的邮件,这个后果谁也担不起。这个解释听着特别顺。 所以我把它当成一个可以证伪的假设去测了:既然enforce要校验证书上的名字,那就把129台完成握手的服务器证书全取下来,逐台看MX主机名在不在证书的SAN列表里。这正是enforce模式下发件方要做的那道校验。 结果是:127台对得上,只有2台对不上。 换句话说,绝大多数站就算今天把mode改成enforce,证书这一关也是过得去的。技术上没有那道拦路虎,我原来那个解释站不住脚。 这是本轮最值得记下来的一次修正。它把结论从“有客观障碍所以做不了”,改成了“没什么障碍,就是没人做”——后者难听得多,但它才是实测出来的那个。 ## 那到底为什么没人做 把技术障碍排除之后,剩下的解释只能到别处去找。我能想到的有三条,而且三条都不是技术问题。 第一条,没有任何外部力量在推。SPF、DKIM、DMARC这几年铺得飞快,不是因为大家突然有了安全意识,是因为Gmail和Yahoo在2024年划了线:不配就限流。收件方向没有这样一条线。你不配MTA-STS,没有任何一家邮件服务商会因此少收你一封信,也没有哪个合规框架点名要求它。一件事没有被强制,就只能靠自觉,而自觉在企业里是最不稳定的一种资源。 第二条,出了事你也不会知道。降级攻击的特征是无声无息:你的服务器照常收信,日志里是一次正常的连接,只不过没走TLS。除非你配了TLS-RPT,否则这件事在你的可观测范围之外——而TLS-RPT本身也得先配。这构成一个闭环:你需要先做这件事,才能知道自己有多需要做这件事。这类闭环在工程里通常都以“没人做”收场。 第三条,它的KPI归属不明。发信那一头挂在市场部或增长团队名下,投递率是他们的数字,掉了有人急。收信这一头属于IT,而IT的考核指标里通常只有“邮件能不能正常收发”,加密与否不在其中。一件事没有主人,它就永远排在待办列表的下半截。 三条合起来,130比1这个数字就不奇怪了。奇怪的反而是那1个:ritual.com把max_age写成364天、版本号停在2024年1月——有人在两年多前认真做完了这件事,然后再也没碰过它。这是配置该有的样子。 对不上的那2台,各有各的看点,下一节单独说。 ## 有一台收信服务器的证书,两年前就过期了 dji.com的MX指向mail.djicorp.com。连上去,STARTTLS宣告了,握手也成功了,取下来的证书长这样: - 主体名和签发者名完全相同(CN=dd40882ba0ea),也就是一张自签证书; - SAN列表一条都没有,连它自己的主机名都没写; - 签发于2014年9月15日,有效期截止到2024年9月12日。 按本轮实测的2026年9月2日算,这张证书已经过期720天。连了三次,三次拿到的是同一张,指纹一字不差。 这个例子把机会性加密的荒诞之处摊得明明白白:加密在跑,而且跑得好好的,因为根本没有人去看这张证书。STARTTLS的默认玩法就是不校验——反正验不过也是退回明文,那还不如不验直接加密,好歹能挡住被动窃听。于是一张过期两年、没有任何身份信息的自签证书,照样每天在替这个域名加密着所有进来的邮件。它加的密在密码学上是货真价实的;它证明不了对面是谁,一点都证明不了。 另一台是byredo.com。它的MX指向母公司Puig的域名mail-ib-1.puig.com,实际应答的却是思科IronPort托管网关,证书主体写着CN=mx1.hc1069-92.c3s2.iphmx.com,SAN里8个名字全是iphmx.com和cisco.com下面的,没有一个和puig.com沾边。 这不是配错了,这是没配。用云邮件网关的时候,把自己的域名CNAME到服务商的入口再给它签一张证书是要额外做一步的,多数人不做,因为不做也能收信。只有在有人真的去校验的那一刻,这一步的价值才会显现——而现在,没有人校验。 顺便说一句,dji.com的这张证书要是长在网站上,早就被浏览器拦下来了,用户会看到一整页红色警告。同一个组织,同一批人管的基础设施,网页那头的证书必然是新鲜有效的(否则生意做不成),邮件这头挂着一张2014年的自签证书没人管——差别不在能力,在有没有人盯着。这一点和之前测支付页CSP (https://zhangwenbao.com/payment-page-csp-factory-default-script-policy.html)时看到的是同一个道理:配了不代表在管事。 ## DANE这条路,为什么56个站根本走不上去? MTA-STS不是唯一的解法,还有一条更早的路叫DANE,标准是RFC 7672 (https://www.rfc-editor.org/rfc/rfc7672.html)。思路完全不同:MTA-STS把策略放在HTTPS上、靠证书体系来证明;DANE把收信主机的证书指纹直接写进DNS,靠DNSSEC签名来证明。不用CA,不用HTTPS,一条TLSA记录搞定。 听起来更干净,实际铺得更慢。原因在于它有两个前提,而且分属两个主体: - 收信主机那一侧要发布TLSA记录——这件事由邮箱平台做,你做不了; - 你自己的域名和平台的域名都要签了DNSSEC——不然那条TLSA记录本身就可能被伪造,写了等于没写。 实测下来,130个站里MX主机带TLSA记录的只有2个:assos.com和ikea.com,它们的收信入口都在微软的mx.microsoft域下,而这条TLSA是微软发的,不是这两家品牌发的(微软在自己的文档 (https://learn.microsoft.com/en-us/exchange/security-and-compliance/how-dane-secures-email)里写明了这套记录由平台侧发布与轮换)。巧的是,这2个站恰好也是自己签了DNSSEC的——两个前提由两方各满足一个,凑齐了。 再看另一头就更清楚了。我把各个邮箱平台自己的域名拿去查DS记录,两个DNS服务商各查一遍对账,结果一致: - outlook.com、mx.microsoft、pphosted.com:签了; - google.com、googlemail.com、mimecast.com、barracudanetworks.com、aliyun.com、qq.com:没签。 用Gmail收信的那56个站,DANE这条路是彻底走不通的,不管它们自己多努力——因为google.com自己没签DNSSEC,收信主机在DNS里根本没法证明自己。这56家品牌里但凡有谁认真研究过DANE,最后都会得到同一个结论:这件事不由我决定。 这就是本轮最想说的那句话的第一个落点:你能不能做成一件事,一半的开关不在你手上。 ## 只装了报警器没装锁,是什么状态? 和MTA-STS配套的还有一个东西叫TLS-RPT,标准是RFC 8460 (https://www.rfc-editor.org/rfc/rfc8460.html)。它的作用是让发件方每天把“我给你发信时加密成功了几次、失败了几次、为什么失败”汇总成一份报告寄给你。 131个域名里配了TLS-RPT的有5个:ikea.com、notino.com、ritual.com、monos.com、philips.com。 前三个是配了MTA-STS的那三家,顺理成章。后两个有意思:monos.com和philips.com配了TLS-RPT,却没有配MTA-STS。 这个组合翻译成人话是:我装了个报警器,但门上没锁。报告会一天天寄过来,告诉你今天又有多少封信是明文送达的,而你手上没有任何机制去改变这件事——因为你没声明过要求加密,发件方也就没有义务加密。 收报告的地址也值得看一眼。ikea.com寄到MTA-STS.RUA@ikea.com、notino.com寄到tlsrpt@notino.com,是自己收;ritual.com寄到easydmarc的地址、monos.com寄到mailhardener、philips.com寄到tls-report@vali.email,是交给第三方服务商解析。5家里3家把回执交给了外面的人。 这个比例本身不算问题,报告是结构化的JSON,交给专业工具解析比自己写脚本靠谱。但它和之前查域名验证记录 (https://zhangwenbao.com/dns-txt-domain-verification-records-audit.html)时看到的现象是一回事:域名根上每多一条记录,就多一个外部依赖,而这个依赖通常没有台账、没有到期提醒、也没有人定期复核它还在不在服务。第三方组件会自己变 (https://zhangwenbao.com/third-party-default-changes-silent-drift-audit.html),而变的那一天不会有人通知你。 还有一层更细的:ikea.com和ritual.com的_mta-sts那条TXT并不是直接写在自己域上的,而是CNAME委托出去的——ikea指向_mta-sts.smart.ondmarc.com,ritual指向ritual_com__mta_sts.easydmarc.pro。这意味着策略的版本号由服务商掌握,续不续、改不改,取决于合同还在不在。上一轮查CAA记录 (https://zhangwenbao.com/certificate-transparency-caa-issuance-audit.html)时也是同样的形态:查得到的那份配置,主人常常不是品牌自己。 ## 这一轮,我的尺子错在哪几处? 四处,其中两处改变了结论方向。 ## 第一处:8个站看着配了一半,其实一个字都没配 第一遍抓策略文件的时候,除了3个正常返回的,还有一批很暧昧的结果:bollandbranch.com、on.com、shopify.com三个站返回404,graza.co和rab.equipment的证书验不过,aliexpress.com返回302跳到自己的错误页,functionofbeauty.com返回503,zalando.com更绝——返回200,但content-type是text/html,正文48385字节,是一整张网页。 按字面读,这8个站都属于“建了mta-sts子域但没放对文件”,也就是配了一半。这个结论我差点就写下去了。 幸好补了一个对照实验:拿一个绝对不存在的子域名(zzq-nonexistent-104-probe.域名)去查A记录。8个站全部有解析。而3个真配了MTA-STS的站,同一个随机子域查过去,一条记录都没有。 真相是这8个站配了泛解析:*.域名指向主站。于是mta-sts.域名自动就有了地址,请求打到主站的Web服务器上,得到的是主站对一个未知路径的常规反应——404、跳转、503,或者zalando那种返回200的软404页面。它们不是配了一半,它们一个字都没配,是泛解析让子域看上去存在。 这个坑值得记牢:只要一个域名开了泛解析,任何“子域存在与否”的判断都会失真,得先拿随机子域打一发基线。之前测子域名清单 (https://zhangwenbao.com/subdomain-inventory-hosting-noindex-audit.html)和软404 (https://zhangwenbao.com/apache-errordocument-custom-error-pages-http-status-codes-soft-404.html)的时候都碰过这类问题,这次换了个壳子又来了一遍。 再往上抽一层,这和测Vary响应头 (https://zhangwenbao.com/vary-header-declared-vs-actual-response-variation.html)时踩的是同一个坑:拿到一个非空的答案,就默认这个答案是关于你问的那个问题的。实际上服务器只是对任何请求都给点什么,你问什么它并不关心。响应头自相矛盾 (https://zhangwenbao.com/http-response-header-self-contradiction-audit.html)那一轮也是,两个字段各说各话,是因为它们由两个互不知情的组件写下的。 ## 第二处:把“它说它支持”当成了“它真能加密” 第一版统计里我拿EHLO能力清单里有没有STARTTLS这个字符串来算覆盖率,算出来130台全支持。shein.com那台就藏在这个数字里:它宣告了,也对STARTTLS命令回了220,然后在TLS握手那一步把连接掐了,三次复测行为一致。 严格说这不算“支持”。修正后的口径是:130台宣告,129台真正握上手。差别只有一台,但这一台恰好是整篇文章论点的活标本——宣告和实际是两回事,而这一次栽在这个坑里的是我自己。 ## 第三处:拿证书指纹当站点属性,差点写出一个假差异 做稳定性复测的时候,20台主机里有7台两次拿到的证书指纹不一样。第一反应是发现了什么大新闻。 拉开看,7台全是微软那组品牌-com.mail.protection.outlook.com的主机,两次指纹在两个固定值之间来回跳,而STARTTLS宣告和SAN匹配两项判定完全一致。挑一台连续连6次,结果是8a8d…和e0c3…交替出现,两张证书都能匹配上主机名;同样连6次的aspmx.l.google.com则始终是同一张。 结论是微软那个入口后面至少有两台机器,各持一张证书,负载均衡随机分配。要是我拿指纹去比对“不同品牌是不是用了不同证书”,会得到一堆纯属噪音的差异。这条判据的复核方式还是那一条:任何“A和B不一样”的结论,先问一句“A和A自己一样吗”。 ## 第四处:一次连不上就当人家不支持 第一轮有一台Barracuda的网关超时,第二轮重试仍失败,第三轮通了,TLS和证书全都正常。被拦或者超时是概率事件,不是站点属性——这一条我上一轮刚记过,这一轮又碰上了,说明重试补分母这一步不该等出问题再做,应该写死在流程里。 还有一件事得说在前面:POST方向的行为本轮一个都没测。要判断一台服务器是不是真的会拒收明文投递,唯一的办法是真的对它发起一次不加密的投递尝试,那是对别人的生产系统做写操作,不能干。所以本文所有结论都止步于“连接、协商、握手、取证书”这四步,再往后的行为一律没有实测数据。 ## 自己的域名该怎么查? 不用装工具,三条命令十分钟能查完。 第一步,看自己有没有声明。 dig +short TXT _mta-sts.你的域名 dig +short TXT _smtp._tls.你的域名 curl -sI https://mta-sts.你的域名/.well-known/mta-sts.txt 三条全空,说明你在这一层是完全默认状态:任何人给你发信,加不加密由对方决定,被降级了你也不会知道。顺便一提,如果连MX都查不出来或者解析很慢,那就是另一码事了,先去排查DNS这一层 (https://zhangwenbao.com/overseas-store-unreachable-slow-network-layer-diagnosis-dns-routing-cdn.html)。 第二步,看自己的收信服务器怎么应答。 dig +short MX 你的域名 openssl s_client -starttls smtp -connect 你的MX主机:25 -servername 你的MX主机 重点看三样:证书上的名字里有没有你的MX主机名、有效期还剩多久、签发者是谁。如果像本文那台那样是自签的、或者名字完全对不上,那么今天先别切enforce,先把证书这件事和邮箱服务商对齐。 第三步,如果决定要上,顺序别搞反。 - 先把TLS-RPT配上,让报告先跑起来,这一步零风险,纯粹是加一双眼睛; - 收两三周报告,确认没有持续失败的发件方; - 再发布mode: testing的策略,同时确保mta-sts子域的证书在你的续期流程里,别漏了它——这台机器上的证书过期,整份策略当场失效,而这件事不会有任何人提醒你,参见证书有效期正在往47天砍 (https://zhangwenbao.com/tls-certificate-lifetime-47-days-renewal-automation.html)这个大背景; - testing跑满一个完整的业务周期(包含月末、大促这类邮件高峰),报告干净了,再改成enforce; - max_age一开始别写太长,从86400起步,稳定之后再往上加到几周、几个月。写成一年的前提是你确信这份策略不需要临时撤回。 另外提醒一句:如果域名开了泛解析,第一步的curl大概率会返回一个200的主站页面,看着像配好了。判断标准不是有没有响应,是响应体第一行是不是version: STSv1,以及content-type是不是text/plain。zalando.com那个48KB的HTML就是活教材。 最后说说值不值得做。保哥的看法是:不必用“防住了多少次攻击”来算这笔账,针对特定品牌的邮件降级攻击本来就不常见,真按次数算,这笔投入永远算不平。 值得做的理由是另一个。你的域名根上 (https://zhangwenbao.com/dns-txt-domain-verification-records-audit.html)已经躺着十几条各式各样的记录,其中多数是别人让你加的,加完就没人再看。MTA-STS是少数几个反过来的:加完之后,这条链路从“大概是加密的”变成“确定是加密的”,出了岔子会有报告寄到你邮箱里。把一件不可观测的事变成可观测的,这个收益不用等到出事才兑现。 本轮通篇看下来,最扎眼的其实不是那个1,是那张2014年签发、2024年到期、今天还在替一个品牌加密所有来信的自签证书。它一天没出过故障,因为从来没有人要求它证明自己是谁。默认状态就是这样:它不报错,它只是把标准降到了你不会注意到的高度。 ## 常见问题解答 ## 没配MTA-STS,我的邮件就是明文传输的吗? 不是。本轮实测130台收信服务器全都宣告支持STARTTLS,129台真正完成了TLS握手,正常情况下邮件是加密传输的。区别在于这份加密是对方自愿提供的,不是你强制要求的:路径上有人主动篡改协商过程时,发件方会静默退回明文,而你这头看不出任何异常。MTA-STS解决的正是这个“静默”问题。 ## MTA-STS的testing模式有用吗,还是等于没配? 有用,但用处只有一半。testing模式下发件方会照常投递,同时把校验结果写进报告寄给你,所以你能提前发现哪些发件方会失败、失败在哪一步,这是切enforce之前的必要准备。但它不拦截,也就不提供任何强制加密的保证。本轮3个配了MTA-STS的站里有2个停在testing,从策略里的版本号形态看,其中一个已经停了一年多。 ## 用Gmail或Microsoft 365收信,还需要自己配吗? 需要,因为MTA-STS策略是绑在你的域名上的,不是绑在平台上的。平台负责让收信主机的证书是有效的、名字是对得上的(本轮实测127台证书名字都对得上),但DNS里那条TXT声明、HTTPS上那份策略文件、以及mta-sts子域的证书,这三样都得你自己准备。反过来说,正因为平台这一侧已经做好了,你这一侧的工作量比想象中小。 ## MTA-STS和DANE该选哪个? 先看你的邮箱平台支持哪个,这一条基本能替你做完决定。DANE要求收信主机发布TLSA记录、并且平台域名签了DNSSEC,本轮实测中google.com没有签,所以用Gmail收信的56个站没得选,只能走MTA-STS;微软那一侧两个条件都具备,可以两条路都走。两者不冲突,同时配上是推荐做法。 ## 配了之后会不会导致收不到邮件? testing模式不会,它不拦截任何投递。enforce模式存在这个风险,触发条件是发件方无法验证你的收信主机身份——比如证书过期、证书上的名字和MX主机名对不上、或者策略文件里漏写了某台MX。所以切换前必须做两件事:核对策略里的mx清单和真实MX记录完全一致,以及确认每台MX主机的证书名字都能对上。本轮130个站里,有2台服务器的证书名字对不上,它们如果直接切enforce就会真的收不到信。 ## mta-sts这个子域,用现有的Web服务器承载可以吗? 可以,但要注意三点。一是它必须有独立且有效的证书,主域的通配符证书能不能覆盖取决于证书本身,得实际验一遍;二是策略文件的content-type必须是text/plain,用默认的静态目录托管通常没问题,但如果被前端框架接管了路由,很容易变成text/html;三是RFC明确规定发件方不得跟随重定向,所以这个地址不能配成301或302跳到别处,本轮就有一个站的请求被跳去了主站的错误页。 ## 怎么知道有没有人真的在用它? 配上TLS-RPT就知道了。报告里会写明每个发件方的连接总数、成功数、失败数和失败类型,通常一天一份。这也是本文建议把TLS-RPT放在第一步的原因:它零风险、零副作用,而且能在你还没做任何强制之前,先让你看见这条链路上真实发生了什么。 ## 权威参考资料 ## 整站都开了HTTP/3,可做SEO唯一在乎的那个请求,走的还是HTTP/2 - URL:https://zhangwenbao.com/http3-alt-svc-discovery-first-request-crawler-seo.html - 分类:HTTPS与安全加固 - 发布:2026-07-31 | 更新:2026-07-31 - 摘要:实测一个页面168个资源里125个走h3,唯独HTML文档走h2;刷新才升级。123站查下来声明39个、DNS17个、真连49个,还有11站服务开着不打广告。 - 关键词:技术SEO,Nginx,爬虫抓取,HTTP/3 > **TLDR**:摘要:用真实Chrome打开cloudflare.com,168个资源里有125个走了HTTP/3,唯独HTML文档本身走的是HTTP/2。刷新一次再看,文档才变成h3。中间唯一变的东西是浏览器记住了那条Alt-Svc。这就是整件事的要害:HTTP/3必须先从一个TCP响应里被告知存在,所以它永远赶不上一次访问的第一个请求——而爬虫抓一个页面,通常只发那一个请求。123个站查下来,声明了h3的有39个、DNS里查得到的只有17个、我这条线真连得上的又是49个,三种查法三个答案。还有11个站UDP上服务开着却一个字的广告都没打。 > 摘要:用真实Chrome打开cloudflare.com,168个资源里有125个走了HTTP/3,唯独HTML文档本身走的是HTTP/2。刷新一次再看,文档才变成h3。中间唯一变的东西是浏览器记住了那条Alt-Svc。这就是整件事的要害:HTTP/3必须先从一个TCP响应里被告知存在,所以它永远赶不上一次访问的第一个请求——而爬虫抓一个页面,通常只发那一个请求。123个站查下来,声明了h3的有39个、DNS里查得到的只有17个、我这条线真连得上的又是49个,三种查法三个答案。还有11个站UDP上服务开着却一个字的广告都没打。 这件事是从一个很小的疑问开始的。保哥在给一个外贸客户看服务器配置的时候,看到运维在nginx里加了三行HTTP/3的配置,交付文档上写着已启用HTTP/3,页面加载更快。配置本身没写错,nginx -t 通过了,reload也成功了,UDP端口确实在监听。 但保哥当时想到一个问题:这个客户站的流量结构里,有相当一部分是搜索引擎的爬虫。那么爬虫抓页面的时候,走的到底是HTTP/3还是HTTP/2? 这个问题查不到现成答案。全网关于HTTP/3的中文文章,九成在讲QUIC的原理、讲UDP怎么解决队头阻塞、讲怎么在nginx里配那三行,剩下一成在跑测速对比。没有一篇回答一个更基础的问题:一个客户端要走HTTP/3,它是怎么知道这个站支持HTTP/3的。 而这个问题的答案,恰好把整件事的性价比翻了个面。 ## Chrome上量出来的第一件事:125个资源走了h3,文档没走 先不谈规范,直接看真实浏览器干了什么。 用一个干净的标签页打开 https://www.cloudflare.com/,等页面加载完,在控制台里读Performance API。PerformanceResourceTiming 有一个字段叫 nextHopProtocol,它给的是这条请求实际协商到的协议,不是猜的。 对象 | 数量 | 实际协议 | HTML文档本身 | 1 | h2 | 同源资源(脚本、样式、图片) | 125 | h3 | 第三方域资源 | 8 | h2 | 被拦截或未上报时序的 | 35 | (空) | Cloudflare自己的官网,HTTP/3的推广者,一个页面里125个资源跑在h3上。而那个决定这个页面能不能被搜索引擎读到的HTML文档,走的是h2。 然后做一件很简单的事:在同一个标签页里,把同一个地址再打开一遍。 第几次访问 | 文档协议 | 同源资源里h3的数量 | 第1次 | h2 | 125 | 第2次 | h3 | 111 | 同一个浏览器、同一个地址、同一条网络,中间没有改任何配置。唯一的变量是这个浏览器之前来过一次。 换一个站再验一遍。shopify.com首次访问,文档 h2,同源资源全部 h3。规律一样。 ## 这个规律不是Cloudflare特有的 保哥后来在自己的服务器上搭了一套完全可控的环境重做这个实验(下面第六节会详细写),结论一模一样:浏览器第一次碰到一个源,只能走TCP;从第二个请求开始,才有资格走UDP。 如果一个页面有一百个资源,那99个资源享受到了HTTP/3,只有1个没有——听起来损失很小。但如果一个客户端只发一个请求就走人,那它享受到的HTTP/3比例,是零。 ## 为什么第一个请求注定走不到HTTP/3? 这不是实现的疏漏,是协议设计上的必然。 HTTP/2是通过TLS握手里的ALPN扩展协商出来的——客户端在ClientHello里带上我会h2,服务端在同一次握手里回那就h2。整个过程在一次TCP连接内完成,不需要额外往返,也不需要事先知道任何东西。 HTTP/3不行。它跑在UDP上,跟TCP是两条完全独立的通道,那次TLS握手根本不在同一个地方发生。RFC 9114第3.1.1节把发现机制写得很直白: > An HTTP origin can advertise the availability of an equivalent HTTP/3 endpoint via the Alt-Svc HTTP response header field or the HTTP/2 ALTSVC frame ([ALTSVC]) using the "h3" ALPN token. On receipt of an Alt-Svc record indicating HTTP/3 support, a client MAY attempt to establish a QUIC connection to the indicated host and port; if this connection is successful, the client can send HTTP requests using the mapping described in this document. 拆开看这两句话,有三个地方值得停一下。 ## 第一,广告牌挂在响应里,不是请求里 Alt-Svc 是一个响应头。要读到它,得先发一个请求;要发请求,得先建连接;而这个连接只能是TCP。所以整条链路是这样的: 步骤 | 走哪条路 | 发生了什么 | 第1步·首次请求 | TCP / h2 | 拿到响应,顺便读到 Alt-Svc: h3=":443" | 第2步·记进缓存 | — | 按 ma 指定的秒数记住 | 第3步·下一个请求 | UDP / h3 | 直接朝UDP 443发QUIC握手 | 换句话说,HTTP/3的入场券,得先坐着HTTP/2进场才能领到。 ## 第二,那个词是MAY,不是MUST 规范说客户端可以去试试QUIC连接,没说必须。所以就算广告牌挂着,客户端也完全有权当没看见——比如它判断当前网络的UDP不可靠,或者它压根没实现QUIC。这一点对爬虫尤其重要:一个只想把HTML抓回去的程序,没有任何动机去多试一条协议。 ## 第三,记住这件事是有期限的 Alt-Svc 里的 ma 参数(max-age)规定了这条广告能被记多久。保哥统计了123个站的实际取值: ma取值 | 换算 | 站数 | 86400 | 24小时 | 28 | 2592000 | 30天 | 9 | 93600 | 26小时 | 2 | 中位数是24小时。这意味着一个正常用户如果隔了一天多再来,他的第一个请求又会退回TCP,整个升级过程从头再来一遍。 ## 顺手发现:10个站的广告牌上还挂着2021年的版本号 统计版本token的时候冒出来一件挺有意思的事。39个声明了h3的站里: token | 是什么 | 站数 | h3 | RFC 9114正式版 | 39 | h3-29 | 2020年的草案第29版 | 10 | h3-27 | 草案第27版 | 4 | h3-Q050 | Google自己的gQUIC变体 | 1 | HTTP/3在2022年6月就成了正式标准。这些草案版本对应的浏览器实现早就被删干净了,现在没有任何一个主流客户端会去连 h3-29。但这些token还老老实实地挂在响应头里,每一次响应都多发几十个字节,给谁看的没人知道。 这是一个很典型的配置化石:某年抄的一份配置,里面写着当时该写的东西,之后再没人回头看过。响应头这个地方特别容易长化石,因为它错了也不报错、多了也不报错。 这里还有个容易被忽略的细节:这份缓存是绑在浏览器配置文件上的。无痕窗口每开一次就是一张白纸,所以拿无痕窗口测出来的协议永远是h2,而你会以为是配置没生效。用错了工具就会查出错误的结论 (https://zhangwenbao.com/curl-head-get-response-header-diagnosis-seo.html),这类事在协议这一层特别容易踩。 ## 爬虫为什么永远停在第一个请求上? 现在把这个机制套到搜索引擎爬虫身上。 Googlebot抓一个页面的典型行为是:发一个GET请求,拿到HTML,解析出链接,然后把这个URL从队列里划掉。它不会为了拿到一张图片而在同一条连接上追加请求(渲染服务是另一套流程,稍后说)。所以从连接的角度看,它的行为特征是: - 每个URL基本上就是一次请求 - 连接不长期保持,抓取任务之间没有会话延续 - 没有本地配置文件去持久化Alt-Svc这类信息 把这三条跟上一节的机制放在一起,结论就出来了:爬虫抓走的那份HTML,几乎不可能是通过HTTP/3传输的。它每次都是那个第一次,而第一次永远走TCP。 ## 这跟Google官方说法对得上吗? Google在抓取基础设施的文档里明确写过Googlebot支持HTTP/1.1和HTTP/2,并且说明抓取协议由Google侧决定、站点无法指定。至于HTTP/3,官方文档里没有承诺支持——而按照上面的发现机制,就算它支持,也用不上。 这跟保哥之前写过的另一件事结构上是同一类问题:Client Hints那套机制要求先有一次请求告诉浏览器要带什么,后续请求才带上 (https://zhangwenbao.com/vary-header-cache-fragmentation-googlebot-version-seo.html),而爬虫每次都是第一次,所以那套机制对它天然失效。Alt-Svc是同一个形状的坑,只不过这次失效的不是内容协商,是整条传输协议。 ## 那渲染的时候呢? Google的渲染服务在执行JavaScript时确实会去抓页面引用的子资源,这个过程里理论上可以复用连接、也可以在拿到Alt-Svc之后升级。但这不改变一个事实:决定这个页面是否值得进索引、标题是什么、canonical指向哪里的那份HTML,是在渲染之前就被抓走的。那一份,走的是TCP。 所以真实的账是这样的:HTTP/3能帮到的是打开页面之后那一大堆资源的加载体验,帮不到的恰恰是抓取这一环。它是一个用户体验优化,不是一个抓取优化。两者在SEO里都算数,但不能混着说。 ## 一个站到底开没开HTTP/3,为什么三种查法给三个答案? 为了把这件事量出来,保哥拉了一个125个域名的样本池,同时用三种方式去问同一个问题——这个站支持HTTP/3吗。 - 问HTTP响应:发一个普通的HTTPS请求,看响应头里有没有 Alt-Svc 带h3 - 问DNS:查HTTPS类型的资源记录(RFC 9460定义的SVCB家族),看alpn参数里有没有h3 - 直接去敲门:用一个真正的QUIC客户端朝UDP 443发起h3握手,看握不握得上 查法 | 命中站数 | 占可达样本 | 响应头里声明了h3 | 39 | 31.7% | DNS里查得到h3 | 17 | 13.6% | 真握手握得上 | 49 | 39.2% | 三个数,而且它们互相不是包含关系。这里面藏着两组更有意思的差集。 ## 11个站服务开着,广告一个字没打 有11个站——包括developer.mozilla.org、www.ebay.com、www.paypal.com、www.python.org、www.target.com——UDP 443上确确实实跑着HTTP/3,握手一次就通,但它们的HTTP响应里没有Alt-Svc头。 这意味着什么?意味着这些站的HTTP/3服务在那儿空转。浏览器没有任何途径知道它存在,于是永远不会去连。服务器为它分配了端口、进程和证书,收到的请求数是零。 这类站的常见成因是响应头被中间层洗掉了:源站发了Alt-Svc,前面一层反向代理或WAF没有透传。这跟保哥之前拆过的那类配置改完每次都通过、副作用却一声不吭 (https://zhangwenbao.com/nginx-config-silent-seo-side-effects-audit.html)的问题是一路的——配置是对的,链路把它吃掉了。 ## 同一个CDN后面,19个站开了、20个站没开 还有一组数字值得单拎出来。把39个声明了h3的站按 server 头归类,再把84个没声明的也归一遍: 服务器与CDN | 声明了h3 | 没声明 | Cloudflare | 19 | 20 | nginx | 2 | 13 | Vercel | 3 | 9 | Netlify | 0 | 8 | Google Frontend | 3 | 1 | Cloudflare那一行几乎是对半开。同一个CDN、同一套边缘基础设施,一半的站声明了HTTP/3,另一半没有。这说明它不是CDN统一发下来的,而是每个站自己面板上的一个开关。 Netlify那一行是0比8,一个都没有。这类平台型托管的选择往往更整齐,因为它是平台层的决策而不是用户的决策。 这一栏数据的实际用处是:如果你在用Cloudflare而想确认自己开没开,别去查Cloudflare官方文档说支不支持,直接拿curl看你自己域名的响应头。厂商支持和你的站启用了,是两个独立的事实。 ## 还有1个站,广告打了但敲不开 反方向也有一个:www.cloudflare.com声明了h3,保哥这条线上却握不上手。这个数只有1,样本太小不足以下结论,但它提醒了一件必须交代的事—— UDP 443这条路,本身就不像TCP 443那么畅通。保哥的125站里有26个h3握手报连接错误。这里面分不清哪些是站没开、哪些是路上被丢包。中国大陆方向的运营商网络对UDP高流量的处理策略跟TCP不一样,这是行业里公开讨论过很多年的事。 所以对做外贸独立站的人,这条要额外记一笔:你在国内测出来的HTTP/3可用性,跟你的海外用户测出来的可能完全是两回事。这跟海外用户说站点打不开时要分层排查网络路径 (https://zhangwenbao.com/overseas-store-unreachable-slow-network-layer-diagnosis-dns-routing-cdn.html)是同一个方法论:先分清是服务的问题还是路的问题。 ## 握手时长的分布也说明问题 分位 | h3握手耗时 | p10 | 50 ms | 中位 | 155 ms | p90 | 2225 ms | max | 7195 ms | 中位155毫秒是个很漂亮的数字,但p90到了2.2秒。这条尾巴不是服务器慢,是UDP包在路上重传。协议再先进,也架不住路不通。 ## DNS里那条记录能不能救回第一个请求? 前面说HTTP/3赶不上第一个请求,其实留了个口子没堵:如果客户端在发请求之前就知道这个站支持h3呢? 这正是RFC 9460定义的HTTPS资源记录要解决的问题。它是一条DNS记录,跟A记录一起在解析阶段就返回,里面可以带alpn参数。客户端解析域名的时候顺手就知道了这个站会h3,于是可以跳过TCP那一轮,第一个请求就朝UDP去。 听上去完美。实际覆盖率是这样的: 指标 | 站数 | 占比 | 有HTTPS类型DNS记录 | 33 | 26.4% | 其中alpn参数里含h3 | 17 | 13.6% | h3排在alpn第一位 | 15 | 12.0% | 同时带IP提示(ipv4hint) | 15 | 12.0% | 声明了Alt-Svc却没有这条记录 | 23 | — | 最后一行是重点:39个声明了HTTP/3的站里,有23个没有配DNS记录。这23个站的每一次首访,都注定要先走一趟TCP。 ## 就算配了,也不一定用得上 更扫兴的是,cloudflare.com和shopify.com这两个站的HTTPS记录都配得好好的、alpn里h3排第一位,而本文开头的Chrome实测里,它们的首次访问依然走的h2。 原因是浏览器要用上这条记录,得能查到它。而系统默认的DNS解析走的是操作系统的stub resolver,多数情况下只问A和AAAA记录,拿不到HTTPS类型。Chrome只有在开启了安全DNS(DNS over HTTPS)的时候,才有稳定的途径拿到这条记录。而这个开关的默认状态、以及企业策略下的实际生效情况,站长这一侧管不着。 所以这条路的实际情况是:它是唯一能让首次连接就走h3的机制,覆盖率13.6%,而且配了也要看客户端脸色。爬虫这一侧就更不用指望了——爬虫的解析器为什么要多查一条它用不上的记录。 ## 那17条记录里还藏着一个附赠品 把这17条HTTPS记录的参数逐个解出来之后,有一个细节值得说:其中15条同时带了ipv4hint和ipv6hint。 这两个参数的作用是把IP地址直接塞进这条记录里,客户端解析到HTTPS记录的同时就拿到了IP,不用再单独发一次A记录查询。省下来的是一个完整的DNS往返。 参数 | 作用 | 17条记录里的覆盖 | alpn | 告知支持哪些协议 | 17 | ipv4hint | 顺带给IPv4地址 | 15 | ipv6hint | 顺带给IPv6地址 | 14 | 换个角度看这件事:这条记录的价值并不全在HTTP/3上。就算你完全不打算开HTTP/3,配一条带IP提示的HTTPS记录,也能给支持它的客户端省掉一次解析往返。这是为数不多的、对首次访问也有效的优化——而首次访问,正是爬虫唯一会做的那件事。 代价是什么呢?多数DNS服务商的控制台上就是加一条记录的事。跟证书这类必须持续维护的东西 (https://zhangwenbao.com/tls-certificate-lifetime-47-days-renewal-automation.html)不一样,这条配完基本不用管。 ## 在自己的服务器上,这件事能不能复现? 测别人的站有个天然的弱点:你看不到服务端那一侧发生了什么。所以保哥在自己的服务器上搭了一套完整可控的环境,把这件事从两头都量一遍。 环境是这样的:nginx 1.28.3,独立实例跑在 /tmp 下的一套配置里,不碰生产站;证书直接用本站的真证书,这样浏览器不会因为证书问题拒绝QUIC;开了五个端口做不同的对照组。 端口 | 配置 | 用来验证什么 | 18801 | listen ssl + listen quic + Alt-Svc | 教程里的标准写法 | 18802 | 只有 listen ssl | 纯HTTP/2对照组 | 18803 | 开了quic,不发Alt-Svc | 广告牌到底是不是开关 | 18804 | 只写 listen quic,没写ssl | 漏配TCP的后果 | 18805 | 加了 ssl_early_data on | 0-RTT的实况 | 为了保证服务端收到的东西跟客户端发出去的一致,每个端口的响应都用Lua把实际收到的内容回显出来——协议版本、Cookie的实际字节数、头字段条数、nginx记录的请求长度。这是这类实验的必备品:只要有一档回显对不上,整组数据就该作废。 ## 用真实Chrome打自己的实验台 浏览器访问 https://zhangwenbao.com:18801/,服务端回显如下: 第几次 | 服务端回显的协议 | 收到的Cookie字节 | 头字段条数 | nginx记的请求长度 | 第1次 | HTTP/2.0 | 529 | 14 | 884 | 第2次 | HTTP/3.0 | 529 | 13 | 30 | 第一次h2、第二次h3,跟在cloudflare.com上看到的一模一样。这回是在完全自己掌握的服务器上复现的,配置是自己写的,日志是自己看的,没有任何中间层可以背锅。 顺带这张表还暴露了一件跟本题无关但很值钱的事:同一份529字节的Cookie,nginx在HTTP/2上把请求长度记成884,在HTTP/3上记成30。Cookie的回显两次都是529,说明请求内容一个字节没少,是日志失真。这个变量在HTTP/1.1和HTTP/2之间就已经不可横向比了 (https://zhangwenbao.com/http2-hpack-cookie-request-header-bytes-seo.html),到了HTTP/3差距拉到29倍。任何按日志统计上行流量的脚本,切协议之后会直接给出错误的曲线。 ## 配置写对了、nginx -t也通过了,服务为什么还是没人来? 实验台上剩下三个端口,回答的是同一类问题:哪些错误是配置检查抓不住的。 ## 18803:不发Alt-Svc,服务照样在 这个端口开了QUIC,但一个字节的 Alt-Svc 都不发。保哥用QUIC客户端直接朝它敲门——握手成功,返回HTTP/3.0,一切正常。 所以 Alt-Svc 是一块广告牌,不是开关。不挂广告牌,服务照样在UDP上等着,只是没有一个浏览器会知道该往那儿去。这两件事在配置文件里是两行不相干的指令,删掉一行,另一行不会报错。 ## 18804:只写了listen quic,没写listen ssl 这是一个真实存在的抄配置事故形态——教程里写了两行 listen,只抄了后面那行。结果是: 探测方式 | 结果 | nginx -t | 通过 | 启动 | 成功,UDP端口正常监听 | TCP侧curl | 连接失败,返回码000 | UDP侧QUIC客户端 | 握手成功,返回HTTP/3.0 | 服务确实在跑,而且跑得挺好。但浏览器永远发现不了它——因为发现HTTP/3的唯一途径是从TCP响应里读Alt-Svc,而TCP这一侧压根没人应答。用户看到的是站点打不开,日志里干干净净什么都没有,因为请求根本没到达。 这个配置在语法检查里是完美的。这就是这类问题的性格:服务器没有说不,它只是做不到 (https://zhangwenbao.com/slow-server-truncated-response-crawl-rate-seo.html),而所有的检查工具都只会问它说了什么。 ## 18805:ssl_early_data与0-RTT 0-RTT是HTTP/3宣传里最常被拿出来说的卖点——恢复一条之前的连接时可以零往返直接发数据。实验台上开了 ssl_early_data on,连了两次,两次都正常返回HTTP/3.0。 但这里有一个必须写清楚的前提:这台服务器的nginx是用OpenSSL 1.1.1编译的,走的是nginx提供的QUIC兼容层。nginx官方文档明确写过,用这个兼容层时不支持0-RTT。也就是说,配置文件里那行 ssl_early_data on 写了也不生效,而 nginx -t 一样通过、启动一样正常、错误日志里一行提示都没有。 这已经是这一节里第三个写了但不生效、而且完全静默的例子了。HTTP/3这块地方盛产这种配置。 ## 本站开过HTTP/3,22分钟之后关掉了 写到这儿保哥去翻了一下自己服务器上的配置备份,发现了一件挺尴尬的事。 2026年6月21日11点57分,本站的nginx配置里确实有过这么几行: listen 443 quic; http3 on; ssl_early_data on; add_header Alt-Svc 'h3=":443"; ma=86400' always; 而同一天的配置备份文件名里,带着 disableh3 三个字,时间戳是11点57分30秒。开了不到半小时就关掉了。 当时关掉的原因已经无从考证,但这件事本身很能说明问题:一个自己写技术博客、自己管服务器的人,把HTTP/3开起来又关掉,中间没有留下任何一条为什么的记录。这大概是很多站的HTTP/3的真实生命周期——跟着教程开一次,遇到点说不清的现象就关掉,然后再也不提。 而这次做完实验之后,保哥反倒更倾向于先不开。理由在下一节。 ## 那这个HTTP/3到底还要不要开? 把前面所有的数据摆在一起,答案不是要或不要,而是看你的流量结构里谁占大头。 ## 先看这三个判据 判据 | 怎么量 | 倾向 | 回访用户占比 | 分析工具里的新访客与回访比例 | 回访多 → 值得开 | 单页资源数 | 一个典型页面加载多少个同源请求 | 资源多 → 值得开 | 用户网络质量 | 移动端占比、弱网地区占比 | 弱网多 → 值得开 | 这三条全都指向同一个方向:HTTP/3的收益,跟一次会话里请求数量的多少成正比,跟单次请求的重要性无关。而SEO关心的恰恰是那个数量为1、重要性为100的请求。 ## 如果决定开,这几件事一件都不能少 - 两行listen都要写。listen 443 ssl 和 listen 443 quic 缺一不可,缺前者用户直接打不开,缺后者广告牌指向一个空地址。 - Alt-Svc必须真的发出来。配置里加了不等于用户收到了,中间任何一层代理都可能把它吃掉。判据是拿curl看响应头,不是看配置文件。 - 云防火墙要放行UDP 443。很多云厂商的安全组默认只开TCP,UDP这条要单独加,加漏了的表现是端口在监听但外面连不上。 - 配一条HTTPS类型的DNS记录。这是唯一能救回首次连接的东西,虽然只有13.6%的站配了,但配它的成本几乎为零。 - 别指望0-RTT。先确认你的nginx是用什么TLS库编译的,用兼容层的话这个开关是装饰品。 ## 那条最容易漏的:UDP在防火墙上是另一套规则 这一条单独拎出来,是因为它在实验台上真实卡了保哥一次。 服务器上的端口放行,习惯性写法是按端口号加规则。但TCP和UDP在防火墙里是两个完全独立的协议族——放行了 18801/tcp,不等于放行了 18801/udp。这两条规则得分别加: firewall-cmd --add-port=443/tcp firewall-cmd --add-port=443/udp 更麻烦的是云厂商那一层的安全组。多数云平台的安全组模板默认只给TCP,UDP要手动勾。而漏掉它的表现是最难排查的那一种: 你在服务器上看到的 | 用户实际遇到的 | ss -lun 显示UDP端口正常监听 | QUIC握手包发出去石沉大海 | nginx错误日志一行没有 | 浏览器静默回落TCP | 本机curl测一切正常 | 什么异常都感觉不到 | 三行全对得上,问题却真实存在。因为握手包连服务器的网卡都没摸到,服务端视角是完全干净的。这类问题只有一个查法:从外网、用一个真正的QUIC客户端去敲一次。 ## 如果决定不开,会损失什么 损失的是回访用户从第二个请求开始的那部分传输效率,以及弱网环境下的连接韧性。这两样都是真实收益,但都不落在搜索引擎能看到的那一份HTML 上。 保哥的建议是:如果你的站正在为速度做优化,把力气先花在那些对第一个请求也有效的地方——服务端响应时间、缓存命中、压缩策略。这些每一样都直接作用在爬虫抓的那一次上。TTFB这一层的优化怎么同时作用在体验和抓取两端 (https://zhangwenbao.com/ttfb-multi-layer-cache-core-web-vitals-crawl-budget-seo.html),那篇算过一笔完整的账。 说到底,HTTP/3是一项好技术,只是它的收益曲线跟很多人以为的位置不一样。它给的是一条路上第二辆车之后的所有车更好走,而搜索引擎那辆车,永远是第一辆。 ## 常见问题解答 ## 开了HTTP/3会直接提升排名吗? 不会有直接的排名加成。它可能通过改善真实用户的加载体验,间接作用在Core Web Vitals那几个指标上——但这条链路只对回访用户和资源密集的页面成立,而且首屏那个HTML文档拿不到这份收益。用一句话概括:它是体验侧的优化,不是抓取侧的优化。 ## 怎么确认我的站的HTTP/3真的在工作? 三步,缺一步都可能给出错误结论。第一步用curl看响应头里有没有 Alt-Svc,这验证广告牌挂出来了;第二步在浏览器里连续打开两次同一个页面,看第二次的 nextHopProtocol 是不是h3,这验证客户端能升级;第三步从站外的网络(最好是海外线路)再测一次,这验证UDP 443路上通。只做第一步是最常见的误判来源。 ## Googlebot支持HTTP/3吗? Google官方文档说明Googlebot支持HTTP/1.1和HTTP/2,并且抓取协议由Google侧决定、站点无法指定。HTTP/3没有出现在这个支持列表里。而按照本文拆的发现机制,即便未来支持了,只发一个请求的抓取行为也拿不到升级的机会——除非它去查HTTPS类型的DNS记录。 ## 我用curl测不出HTTP/3,是配置有问题吗? 大概率不是。绝大多数系统自带的curl编译时没有带HTTP/3支持,可以用 curl --version 看Features那一行里有没有HTTP3字样。没有的话它连都连不上,跟你的服务器配置没关系。这也是很多人误判自己配置失败的原因。 ## CDN前面加了HTTP/3,源站还需要配吗? 不需要,也没用。用户是跟CDN边缘节点建立连接的,那一段用什么协议由CDN决定;边缘节点回源那一段是另一条独立连接,绝大多数CDN回源走的仍是HTTP/1.1或HTTP/2。所以在CDN后面的源站上开HTTP/3,既不会被用户用到,也不会被CDN用到。 ## 那11个服务开着却不打广告的站,是不是配错了? 从结果看是无效配置,但成因不一定是配错。更常见的情况是响应头在链路上被某一层洗掉了,源站那边看自己的配置一切正常。判据很简单:在离用户最近的那一层拿curl看响应头,看不到就是没到用户手里,不管配置文件里写了什么。 ## UDP 443被网络限速或阻断了会怎么样? 浏览器会静默地回落到TCP,用户什么都感觉不到,你的日志里也不会有任何异常记录——因为那些UDP包压根没到你的服务器。这类问题的排查特点是只能从客户端一侧看到,服务端视角是完全空白的。这跟中间某一跳丢包但终点一个包不丢 (https://zhangwenbao.com/traceroute-intermediate-hop-latency-loss-misread.html)那类现象一样,观测点选错了就什么也看不见。 ## 权威参考资料 ## 限速规则拒掉的第4个请求是robots.txt,全站SEO抓取会停12小时 - URL:https://zhangwenbao.com/nginx-rate-limit-robots-txt-crawl-rate-seo.html - 分类:HTTPS与安全加固 - 发布:2026-07-30 | 更新:2026-07-30 - 摘要:实测限速拒绝的SEO代价:403对抓取速率完全无效、429与5xx降速作用于整个主机名、Retry-After不写always就只在200上发。含164站两地出口对照普查。 - 关键词:技术SEO,抓取预算,Nginx,服务器加固 > **TLDR**:摘要:限速这件事只有一个对外的出口——状态码。保哥把nginx的limit_req按最常见的抄法配在server级,然后模拟爬虫抓一轮:第4个请求开始被拒,而爬虫那一轮排在第5、第6位的正好是robots.txt和sitemap.xml,两个都吃到503。按Google官方文档的规矩,robots.txt取不到会让它前12小时停止抓取整站。更麻烦的是想把这两个端点摘出来时才发现,limit_req off这条指令根本不存在,配置直接报错。另外实测到:nginx拒绝时默认一个Retry-After都不发;add_header不写always的话,这个头会在200上发出、在429上消失;503一旦带上max-age就会被边缘缓存存住,站恢复了边缘还在发503。 > 摘要:限速这件事只有一个对外的出口——状态码。保哥把nginx的limit_req按最常见的抄法配在server级,然后模拟爬虫抓一轮:第4个请求开始被拒,而爬虫那一轮排在第5、第6位的正好是robots.txt和sitemap.xml,两个都吃到503。按Google官方文档的规矩,robots.txt取不到会让它前12小时停止抓取整站。更麻烦的是想把这两个端点摘出来时才发现,limit_req off这条指令根本不存在,配置直接报错。另外实测到:nginx拒绝时默认一个Retry-After都不发;add_header不写always的话,这个头会在200上发出、在429上消失;503一旦带上max-age就会被边缘缓存存住,站恢复了边缘还在发503。 先把一件事说在前面:本文不反对限速。服务器扛不住的时候必须有人踩刹车,这没得商量。 本文要拆的是,踩刹车这个动作在爬虫那边被翻译成了什么——因为你和爬虫之间只有一条信道,就是HTTP响应,而拒绝这件事在这条信道上只能用状态码来表达。这门语言只有几个词,每个词的代价都不一样,而大多数人是在完全不知道代价表的情况下选的词。 ## 一条限速规则拒掉的,为什么会是robots.txt? 从一个最普通的配置说起。搜“nginx防采集”,排在前面的写法基本都是这个形状: limit_req_zone $binary_remote_addr zone=sz:1m rate=1r/s; server { limit_req zone=sz burst=2 nodelay; ... } 写在server块里,整站一把限。这个位置很关键——limit_req写在哪一层,配额就在哪一层共享。写在server级,意味着这个站上所有的东西共用一份配额:HTML页面、图片、CSS、robots.txt、sitemap.xml,全部记在同一个账本上。 ## 实测:爬虫一轮抓取的第5、第6个请求 保哥在服务器上另起了一个nginx实例(端口18448,不碰生产),用上面那份配置,然后按爬虫一轮抓取的典型顺序连着发6个请求,UA用Googlebot: 顺序 | 请求 | 状态码 | 1 | GET / | 200 | 2 | GET /deep.html | 200 | 3 | GET /deep.html | 200 | 4 | GET /deep.html | 503 | 5 | GET /robots.txt | 503 | 6 | GET /sitemap.xml | 503 | 速率1r/s加burst=2,也就是“瞬时最多放3个”,第4个开始拒。前三个额度被普通页面用掉了,轮到robots.txt的时候账上已经空了。 把access_log打开(加了$limit_req_status变量之后): 200 "GET / HTTP/1.1" lrs=PASSED 200 "GET /deep.html HTTP/1.1" lrs=PASSED 200 "GET /deep.html HTTP/1.1" lrs=PASSED 503 "GET /deep.html HTTP/1.1" lrs=REJECTED 503 "GET /robots.txt HTTP/1.1" lrs=REJECTED 503 "GET /sitemap.xml HTTP/1.1" lrs=REJECTED ## robots.txt拿到503的代价,官方写得很具体 Google在robots.txt规范那份文档 (https://developers.google.com/search/docs/crawling-indexing/robots/robots_txt)里,对robots.txt取不到时的行为分了三段: - 前12小时:Google stops crawling the site but keeps trying to fetch the robots.txt file.——停止抓取整站; - 之后30天:用最后一份能用的旧版本,同时继续尝试取新的。原文还补了一句A 503 (service unavailable) error results in fairly frequent retrying; - 30天之后:如果站点整体是可访问的,就当作没有robots.txt处理。 注意第一段的措辞:不是“这个URL抓不到”,是stops crawling the site。一个几百字节的文本文件取不到,代价是整站停抓半天。 Google在暂停线上业务那份文档 (https://developers.google.com/search/docs/crawling-indexing/pause-online-business)里把这条又单独强调了一遍:Don't return a 503 HTTP response status code for the robots.txt file because this blocks all crawling. 这两份文档讲的都是“你主动关站”的场景,而上面那个实验说明了另一件事:你没打算关站,一条限速规则也能把robots.txt打成503,而且触发条件低得离谱——爬虫抓到第4个页面就够了。 ## 想把robots.txt摘出配额,会先撞上一个不存在的指令 知道问题之后,第一反应通常是这样写: location = /robots.txt { limit_req off; } 这条配置过不了nginx -t: [emerg] invalid parameter "off" in /tmp/d84_nginx2.conf:42 limit_req没有off这个开关。它有burst、有nodelay、有delay,就是没有关掉自己的写法。而nginx的指令继承规则是“本层没写才继承上层”,所以你也没法靠“在location里写一个更宽松的limit_req”来绕——那只会换一个zone继续限。 正解藏在limit_req模块文档 (https://nginx.org/en/docs/http/ngx_http_limit_req_module.html)里一句很不起眼的话:Requests with an empty key value are not accounted.——键值为空的请求不计入。于是做法变成用map把特定路径的键置空: map $uri $exempt_key { default $binary_remote_addr; ~^/robots\.txt$ ""; ~^/sitemap.*\.xml$ ""; ~^/llms\.txt$ ""; } limit_req_zone $exempt_key zone=sz2:1m rate=1r/s; 换成这份配置之后,同样的6个请求: 顺序 | 请求 | 状态码 | access_log里的$limit_req_status | 1到3 | 页面 | 200 | PASSED | 4 | 页面 | 503 | REJECTED | 5 | /robots.txt | 200 | - | 6 | /sitemap.xml | 200 | - | 最后两行的-和第1到3行的PASSED是两个不同的意思:PASSED是“进了账本、这次放行”,-是“压根没记账”。前者会消耗后面请求的额度,后者不会。 顺带一提,这个“空键不计数”的技巧在拦AI爬虫又不误伤搜索引擎 (https://zhangwenbao.com/nginx-ai-bot-blocking-rate-limit-rdns-misblock-account.html)那套配置里同样管用——只不过那边判的是UA,这边判的是路径,机制是同一个。 ## 403、429、503,Google分别怎么算账? 既然只能用状态码说话,那就得把这几个词的代价表看清楚。Google在HTTP状态码、网络与DNS错误那份文档 (https://developers.google.com/search/docs/crawling-indexing/http-network-errors)里把每一档都写明白了,整理成表是这样: 状态码 | 对抓取速率 | 对已索引的URL | 官方原话要点 | 403 / 401 | 没有影响 | 从索引中移除 | The 4xx status codes, except 429, have no effect on crawl rate. | 404 / 410 | 没有影响 | 从索引中移除 | 与其他4xx同等对待 | 429 | 降速 | 先保留,最终丢弃 | treats the 429 status code as a signal that the server is overloaded, and it's considered a server error | 500 / 502 / 503 / 504 | 降速 | 先保留,最终丢弃 | already indexed URLs are preserved in the index, but eventually dropped | 这张表里最值得盯的是第一行。 ## 用403挡爬虫来“减负”,是完全无效的 Google在同一份文档里写了一句几乎是点名的话:Don't use 401 and 403 status codes for limiting the crawl rate.——别用401和403来限制抓取速率。 原因就在表里:4xx(429除外)对抓取速率没有任何影响。你返回403,爬虫不会因此少来,它只会认为这个URL不该被索引,然后把它从索引里删掉。你付出了索引,什么也没换回来。 这条特别值得说,因为403是防护类中间件最爱用的码。WAF拦截、IP黑名单、UA黑名单、Bot管理的默认动作,绝大多数是403。而它在抓取速率这件事上的效果是零。 ## 降速的幅度和范围,比想象的大 两条容易被忽略的细则,都在降低Google抓取速率那份文档 (https://developers.google.com/search/docs/crawling-indexing/reduce-crawl-rate)里: 第一条是幅度。文档说500的情况下The decrease in crawl rate is proportionate to the number of individual URLs that are returning a server error.——降速幅度与出错的URL数量成正比。所以偶发一两个5xx影响很小,而限速规则一旦生效通常是成片触发,出错URL数会瞬间拉高。 第二条是范围,这一条杀伤力最大: > The reduced crawl rate affects the whole hostname of your site (for example, subdomain.example.com), both the crawling of the URLs that return errors, as well as the URLs that return content. 降速作用在整个主机名上,而且明确包括那些返回正常内容的URL。 这句话把很多“精细化限速”的方案直接判了死刑。比如常见的“只给搜索接口限速,其他路径不限”——搜索接口被爬出来的那些参数URL吃到503,降速会连着把你的商品页、文章页一起压下去。分面导航产生的海量参数URL (https://zhangwenbao.com/faceted-navigation-seo-crawl-budget-index-control.html)之所以危险,除了浪费额度,还有这一层:它是最容易触发限速的那类流量,而触发之后代价由全站承担。 ## 还有个时间上限 Google对“用错误码换喘息”这件事给了个明确的时间窗: > If you need to urgently reduce the crawl rate for short period of time (for example, a couple of hours, or 1-2 days), then return 500, 503, or 429 HTTP response status code instead of 200. 紧跟着就是警告: > We don't recommend that you do this for a long period of time (meaning, longer than 1-2 days)... if Googlebot observes these status codes on the same URL for multiple days, the URL may be dropped from Google's index. 一两天是官方给的安全窗口。超过之后URL开始掉索引。而限速规则的特点是一旦配上就长期在那儿,它不会像故障那样被人记得去关掉。 ## 有没有不用状态码的限速办法? 看到这里应该会有一个自然的念头:既然状态码这门语言每个词都有代价,能不能换一条路,直接跟爬虫“打个招呼”说慢点来? 有这么一条路,而且历史比429还老:robots.txt里的Crawl-delay。 坏消息在Google的robots.txt规范文档里,一句话带过: > Google supports the following fields (other fields such as crawl-delay aren't supported): user-agent, allow, disallow, sitemap. 支持的字段只有四个,crawl-delay被点名放在括号里当反例。也就是说,唯一一条不靠状态码的限速通道,对最需要限的那个爬虫是关闭的。 ## 那大家还在写吗?写给谁? 保哥把那批站的robots.txt也一并拉了下来,93个能拿到200的,逐份解析: 统计项 | 数量 | 占比 | 写了Crawl-delay | 24个 | 25.8% | 写了已废弃的Request-rate | 1个 | 1.1% | 写了Sitemap | 82个 | 88.2% | 四分之一的站还在写这个字段。取值分布也挺整齐:10出现34次,1出现16次,剩下的5、4、2、0.5各一两次。 真正有意思的是这些Crawl-delay挂在哪个User-agent段下面: 挂在哪个UA段下 | 出现次数 | ahrefsbot | 11 | ahrefssiteaudit | 10 | mj12bot | 9 | pinterest / pinterestbot | 13 | *(所有爬虫) | 7 | facebookbot / omgilibot / megaindex / rogerbot / baidu | 各1 | 绝大多数是写给SEO工具爬虫的——Ahrefs、Majestic、Moz这一批。挂在*下的只有7次。 这张表其实在讲一件挺合理的事:大家早就知道Crawl-delay对Google没用,所以只拿它去挡那些真的会读它的爬虫。Ahrefs和Bing确实遵守,Google不遵守。于是这个字段在今天的实际角色,是“SEO工具限速开关”,而不是“抓取速率控制器”。 顺带看了一眼这批robots.txt对AI爬虫的点名情况:GPTBot出现在12.9%的站里,ClaudeBot和PerplexityBot各8.6%,Google-Extended7.5%,CCBot6.5%,Bytespider5.4%。拦AI爬虫的三层选型 (https://zhangwenbao.com/block-ai-bots-robotstxt-waf.html)里robots.txt那一层的实际覆盖率,大概就是这个量级。 结论回到主线:声明这条路对搜索引擎是断的,所以本文剩下的篇幅只能继续在状态码上做文章。 ## nginx默认用哪个码拒绝?它带Retry-After吗? 先说默认值。limit_req模块文档 (https://nginx.org/en/docs/http/ngx_http_limit_req_module.html)里写着limit_req_status的默认值是503。也就是说,如果你没显式配过,nginx拒绝超速请求时说的是“服务不可用”,而不是语义上更准确的“你请求太多了”。 在Google的账本里这两个码是等价的(都算服务器错误、都降速),所以这个默认值不算错。但在别的消费者那里区别不小——很多HTTP客户端库对429有专门的退避逻辑,对503没有。 ## 拒绝时带不带Retry-After?实测是不带 RFC 6585定义429的时候写了一句MAY include a Retry-After header indicating how long to wait before making a new request;RFC 9110对503的描述是The server MAY send a Retry-After header field to suggest an appropriate amount of time for the client to wait before retrying。两处都是MAY,不是MUST。 nginx对这个MAY的选择是:不发。实验台上把limit_req打到超限,把响应头里的Retry-After抓出来: 配置 | 超限时的状态码 | Retry-After | limit_req zone=rz1;(默认) | 503 | 无 | limit_req_status 429; | 429 | 无 | 加add_header Retry-After 60 always; | 429 | 60 | 要有这个头,得自己加。这不算意外,nginx一贯的风格就是不替你做决定。 ## 加了却发不出去:always的那个坑 问题出在怎么加。add_header的默认行为是只对一小撮成功类状态码生效,不带always的话,5xx和429上根本不会出现。实验台把两种写法并排跑,同一个zone、同一个速率、同一批请求: 写法 | 第1次(200) | 第2次(429) | 第3次(429) | 第4次(429) | add_header Retry-After 60; | RA=60 | RA=空 | RA=空 | RA=空 | add_header Retry-After 60 always; | RA=60 | RA=60 | RA=60 | RA=60 | 第一行的行为足够荒诞:Retry-After发给了唯一不需要它的那些请求(成功的200),唯独在需要它的那些请求(被拒的429)上消失。 更糟的是这个错误的自检成本。你写完配置,curl -I一下首页,看到Retry-After: 60好好地在那儿,于是打勾收工。可首页那次请求是200,它走的正是add_header默认生效的那条路。要发现这个问题,你得先把自己限到超速,再去看头——而这恰恰是没人会做的一步。 ## Retry-After写什么值,服务器不管 顺手测了三种写法,nginx一律原样发出,不做任何校验: 配置里写的值 | 响应头里的值 | 是否合法 | 120 | Retry-After: 120 | 合法(延迟秒数) | Wed, 21 Oct 2026 07:28:00 GMT | 原样 | 合法(HTTP日期) | soon | Retry-After: soon | 不合法,照发 | RFC 9110第10.2.3节只允许两种形式:HTTP日期或者非负十进制整数秒。第三行那种写法客户端解析不出来,行为等同于没有这个头,而你的配置检查、语法检查、健康检查全都不会拦它。 Google那边的建议倒是很务实:Use the retry-after HTTP header with a best effort date or duration. Use static HTML.——尽力给个值就行,页面用静态HTML。后半句的意思是别让错误页本身再去查一次数据库,那纯属在服务器已经扛不住的时候再补一刀。 ## 现网到底有多少拒绝带了Retry-After? 实验台归实验台,真实世界什么样?保哥对164个电商与SaaS域名的首页各发了一轮请求,只看非200的那些响应里带没带Retry-After。 然后同一批URL、同一个UA、几乎同一时段,从一台机房服务器又跑了一遍。两组结果放一起看: 出口位置 | 非200的响应 | 带Retry-After的 | 占比 | 家用宽带 | 65个 | 23个 | 35.4% | 机房服务器 | 65个 | 1个 | 1.5% | 差距大得离谱。翻明细就明白了:家宽那侧的23个全部是Cloudflare返回的429,值一律是60,一个不多一个不少;机房那侧唯一的那个,是一个301上挂的Retry-After: 0,属于噪声。 换句话说,Retry-After在今天的公网上基本只是Cloudflare挑战页的一个副产品。自建服务器返回的403、500、301,一个都不带。至于更新一些的RateLimit-Limit、RateLimit-Remaining、RateLimit-Reset这组头,164个站里出现了1个,覆盖率0.6%。 ## 换个出口IP,四分之一的站换个状态码 上面那两组数据摆在一起时,保哥先怀疑的是自己:家宽那侧22个429,会不会是自己把人家打限速了? 机房那一轮正好回答了这个问题——而且答案比“是”或“不是”有意思得多。 ## 同一个URL、同一个UA,两地对照 家宽看到 | 机房看到 | 域名数 | 200 | 200 | 70 | 403 | 403 | 23 | 429 | 301 | 14 | 连不上 | 连不上 | 14 | 301 | 301 | 12 | 429 | 200 | 8 | 200 | 403 | 8 | 其他组合 | — | 15 | 两地状态码相同的122个,占74.4%。也就是说,四分之一的站对同一个请求给出了不同的答案,而唯一的变量是请求从哪个IP发出去的。 家宽侧那22个429,在机房侧一个都没复现——14个变成301,8个变成200。机房侧自己的429只有2个,来自另外两个站。反过来,有8个站在家宽给200、在机房给403。 把UA那一维也加进来:家宽侧换成Googlebot UA后状态码改变的比例是15.9%,机房侧只有6.7%。 ## 这意味着什么 两条结论,都挺重要: 第一,你测到的那个429,主要是关于你自己的,不是关于那个站的。它不是速率超限的证据,而是“你这个出口在对方风控模型里的评分”的证据。同一份配置对不同来源给不同答案,本来就是Bot管理产品的正常工作方式。 第二,也是更实用的那条:“我的站有没有误拦搜索引擎”这个问题,从你自己的网络位置是测不出来的。你在办公室curl一下,看到200,什么也证明不了——Googlebot从谷歌的IP段来,和你的出口是两个世界。这件事只有两个可靠的验证路径:从日志侧看(按反向DNS验证过的真Googlebot (https://zhangwenbao.com/server-log-file-analysis-seo-crawl-budget-bot-verification.html)统计它拿到的状态码分布),或者从Search Console的抓取统计信息里看响应类型分布。拿UA字符串模拟爬虫去测自己的站 (https://zhangwenbao.com/crawler-identifier-user-agent-bot-verification-guide.html)能验证UA规则那一层,但验证不了IP信誉那一层。 ## 顺带看到的:谁在拒绝 机房那一轮按Server头分组: Server | 站数 | 首页200率 | 其中403 | cloudflare | 80 | 43.8% | 22 | 没有Server头 | 33 | 27.3% | 5 | nginx | 10 | 100.0% | 0 | netlify | 6 | 50.0% | 0 | vercel | 5 | 60.0% | 0 | akamai | 5 | 0.0% | 4 | Amazon S3 / tengine | 各2 | 100.0% | 0 | 规律很清楚:越是“把磁盘上的文件按规矩发出去”的那一类,越是照单全收;越是在前面加了一层Bot管理的,拒得越狠。这不是在批评谁——挡住恶意流量本来就是这些产品的价值所在。但它确实意味着,把站放到这类平台后面之后,“谁能拿到我的页面”这个决定权就不完全在你手里了,而托管平台悄悄拦掉爬虫 (https://zhangwenbao.com/managed-wordpress-blocks-ai-crawlers-citation-loss.html)这种事,出问题时监控是不会报警的。 ## 拒绝一次只花百分之二的字节,为什么还是亏? 把拒绝的成本算清楚,有助于理解它为什么是笔亏本买卖。 实验台上量了几种响应的字节数(响应头加正文): 响应 | 正文字节 | 响应头字节 | 合计 | 正常页面(200) | 23187 | 234 | 23421 | nginx默认503页 | 190 | 170 | 360 | nginx默认429页 | 162 | 156 | 318 | 自定义维护页(保持503) | 171 | 191 | 362 | 现网那批站也是一样的量级:正常首页过线字节中位数65755,非200响应中位数2085,比值3.17%(家宽那一侧算出来是1.71%)。 而且拒绝还很快。实测nginx拒绝一个请求用0.6毫秒左右,跟发一个静态文件是同一个量级——因为它连磁盘都不用碰。 ## 那为什么还是亏 因为抓取预算有两本账,拒绝在其中一本上几乎免费,在另一本上是全价。 第一本是字节账:压缩没配全会让字节账凭空翻几倍 (https://zhangwenbao.com/content-encoding-negotiation-gzip-brotli-crawl-budget-seo.html),而条件请求做对了能让没变过的页面只花百来字节 (https://zhangwenbao.com/conditional-request-304-etag-crawl-budget-seo.html)。在这本账上,一次503确实只花2%到3%。 第二本是次数账。爬虫这一轮打算抓多少个URL是有数的,你回一个503,这次机会就用掉了——它拿到的不是内容,是一个“稍后再来”。省下的那97%字节,买到的是零。 更要命的是第三笔,前面已经讲过:这次503不只是浪费了一次机会,它还会把整个主机名后续的抓取速率一起压下去,包括那些本来能正常返回的URL。一笔360字节的开销,结算在全站的抓取容量上。 ## 拒绝页面会不会被缓存住? 还有一个更隐蔽的代价:这次拒绝可能不止发生一次。 RFC 9110第15.1节列了默认可以被启发式缓存的状态码:200、203、204、206、300、301、308、404、405、410、414、501。503不在这个名单里,404反而在。所以按规范,一个不带缓存指令的503不该被存下来。 实验台上挂了一层nginx做的边缘缓存,对同一个URL连打3次,看第2、3次是HIT还是MISS: 上游响应 | 第1次 | 第2次 | 第3次 | 裸503(无Cache-Control) | MISS | MISS | MISS | 503 + Cache-Control: no-store | MISS | MISS | MISS | 503 + Cache-Control: public, max-age=600 | MISS | HIT | HIT | 裸503,但边缘配了proxy_cache_valid any 5m | MISS | HIT | HIT | 前两行是好消息:默认行为是对的,不带指令的503不会被缓存。 后两行是两条不同的路,通向同一个坑: - 第三行——很多站会在全站统一加一条Cache-Control: public, max-age=600,通常是在某个“提速”教程里学的。这条头会跟着503一起发出去,于是这个503被存进边缘节点,十分钟内所有人(包括爬虫)拿到的都是它。 - 第四行——proxy_cache_valid any 5m这条配置的意思是“任何状态码都缓存5分钟”,它同样是提速教程里的常客,而any这个词把5xx也包括进去了。 两种情况的后果一样:你的服务器早就恢复正常了,边缘节点还在按缓存时长继续发503。你在源站怎么测都是200,用户和爬虫拿到的是503,而这个时间差正好是你配的那个缓存时长。 顺带一个意外的发现:PHP里只要调了session_start(),它会自动塞一组Cache-Control: no-store, no-cache, must-revalidate。如果你的维护页是个PHP脚本并且开了会话,这组头会把上面第三行那个坑意外地堵上。这大概是全文唯一一个默认值帮上忙的地方。 ## 按IP和按UA限速,为什么对爬虫几乎没用? 还有一个方向的浪费:限速规则的分区键。 limit_req_zone的第一个参数就是分区键,教程里的标准写法是$binary_remote_addr,也就是按客户端IP各算一份配额。这个默认选择对付单机采集器很有效,对付搜索引擎爬虫则基本落空。 ## 按IP:换一个IP就是一份新配额 实验台上配了一个按请求头里的伪造IP分区的zone,速率1r/s,然后打两轮: 打法 | 12次连打的状态码 | 12次都用同一个IP | 200,然后429×11 | 12次各用一个不同IP | 200×12 | 这个结果毫不意外,但把它和爬虫的实际形态对上就有意思了:Googlebot从一整段IP池里出来,Bingbot同理,各家AI爬虫更是分散。按IP限速对它们的实际约束,等于把配额乘上了它们的IP数量。 ## 按UA:Googlebot有不止一个UA 那按UA限速呢?实验台把分区键换成$http_user_agent,用Googlebot的桌面版和移动版两个UA交替请求(两组之间留够冷却时间): 桌面UA第1次 → 200 移动UA第1次 → 200 桌面UA第2次 → 429 移动UA第2次 → 429 两个UA各自拿到了一份完整的配额。这不是bug,是分区键的定义使然——UA字符串不一样,就是两个不同的键。而Google公开的抓取器UA列表里,光是Googlebot这一族就有好几个变体,再加上AdsBot、Google-InspectionTool这些,按UA限速的实际约束力被同样地稀释了。 结论不是“限速没用”,而是:按IP或按UA分区的限速,真正被限死的是单IP单UA的那批客户端——也就是真实用户和小型爬虫;而你真正想控制的大厂爬虫,恰好是最不受这套分区约束的。这大概能算本文里最尴尬的一条。 ## 需要限速的时候,怎么配才不误伤? 把前面所有实测拧成一份可以直接对着改的清单。 ## 第一件事:把机器专属端点摘出配额 必须摘的是robots.txt——它被拒一次的代价是整站停抓12小时,这个代价和任何限速收益都不成比例。建议一起摘的还有sitemap.xml系列、llms.md、RSS输出。做法就是前面那个map置空键的写法,别去找limit_req off。 这些端点有个共同特征:只有机器会访问它们,所以它们出问题时没有任何人类会先发现。 ## 第二件事:状态码和头,一次配对 要做的 | 怎么配 | 不这么配会怎样 | 拒绝码用429 | limit_req_status 429; | 默认是503,语义不准,客户端库的退避逻辑对不上 | 带Retry-After | add_header Retry-After 60 always; | 不写always就只在200上发,在拒绝时消失 | 拒绝页不可缓存 | add_header Cache-Control "no-store" always; | 全站统一的max-age会跟着发出去,被边缘存住 | 边缘别缓存5xx | 检查有没有proxy_cache_valid any | any包含5xx,故障会被固化成缓存时长那么久 | 拒绝页用静态HTML | 指向一个静态文件 | 动态错误页会在服务器最扛不住的时候再压一次数据库 | 状态码别被洗掉 | 检查有没有error_page 503 =200 | 爬虫拿到的是一个正常的、可缓存的“维护中”页面 | ## 第三件事:把观测补上 默认的combined日志格式看不出限速有没有生效。至少要把$limit_req_status加进log_format,它的取值能直接区分三种情况:PASSED(记账了、放行)、DELAYED(记账了、排了队)、REJECTED(记账了、拒了)、空(没进账本)。 加完之后有个立刻能做的自查:按UA分组统计REJECTED的数量,看看被拒的里面有多少是搜索引擎。这个数字应该是0。不是0的话,前面那些代价你已经在付了。 ## 第四件事:三条判据 - 拒绝码只有429和5xx会降抓取速率,403和404不会——但它们会让URL掉出索引。如果你的目的是“让爬虫少来”,403是最差的选项:没有换来减速,还赔了索引。 - 任何长期挂着的限速规则,都在长期地小幅压低你的抓取容量。官方给的安全窗口是1到2天。限速规则和临时故障不一样,它不会有人记得去关。 - 自检拒绝相关的配置,必须在“已经被拒”的状态下做。正常请求下看到的响应头,和被拒时看到的完全是两套。这条对Retry-After、Cache-Control、自定义错误页全部成立。 保哥给一个做宠物用品的客户处理过一次,现象是Search Console里“robots.txt无法读取”隔三差五冒一次,每次持续不到一小时,运维查过去总是正常的。最后是在access_log里加了$limit_req_status才对上:站上有个按小时跑的图片同步任务,跑起来的时候会把同一个IP的额度吃掉一大半,如果Googlebot恰好在那几分钟来取robots.txt,就被拒了。触发条件太窄,人工复现基本不可能,但它每次触发的代价是按小时算的。 这件事和本文开头那个实验是同一个形状:限速规则不知道自己拒的是谁,也不知道被拒的那个URL在别人的规则里意味着什么。它只认速率。而恰好,那个几百字节的文本文件,是整个抓取流程里唯一一个“取不到就全停”的单点。 ## 常见问题解答 ## 限速拒绝该用429还是503? 在Google那边两者等价——429被明确写成it's considered a server error,和5xx走同一套降速与索引保留逻辑。区别在别的消费者身上:很多HTTP客户端库和AI爬虫对429有专门的退避实现,对503没有。所以推荐limit_req_status 429;,语义准确,行为更可预测。但别指望换个码就能规避降速。 ## robots.txt返回429会不会比503好一点? 不会。Google对robots.txt的4xx处理有一句Google's crawlers treat all 4xx errors, except 429, as if a valid robots.txt file didn't exist——429被明确排除在“当作不存在”之外,它和5xx一样落入“取不到”那条分支,同样触发前12小时停抓。robots.txt的正确答案是别让它被限速拦到,而不是换个码。 ## 返回503期间,页面的标题和描述会更新吗? 不会。Google在暂停线上业务那份文档里写着,页面返回503时没有办法刷新标题、描述、元数据和结构化数据。所以用503做长时间维护,除了降抓取速率和最终掉索引,还会让搜索结果里的摘要一直停在旧版本上。 ## 为什么我curl自己的站看到Retry-After,日志里的爬虫却像没收到? 八成是add_header没写always。这个指令默认只在一小撮成功类状态码上生效,你curl首页拿到的是200,头正常出现;而被拒的那些请求是429或503,头不会出现。实测同一份配置,200上RA=60、429上RA为空。自检必须在被拒的状态下做。 ## 用403挡住爬虫,能减轻服务器压力吗? 能减轻这一次的压力,但减不了后续的量。Google文档原文是Don't use 401 and 403 status codes for limiting the crawl rate. The 4xx status codes, except 429, have no effect on crawl rate.——403对抓取速率没有影响,爬虫不会因此少来。代价那边倒是实打实:4xx会让URL从索引里被移除。 ## 限速只配在某个接口上,会影响其他页面吗? 会。Google写得很明确:降速affects the whole hostname of your site,而且包括那些返回正常内容的URL。只要出错的URL数量起来了,整个主机名的抓取速率一起降,商品页文章页无一幸免。所以“只限某个路径”这种精细化方案,精细的只是触发条件,代价范围一点也不精细。 ## 怎么确认自己的站有没有误拦搜索引擎? 不能靠从自己的网络位置去测。实测同一批164个站,家宽出口和机房出口看到的状态码有25.6%不一样,其中14个站在家宽是429、在机房是301。可靠的路径只有两条:一是从access_log里按反向DNS验证过的真爬虫统计它拿到的状态码分布,被拒的应该是0;二是看Search Console抓取统计信息里的响应类型分布。用UA字符串在本地模拟只能验证UA规则那一层,验证不了IP信誉那一层。 ## burst能避免拒绝吗? 能把拒绝换成排队,换不掉代价。不带nodelay时超出速率的请求会被压在队列里等,实测速率1r/s下每个请求要等约0.98秒,而基线是0.6毫秒——状态码全是200,但服务器为这批请求占用的连接时长翻了一千多倍。而抓取容量上限盯的正是这个时长。排队是把明面上的拒绝换成了看不见的降速,这一层展开在服务器变慢时那些不进日志的故障形态 (https://zhangwenbao.com/slow-server-truncated-response-crawl-rate-seo.html)里。 ## 权威参考资料 ## 证书有效期正在砍向47天,一年一换的续期流程撑不到2027年 - URL:https://zhangwenbao.com/tls-certificate-lifetime-47-days-renewal-automation.html - 分类:HTTPS与安全加固 - 发布:2026-07-20 | 更新:2026-07-31 - 摘要:证书有效期2026年起降到200天、2029年降到47天,域名验证复用期最终只剩10天。讲清时间表、ARI续期窗口、OCSP关停的影响与自动化改造清单。 - 关键词:https,Nginx,SSL证书,服务器加固 > **TLDR**:摘要:公开信任的TLS证书最长有效期,已经在2026年3月15日从398天降到200天,2027年降到100天,2029年降到47天。真正会先咬到人的不是这条,是同一份表决里被顺带砍掉的域名验证数据复用期——它最终降到10天,意味着证书还没到期,你上次做的验证就已经不能再用了。与此同时,签发机构开始通过一个标准协议主动告诉客户端“该续了”,而Let's Encrypt在2025年8月关停了OCSP响应器,让全网教程里那行ssl_stapling变成了一句空话。 > 摘要:公开信任的TLS证书最长有效期,已经在2026年3月15日从398天降到200天,2027年降到100天,2029年降到47天。真正会先咬到人的不是这条,是同一份表决里被顺带砍掉的域名验证数据复用期——它最终降到10天,意味着证书还没到期,你上次做的验证就已经不能再用了。与此同时,签发机构开始通过一个标准协议主动告诉客户端“该续了”,而Let's Encrypt在2025年8月关停了OCSP响应器,让全网教程里那行ssl_stapling变成了一句空话。 先给一个具体的日子:2026年3月15日。从那天起,任何一家公开信任的证书颁发机构,都不能再签发有效期超过200天的TLS证书。 如果你的续期流程是“每年提醒自己一次,登后台点一下续费,下载新证书扔到服务器上”,那么这套流程在那一天已经正式作废了。它不会立刻炸——你手上那张老证书还能用到到期——但它下一次轮转时,间隔会突然缩短到不足七个月,再下一轮不足四个月。 更值得注意的是,这不是终点。整条时间表已经排到2029年,而中间还夹着一条几乎没人讨论、却比有效期本身更难受的变化。 ## 一年一换的流程,是从哪一天开始不成立的? ## 表决本身:全票通过,零反对 这项变更来自CA/浏览器论坛第SC081v3号表决《引入有效期与数据复用期缩减时间表》 (https://cabforum.org/2025/04/11/ballot-sc081v3-introduce-schedule-of-reducing-validity-and-data-reuse-periods/),通过时间是2025年4月11日。提案人是苹果的Clint Wilson,联署方包括Sectigo、Google Chrome和Mozilla。 票型值得看一眼:证书签发方25票赞成、0票反对、5票弃权;证书消费方(也就是浏览器厂商那一侧)4票赞成、0票反对。零反对票的意思是,这件事没有任何谈判空间,也不会被推翻。 ## 三个日期,三个数字 生效日期 | 证书最长有效期 | 一年需要轮转几次 | 2026年3月15日之前 | 398天 | 1次 | 2026年3月15日起 | 200天 | 2次 | 2027年3月15日起 | 100天 | 4次 | 2029年3月15日起 | 47天 | 8次 | 最后一行的47是个看着有点怪的数字。业界普遍引用的拆法是:一个月(31天)+半个月(15天)+1天缓冲。这个设计意图很直白——让证书的轮转周期短到人工流程根本跟不上,从而逼迫整个生态走向自动化。 还有个容易踩空的细节:DigiCert关于47天有效期正式落地的说明 (https://www.digicert.com/blog/tls-certificate-lifetimes-will-officially-reduce-to-47-days)里提到,签发方实际会把产品上限压在规则线之下一点点——200天那一档按199天签。原因是有效期计算涉及时区与签发瞬间的微小延迟,卡在整数上有极小概率越界,而越界的后果是这张证书不合规、必须吊销重签。做容量规划时按199算,别按200。 ## 为什么要缩短 公开的理由主要有两条。一条是吊销机制在实践中基本失效:浏览器对吊销检查普遍采用软失败策略,查不到就放行,所以一张被吊销的证书在很多客户端上依然畅通无阻。既然吊销不可靠,那就靠缩短有效期来限制损害窗口。 另一条是加密敏捷性:算法要迁移、密钥要轮转、根证书要更替,而这些动作的速度上限,就是全网证书的最长有效期。 > 换个角度看这件事:证书有效期从来不是给你用的,它是这个体系给自己留的纠错窗口。窗口越短,一次失误的代价越小。 ## 被忽略的另一半:域名验证数据能复用多久? 这一节是这篇文章最想让你记住的部分。绝大多数关于47天的讨论都只讲了有效期,而同一份表决里还有另一组数字。 ## 两条并行的削减线 表决原文里列的方向性目标有三条:非SAN类验证数据从825天降到398天;SAN类验证数据从398天降到10天;证书最长有效期从398天降到47天。 第二条是关键。SAN指的就是证书里那些域名条目,SAN类验证数据说的是“你证明过自己控制这个域名”这件事能被记多久。以前这个记忆是398天——一年之内你续多少次证书,都不用再证明一次。 ## 10天意味着什么 意味着即便证书还有效期,只要距离上次域名验证超过了复用窗口,续期时就必须重新完成一次域名控制验证。对使用ACME协议的自动化流程,这只是多一轮HTTP或DNS挑战,客户端自己就办了。 对手工流程,这是灾难性的。你不再是“下载一个新文件替换旧文件”,而是每一次续期都要重新过一遍验证——放DNS TXT记录、等生效、点验证、再签发。而这套动作在47天周期下一年要做八次。 ## 手上到底有多少张证书,很多人答不上来 在讨论怎么自动化之前,有个更基础的问题:你知道自己名下有几张公开信任的证书吗? 这个问题比听上去难回答。证书可能来自主机商赠送、CDN自动签发、某个同事在某个平台单买、开发环境留下的临时证书、以及早年买的多年期产品。它们分散在不同账号、不同邮箱、不同控制台里。 有一个免费且相当彻底的盘点方法:证书透明度日志。所有公开信任的证书在签发时都必须记入CT日志,也就是说任何一张给你的域名签发过的证书,都在一个公开可查的地方留着记录。在CT日志查询服务里输入你的主域名,能一次看到全部历史签发记录,包括那些你已经忘掉的子域名。 这个动作往往能翻出两类东西:一类是早已废弃但证书还在自动续的老子域名,一类是你完全不知道存在的签发记录——后者需要认真查一下是谁签的。 ## 判据只有一条 可以用一句话判断你的流程会不会被这轮变化打死:如果续期过程里有任何一步需要人打开浏览器,这套流程的寿命就是确定的。不是会不会出事的问题,是哪一次出事的问题。 保哥去年帮一个做家居灯具的客户梳理证书清单,翻出来十一个域名,其中三个是市场部同事早年自己在某平台买的、绑在个人邮箱上、每年手动续。这三个域名分别指向一个落地页、一个法语站和一个客服系统。按当时的续期方式,2027年之后每年要重复三十二次这套动作,而记着这件事的人已经离职了。 ## 证书正在变成一次性用品吗? 如果47天听起来已经够激进,看看这条线的另一端在发生什么。 ## 6天有效期已经正式可用 Let's Encrypt关于6天证书与IP地址证书正式全量开放的公告 (https://letsencrypt.org/2026/01/15/6day-and-ip-general-availability)发布于2026年1月15日。这类证书的有效期是160小时,也就是6天多一点。它不是实验品,是普遍可用的正式产品,走的是ACME客户端里一个叫shortlived的证书配置档,选一下就行。 时间线上它比47天的规则跑得更靠前:2025年2月签出第一张,4月对早期采用者开放,2026年1月正式全量。也就是说在规则要求47天的三年前,6天证书就已经能用了。 ## 为什么会有人要6天的证书 核心理由和缩短有效期是同一条:吊销机制不可靠。一张6天的证书,就算私钥泄露了也只有6天的窗口,而且这个保护是自动生效的,不依赖任何客户端去查吊销状态。 顺带解决的还有一件事:同一份公告里提到,Let's Encrypt开始为IP地址签发证书了。这是它历史上第一次做这件事,对做接口服务、边缘节点、临时环境的场景挺实用。 ## 你该不该现在就切过去 大概率不该,但理由值得说清楚。6天证书对自动化的要求是另一个量级——续期链路里任何一环卡住超过一天,你就已经用掉了六分之一的缓冲。90天证书能容忍一次续期失败,6天证书不能。 合理的用法是把它当成一次压力测试:如果你的流程能扛住6天周期,那么它一定能扛住47天。反过来,如果你连想都不敢想6天,那说明流程里还有需要人参与的环节,而那个环节就是你未来两年要拆掉的东西。 ## 顺带把告警阈值改成比例 很多人的监控告警沿用了一个旧习惯——到期前30天提醒。在398天时代,30天占7.5%,是个合理的缓冲。到了47天时代,30天占64%,意味着证书刚签出来两周多就开始报警,告警会迅速变成噪音,然后被静音。6天证书上这个阈值更是直接失去意义。 合理的做法是把阈值改成按比例算,比如剩余寿命低于三分之一时提醒。这条改动很小,但它决定了你的告警在两年后还有没有人看。 ## 上限是200天,那大家实际在用多少天? 这个问题在所有讨论47天的文章里都没人问,而它的答案会让你对整件事的判断彻底改变。 ## 6个站点的实测结果 保哥在2026年7月直接用openssl s_client拉了6个站点当时正在服役的证书,读出签发时间和到期时间: 站点 | 签发机构 | 签发日 | 到期日 | 实际有效期 | letsencrypt.org | Let's Encrypt | 7月6日 | 10月4日 | 90天 | shopify.com | Google Trust Services | 6月12日 | 9月10日 | 90天 | cloudflare.com | Google Trust Services | 7月8日 | 10月6日 | 90天 | github.com | Sectigo | 7月3日 | 9月30日 | 89天 | stripe.com | DigiCert | 7月28日 | 11月12日 | 107天 | 本站 | Let's Encrypt | 6月14日 | 9月12日 | 90天 | 规则允许200天,实测6个站里5个是90天,剩下一个107天。没有任何一个用满上限,连一半都没用到。 ## 这组数字真正说明的事 200天这个上限,在真实世界里根本不是约束条件。已经跑ACME自动化的站点,早在规则收紧之前就自发把周期压到了90天左右——因为对一台会自己续期的机器来说,续得频不频跟人没关系,而周期短意味着出问题时暴露窗口小。 于是这轮缩短的真实含义就浮出来了:它对已经自动化的人是零影响,它精确地、只打击还在手工续期的那一批。这不是副作用,这就是设计意图——用规则把最后那批不肯自动化的人清出去。 顺带看stripe.com那一行。DigiCert是典型的商业CA,客户里有大量还在走采购流程的企业,而它给出的实际有效期是107天——已经踩在2027年那一档的门槛上了。连最保守的那一侧都已经提前落位,说明整个行业对时间表的信心相当足。 ## 一个能顺手看出签发方风格的小细节 把上面几张证书的时间精确到秒去看,会发现两种风格:Let's Encrypt和Google Trust Services的时间戳带着具体的时分秒,是签发那一瞬间;Sectigo和DigiCert的则是整齐的00:00:00到23:59:59,按自然日对齐。 这个差别本身无害,但它会影响你算剩余天数的方式。按自然日对齐的证书,实际可用时间会比你算出来的多出不到一天;按签发瞬间的证书则一秒不多。在398天时代这点误差可以忽略,在47天时代它占了2%,写监控阈值时值得留意。 ## 谁来决定这张证书什么时候续? 答案在2025年正式变了,而且变得比大多数人意识到的更彻底。 ## ARI把决定权交给了签发方 ACME Renewal Information扩展已经在2025年9月作为RFC 9773标准文档 (https://www.rfc-editor.org/rfc/rfc9773.html)正式发布。它做的事情用一句话说就是:你的ACME客户端不再自己决定什么时候续期,而是去问签发机构。 机制很简单。客户端定期请求一个renewalInfo端点,拿回一个suggestedWindow,里面是一个开始时间和一个结束时间: { "suggestedWindow": { "start": "2026-02-03T04:00:00Z", "end": "2026-02-04T04:00:00Z" } } 客户端的正确行为是在这个窗口里随机挑一个时刻去续,而不是掐着开始时间一拥而上。Let's Encrypt返回的Retry-After是21600秒,也就是6小时,客户端应当按这个间隔重新轮询。 ## 这不是便利性功能,是应急通道 ARI真正的设计目的容易被误解成“帮你省心”。它的核心用途是批量吊销事件。 假设某家CA发现自己签发流程里有缺陷,按规则必须在24小时内吊销一批证书。在ARI出现之前,这意味着几百万个站点在同一天集体挂掉,而且它们的续期脚本还在按自己的老日程慢悠悠地等。有了ARI,CA可以提前把这批证书的建议窗口整体前移,让客户端在吊销发生之前就悄悄换好新证书。 签发方的公开说明里举过Shopify这类需要为数百万商户自定义域名维护证书的场景,这也是ARI最能体现价值的地方——没有全局协调的话,海量续期请求会挤成一个尖峰,而那个尖峰本身就是一次事故。 ## 窗口内随机取值这条要求不是客套 RFC里对客户端行为写得很明确:拿到窗口之后要在其中随机挑一个时刻,而不是一到窗口起点就发请求。这条要求经常被实现者当成建议忽略掉,但它是整个机制能不能成立的前提。 道理不难理解。签发方之所以给的是一个窗口而不是一个时刻,就是为了把负载摊开。如果所有客户端都掐着起点发请求,那么窗口的宽度等于零,尖峰照旧——随机化不是优化,它是这个设计能工作的必要条件。 顺带说一句自己写脚本时的坑:很多人用cron的固定分钟数来跑续期,比如每天凌晨3点整。全世界用同一个教程的人,都在3点整。这跟ARI想解决的问题正好是反的。加一行随机sleep的成本几乎为零,但它决定了你是那个尖峰的一部分,还是被摊平的那部分。 ## 你要做的只有一件事 确认你的ACME客户端版本足够新并且启用了ARI。Certbot从4.x起默认检查renewalInfo端点,一旦拿到窗口,就会覆盖它自己那条“剩30天才续”的老规则。acme.sh、lego、Caddy内置的证书管理也都已支持。 需要留意的是频率:ARI要求客户端定期轮询,而不是一天跑一次就完事。很多人的cron写的是每天凌晨三点跑一次certbot renew,在47天时代这个频率偏低,建议改成每天两到四次,并加上随机延迟避免整点集中。 # 每6小时跑一次,并随机延后0到3600秒 0 */6 * * * sleep $((RANDOM \% 3600)) && certbot renew --quiet 把这条接进服务器的定时任务体系时,可以参考用cron把独立站服务器运维自动化 (https://zhangwenbao.com/linux-cron-shell-independent-site-automation-ops-backup-sitemap-ssl.html)那篇里备份、缓存、证书几件事的编排顺序。 ## nginx里那行ssl_stapling,2025年8月之后还在做什么? 答案是:什么都不做,而且不报错。 ## OCSP整个退场了 Let's Encrypt关于2025年终止OCSP支持的公告 (https://letsencrypt.org/2024/12/05/ending-ocsp)给出的时间表分三步:2025年1月30日起,除非账户此前签发过带OCSP Must-Staple扩展的证书,否则带该扩展的签发请求一律失败;2025年5月7日起,所有带该扩展的请求失败(包括续期),同时证书里的OCSP地址被移除、换成CRL地址;2025年8月6日,OCSP响应器彻底关停。 停用的理由是隐私。公告里的说法是,当访客的浏览器通过OCSP查询证书吊销状态时,运营响应器的那家CA就立刻知道了这个IP正在访问哪个网站。这个副作用存在了很多年,只是一直被当成可以接受的代价。 ## 那行配置现在的行为 全网2020年前后的nginx教程里,几乎都有这么两行: ssl_stapling on; ssl_stapling_verify on; 对Let's Encrypt签发的证书,这两行现在是空转的。nginx官方对ssl_stapling指令的说明 (https://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_stapling)里写明,装订所需的OCSP响应地址取自证书里的Authority Information Access扩展。证书里没有这个地址,nginx就无从装订,只会在错误日志里留下一条告警,然后照常提供服务。 所以这两行的实际状态是:不出错、不生效、不提醒。它是一段很典型的配置遗迹——所有人都抄,没有人验证过它还在不在工作。 ## 删还是留 如果你的证书全部来自Let's Encrypt,删掉这两行,顺便把错误日志清干净。如果你还有商业CA签发的证书(部分仍提供OCSP),可以保留,但要意识到它的价值正在快速归零——整个行业都在往CRL和短有效期这个方向走,OCSP的退场只是时间问题。 顺带把老配置里其他几处一起看了:ssl_protocols里如果还留着TLSv1和TLSv1.1,删掉;ssl_session_tickets在单机场景建议关掉;HSTS的配置和preload提交流程可以对照HTTPS站点开启HSTS的配置与preload提交 (https://zhangwenbao.com/https-hsts.html)那篇复核一遍。 ## 证书过期一小时,SEO要还多久? 这是独立站主最该关心的一节,因为它的账不是按小时算的。 ## 证书过期的故障形态很特别 大多数线上故障会在服务器上留下痕迹:5xx日志、进程崩溃、负载飙高。证书过期不会。服务器完全正常,nginx照常返回200,是客户端在握手阶段就单方面终止了连接。你的access log里看不到这些失败的请求,因为它们压根没走到HTTP层。 爬虫这一侧的表现是抓取失败。连续的抓取失败会触发抓取速率下调,而这个下调的恢复是不对称的——降下来很快,涨回去要按周计。这个机制和服务器响应变慢时的表现类似,服务器变慢导致抓取速率被调低而日志里一条错误都没有 (https://zhangwenbao.com/slow-server-truncated-response-crawl-rate-seo.html)那篇里描述的正是同一类“无声故障”。 ## HSTS会把损失放大 如果你开了HSTS,浏览器在证书出问题时不再提供“继续访问”的选项——这正是HSTS想要的效果,但它意味着证书过期期间你的站点对回头客是彻底不可达的,连绕过都做不到。 preload列表让这件事更彻底:即便是第一次访问的用户也会被拦下。开了HSTS preload的站点,证书自动续期就从“最好有”变成了“必须有”。 ## 混合内容与跳转链的连带风险 换证书本身不改变协议,但很多人在处理证书问题时会顺手动跳转规则,这是另一个常见的翻车点。HTTP到HTTPS的跳转如果配成了链式(先跳www再跳https,或者反过来),每多一跳就多一份延迟和一次抓取消耗。跳转的正确写法可以对照HTTP与HTTPS双向301跳转的实战配置 (https://zhangwenbao.com/301-url-redirection-http-jumps-to-https-and-https-jumps-to-http.html)核一遍,多域名场景则看Nginx开启HTTPS后多域名301跳转到主域名的完整配置 (https://zhangwenbao.com/nginx-open-ssl-www-and-http-all-jump-to-non-www-https-domain.html)。 ## 通配符、多域名和CDN,各自会卡在哪一步? 把自动化落到具体架构上,卡点其实就那么几处,而且每一处都有明确的解法。 ## 通配符证书只能走DNS验证 这是ACME协议的硬约束:申请*.example.com这种通配符证书,唯一可用的验证方式是DNS-01,也就是往域名的TXT记录里写一个值。HTTP验证做不到,因为通配符覆盖的子域名是无限的,没法逐个放文件。 后果是通配符证书的自动化,必然依赖DNS服务商的API。如果你的域名托管在一个不提供API的地方,自动化这条路直接被堵死——在47天时代,DNS服务商有没有API,已经从一个便利性问题变成了架构选型问题。 典型配置长这样,以Cloudflare为例: certbot certonly \ --dns-cloudflare \ --dns-cloudflare-credentials /root/.secrets/cf.ini \ --dns-cloudflare-propagation-seconds 30 \ -d example.com -d "*.example.com" 那个propagation-seconds参数经常被漏配。DNS记录写进去到全球可查有传播延迟,certbot默认等的时间对某些服务商偏短,验证会随机失败——而且是随机的,你重跑一次可能就过了,于是问题被当成偶发忽略掉,直到某次续期连着失败几回。 ## 多域名证书的SAN条目有上限 一张证书可以覆盖多个域名,做法是把它们都写进SAN字段。多数CA的上限在100个左右。真正的问题不在数量,在耦合:SAN里任何一个域名验证失败,整张证书都签不出来。 所以一张塞了五十个域名的证书,等于把五十个独立的失败点串成了一条链。做站群或者多语言站的话,按业务边界拆成几张证书,比图省事塞成一张要稳得多。 ## CDN后面其实有两张证书 这一条最容易被忘掉。站点接了CDN之后,链路上有两段TLS:访客到CDN边缘一张,CDN回源到你服务器一张。 前者通常由CDN托管,自动续,你基本不用管。后者是你自己的源站证书,而很多人在配回源时选了“不验证证书”或者干脆挂了一张十年期的自签名证书,然后就再没看过它。 这张证书过期的故障表现是CDN回源失败,用户看到的是CDN返回的错误页,而你去查源站是好的、查CDN配置也是对的,排查方向很容易跑偏。把源站证书也纳入到期监控,是接CDN之后必须补的一课。这一层的边界划分可以对照Apache的htaccess六层治理里重写、缓存与HSTS的分工 (https://zhangwenbao.com/apache-htaccess-seo-6-layer-rewrite-cache-canonical-hsts.html)那篇,虽然那篇讲的是Apache,但分层的思路是通用的。 ## 47天时代的续期流程该长什么样? 把前面的约束合起来,可以推出一套不依赖记忆力的流程。 ## 四条硬要求 - 全自动:整个链路里不能有需要人操作的步骤,包括域名验证。这意味着DNS挑战要用API而不是手工加记录。 - 高频轮询:每天至少两次,配合随机延迟,让ARI的窗口有机会被及时看到。 - 装载后自动重载:新证书写盘之后必须触发服务重载,且重载失败要能被感知。 - 独立监控:到期监控不能跑在被监控的那台机器上,也不能依赖被监控的那张证书。 ## nginx侧的三个实操细节 第一,用reload不用restart。reload是平滑的,已有连接会被老进程处理完;restart会掐断所有在途连接。certbot的deploy hook里写这一行就够: --deploy-hook "nginx -t && systemctl reload nginx" 注意前面那个nginx -t。没有语法校验的reload是危险的:配置有错时reload会失败,而nginx会继续用着旧配置和旧证书跑,直到旧证书过期。这个失败是静默的,除非你专门去看返回码。 第二,ssl_certificate指向certbot的live目录,那是一组符号链接,certbot每次续期只更新链接指向,nginx配置一个字都不用改。直接指向archive目录里的具体文件是常见错误,续期之后nginx还在读老文件。 第三,证书链要完整。fullchain.pem而不是cert.pem——后者只有站点证书,没有中间证书。桌面浏览器往往能自己补上中间证书所以看不出问题,很多移动端和命令行工具不会,这类故障的典型症状是“电脑上打得开手机上打不开”。 ## 监控要怎么做才不会自己把自己关在门外 这条单独说,因为它是个很容易掉进去的逻辑陷阱:你把证书到期监控部署在自己的服务器上,用自己的域名做健康检查接口。证书一过期,监控服务本身第一个上不来,于是它没有报警。 这大概是运维界最标准的一次自锁门——钥匙就在屋里。 正确做法是把证书到期检查放在站点之外:用第三方监控服务,或者用一台完全独立的小机器跑一条定时任务。检查逻辑本身很简单: echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null \ | openssl x509 -noout -enddate 把输出的到期时间和当前时间做差,低于剩余寿命的三分之一就告警。这条命令还有个附带好处:它走的是真实的TLS握手,所以证书链不完整、SNI配错、中间证书缺失这些问题它都能一起测出来,比单纯读本地文件的到期日靠谱得多。 ## 照着这份表把迁移动作排完 最后给一份可以直接执行的清单,按优先级排序。 动作 | 为什么现在做 | 验证方式 | 盘出所有域名与证书清单 | 手工签发的那几张通常不在任何清单里 | 对每个域名跑一次openssl检查 | 把手工流程全部换成ACME | 验证数据复用期最终降到10天 | 让它自己成功续一次 | ACME客户端升级到支持ARI的版本 | 签发方可能提前要求你续 | 看日志里有没有renewalInfo请求 | 轮询频率提到每天两次以上 | 一天一次接不住提前的窗口 | 检查cron表达式 | deploy hook加上配置校验 | reload失败是静默的 | 故意写错一行配置试一次 | 告警阈值从固定天数改成比例 | 30天在47天周期里是64% | 算一遍触发时机 | 监控迁出被监控的机器 | 证书挂了监控也会跟着挂 | 把站点停掉,看告警来不来 | 删掉失效的ssl_stapling配置 | OCSP响应器已关停 | 错误日志里那条告警消失 | 倒数第二行的验证方式值得特别执行一次。没有演练过的告警,跟没有告警在统计上是同一件事——这条经验在容器场景里同样成立,Docker发布端口后ufw规则形同虚设 (https://zhangwenbao.com/docker-compose-published-ports-ufw-bypass-defaults.html)那篇讲的也是同一类问题:配置看起来生效了,但没有人从外面验证过。 还有一条容易被漏掉的:如果你的站点在CDN后面,证书其实有两张——CDN到访客那张,和源站到CDN那张。后者经常被设成十年有效的自签名证书然后就再没人管过,而它一旦过期,故障表现是CDN回源失败,排查方向很容易跑偏。服务器位置和CDN的关系可以顺带看看服务器放美国还是国内对Google排名的真实影响 (https://zhangwenbao.com/server-location-cdn-google-ranking-myth.html)那篇里的分层拆解。 ## 常见问题解答 ## 47天是从2026年就开始执行吗? 不是。2026年3月15日起是200天,2027年3月15日起是100天,47天要到2029年3月15日才生效。但把迁移工作拖到2028年是危险的,因为100天那一档已经足以让手工流程崩溃,而且域名验证数据复用期的缩减是同步推进的,它比有效期更早开始咬人。 ## 我用的是商业CA买的证书,也受这个限制吗? 受。这条规则针对的是所有公开信任的TLS证书,也就是所有能被主流浏览器默认信任的证书,跟你花多少钱买的没关系。只有私有CA签发的、仅在你自己内部信任的证书不受此约束。 ## 把证书买成三年期,是不是就能躲过去? 躲不过。现在能买到的所谓多年期产品,本质是多年期的订阅额度,实际签发的每一张证书依然受当期有效期上限约束,到期后从订阅里再签一张。付费周期和证书有效期是两件事,很多人在这里理解错了。 ## 启用ARI之后,续期会变得更频繁吗? 通常不会。正常情况下签发机构给出的建议窗口就落在证书寿命末段,跟你自己算的时间点差不多。ARI真正发挥作用是在异常情况下——比如需要批量吊销时,签发方会把窗口整体前移,让你在事故发生前就换好新证书。所以它更像一条应急通道,而不是日常的加速器。 ## OCSP停用之后,证书吊销还能被检查到吗? 能,走的是CRL。Let's Encrypt在移除OCSP地址的同时在证书里加入了CRL地址,主流浏览器有各自的吊销信息分发机制来消费这些数据。对站点运营方来说这一变化基本无感,唯一需要做的就是把配置里那行失效的ssl_stapling清理掉。 ## 证书自动续期成功了,但网站还是用着旧证书,是什么原因? 最常见的是重载没有真正执行。certbot把新文件写好了,但deploy hook没配、或者hook里的配置校验失败导致reload被跳过,nginx依然拿着内存里的旧证书在跑。判断方法是比较磁盘上证书文件的到期时间和用openssl从443端口拿到的到期时间,两者不一致就是这个问题。次常见的是配置指向了archive目录里的具体文件而不是live目录的符号链接。 ## 权威参考资料 ## 照着加固清单加的一行响应头,把嵌进来的地图和支付按钮一起变成了摆设 - URL:https://zhangwenbao.com/permissions-policy-iframe-third-party-widget-silent-denial.html - 分类:HTTPS与安全加固 - 发布:2025-12-31 | 更新:2026-07-29 - 摘要:实测Permissions-Policy:把camera=(self)这个默认值显式写进响应头,跨源iframe里的第三方组件立刻拿不到摄像头,什么都不写反而能用。含单变量对照、六种写错方式与149站普查。 - 关键词:运维,响应头 > **TLDR**:摘要:这行响应头最反直觉的一点是,把某项能力按照它本来的默认值原样写进去,和一个字都不写,结果不一样。实验台上的单变量对照摆在那儿:头里根本不提摄像头,嵌进来的第三方组件用得了摄像头;把camera=(self)写上去——self正是它文档里写明的默认值——同一个组件立刻拿不到。149个现网站点里有28个发了这行头,其中15个站的取值一个字符都不差,横跨DTC品牌、Magento商城和技术问答社区。而在另一个主流浏览器内核上,这行头压根不执行。 > 摘要:这行响应头最反直觉的一点是,把某项能力按照它本来的默认值原样写进去,和一个字都不写,结果不一样。实验台上的单变量对照摆在那儿:头里根本不提摄像头,嵌进来的第三方组件用得了摄像头;把camera=(self)写上去——self正是它文档里写明的默认值——同一个组件立刻拿不到。149个现网站点里有28个发了这行头,其中15个站的取值一个字符都不差,横跨DTC品牌、Magento商城和技术问答社区。而在另一个主流浏览器内核上,这行头压根不执行。 先把场景摆出来。页面上嵌着一个第三方的门店地图,用户点“查找附近门店”,地图请求一次定位,把最近的几家标出来——这条路径是本地搜索 (https://zhangwenbao.com/local-seo-google-entity-business-category.html)转化里最短的那一段。功能上线之后一直没出过事。某天安全同事按一份加固清单给全站补了几行响应头,当天下午客服工单 (https://zhangwenbao.com/customer-service-seo-collaboration-7-actions-tickets-help-center.html)里开始出现同一句话:按钮点了没反应。 接下来的排查会依次撞上几堵墙。服务器访问日志干净,那次定位请求根本没发出去过;第三方组件方回复说他们那边一切正常,也确实正常;前端同事在本地跑一遍,好好的——因为本地开发服务器不走生产那份配置。整条链路上没有一个人手里有能证明出事的证据。 罪魁是Permissions-Policy。它是一行HTTP响应头,管的不是这次请求怎么处理,而是这个页面以及页面里嵌的东西被允许调用哪些浏览器能力:摄像头、麦克风、定位、支付、剪贴板、传感器。它跟那些管索引和缓存的响应头 (https://zhangwenbao.com/http-response-headers-seo-x-robots-cache-vary-canonical-mechanism.html)不是一路货色,后者影响的是搜索引擎怎么看你,这一行影响的是浏览器让不让你的代码干活。 写法看上去很朴素: Permissions-Policy: geolocation=(), camera=(self), payment=* 三种取值:()是谁都不许,(self)是只有本站,*是所有人。看着像三档开关,简单到不值得写一篇文章。麻烦恰恰在这儿——它的行为跟这三个词的字面意思对不上,而且对不上的方式非常安静。 下面的结论不是读文档读出来的。保哥搭了一套双主机名实验台:一台当自家站点,另一台当第三方组件的宿主,两边都是真实的HTTPS服务器、真实的网络栈,页面里跑一组探针去调定位、摄像头、麦克风和支付接口,把每次调用落到哪个回调、抛的是哪个错误名全部记下来。加上一轮149个现网站点的普查,一共跑了三十多组对照。每一组都配了阳性锚点,锚点不成立的那组数据一律作废。 ## 加固上线当天,页面上会先有哪几样东西没反应? 先看故障长什么样,因为它长得跟别的故障都不像。 ## 四类能力的失败姿势各不相同 实验台上把四种最常用的能力挨个调一遍,被策略挡住时的表现是这样的: 能力 | 被挡住时发生什么 | 代码里能不能接住 | 定位 | 错误回调被调用,错误码是1,也就是“权限被拒绝” | 只有写了第二个回调才接得住 | 摄像头 | Promise被拒,错误名NotAllowedError | 写了catch就接得住 | 麦克风 | 同上 | 同上 | 支付 | 构造函数直接抛SecurityError | 要用try包起来才接得住 | 定位那一行是最要命的。getCurrentPosition的第二个参数是错误回调,它在语法上是可选的,大量教程里的示例代码只写第一个参数。策略一旦把定位关掉,这类代码的表现就是:函数调用返回了,什么都没发生,也没有任何异常。用户看到的是一个点下去毫无动静的按钮。 ## 摄像头那个错误名,跟用户点“拒绝”是同一个 MDN在摄像头指令那一页写得很清楚:策略挡住这项能力时,getUserMedia()返回的Promise会以NotAllowedError被拒。问题是,用户在权限弹窗上点“拒绝”,抛的也是NotAllowedError。 > 同一个错误名对应两件完全不同的事:一件是这个用户不想给你,另一件是这个站的运维不让任何人给你。前者是产品问题,后者是配置问题,而代码拿到的信息一模一样。 于是几乎所有集成代码都会走进同一个分支——弹一句“您拒绝了摄像头权限,请在浏览器设置里打开”。用户老老实实去设置里翻一圈,翻不到任何被拒绝的记录,因为他根本没被问过。 ## 服务端这一侧完全是空的 这一条跟嵌入类响应头拦住渲染 (https://zhangwenbao.com/htaccess-x-frame-options.html)的那类故障是同一个家族:判决在用户的浏览器里执行,你的服务器和第三方的服务器都不参与。访问日志没有异常,错误率不动,可用性拨测全绿。告警体系 (https://zhangwenbao.com/seo-monitoring-alerting-regression-detection-system.html)里所有指标都是好的,唯一变化的是某个按钮的点击转化率,而那个数字通常没人单独盯。 ## 不发这行头的时候,跨源iframe里的摄像头本来是开还是关? 这是所有误解的起点。很多人默认“我没配就是全开着”,于是加固的时候心里想的是“把不需要的关掉”。实测正好相反。 ## 基线组:什么都不写,第三方组件也拿不到 实验台A组:顶层页面不发任何策略头,页面里嵌一个跨源的iframe,iframe上不写allow属性。结果是四项能力全部被拒——定位错误码1、摄像头和麦克风NotAllowedError、支付SecurityError,控制台四条红字。 原因写在MDN的指令参考页上: > The default allowlist for camera is self. 默认名单是self,意思是只有本站自己能用。跨源的iframe不是本站自己,所以默认状态下它就没有这项能力。你什么都不配的时候,第三方组件的摄像头、麦克风、定位本来就是关着的。 ## 那为什么现网那些组件用得好好的? 因为它们的接入代码里带了allow属性。B组把iframe写成allow="geolocation; camera; microphone; payment",其余一个字不改,四项能力全部通过,定位真的返回了坐标。 这就是allow属性的作用:把父页面手里有的能力,委派给这个特定的子框架。你在第三方文档里抄来的那段嵌入代码,末尾那一长串allow="..."不是装饰,是这个组件能不能干活的开关。普查里的证据很直白——149个站首页上的跨源iframe一共40个,写了allow属性的只有8个,而这8个里有5个是视频播放器。 > 写了allow的都是“不写立刻看得出坏”的场景,视频播不了谁都看得见。不写的那32个,坏了也没人看得见。 ## 把默认值原样写进去,为什么结果就变了? 这是本文最核心的一组对照,也是最反直觉的一条。为了排除干扰,保哥把这一组做成了严格的单因素隔离 (https://zhangwenbao.com/seo-ab-testing-experiment-design-statistical-power-single-factor.html):只动一个变量,就是头里那一项写不写、以及写成什么。iframe的allow属性、浏览器、页面代码、权限授予状态全部保持不变。 顶层响应头里的摄像头这一项 | 顶层页面自己 | 跨源iframe里的组件 | 根本不写(头里只有别的指令) | 可用 | 可用 | camera=(self)——就是它的默认值 | 可用 | 被拒 | camera=* | 可用 | 可用 | camera=(self "组件的源") | 可用 | 可用 | camera=() | 被拒 | 被拒 | 第一行和第二行的差别,是这篇文章存在的理由。文档说摄像头的默认名单就是self;把这个默认值原样敲进响应头,跨源组件从能用变成了不能用。什么都不写和写成默认值,在语义上应该完全等价,在实测里是两个相反的结果。 ## 为什么会这样 MDN那句关于继承的原话给了线索: > For an 在浏览器打开。如果设置生效,iframe 区域应该是空白或显示"example.com refused to connect"。Console 也会有 X-Frame-Options 拒绝错误。 ## CSP frame-ancestors 是未来方向 ## 为什么 CSP 替代 X-Frame-Options X-Frame-Options 在 W3C 标准里已被标记为 Deprecated,新标准是 CSP(Content Security Policy)的 frame-ancestors 指令。功能比 X-Frame-Options 强: - 支持多个白名单域名(frame-ancestors 'self' https://trusted-partner.com https://another.com)。 - 支持通配符(frame-ancestors *.example.com)。 - 支持 scheme 匹配(frame-ancestors 'self' https: 表示同源 + 任意 HTTPS)。 - 没有 ALLOW-FROM 那种废弃问题。 ## 双写兼容老浏览器 建议双写——X-Frame-Options 给老浏览器(IE 11、老 Android)兜底,CSP frame-ancestors 给现代浏览器: Header always set X-Frame-Options SAMEORIGIN Header always set Content-Security-Policy "frame-ancestors 'self';" 如果两者都设置且策略不同,现代浏览器以 CSP 为准。配置不一致没关系——CSP 会覆盖 X-Frame-Options。 ## 白名单单个或多个域名 Header always set Content-Security-Policy "frame-ancestors 'self' https://partner1.com https://partner2.com;" 这就允许同源页面 + 两个合作伙伴域名嵌入。如果完全不允许嵌入: Header always set Content-Security-Policy "frame-ancestors 'none';" ## 常见踩坑与排查 ## .htaccess 已有内容覆盖了原有规则 很多人直接整段覆盖 .htaccess 结果把人家的伪静态 (https://zhangwenbao.com/tools/rewrite-generator.php)规则、防盗链 (https://zhangwenbao.com/using-htaccess-to-set-up-wordpress-anti-stealing-link.html)规则、错误页规则都覆盖了,站点 404。每次都先下载原文件备份再追加,不要覆盖。如果不知道现有内容是什么,先备份再说。 ## CDN 或反向代理覆盖响应头 站点前面挂了 Cloudflare、阿里云 CDN、腾讯云 CDN 时,原站设置的头可能被 CDN 替换或剥离。需要在 CDN 控制面板里同步配置: - Cloudflare:Rules → Transform Rules → Modify Response Header → Add Static。 - 阿里云 CDN:缓存配置 → HTTP 头部设置 → 添加。 - 腾讯云 CDN:高级配置 → HTTP Header 配置。 验证方式:直接 curl 站点域名(走 CDN)的响应头,对比 curl 源站 IP 的响应头。两者应该一致。 ## 开发本地 iframe 嵌入预览失效 同源策略里端口也算源的一部分,http://localhost:3000 嵌 http://localhost:8000 是跨源。开发环境下要预览: - 临时改成 X-Frame-Options: DENY(开发完恢复 SAMEORIGIN)。 - 或者前端开发服务器配代理把后端请求代理到同端口。 - 或者用 CSP frame-ancestors 加白名单 localhost。 ## WordPress / Typecho 等 CMS 后台预览失效 设置 SAMEORIGIN 后如果同源仍被拦,检查后台和前台是否真同源——端口、协议、子域、路径前缀都不能差。常见情况是后台用 https://example.com/wp-admin/ 但前台用 http://example.com/(HTTP),协议不一致就被认为跨源。修复 HTTPS 一致性即可。 ## 360 检测显示已修复但 Chrome 还是报旧值 清浏览器缓存或用无痕模式打开。CDN 缓存也要刷——Cloudflare 在 Caching 面板手动 Purge。验证时优先用 curl 拿真实响应头,浏览器有时候缓存得太狠。 ## 常见问题解答 ## 设置后我自己后台预览功能也不能用了怎么办 大概率你用的是 DENY 改成 SAMEORIGIN 即可。如果同源仍然被拦,检查后台和前台是不是真同源——端口、协议、子域都不能差。常见情况是后台用 admin.example.com 但前台用 example.com,子域不同算跨源。要么改成同子域,要么用 CSP frame-ancestors 加白名单。 ## Nginx 的话怎么配 直接在 server 块里加 add_header X-Frame-Options SAMEORIGIN always 那一行。Nginx 没有 .htaccess,所有配置都在 nginx.conf 或 conf.d 下。改完 nginx -t 验证语法再 systemctl reload nginx。注意 add_header 在 location 块里会覆盖外层 server 块——如果某个 location 已经有 add_header 又想加新头,必须在那个 location 里把所有头都重新写一遍。 ## 360 检测显示已修复但 Chrome 还是报旧值怎么处理 清浏览器缓存或者用无痕模式打开。CDN 缓存也要刷——Cloudflare 在 Caching 面板手动 Purge。验证时优先用 curl 拿到真实响应头,浏览器有时候缓存得太狠。如果 curl 显示新值但浏览器还是旧值就是浏览器缓存问题,反之就是 CDN 或反向代理问题。 ## 能不能只对部分页面设置 SAMEORIGIN 其他页面 DENY 可以。.htaccess 里用 FilesMatch 或 If 条件块针对特定 URL 设不同值。但这种做法维护成本高建议要么全站统一 SAMEORIGIN,要么细粒度场景改用 CSP 的多个策略文件。CSP frame-ancestors 支持更灵活的白名单,更适合这种场景。 ## HTTP 站点能加 HSTS 头吗 不能。HSTS 是强制 HTTPS 的指令,只在 HTTPS 响应里有意义。HTTP 站点的浏览器会忽略这个头。如果你的站点是 HTTP,先把 HTTPS 部署好(Let's Encrypt 免费证书)再加 HSTS。HTTP 直接加 HSTS 既无效又可能在切换协议时出 bug。 ## X-Frame-Options 和 CSP frame-ancestors 冲突时谁优先 现代浏览器以 CSP 为准。两者都设置且策略不一致时,浏览器优先看 CSP frame-ancestors。所以双写时不用担心策略冲突——CSP 自动接管。但如果你只想看 X-Frame-Options 生效(比如调试老浏览器),可以临时去掉 CSP 那行。 ## 设置后第三方支付页面的 iframe 跳转失败 支付宝、微信支付的部分支付方式确实用 iframe 嵌入支付页。如果你的网站需要内嵌支付 SDK 的 iframe,并且支付方域名固定,建议用 CSP frame-ancestors 加白名单——而不是 X-Frame-Options(因为 X-Frame-Options 是单值不支持白名单)。或者把支付环节改成完整跳转到支付方页面(顶级跳转),支付完成后再回跳。 ## Permissions-Policy 设置后某个 Feature 不能用怎么办 Permissions-Policy 的语法是允许列表写法 feature=(allowlist)。空括号意味着完全禁用。要允许同源用某个 Feature 写 feature=(self),允许特定域名写 feature=("https://trusted.com")。常见错误是直接 geolocation=() 后忘了把同源加回来,导致自家网站调用地理位置失败。修复就是改成 geolocation=(self)。 ## 共享主机 mod_headers 被禁用怎么办 少数廉价共享主机为了限制资源会禁用 mod_headers。这种情况只能:用 PHP header() 在主题层面设置(生效范围仅限通过 PHP 渲染的页面,静态资源不带头);或者换主机。云主机(阿里云、腾讯云、Vultr)99% 都支持 mod_headers,没必要为这点限制忍受。 ## X-Frame-Options 跟 SEO 到底有没有关系? 很多人配安全头是冲着"过安全检测"去的,但这里有个常被忽略的点:在所有安全响应头里,真正和 SEO 沾边的恰恰只有 X-Frame-Options(以及它的现代替身 CSP frame-ancestors)这一个。Google 的 John Mueller 在一次技术 SEO 的讨论里就说得很直接——他能想到唯一可能影响 SEO 的安全头,就是用来阻止别的站点 iframe 嵌套你内容的那个,要么是老的 X-Frame-Options,要么是 CSP 的 frame-ancestors;除此之外的安全头,更多是安全层面的事,跟排名没什么直接关系。 为什么偏偏是它?回到前面讲的 iframe 机制就懂了。如果你不挡 iframe 嵌套,别的站点可以把你的整页内容用一个对嵌套不设 CSP frame-ancestors 限制 (https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/frame-ancestors)的页面整个套进去——理论上,这就给了别人借你的内容去博取展示、甚至让搜索引擎在内容归属上犯迷糊的空间,稀释你自己的可见度,严重的就是变相的内容盗用。X-Frame-Options 把这条路堵死,既是一道安全防线,也顺手保住了"我的内容只在我自己的域名下被展示"这件对 SEO 真正有意义的事。 那其余的安全头(HSTS、X-Content-Type-Options、Referrer-Policy、CSP 的其它指令)是不是就跟 SEO 无关、可以不管了?也不能这么推。它们不直接动排名,但它们防的是站点被入侵挂马这件事——一旦被黑,攻击者往你页面里注入垃圾链接、隐藏文本,甚至塞进恶意 iframe,Google 不光会在搜索结果里给你挂上"此网站可能已被黑客入侵"的警告,目标关键词的排名也会跟着崩,恢复起来还得走一遍安全问题的排查和申诉。Google 搜索中心专门有一份讲什么是被黑内容、它怎么伤害站点搜索表现的文档 (https://developers.google.com/search/docs/advanced/security/what-is-hacked),把代码注入、页面注入、恶意 iframe 这些手法对排名的影响讲得很清楚。 所以保哥的建议是:别因为 Mueller 一句"只有 X-Frame-Options 直接相关",就把其它安全头当摆设。直接相关的那一个当然要配好,但其余几个是地基——地基塌了(站点被黑掉了排名),上面的 SEO 做得再漂亮也是白搭。把整组安全头一起配齐、定期复扫,才是对排名最稳妥的兜底。 顺着这条思路再走一步:既然 X-Frame-Options 是唯一和排名直接沾边的安全头,它就该被正式列进技术 SEO 审计,而不是只当成运维那边的安全任务。保哥做站点体检时,会把这组响应头按优先级分三档——HSTS、X-Content-Type-Options、X-Frame-Options 是必配档,分别守住降级劫持、MIME 嗅探和内容归属。 CSP(含 frame-ancestors)是强烈推荐档,它是 X-Frame-Options 的现代超集,能顺手把脚本注入也一起防了;Referrer-Policy、Permissions-Policy 是可选档,配上更稳,缺了不致命。审计时按这个顺序查、缺哪档补哪档,比盲目追 securityheaders.com 满分更有的放矢。 还有个容易踩空的点:别以为装了 SEO 插件就把这些响应头一起管了。主流的 Yoast、Rank Math 这类 SEO 插件并不负责输出安全头,真正能落地的要么是服务器层配置(前面的 Nginx 或 .htaccess 方案),要么是专门的安全或缓存类插件来代劳。换句话说,安全头这件事 SEO 插件帮不上忙,审计清单里得单独立一项,明确指给运维或专门工具去做,别让它在“以为有人管”的缝里漏掉。 ## 总结 X-Frame-Options 是 Web 安全里"投入产出比最高"的配置之一——加一行 .htaccess 或一行 Nginx 配置,就能根除点击劫持类攻击。配上 X-Content-Type-Options、Referrer-Policy、Permissions-Policy、HSTS、CSP 这一组现代安全头,你的站点在 securityheaders.com 上的评级可以从 F 直接拉到 A+。 如果只是接 360 安全检测的那一条提示做应急修复,本文方案 A(Nginx / Apache 配置或 .htaccess)就够用。如果是要做认真的安全加固,建议直接上 CSP + 完整安全头组合,并把 securityheaders.com 评级 A+ 作为长期持有的指标。安全这件事是"持有"不是"达成"——配置只是起点,每隔 3-6 个月扫一次确保没回退。 ## 权威参考资料