<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Tutorial · XB 现场笔记</title><link>https://www.xbcatm.com/zh/tags/tutorial/</link><description>基于 HTTP、DNS、TLS、Caddy、SSH、发布与恢复的可复现运维现场笔记。</description><generator>Hugo</generator><language>zh-CN</language><atom:link href="https://www.xbcatm.com/zh/tags/tutorial/index.xml" rel="self" type="application/rss+xml"/><item><title>发布一个静态站点并证明边缘一致</title><link>https://www.xbcatm.com/zh/tutorials/ship-a-static-release/</link><pubDate>Tue, 11 Aug 2026 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/zh/tutorials/ship-a-static-release/</guid><description>构建并暂存静态产物，切换发布指针，验证公共响应样本，并明确回滚与证据边界。</description><content:encoded>&lt;p&gt;一次发布，只有在所需公共边缘检查与选定产物一致时才算完成，而不是上传退出码为零就完成。本教程串起&lt;strong&gt;构建&lt;/strong&gt;、&lt;strong&gt;校验&lt;/strong&gt;、&lt;strong&gt;发布&lt;/strong&gt;与&lt;strong&gt;验证&lt;/strong&gt;，同时区分本地检查和部署变更。发布会改变服务器；结尾的探针才是只读网络观察。&lt;/p&gt;
&lt;p&gt;命令假定使用 Bash、GNU find/sort/sha256sum、rsync、Python 3 与项目固定版本的 Hugo。构建步骤从仓库根运行。接触线上主机前，先在一次性目录演练&lt;a href="https://www.xbcatm.com/zh/decisions/adr-complete-directory-release/"&gt;目录切换示例&lt;/a&gt;。明确实际服务根、部署负责人、上一版目标与回滚过程。下面的 &lt;code&gt;/srv&lt;/code&gt; 布局要求部署已配置完成，不是仓库会自动安装的东西。&lt;/p&gt;
&lt;h2 id="1-从已记录输入构建一份产物"&gt;1. 从已记录输入构建一份产物&lt;/h2&gt;
&lt;p&gt;从干净源码版本开始，运行项目构建并记录已安装生成器版本：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; -eu
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git status --short
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git rev-parse HEAD
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;hugo version
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;make build
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;短状态为空可支持干净工作树的判断，但命令本身不会强制执行。在称其为某个提交的发布前，应检查所有变更。&lt;code&gt;make build&lt;/code&gt; 将短提交号和当前 UTC 构建时间写入 &lt;code&gt;data/buildinfo.toml&lt;/code&gt;，再生成 &lt;code&gt;public/&lt;/code&gt;。页面中的标记说明记录了哪些输入，不能证明有未提交修改的工作树只含那个提交。&lt;/p&gt;
&lt;p&gt;固定 Hugo 与 vendored 主题可减少漂移，但构建时间字段使不同时间的普通构建不一定逐字节相同。应构建选定发布一次、验证后传输这份准确产物，而不是在服务器另行重建。&lt;/p&gt;
&lt;h2 id="2-检查完整离线契约"&gt;2. 检查完整离线契约&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;make check&lt;/code&gt; 检查当前产物的生成视觉资源、离线包新鲜度、内部链接、无脚本输出、来源链接及下载校验和。完整仓库关卡则使用：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; -eu
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;make verify
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;make check-upstream
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;完整关卡还运行回归测试、双语文章检查和审阅清单新鲜度校验，再重新构建并检查输出。因此要在最后一次构建后才生成发布清单。构建成功及审阅记录未过期，不能证明每篇文章技术正确，也不证明公共服务器使用了预期头部。页数与耗时会变化，应保留实际输出，不与文章里复制来的计数作比较。&lt;/p&gt;
&lt;h2 id="3-用清单标识产物字节"&gt;3. 用清单标识产物字节&lt;/h2&gt;
&lt;p&gt;清单放在 &lt;code&gt;public/&lt;/code&gt; 外，避免把自身纳入哈希。NUL 分隔文件名可保留空格及其他 shell 敏感字符：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; -euo pipefail
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;manifest&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;/release.sha256&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; public
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; find . -type f -print0 &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nv"&gt;LC_ALL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;C sort -z &lt;span class="p"&gt;|&lt;/span&gt; xargs -0 sha256sum
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;)&lt;/span&gt; &amp;gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$manifest&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; public &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; sha256sum -c &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$manifest&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sha256sum &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$manifest&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;GNU 校验行的摘要在文件名前。&lt;code&gt;sha256sum -c&lt;/code&gt; 成功说明已列出的文件匹配，不证明没有额外文件。因此应暂存到全新的空发布目录。另行检查符号链接：&lt;code&gt;find -type f&lt;/code&gt; 不包含它们，而意外链接可能暴露预期根外的文件。将清单及其自身摘要留在发布记录中；校验和证明相对于清单的一致性，不能证明不可信清单的真实性。&lt;/p&gt;
&lt;h2 id="4-暂存并选择完整发布"&gt;4. 暂存并选择完整发布&lt;/h2&gt;
&lt;p&gt;假定 Caddy 已服务 &lt;code&gt;/srv/www/example-site/current&lt;/code&gt;，它是指向 &lt;code&gt;releases/&lt;/code&gt; 内部的符号链接，所有权与读取权限正确。&lt;code&gt;releases&lt;/code&gt; 父目录须事先存在。发布者必须串行执行，并选择唯一发布 ID。把此块作为独立 Bash 脚本，在部署机器上包含已验证 &lt;code&gt;public/&lt;/code&gt; 与 &lt;code&gt;release.sha256&lt;/code&gt; 的目录中运行。运行前按部署策略检查产物的所有权与读取权限。此块自行设置清单路径和遇错停止策略；前置条件、复制或校验和失败时，必须在选择指针前停止。它拒绝暂存产物内的符号链接；这是会修改部署的步骤。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; -euo pipefail
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;base&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/srv/www/example-site
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;manifest&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;/release.sha256&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;test&lt;/span&gt; -f &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$manifest&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;test&lt;/span&gt; -d public
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;release_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;20261015T120000Z
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;release&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$base&lt;/span&gt;&lt;span class="s2"&gt;/releases/&lt;/span&gt;&lt;span class="nv"&gt;$release_id&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;test&lt;/span&gt; -L &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$base&lt;/span&gt;&lt;span class="s2"&gt;/current&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;previous&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;readlink &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$base&lt;/span&gt;&lt;span class="s2"&gt;/current&amp;#34;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mkdir &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$release&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="c1"&gt;# Fail if this release already exists.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rsync -a public/ &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$release&lt;/span&gt;&lt;span class="s2"&gt;/&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$release&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; sha256sum -c &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$manifest&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;links&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;find &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$release&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; -type l -print -quit&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; -n &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$links&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Refusing symlinks in the staged artifact&amp;#34;&lt;/span&gt; &amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;exit&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Ownership and readable permissions must match the deployment policy.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;python3 - &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$base&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$release_id&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;&amp;lt;&amp;#39;PYTHON&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;from pathlib import Path
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;import os, sys, uuid
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;base = Path(sys.argv[1])
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;release = base / &amp;#34;releases&amp;#34; / sys.argv[2]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;current = base / &amp;#34;current&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;if not current.is_symlink() or not release.is_dir():
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; raise SystemExit(&amp;#34;Expected current symlink and a complete release directory&amp;#34;)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;pending = base / (&amp;#34;.current-&amp;#34; + uuid.uuid4().hex)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;pending.symlink_to(os.path.relpath(release, base), target_is_directory=True)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;os.replace(pending, current)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;print(&amp;#34;selected:&amp;#34;, os.readlink(current))
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;PYTHON&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Previous target: %s\n&amp;#39;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$previous&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;临时链接与 &lt;code&gt;current&lt;/code&gt; 共用父目录，因此替换通过同文件系统的一次 rename 完成。普通 &lt;code&gt;mv new existing-directory&lt;/code&gt; 不会交换两个非空目录。不要用此片段转换已有的真实目录服务根，应单独规划迁移。在预发布环境验证真实服务器的根解析及文件系统行为。&lt;/p&gt;
&lt;p&gt;旧目标仍在磁盘上，不等于切换后旧资源 URL 仍可访问。应在新服务命名空间或稳定资源库中保留旧版指纹资源字节。浏览器多个请求可能跨越两个发布，缓存也可能持有旧 HTML。原子命名空间替换不是跨请求快照隔离，也不是断电持久性保证。&lt;/p&gt;
&lt;h2 id="5-验证实际交付字节与策略"&gt;5. 验证实际交付字节与策略&lt;/h2&gt;
&lt;p&gt;切换后，用固定表示选择比较公共响应样本与相应产物文件：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; -euo pipefail
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://www.xbcatm.com/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;curl -sS --max-time &lt;span class="m"&gt;20&lt;/span&gt; -H &lt;span class="s1"&gt;&amp;#39;Accept-Encoding: identity&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -D served.headers -o served.html -w &lt;span class="s1"&gt;&amp;#39;%{http_code}&amp;#39;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$status&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;200&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cmp public/index.html served.html
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sh public/downloads/check-http-headers.sh &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;从暂存时使用的同一产物目录运行此块。头部辅助脚本使用该可信产物内的副本；此步骤无需源码工作树。&lt;code&gt;cmp&lt;/code&gt; 成功证明这个首页样本逐字节匹配。遇到意外重定向、返回 200 的代理错误页、CDN 转换或编码响应时，需要调查。检查 &lt;code&gt;served.headers&lt;/code&gt; 中的 &lt;code&gt;Content-Encoding&lt;/code&gt;、缓存年龄、CSP 与内容类型。头部脚本打印响应及最终状态，不会强制所有预期条件。&lt;/p&gt;
&lt;p&gt;对中文内容、嵌套文章、CSS、SVG、下载项、重定向及错误路由重复适用检查。记录 URL 到文件的映射，包括目录索引。比较服务器磁盘树与清单证明的是暂存一致性，不是所有公共缓存节点的交付结果。完整边缘论断需要更广泛抓取和表示感知检查；少数探针始终只是样本。&lt;/p&gt;
&lt;p&gt;切换后的检查失败时，新版仍被选中，直到你采取行动。用新临时链接指向已记录的 &lt;code&gt;previous&lt;/code&gt; 值，再次原子替换 &lt;code&gt;current&lt;/code&gt;，随后重做探针。不要只移走新树，诊断期间也不要删除证据或上一版。&lt;/p&gt;
&lt;h2 id="6-分别检查-tls-身份和到期窗口"&gt;6. 分别检查 TLS 身份和到期窗口&lt;/h2&gt;
&lt;p&gt;从仓库根运行到期辅助脚本：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sh static/downloads/check-tls-expiry.sh www.xbcatm.com &lt;span class="m"&gt;30&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;它提取证书、打印日期、检查剩余到期间隔，却不是完整 TLS 身份测试：当前实现没有让主机名或信任验证失败成为致命错误。应按照&lt;a href="https://www.xbcatm.com/zh/tutorials/audit-a-public-edge/"&gt;公共边缘信任教程&lt;/a&gt;执行显式验证握手与 SAN 检查。不能从“至少还有 30 天有效期”推断信任链或吊销检查成功。&lt;/p&gt;
&lt;p&gt;临近过期是另一项发布风险。保留实际 &lt;code&gt;notAfter&lt;/code&gt;、信任结果与客户端环境，不要把历史签发者或日期复制到发布日志。&lt;/p&gt;
&lt;h2 id="7-dns-变化时纳入缓存窗口"&gt;7. DNS 变化时纳入缓存窗口&lt;/h2&gt;
&lt;p&gt;纯内容发布不一定改变 DNS。若主机或边缘变化，应在缓存过期期间继续维护旧目标服务。变更时才降低 TTL，不会缩短已经按旧 TTL 缓存的条目。解析器 serve-stale、负缓存和客户端缓存会让窗口更复杂；TTL 不是保证所有客户端都完成迁移的统一截止点。CAA 变化影响签发策略，不负责把流量路由到新内容。&lt;/p&gt;
&lt;h2 id="8-保留演练过的恢复路径"&gt;8. 保留演练过的恢复路径&lt;/h2&gt;
&lt;p&gt;上一版目录有利于快速回滚，但不是独立备份。磁盘故障、误清理或部署账户被攻破，都可能影响所有本地发布。应保留源码、构建输入、配置及适当的独立产物备份，并在隔离环境演练恢复。一次成功演练证明的是那个时刻的那条恢复路径，不会永久保证之后的变更。&lt;/p&gt;
&lt;h2 id="9-精确记录结果"&gt;9. 精确记录结果&lt;/h2&gt;
&lt;p&gt;保留源码版本与干净工作树状态、构建元数据和工具版本、清单摘要、选中与上一版目标、探针时间、HTTP/TLS 观察及任何回滚。说明覆盖的是样本、全部映射 URL，还是多个观测点。关卡失败时，应记录失败与恢复过程，而不是“它工作过”。&lt;/p&gt;
&lt;p&gt;切换前失败可以让旧版保持不变；切换后恢复则是需要单独验证的显式操作。区分这两点，才能让发布路径可审计，而不假装每次失败都会自动恢复旧树。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://download.samba.org/pub/rsync/rsync.1"&gt;rsync 手册&lt;/a&gt;，归档复制与源路径尾斜线语义，已核验 2026-10-15。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc8767.html"&gt;RFC 8767：Serving Stale Data&lt;/a&gt;，TTL 过期后的 DNS serve-stale 行为，已核验 2026-10-15。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://manpages.debian.org/trixie/coreutils/sha256sum.1.en.html"&gt;GNU sha256sum 手册，Debian 软件包&lt;/a&gt;，校验和输出及验证选项，已核验 2026-10-15。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://docs.python.org/3/library/os.html#os.replace"&gt;Python &lt;code&gt;os.replace&lt;/code&gt;&lt;/a&gt;，原子替换与文件系统限制，已核验 2026-10-15。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://caddyserver.com/docs/caddyfile/directives/file_server"&gt;Caddy &lt;code&gt;file_server&lt;/code&gt;&lt;/a&gt;，根与符号链接处理，已核验 2026-10-15。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://curl.se/docs/manpage.html"&gt;curl 手册&lt;/a&gt;，状态捕获、响应头及内容解码，已核验 2026-10-15。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://docs.openssl.org/3.0/man1/openssl-s_client/"&gt;OpenSSL &lt;code&gt;s_client&lt;/code&gt;&lt;/a&gt;，显式证书验证，已核验 2026-10-15。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://reproducible-builds.org/docs/definition/"&gt;Reproducible Builds：定义&lt;/a&gt;，产物身份与可复现性的区别，已核验 2026-10-15。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>像客户端一样读取活动边缘</title><link>https://www.xbcatm.com/zh/tutorials/http-edge-walkthrough/</link><pubDate>Fri, 07 Aug 2026 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/zh/tutorials/http-edge-walkthrough/</guid><description>用 curl 检查方法、表示元数据、范围与缓存，区分协议要求和端点的具体选择。</description><content:encoded>&lt;p&gt;HTTP 基础篇的每条笔记各取一纸契约并展示单独的证据。本教程则按顺序走完一次客户端请求触达的完整路径，只用只读命令。目标是区分协议要求与某个服务器的选择，而不是要求所有端点都返回相同状态和头部。&lt;/p&gt;
&lt;p&gt;我们全程用 &lt;code&gt;www.xbcatm.com&lt;/code&gt;。把它换成你要检查的主机。请求使用 HTTP 安全方法，但仍可能产生访问日志或消耗速率额度。只检查你有权查询的端点。下面输出块是示意，不是当前生产测量。请在一次性工作目录运行 shell 片段，保留错误，并记录时间和客户端版本。&lt;/p&gt;
&lt;h2 id="1-先确定方法可以问什么"&gt;1. 先确定方法可以问什么&lt;/h2&gt;
&lt;p&gt;在读响应体之前，先确定服务器允许你问什么。静态处理器通常支持 GET 和 HEAD，代理也可能实现 OPTIONS。先询问边缘对目标资源的处理：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ curl -sS --max-time 20 -D - -o /dev/null -w &amp;#39;OPTIONS -&amp;gt; HTTP %{http_code}\n&amp;#39; -X OPTIONS https://www.xbcatm.com/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;HTTP/2 405
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;allow: GET, HEAD
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;OPTIONS -&amp;gt; HTTP 405
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;若服务器识别 OPTIONS、但该资源不支持它，&lt;code&gt;405 Method Not Allowed&lt;/code&gt; 是合法答案，且 405 必须携带 &lt;code&gt;Allow&lt;/code&gt;。成功的 200 或 204 OPTIONS 响应也可能正确。&lt;code&gt;501&lt;/code&gt; 表示服务器未实现该方法。一次 OPTIONS 不能证明其他方法的行为。不要为了测试拒绝而向不熟悉的线上服务发送 POST：它不是安全方法，单有 200 也不能描述其副作用。&lt;/p&gt;
&lt;h2 id="2-读取表示行"&gt;2. 读取表示行&lt;/h2&gt;
&lt;p&gt;第一行告诉你协商出来的交换；头部随后告诉你将要到达的是什么字节。像客户端看到的那样捕获它们、丢弃响应体：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ curl -sS --max-time 20 -D - -o /dev/null https://www.xbcatm.com/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;HTTP/2 200
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;accept-ranges: bytes
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;content-length: 15890
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;content-security-policy: default-src &amp;#39;self&amp;#39;; base-uri &amp;#39;self&amp;#39;; form-action &amp;#39;none&amp;#39;; frame-ancestors &amp;#39;none&amp;#39;; object-src &amp;#39;none&amp;#39;; script-src &amp;#39;none&amp;#39;; style-src &amp;#39;self&amp;#39;; img-src &amp;#39;self&amp;#39; data:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;content-type: text/html; charset=utf-8
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;etag: &amp;#34;dkx84jwth7nkc9e&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;last-modified: Mon, 24 Aug 2026 13:55:23 GMT
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;strict-transport-security: max-age=31536000; includeSubDomains
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;x-content-type-options: nosniff
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;三个头部值得停下来。&lt;code&gt;content-type: text/html; charset=utf-8&lt;/code&gt; 命中了媒体类型，&lt;code&gt;content-type-and-nosniff&lt;/code&gt; 说明为什么它必须描述你真正发送的字节。&lt;code&gt;etag: &amp;quot;dkx84jwth7nkc9e&amp;quot;&lt;/code&gt; 交给你强验证器。&lt;code&gt;x-content-type-options: nosniff&lt;/code&gt; 限制浏览器的 MIME 处理，尤其是脚本与样式表抓取；它不会验证正文确实是 HTML。每一个都是证据而不是装饰，合在一起是说：该响应声明了应与正文及预期策略比较的元数据。语法上为强 ETag 的值仍是服务器的声明，不是独立的内容哈希证明。&lt;/p&gt;
&lt;h2 id="3-对同一表示比较-head-与-get"&gt;3. 对同一表示比较 HEAD 与 GET&lt;/h2&gt;
&lt;p&gt;HEAD 使用 GET 的语义，但不带响应正文。服务器应返回相同头部，不过可以省略只有生成内容时才能确定的字段。&lt;code&gt;Content-Length&lt;/code&gt; 可省略；若在 HEAD 中发送，它必须等于相应 GET 会发送的内容大小。应在内容稳定时使用相同编码及其他选择头比较：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ curl -sSI --max-time 20 -H &amp;#39;Accept-Encoding: identity&amp;#39; https://www.xbcatm.com/ | grep -i content-length
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;content-length: 15890
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ curl -sS --max-time 20 -H &amp;#39;Accept-Encoding: identity&amp;#39; https://www.xbcatm.com/ | wc -c
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;15890
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;两者一致：都是 15890。这就是 &lt;code&gt;head-request-parity&lt;/code&gt; 契约。仅缺少 HEAD 长度不是协议违规。长度不符需要调查，也要排除两次请求之间发生发布、或选中不同表示的可能。没有 &lt;code&gt;pipefail&lt;/code&gt; 的 shell 中，传给 &lt;code&gt;wc&lt;/code&gt; 的管道可能隐藏 curl 失败；自动关卡应先确认 curl 成功保存正文，再统计文件字节。&lt;/p&gt;
&lt;h2 id="4-用范围请求精确的字节"&gt;4. 用范围请求精确的字节&lt;/h2&gt;
&lt;p&gt;因为源站知道自己的表示，它可以只服务其中一部分。范围请求要求一个字节区间，只得到那个区间：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ curl -sS --max-time 20 -D - -o /dev/null -H &amp;#39;Accept-Encoding: identity&amp;#39; -H &amp;#39;Range: bytes=0-99&amp;#39; -w &amp;#39;HTTP %{http_code} bytes=%{size_download}\n&amp;#39; https://www.xbcatm.com/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;HTTP/2 206
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;content-range: bytes 0-99/15890
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;content-length: 100
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;HTTP 206 bytes=100
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;对于至少 100 字节的表示，成功的单范围响应应有 &lt;code&gt;206&lt;/code&gt;、匹配的 &lt;code&gt;Content-Range: bytes 0-99/…&lt;/code&gt;，以及 100 个内容字节。服务器可合法忽略 Range 并返回完整 200，这不说明它缺少校验器。无法满足的范围可能返回 416。普通 Range 请求不要求 ETag；使用 &lt;code&gt;If-Range&lt;/code&gt; 恢复特定表示，或安全拼接部分响应时，才要遵守相关强校验器规则。范围针对所选编码后的字节，因此这里显式选择 identity。&lt;/p&gt;
&lt;h2 id="5-用验证器让缓存重新验证"&gt;5. 用验证器让缓存重新验证&lt;/h2&gt;
&lt;p&gt;现在把方法、表示与字节选择通过缓存结合。新鲜的已存响应可能无需检查校验器就可复用。测试重新验证时，应抓取当前响应，并用相同表示选择重放其实际 ETag，不要复制文章里的旧标签：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; -eu
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://www.xbcatm.com/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;work&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;mktemp -d&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;curl -sS --max-time &lt;span class="m"&gt;20&lt;/span&gt; -H &lt;span class="s1"&gt;&amp;#39;Accept-Encoding: identity&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -D &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$work&lt;/span&gt;&lt;span class="s2"&gt;/headers&amp;#34;&lt;/span&gt; -o &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$work&lt;/span&gt;&lt;span class="s2"&gt;/body&amp;#34;&lt;/span&gt; -w &lt;span class="s1"&gt;&amp;#39;%{http_code}&amp;#39;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$status&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;200&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;etag&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;awk &lt;span class="s1"&gt;&amp;#39;tolower($1)==&amp;#34;etag:&amp;#34; {sub(/\r$/, &amp;#34;&amp;#34;); sub(/^[^:]*:[ \t]*/, &amp;#34;&amp;#34;); print; exit}&amp;#39;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$work&lt;/span&gt;&lt;span class="s2"&gt;/headers&amp;#34;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;test&lt;/span&gt; -n &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$etag&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;No ETag: inspect Last-Modified instead&amp;#39;&lt;/span&gt; &amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;2&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit&lt;/span&gt; 1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -sS --max-time &lt;span class="m"&gt;20&lt;/span&gt; -H &lt;span class="s1"&gt;&amp;#39;Accept-Encoding: identity&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -H &lt;span class="s2"&gt;&amp;#34;If-None-Match: &lt;/span&gt;&lt;span class="nv"&gt;$etag&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; -D - -o /dev/null &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -w &lt;span class="s1"&gt;&amp;#39;HTTP %{http_code}\n&amp;#39;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;把此片段作为独立 shell 脚本运行。只有初次传输完整成功且状态为 200，才有可用的已存响应；错误表示也可能有 ETag。若所选表示仍匹配且正常处理成功，预期得到 304，并用更新后的元数据复用这份完整正文。表示变化时，200 完全可能是正确结果。检查结果期间保留临时目录，完成后仅删除此处生成的目录。&lt;code&gt;If-None-Match&lt;/code&gt; 使用弱比较，所以 304 不一定证明字节完全一致。新鲜度寿命与当前年龄决定能否直接复用；校验器回答的是另一问题，本身不设定寿命。&lt;/p&gt;
&lt;h2 id="6-协商更小的表示"&gt;6. 协商更小的表示&lt;/h2&gt;
&lt;p&gt;同样的内容在客户端表明能解压时可以传递更少字节。请求 gzip 并观察正文缩水而语义不变：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ curl -sS --max-time 20 -H &amp;#39;Accept-Encoding: identity&amp;#39; -o /dev/null -w &amp;#39;no-enc: %{size_download} bytes\n&amp;#39; https://www.xbcatm.com/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ curl -sS --max-time 20 -o /dev/null -H &amp;#39;Accept-Encoding: gzip&amp;#39; -w &amp;#39;gzip: %{size_download} bytes\n&amp;#39; https://www.xbcatm.com/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;no-enc: 15890 bytes
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;gzip: 4368 bytes
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;示例大小展示一种可能的缩减；服务器也可以选择 identity。这是 &lt;code&gt;http-compression-negotiation&lt;/code&gt; 笔记的核心：改变选中的表示会改变线上字节，元数据（&lt;code&gt;content-length&lt;/code&gt;、&lt;code&gt;encoding&lt;/code&gt;）也随之改变。请求压缩的客户端与存储编码形式的缓存必须就选了哪种表示达成一致，否则缓存可能交付不匹配结果。除字节数外，还应检查 &lt;code&gt;Content-Encoding&lt;/code&gt; 与 &lt;code&gt;Vary: Accept-Encoding&lt;/code&gt;。手工设置 &lt;code&gt;Accept-Encoding&lt;/code&gt; 不会让 curl 自动解压；&lt;code&gt;--compressed&lt;/code&gt; 才会请求受支持编码并解码响应。应比较解码后的正文来确认内容相同，也不要假定不同编码共用同一个强 ETag。&lt;/p&gt;
&lt;h2 id="7-跟随-url-变化时的故事"&gt;7. 跟随 URL 变化时的故事&lt;/h2&gt;
&lt;p&gt;身份变化时，响应应说明故事在哪里继续，而不去猜正文：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ curl -sS --max-time 20 -o /dev/null -w &amp;#39;%{http_code} -&amp;gt; %{redirect_url}\n&amp;#39; -I http://www.xbcatm.com/ | head -1
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;308 -&amp;gt; https://www.xbcatm.com/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;308 Permanent Redirect&lt;/code&gt; 保持方法与故事，正是 &lt;code&gt;redirects-and-status-codes&lt;/code&gt; 关心的。无论是永久移动的 301/308，还是由站点处理的 404，状态行都是故事的第一句，应在读正文前先读。&lt;/p&gt;
&lt;h2 id="8-最后让爬虫读地图"&gt;8. 最后让爬虫读地图&lt;/h2&gt;
&lt;p&gt;边缘也会被根本不请求 HTML 的机器读取。发现机制回答两个不同的问题，&lt;code&gt;robots-and-sitemaps&lt;/code&gt; 有说明。确认发现机制指向真实 sitemap：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ curl -sS --max-time 20 https://www.xbcatm.com/robots.txt
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;User-agent: *
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Disallow:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Sitemap: https://www.xbcatm.com/sitemap.xml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;也应抓取 sitemap URL，核对规范主机名与语言 URL。&lt;code&gt;robots.txt&lt;/code&gt; 是爬虫指引，不是访问控制；被列入 sitemap 不保证抓取或索引。&lt;/p&gt;
&lt;h2 id="所有检查一览"&gt;所有检查一览&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;检查&lt;/th&gt;
&lt;th&gt;应检查什么&lt;/th&gt;
&lt;th&gt;笔记&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;OPTIONS&lt;/code&gt; 状态 + Allow&lt;/td&gt;
&lt;td&gt;对该资源观察到的方法策略&lt;/td&gt;
&lt;td&gt;http-method-semantics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;content-type&lt;/code&gt; + 正文 + &lt;code&gt;nosniff&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;声明的类型与 MIME 处理策略&lt;/td&gt;
&lt;td&gt;content-type-and-nosniff&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HEAD/GET 元数据&lt;/td&gt;
&lt;td&gt;对应表示，允许规定的字段省略&lt;/td&gt;
&lt;td&gt;head-parity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Range + Content-Range&lt;/td&gt;
&lt;td&gt;观察到的部分传输支持&lt;/td&gt;
&lt;td&gt;http-range-requests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;抓取的 &lt;code&gt;ETag&lt;/code&gt; -&amp;gt; 304&lt;/td&gt;
&lt;td&gt;条件表示匹配&lt;/td&gt;
&lt;td&gt;conditional-requests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;308 保留方法&lt;/td&gt;
&lt;td&gt;重定向保留故事&lt;/td&gt;
&lt;td&gt;redirects-and-status-codes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;robots -&amp;gt; sitemap&lt;/td&gt;
&lt;td&gt;发现答案不同&lt;/td&gt;
&lt;td&gt;robots-and-sitemaps&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;每一行原来是一条笔记；现在是你可对任意边缘运行的命令。需要细节时读链接的笔记，把这个表留作再次检查时的速查。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://fetch.spec.whatwg.org/#x-content-type-options-header"&gt;WHATWG Fetch Standard&lt;/a&gt;，nosniff 处理与请求目标，已核验 2026-10-15。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc9110.html"&gt;RFC 9110 — HTTP Semantics&lt;/a&gt;，方法、验证器与状态码，已核验 2026-10-15。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc9111.html"&gt;RFC 9111 — HTTP Caching&lt;/a&gt;，新鲜度与重新验证，已核验 2026-10-15。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc7538.html"&gt;RFC 7538 — 308 Permanent Redirect&lt;/a&gt;，保留方法的重定向，已核验 2026-10-15。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://curl.se/docs/manpage.html"&gt;curl 手册&lt;/a&gt;，只读检查选项，已核验 2026-10-15。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>审计公共边缘的信任</title><link>https://www.xbcatm.com/zh/tutorials/audit-a-public-edge/</link><pubDate>Thu, 06 Aug 2026 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/zh/tutorials/audit-a-public-edge/</guid><description>一次只读演练，像细心运维那样检查安全头、TLS 身份、证书链、主机密钥、CAA 与 Cookie 作用域。</description><content:encoded>&lt;p&gt;Web 安全系列把设置当作带作用域与失败模式的契约。本教程检查你有权查询的主机。HTTP、TLS 与 DNS 请求是只读观察，但仍会产生日志和流量。SSH 检查读取你自己的信任记录；部署主机可能不同于网站主机。&lt;/p&gt;
&lt;p&gt;用一次性本地工作目录保存输出。这里没有任何输出被当作刚测得的 &lt;code&gt;www.xbcatm.com&lt;/code&gt; 生产状态；命令通过，只能证明它实际检查的性质。&lt;/p&gt;
&lt;h2 id="1-从边缘声明的策略开始"&gt;1. 从边缘声明的策略开始&lt;/h2&gt;
&lt;p&gt;抓取 GET 响应，不默默跟随重定向，再检查状态与头部：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;www.xbcatm.com
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;https://&lt;/span&gt;&lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="s2"&gt;/&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -sS --max-time &lt;span class="m"&gt;20&lt;/span&gt; -D headers.txt -o body.html &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cat headers.txt
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;仓库预期策略包含以下示意字段：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Content-Security-Policy: default-src &amp;#39;self&amp;#39;; base-uri &amp;#39;self&amp;#39;; form-action &amp;#39;none&amp;#39;; frame-ancestors &amp;#39;none&amp;#39;; object-src &amp;#39;none&amp;#39;; script-src &amp;#39;none&amp;#39;; style-src &amp;#39;self&amp;#39;; img-src &amp;#39;self&amp;#39; data:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Strict-Transport-Security: max-age=31536000; includeSubDomains
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;X-Content-Type-Options: nosniff
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;X-Frame-Options: DENY
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;script-src 'none'&lt;/code&gt; 是适合本站无脚本要求的严格选择，不是所有静态站的通用配方。CSP 由浏览器执行；报告模式不会阻止执行。应读完整策略，检查更具体的脚本指令，并比较普通页与错误路由。&lt;code&gt;nosniff&lt;/code&gt; 限制 MIME 处理，尤其是脚本和样式表目标，但不证明正文的媒体类型正确。&lt;/p&gt;
&lt;p&gt;HSTS 从有效 HTTPS 响应学习，作用于发送它的主机。&lt;code&gt;www.example.com&lt;/code&gt; 的 &lt;code&gt;includeSubDomains&lt;/code&gt; 覆盖该名称的后代，不覆盖兄弟 &lt;code&gt;api.example.com&lt;/code&gt; 或父域 &lt;code&gt;example.com&lt;/code&gt;。采用较长时长前先清点实际范围。代理增加或移除头部会改变浏览器契约，因此产物检查与边缘检查需要分开。&lt;/p&gt;
&lt;h2 id="2-读取证书日期前先验证-tls-身份"&gt;2. 读取证书日期前先验证 TLS 身份&lt;/h2&gt;
&lt;p&gt;SNI 选择目标虚拟主机，不验证证书名称。应让验证失败成为致命错误，并显式启用主机名验证。使用与受测客户端相适应、保持更新的信任库。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;openssl s_client -connect &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="s2"&gt;:443&amp;#34;&lt;/span&gt; -servername &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -verify_hostname &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; -verify_return_error -showcerts &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;lt;/dev/null &amp;gt; tls.txt 2&amp;gt; tls.log
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Continue only if the command succeeded; preserve tls.log on failure.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;openssl x509 -in tls.txt -noout -subject -issuer -dates -ext subjectAltName
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;分别检查每步结果，或在失败即停止的 shell 设置下运行。隐藏 stderr 或把结果管道给成功的 &lt;code&gt;x509&lt;/code&gt;，可能掩盖握手失败。应检查 &lt;code&gt;subjectAltName&lt;/code&gt; 的 DNS 名称；显示的 subject CN 不足以完成现代服务身份验证。某些库版本仍保留旧式 CN 回退，因此也要明确检查预期 SAN 存在。合法匹配的通配符可以有效，不相关的证书则不行。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;notAfter&lt;/code&gt; 给出到期时刻，不证明信任链有效、主机名匹配，也不说明检查过吊销。记录日期与测量出的安全窗口，不要依赖记忆。OpenSSL 的结果描述其已配置的信任库与检查，不代表所有浏览器。&lt;/p&gt;
&lt;h2 id="3-读取呈现链不虚构固定数量"&gt;3. 读取呈现链，不虚构固定数量&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;-showcerts&lt;/code&gt; 输出的是服务器发送的证书，不是它们已经组成已验证链的声明。以下命令仅显示数量：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;grep -c &lt;span class="s1"&gt;&amp;#39;BEGIN CERTIFICATE&amp;#39;&lt;/span&gt; tls.txt
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;不存在“必须四张”的要求。服务器通常提供叶证书及必要的中间证书；根一般属于客户端信任库，不必发送。数量随 CA 与所选路径变化。只有一张时值得调查是否缺少中间证书，但数量本身既不代表成功，也不代表失败。&lt;/p&gt;
&lt;p&gt;应使用第 2 步真实的验证结果，再调查签发者或 SAN 变化、中间证书缺失、到期及信任库差异。中间证书轮换可能只是正常 CA 维护；数量没变也可能出错。这个计数命令不会自动与任何预期值作比较。&lt;/p&gt;
&lt;h2 id="4-重新验证已抓取的表示"&gt;4. 重新验证已抓取的表示&lt;/h2&gt;
&lt;p&gt;缓存是另一份契约，不是 TLS 身份证据。按照 &lt;a href="https://www.xbcatm.com/zh/tutorials/http-edge-walkthrough/"&gt;HTTP 演练&lt;/a&gt; 抓取当前 ETag，并对相同表示选择发出条件请求。匹配的条件 GET 可以返回 304；两次请求之间发生发布，则可能合法返回 200。旧的硬编码校验器不是变化站点的可复用测试夹具。&lt;/p&gt;
&lt;p&gt;无脚本策略不等于 CSP 永远不变。安全头可以独立于 HTML 改变，正文 ETag 也不是所有响应头的指纹。发布后应再次记录真实策略。&lt;/p&gt;
&lt;h2 id="5-找到适用的-caa-策略"&gt;5. 找到适用的 CAA 策略&lt;/h2&gt;
&lt;p&gt;CAA 约束合规证书机构的签发，不是证书固定，也不会追溯地使已签发证书失效。先检查查询名称及其祖先，保留响应状态，避免把 DNS 失败误当成空答案。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;dig www.xbcatm.com CAA +noall +comments +answer
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;dig xbcatm.com CAA +noall +comments +answer
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;www&lt;/code&gt; 的空答案不代表签发无限制。RFC 8659 会向上寻找第一个适用的非空 CAA 集合，每次查找按 DNS 规则跟随别名；需要时继续检查祖先。检查 &lt;code&gt;issue&lt;/code&gt;，涉及通配符签发时还要看 &lt;code&gt;issuewild&lt;/code&gt;。只有 &lt;code&gt;iodef&lt;/code&gt; 报告地址并不限制签发。若没有适用限制，CAA 不增加签发者限制，但常规域名验证与 CA 规则仍适用。&lt;/p&gt;
&lt;p&gt;应将 CA 文档规定的 CAA 标识与适用策略比较，不是只看叶证书中供人阅读的 issuer CN。今天的 CAA 结果证明的是今天的 DNS 策略，不证明证书签发时存在什么策略。&lt;/p&gt;
&lt;h2 id="6-检查部署主机已记录的-ssh-身份"&gt;6. 检查部署主机已记录的 SSH 身份&lt;/h2&gt;
&lt;p&gt;使用真实 SSH 目标；非默认端口需要方括号主机加端口形式。&lt;code&gt;ssh-keygen -F&lt;/code&gt; 能找到直接 grep 主机名会遗漏的哈希 known-host 条目。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;deploy_host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;deploy.example.test
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ssh-keygen -F &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$deploy_host&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; -f &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.ssh/known_hosts&amp;#34;&lt;/span&gt; &amp;gt; host-record.txt
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ssh-keygen -lf host-record.txt
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;把指纹与独立可信记录比较，例如提供商控制台或已验证的管理员渠道。这只读取本地记录，不证明远端现在呈现什么密钥。网络扫描到的密钥本身也不能完成身份认证。更新信任前先调查变化；重建只是可能原因，不是自动接受的许可。&lt;/p&gt;
&lt;h2 id="7-cookie-作用域不超过必要范围"&gt;7. Cookie 作用域不超过必要范围&lt;/h2&gt;
&lt;p&gt;检查已抓取响应中的所有 &lt;code&gt;Set-Cookie&lt;/code&gt;，以及相关重定向或应用路由。一个页面没设置 Cookie，不证明整站从不设置。主机专用 Cookie 省略 &lt;code&gt;Domain&lt;/code&gt;；添加父域 Domain 会扩大接收它的主机范围。&lt;code&gt;Path&lt;/code&gt; 控制发送范围，但不是防御同源代码的安全边界。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Secure&lt;/code&gt; 限制传输，&lt;code&gt;HttpOnly&lt;/code&gt; 阻止脚本访问 Cookie，&lt;code&gt;SameSite&lt;/code&gt; 控制规定的跨站发送行为。它们针对不同风险，不能互相代替；现代浏览器中的 &lt;code&gt;SameSite=None&lt;/code&gt; 需要 &lt;code&gt;Secure&lt;/code&gt;。对本站无账户的内容模型，扩大作用域前应先问为什么需要 Cookie。共享审计记录时应隐藏 Cookie 值。&lt;/p&gt;
&lt;h2 id="保留范围明确的审计记录"&gt;保留范围明确的审计记录&lt;/h2&gt;
&lt;p&gt;记录 UTC 时间、客户端版本、请求及最终 URL、状态、强制策略头、TLS 验证结果与日期、适用 CAA 查询及 SSH 记录来源。区分观察成功、意外结果与未执行检查。这几项观察不止“两条命令”，也不能证明整个服务安全。&lt;/p&gt;
&lt;p&gt;证书、DNS、代理或部署变化后，重做相关检查。保留旧结果以发现漂移；论断依赖浏览器执行时，应另行测试真实浏览器行为。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://fetch.spec.whatwg.org/#x-content-type-options-header"&gt;WHATWG Fetch Standard&lt;/a&gt;，nosniff 处理与请求目标，已核验 2026-10-15。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://docs.openssl.org/3.0/man1/openssl-s_client/"&gt;OpenSSL &lt;code&gt;s_client&lt;/code&gt;&lt;/a&gt;，SNI、主机名验证、致命验证错误与呈现链，已核验 2026-10-15。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc9525.html"&gt;RFC 9525：Service Identity in TLS&lt;/a&gt;，基于 SAN 的服务身份与通配符匹配，已核验 2026-10-15。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc8659.html"&gt;RFC 8659：CAA&lt;/a&gt;，适用记录集搜索与签发策略，已核验 2026-10-15。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc6797.html"&gt;RFC 6797：HSTS&lt;/a&gt;，主机范围与策略处理，已核验 2026-10-15。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://man.openbsd.org/ssh-keygen"&gt;OpenSSH &lt;code&gt;ssh-keygen&lt;/code&gt;&lt;/a&gt;，已知主机查找与指纹显示，已核验 2026-10-15。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://www.w3.org/TR/CSP3/"&gt;W3C：CSP Level 3&lt;/a&gt;，强制与报告模式及脚本指令回退（工作草案），已核验 2026-10-15。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-Cookie"&gt;MDN：Set-Cookie&lt;/a&gt;，Cookie 属性与边界，已核验 2026-10-15。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item></channel></rss>