<?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 Field Notes</title><link>https://hk.xbcatm.com/tags/tutorial/</link><description>Illustrated field notes on HTTP, DNS, TLS, Caddy, SSH, releases, and recovery.</description><generator>Hugo</generator><language>en-us</language><atom:link href="https://hk.xbcatm.com/tags/tutorial/index.xml" rel="self" type="application/rss+xml"/><item><title>Ship a static site and prove the edge agrees</title><link>https://hk.xbcatm.com/tutorials/ship-a-static-release/</link><pubDate>Tue, 11 Aug 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/tutorials/ship-a-static-release/</guid><description>Build and stage a static artifact, switch a release pointer, and verify sampled public responses with explicit rollback and evidence limits.</description><content:encoded>&lt;p&gt;A release is complete when the required public-edge checks agree with the artifact you chose, not merely when upload exits zero. This tutorial connects &lt;strong&gt;build&lt;/strong&gt;, &lt;strong&gt;check&lt;/strong&gt;, &lt;strong&gt;publish&lt;/strong&gt;, and &lt;strong&gt;verify&lt;/strong&gt;, while distinguishing local checks from deployment changes. Publishing changes the server; only the final probes are read-only network observations.&lt;/p&gt;
&lt;p&gt;The commands assume Bash, GNU find/sort/sha256sum, rsync, Python 3, and the project&amp;rsquo;s pinned Hugo. Run build steps from the repository root. Before touching a live host, rehearse the &lt;a href="https://hk.xbcatm.com/decisions/adr-complete-directory-release/"&gt;directory-switch example&lt;/a&gt; in a disposable directory. Know the actual serve root, who owns deployment, the previous release target, and the rollback procedure. The &lt;code&gt;/srv&lt;/code&gt; layout below is an example requiring an already configured deployment, not something this repository automatically installs.&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/tutorial-release.detail.en.svg" alt="Build and validate one exact artifact, create its manifest after the last build, and verify a new empty stage. Preselection failure stops with the old release unchanged. Selection replaces the pointer. Afterwards, public responses are sampled against artifact files; failure leaves the new release selected until explicit rollback to the recorded previous target and repeat probes. Atomic namespace replacement does not make multiple requests coherent or guarantee power-loss durability. Disk checks, served bytes, TLS identity, expiry, DNS cache changes and independent backup each require their own evidence." width="720" height="4720"&gt;&lt;figcaption&gt;The pointer switch changes the failure boundary. Before it, stop with the old release; after it, roll back explicitly and verify again. Disk checks and edge samples establish different facts.&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="1-build-one-artifact-from-recorded-inputs"&gt;1. Build one artifact from recorded inputs&lt;/h2&gt;
&lt;p&gt;Start from a clean source revision. Run the project build and record the installed generator version:&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;An empty short status supports a clean-tree claim; the command itself does not enforce one. Inspect any changes before calling the build a release of that commit. &lt;code&gt;make build&lt;/code&gt; writes &lt;code&gt;data/buildinfo.toml&lt;/code&gt; with the short commit and current UTC build time, then generates &lt;code&gt;public/&lt;/code&gt;. The rendered marker identifies the recorded inputs; it is not proof that a dirty checkout contained only that commit.&lt;/p&gt;
&lt;p&gt;Pinning Hugo and the vendored theme reduces drift, but the build-time field means ordinary builds at different times need not be byte-identical. Build the chosen release once, validate it, and transfer that exact artifact rather than rebuild independently on the server.&lt;/p&gt;
&lt;h2 id="2-check-the-complete-offline-contract"&gt;2. Check the complete offline contract&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;make check&lt;/code&gt; checks generated visuals, offline-pack freshness, internal links, script-free output, source links, and downloadable checksums for the current artifact. For the full repository gate, use:&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;The full gate also runs regression tests, bilingual-post checks, and review-ledger freshness, then rebuilds and checks output. Therefore create the release manifest only after the last build. Build success and a current review ledger do not establish technical correctness of every article or prove the public server uses the intended headers. Page counts and timings vary; retain actual output rather than comparing to a copied example count.&lt;/p&gt;
&lt;h2 id="3-name-the-artifact-bytes-in-a-manifest"&gt;3. Name the artifact bytes in a manifest&lt;/h2&gt;
&lt;p&gt;Before hashing, verify that &lt;code&gt;public/&lt;/code&gt; contains only directories and regular files, and stop all writers until staging is complete. The commands below do not create a snapshot or check for every special-file type. Keep the manifest outside &lt;code&gt;public/&lt;/code&gt; so it cannot hash itself. NUL-delimited filenames preserve spaces and other shell-sensitive characters:&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 checksum lines contain the digest before the filename. A successful &lt;code&gt;sha256sum -c&lt;/code&gt; verifies listed files, not that no extra files exist. A new empty release directory avoids stale destination files, but does not detect extra source files added after hashing. To establish complete regular-file inventory parity, recompute the staged manifest with the same path/sort/hash rules and compare the full lists before selection. Inspect symlinks separately: &lt;code&gt;find -type f&lt;/code&gt; does not include them, and serving an unintended symlink can expose files outside the intended root. Keep the manifest and its own digest with the release record; a checksum proves consistency against that manifest, not authenticity of an untrusted manifest.&lt;/p&gt;
&lt;h2 id="4-stage-and-select-a-complete-release"&gt;4. Stage and select a complete release&lt;/h2&gt;
&lt;p&gt;Assume Caddy already serves &lt;code&gt;/srv/www/example-site/current&lt;/code&gt;, which is a symlink into &lt;code&gt;releases/&lt;/code&gt;, with correct ownership and read permissions. The &lt;code&gt;releases&lt;/code&gt; parent must already exist. Serialize deployers and choose a unique release ID. Run this block as a standalone Bash script on the deployment machine, from the directory containing the validated &lt;code&gt;public/&lt;/code&gt; and &lt;code&gt;release.sha256&lt;/code&gt;. Check the artifact’s ownership and readable permissions against the deployment policy before running it. It establishes its own manifest path and fail-fast policy; a failed precondition, copy, or checksum must stop before pointer selection. It refuses symlinks inside the staged artifact and is a mutating deployment step.&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;The symlink replacement uses one same-filesystem rename because the pending link and &lt;code&gt;current&lt;/code&gt; share a parent. An ordinary &lt;code&gt;mv new existing-directory&lt;/code&gt; does not exchange non-empty directories. Do not convert an existing real-directory serve root with this snippet; plan that migration separately. Validate the actual server&amp;rsquo;s root resolution and filesystem behavior in staging.&lt;/p&gt;
&lt;p&gt;The old target stays on disk, but that alone does not make an old asset URL reachable after switching. Retain fingerprinted asset bytes in the newly served namespace or a stable asset store. Multiple browser requests can span releases, and caches may keep old HTML. Atomic namespace replacement is not cross-request snapshot isolation or a power-loss durability guarantee.&lt;/p&gt;
&lt;h2 id="5-verify-delivered-bytes-and-policy"&gt;5. Verify delivered bytes and policy&lt;/h2&gt;
&lt;p&gt;After switching, compare a sampled public body with the corresponding artifact file, using a fixed representation selection:&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;Run this block from the same artifact directory used for staging. The header helper is the trusted copy inside that artifact; a source checkout is not required for this step. &lt;code&gt;cmp&lt;/code&gt; succeeding establishes exact parity for this homepage sample. An unexpected redirect, proxy-generated 200 error page, CDN transformation, or encoded response needs investigation. Inspect &lt;code&gt;served.headers&lt;/code&gt;, including &lt;code&gt;Content-Encoding&lt;/code&gt;, cache age, CSP, and content type. The header helper prints responses and final status; it does not enforce all your expectations.&lt;/p&gt;
&lt;p&gt;Repeat for Chinese content, a nested article, CSS, an SVG, a download, redirects, and the error route. Document the URL-to-file mapping, including directory indexes. The shown listed-file check establishes consistency for those files, not a complete staged inventory or what all public cache nodes deliver. A complete edge claim requires broader retrieval and representation-aware checks; a few probes remain samples.&lt;/p&gt;
&lt;p&gt;If a post-switch check fails, the new release is still selected until you act. Use a new temporary symlink targeting the recorded &lt;code&gt;previous&lt;/code&gt; value, atomically replace &lt;code&gt;current&lt;/code&gt; again, and repeat the probes. Do not merely move the new tree away, and do not delete evidence or the previous release while diagnosing.&lt;/p&gt;
&lt;h2 id="6-check-tls-identity-and-the-expiry-window-separately"&gt;6. Check TLS identity and the expiry window separately&lt;/h2&gt;
&lt;p&gt;From the repository root, the expiry helper is:&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;It extracts a certificate, prints dates, and checks the remaining expiry interval. It is not a full TLS identity test: its current implementation does not make hostname/trust verification failure fatal. Follow the &lt;a href="https://hk.xbcatm.com/tutorials/audit-a-public-edge/"&gt;public-edge trust tutorial&lt;/a&gt; for an explicit verified handshake and SAN check. Do not infer trust-chain or revocation success from “valid for at least 30 days.”&lt;/p&gt;
&lt;p&gt;A near-expiry certificate is a separate release concern. Preserve the actual &lt;code&gt;notAfter&lt;/code&gt;, trust result, and client environment rather than copying a historical issuer or date into the release log.&lt;/p&gt;
&lt;h2 id="7-include-the-dns-cache-window-when-dns-changes"&gt;7. Include the DNS cache window when DNS changes&lt;/h2&gt;
&lt;p&gt;A content-only release need not change DNS. If the host or edge changes, preserve service at the old destination while caches expire. Lowering TTL at the moment of the change does not shorten entries already cached with the old TTL. Resolver serve-stale behavior, negative caching, and client caches can complicate the window; TTL is not a universal deadline by which every client has moved. CAA changes affect issuance policy, not routing to the new content.&lt;/p&gt;
&lt;h2 id="8-keep-a-rehearsed-restore-path"&gt;8. Keep a rehearsed restore path&lt;/h2&gt;
&lt;p&gt;The previous directory is useful for fast rollback, but it is not an independent backup. A disk failure, accidental cleanup, or compromised deployment account may affect every local release. Retain source, build inputs, configuration, and an appropriate separate artifact backup; rehearse restoring them in isolation. One successful rehearsal is evidence about that recovery path at that time, not a permanent guarantee for later changes.&lt;/p&gt;
&lt;h2 id="9-record-the-outcome-precisely"&gt;9. Record the outcome precisely&lt;/h2&gt;
&lt;p&gt;Keep the source revision and clean-tree status, build metadata and tool versions, manifest digest, selected and previous targets, probe timestamps, HTTP/TLS observations, and any rollback. State whether verification covered samples, all mapped URLs, or multiple vantage points. If a gate failed, record the failure and recovery rather than “it worked.”&lt;/p&gt;
&lt;p&gt;Before selection, a failure can leave the old release untouched. After selection, recovery is an explicit operation with its own verification. This distinction makes the release path auditable without pretending every failure automatically restores the old tree.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&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 manual&lt;/a&gt;, archive copying and source trailing-slash semantics, checked 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;, DNS serve-stale behavior beyond TTL expiry, checked 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 manual, Debian package&lt;/a&gt;, checksum output and verification flags, checked 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;, atomic replacement and filesystem limits, checked 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;, root and symlink handling, checked 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 manual&lt;/a&gt;, status capture, headers, and content decoding, checked 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;, explicit certificate verification, checked 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: definitions&lt;/a&gt;, artifact identity versus reproducibility, checked 2026-10-15.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Read a live edge the way a client does</title><link>https://hk.xbcatm.com/tutorials/http-edge-walkthrough/</link><pubDate>Fri, 07 Aug 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/tutorials/http-edge-walkthrough/</guid><description>Inspect methods, representation metadata, ranges, and caching with curl, separating protocol requirements from endpoint-specific choices.</description><content:encoded>&lt;p&gt;The HTTP-foundations notes each take one contract and show single pieces of evidence. This tutorial walks the whole path a single client request touches, in order, using only read-only commands. The aim is to distinguish protocol requirements from choices made by a particular server, rather than demand the same status and headers from every endpoint.&lt;/p&gt;
&lt;p&gt;We use &lt;code&gt;www.xbcatm.com&lt;/code&gt; throughout. Replace it with the host you are inspecting. The requests use safe HTTP methods; they can still create access logs or consume rate limits. Inspect only endpoints you are permitted to query. Output blocks below are illustrative, not current production measurements. Run shell snippets in a disposable working directory, keep failures visible, and record time and client version.&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/tutorial-http.detail.en.svg" alt="A numbered HTTP journey separates method choices, metadata, HEAD parity, byte ranges, conditional reuse and encoding. The current successful 200 supplies an actual validator and complete body; matching 304 retains that body. Status and length examples are not universal promises." width="720" height="3750"&gt;&lt;figcaption&gt;Keep the selected representation consistent, and interpret each observed response within its own contract. No live edge was queried for this illustration.&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="1-start-with-what-a-method-may-ask"&gt;1. Start with what a method may ask&lt;/h2&gt;
&lt;p&gt;Before you read a response body, pin down what the server will allow you to ask. A static handler often supports GET and HEAD, while a proxy may also implement OPTIONS. Ask the edge about the target resource:&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;&lt;code&gt;405 Method Not Allowed&lt;/code&gt; is valid when OPTIONS is recognized but not supported for this resource, and a 405 must include &lt;code&gt;Allow&lt;/code&gt;. A successful 200 or 204 OPTIONS response can also be correct. &lt;code&gt;501&lt;/code&gt; means the method is not implemented by the server. An OPTIONS result is not proof that every other method behaves correctly. Do not send POST merely to test rejection on an unfamiliar live service: it is not a safe method, and a 200 alone does not describe its side effects.&lt;/p&gt;
&lt;h2 id="2-read-the-representation-line"&gt;2. Read the representation line&lt;/h2&gt;
&lt;p&gt;The first line tells you the negotiated exchange; the headers then tell you what kind of bytes are coming. Capture them exactly as a client sees them, discarding the body:&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;Three headers deserve a pause. &lt;code&gt;content-type: text/html; charset=utf-8&lt;/code&gt; names the media type, and &lt;code&gt;content-type-and-nosniff&lt;/code&gt; shows why it must describe the actual bytes you send. &lt;code&gt;etag: &amp;quot;dkx84jwth7nkc9e&amp;quot;&lt;/code&gt; hands you a strong validator. &lt;code&gt;x-content-type-options: nosniff&lt;/code&gt; constrains browser MIME handling, notably for script and stylesheet fetches; it does not validate the body as HTML. Each is evidence, not decoration, and together they say: this response advertises metadata to compare with its body and intended policy. A syntactically strong ETag is a server assertion, not independent proof of a content hash.&lt;/p&gt;
&lt;h2 id="3-compare-head-and-get-for-the-same-representation"&gt;3. Compare HEAD and GET for the same representation&lt;/h2&gt;
&lt;p&gt;HEAD has GET semantics without a response body. Servers should return the same header fields but may omit fields whose values are determined only while generating content. &lt;code&gt;Content-Length&lt;/code&gt; is optional; if sent on HEAD, it must equal the size of the content a corresponding GET would send. Compare stable requests with the same encoding and other selection headers:&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;They match: 15890 on both. That is the &lt;code&gt;head-request-parity&lt;/code&gt; contract. An absent HEAD length alone is not a protocol violation. A mismatched value needs investigation, including a deployment between the requests or different selected representations. A pipeline to &lt;code&gt;wc&lt;/code&gt; can hide curl failure in shells without &lt;code&gt;pipefail&lt;/code&gt;; for an automated gate, save the body only after curl succeeds and count that file.&lt;/p&gt;
&lt;h2 id="4-ask-for-precise-bytes-with-a-range"&gt;4. Ask for precise bytes with a range&lt;/h2&gt;
&lt;p&gt;Because the origin knows its representation, it can serve part of it. A range request asks for a byte span and gets only that span:&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;For a representation at least 100 bytes long, a successful single-range response should have &lt;code&gt;206&lt;/code&gt;, a matching &lt;code&gt;Content-Range: bytes 0-99/…&lt;/code&gt;, and 100 content bytes. A server may legally ignore Range and return a full 200; that does not imply it lacks validators. An unsatisfiable range can return 416. A plain Range request does not require an ETag. Resuming a particular representation with &lt;code&gt;If-Range&lt;/code&gt;, or safely combining partial responses, needs the relevant strong-validator rules. Ranges refer to the selected encoded bytes, hence the explicit identity encoding here.&lt;/p&gt;
&lt;h2 id="5-let-the-cache-revalidate-with-a-validator"&gt;5. Let the cache revalidate with a validator&lt;/h2&gt;
&lt;p&gt;Now combine method, representation, and byte selection through caching. A fresh stored response may be reusable without checking a validator. To test revalidation, capture the current response and replay its actual ETag using the same representation selection. Do not copy an old tag from an article:&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;Run this fragment as a standalone shell script. It must finish the initial transfer and require status 200 before storing a usable response; an error representation can also have an ETag. If the selected representation still matches and normal processing succeeds, expect 304 and reuse that complete stored body with updated metadata. A changed representation can correctly return 200. Keep the temporary directory while inspecting the result, then remove only that generated directory when finished. &lt;code&gt;If-None-Match&lt;/code&gt; uses weak comparison, so a 304 need not prove byte identity. Freshness lifetime and current age decide reuse without validation; a validator answers a different question and does not itself set that lifetime.&lt;/p&gt;
&lt;h2 id="6-negotiate-a-smaller-representation-when-you-can"&gt;6. Negotiate a smaller representation when you can&lt;/h2&gt;
&lt;p&gt;The same content can travel in fewer bytes when the client says it can decompress. Offer gzip and watch the body shrink while the meaning stays identical:&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;These example sizes illustrate a possible reduction; the server may choose identity instead. That is the &lt;code&gt;http-compression-negotiation&lt;/code&gt; note&amp;rsquo;s core idea: changing the selected representation changes the bytes on the wire, and the metadata (&lt;code&gt;content-length&lt;/code&gt;, &lt;code&gt;encoding&lt;/code&gt;) changes with it. A client that requests compression and a cache that stores the encoded form must agree on which representation they selected, otherwise the cache can serve a mismatch. Inspect &lt;code&gt;Content-Encoding&lt;/code&gt; and &lt;code&gt;Vary: Accept-Encoding&lt;/code&gt;, not just the byte count. A manually supplied &lt;code&gt;Accept-Encoding&lt;/code&gt; does not make curl automatically decompress; &lt;code&gt;--compressed&lt;/code&gt; requests supported encodings and decodes the response. Compare decoded bodies to establish equal content, and do not assume strong ETags remain identical across encodings.&lt;/p&gt;
&lt;h2 id="7-follow-the-story-when-a-url-changes"&gt;7. Follow the story when a URL changes&lt;/h2&gt;
&lt;p&gt;When identity changes, the response should say where the story continues rather than guess the body:&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; keeps the method and preserves the story, which is the concern of &lt;code&gt;redirects-and-status-codes&lt;/code&gt;. Whether it is a 301/308 for a permanent move, or a 404 handled by the site, the status line is the first sentence of the story and should be read before the body.&lt;/p&gt;
&lt;h2 id="8-let-the-crawler-read-the-map-last"&gt;8. Let the crawler read the map last&lt;/h2&gt;
&lt;p&gt;The edge is also read by machines that do not request HTML at all. Discovery answers two different questions, as &lt;code&gt;robots-and-sitemaps&lt;/code&gt; explains. Confirm discovery points at the real 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;Fetch the sitemap URL too and check canonical host and language URLs. &lt;code&gt;robots.txt&lt;/code&gt; is crawler guidance, not access control; listing a URL in a sitemap does not guarantee crawling or indexing.&lt;/p&gt;
&lt;h2 id="the-whole-path-in-one-glance"&gt;The whole path in one glance&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;th&gt;What to inspect&lt;/th&gt;
&lt;th&gt;Note&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; status + Allow&lt;/td&gt;
&lt;td&gt;observed method policy for this resource&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; + body + &lt;code&gt;nosniff&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;advertised type and MIME-handling policy&lt;/td&gt;
&lt;td&gt;content-type-and-nosniff&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HEAD/GET metadata&lt;/td&gt;
&lt;td&gt;corresponding representation, with allowed omissions&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;observed partial-transfer support&lt;/td&gt;
&lt;td&gt;http-range-requests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;captured &lt;code&gt;ETag&lt;/code&gt; -&amp;gt; 304&lt;/td&gt;
&lt;td&gt;conditional representation match&lt;/td&gt;
&lt;td&gt;conditional-requests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;308 follows method&lt;/td&gt;
&lt;td&gt;redirects preserve the story&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;discovery answers differ&lt;/td&gt;
&lt;td&gt;robots-and-sitemaps&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Each row was a note; now it is a command you can run against any edge. Read the linked note when you need the details, and keep this table as your re-check before you trust an endpoint.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&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 processing and request destinations, checked 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;, methods, validators, and status codes, checked 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;, freshness and revalidation, checked 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;, method-preserving redirect, checked 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 manual&lt;/a&gt;, read-only inspection flags, checked 2026-10-15.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Audit the trust of a public edge</title><link>https://hk.xbcatm.com/tutorials/audit-a-public-edge/</link><pubDate>Thu, 06 Aug 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/tutorials/audit-a-public-edge/</guid><description>A read-only walkthrough that inspects security headers, TLS identity, certificate chain, host keys, CAA, and cookie scope the way a careful operator would.</description><content:encoded>&lt;p&gt;The web-security series treats settings as contracts with scope and failure modes. This walkthrough inspects a host you are authorized to query. The HTTP, TLS, and DNS requests are read-only observations, although they still generate logs and traffic. SSH checks read your own trust record; the deployment host may differ from the website hostname.&lt;/p&gt;
&lt;p&gt;Use a disposable local working directory for captured output. No output shown here is a fresh measurement of &lt;code&gt;www.xbcatm.com&lt;/code&gt;, and a passing command proves only the property it actually checks.&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/tutorial-security.detail.en.svg" alt="Independent security evidence lanes distinguish SNI from verification, expiry from trust, variable presented chains, HSTS host scope, applicable CAA issuance policy, local SSH records, cookie risk controls and body ETag limits. The observations do not certify the whole service secure." width="720" height="4260"&gt;&lt;figcaption&gt;Each observation supports a bounded claim. Header presence, certificate dates and local records cannot combine into a whole-service safety badge.&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="1-start-with-the-policy-the-edge-advertises"&gt;1. Start with the policy the edge advertises&lt;/h2&gt;
&lt;p&gt;Fetch a GET response without silently following redirects, then inspect its status and headers:&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;The repository&amp;rsquo;s intended policy includes the following illustrative fields:&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; is a strict choice for this script-free site, not a universal recipe for every static site. The browser enforces CSP; a report-only header does not block execution. Read the complete policy for more-specific script directives, and compare normal pages and error routes. &lt;code&gt;nosniff&lt;/code&gt; constrains MIME handling, notably script/style destinations, but does not certify the body&amp;rsquo;s media type.&lt;/p&gt;
&lt;p&gt;HSTS is learned from valid HTTPS responses and applies to the host that sent it. A policy received from &lt;code&gt;www.example.com&lt;/code&gt; with &lt;code&gt;includeSubDomains&lt;/code&gt; covers descendants of that name, not its sibling &lt;code&gt;api.example.com&lt;/code&gt; or parent &lt;code&gt;example.com&lt;/code&gt;. Inventory the actual scope before adopting a long duration. A proxy adding or stripping headers changes the browser contract, which is why artifact checks and edge checks are separate.&lt;/p&gt;
&lt;h2 id="2-verify-tls-identity-before-reading-certificate-dates"&gt;2. Verify TLS identity before reading certificate dates&lt;/h2&gt;
&lt;p&gt;SNI selects the intended virtual host; it does not verify the certificate name. Make verification failure fatal and explicitly request hostname validation. Use an up-to-date trust store appropriate to the clients under test.&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;Run these as separate checked steps, or under a shell setup that stops on failure. Hiding stderr or piping into a successful &lt;code&gt;x509&lt;/code&gt; command can conceal an unsuccessful handshake. Inspect the DNS names in &lt;code&gt;subjectAltName&lt;/code&gt;; the displayed subject CN is not sufficient for modern service-identity validation. Some library versions retain legacy CN fallback, so explicitly check that the expected SAN is present. A valid matching wildcard may be legitimate; an unrelated certificate is not.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;notAfter&lt;/code&gt; gives the expiry time. It does not prove the chain is trusted, the hostname matches, or revocation was checked. Record the date and the measured safety window rather than relying on memory. OpenSSL&amp;rsquo;s result describes its configured trust store and checks, not every browser&amp;rsquo;s behavior.&lt;/p&gt;
&lt;h2 id="3-read-the-presented-chain-without-inventing-a-fixed-count"&gt;3. Read the presented chain without inventing a fixed count&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;-showcerts&lt;/code&gt; output contains certificates sent by the server, not a declaration that they form a verified chain. This command displays their count only:&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;There is no required count of four. The server normally supplies the leaf and necessary intermediate certificates; a root usually belongs in the client&amp;rsquo;s trust store and need not be sent. Chain length varies with the CA and chosen path. One certificate warrants investigating whether intermediates are needed, but a count alone is neither success nor failure.&lt;/p&gt;
&lt;p&gt;Use the actual verification result from step 2, then investigate issuer/SAN changes, missing intermediates, expiry, and trust-store differences. An intermediate rotation can be normal CA maintenance. A numerically unchanged chain can still be wrong, and the count command does not automatically compare anything with an expected value.&lt;/p&gt;
&lt;h2 id="4-revalidate-the-captured-representation"&gt;4. Revalidate the captured representation&lt;/h2&gt;
&lt;p&gt;Caching is a separate contract, not evidence of TLS identity. Capture a current ETag and use it on a request selecting the same representation, as in the &lt;a href="https://hk.xbcatm.com/tutorials/http-edge-walkthrough/"&gt;HTTP walkthrough&lt;/a&gt;. A matching conditional GET can return 304; a deployment between requests can legitimately produce 200. An old hardcoded validator is not a reusable test fixture for a changing site.&lt;/p&gt;
&lt;p&gt;A no-script policy does not mean CSP never changes. Security headers can change independently of HTML, and a body ETag does not fingerprint every response header. Record the actual policy again after release.&lt;/p&gt;
&lt;h2 id="5-find-the-relevant-caa-policy"&gt;5. Find the relevant CAA policy&lt;/h2&gt;
&lt;p&gt;CAA constrains issuance by compliant certificate authorities. It is not certificate pinning and does not retroactively invalidate an already issued certificate. Start by inspecting the queried name and its ancestors; retain response status so a DNS failure is not mistaken for an empty answer.&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;An empty answer at &lt;code&gt;www&lt;/code&gt; does not mean issuance is unrestricted. RFC 8659 searches upward until the first relevant nonempty CAA set, following aliases during each lookup; continue through applicable ancestors if necessary. Inspect &lt;code&gt;issue&lt;/code&gt;, and &lt;code&gt;issuewild&lt;/code&gt; when wildcard issuance is involved. An &lt;code&gt;iodef&lt;/code&gt; report address alone does not restrict issuance. If no restricting policy applies, CAA adds no issuer restriction, but normal domain validation and CA rules still apply.&lt;/p&gt;
&lt;p&gt;Compare the CA&amp;rsquo;s documented CAA identifier with the relevant policy, not merely the leaf&amp;rsquo;s human-readable issuer CN. Today&amp;rsquo;s CAA result is evidence about today&amp;rsquo;s DNS policy, not proof of what existed when the certificate was issued.&lt;/p&gt;
&lt;h2 id="6-check-the-deployment-hosts-recorded-ssh-identity"&gt;6. Check the deployment host&amp;rsquo;s recorded SSH identity&lt;/h2&gt;
&lt;p&gt;Use the actual SSH destination, including the bracketed host/port form for a non-default port. &lt;code&gt;ssh-keygen -F&lt;/code&gt; can find hashed known-host entries that a text grep of the hostname misses.&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;Compare the fingerprint with an independently trusted record, such as the provider console or a verified administrator channel. This reads the local record; it does not prove which key the remote host presents now. A network key scan alone would not authenticate that key either. Investigate a changed key before updating trust; a rebuild is one possible explanation, not automatic permission to accept it.&lt;/p&gt;
&lt;h2 id="7-keep-cookie-scope-no-broader-than-necessary"&gt;7. Keep cookie scope no broader than necessary&lt;/h2&gt;
&lt;p&gt;Inspect all &lt;code&gt;Set-Cookie&lt;/code&gt; fields in the captured response and any relevant redirect or application route. No cookie on one page does not establish that the entire site never sets cookies. A host-only cookie omits &lt;code&gt;Domain&lt;/code&gt;; adding a parent Domain broadens the hosts receiving it. &lt;code&gt;Path&lt;/code&gt; controls delivery but is not a security boundary against same-origin code.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Secure&lt;/code&gt; restricts transport, &lt;code&gt;HttpOnly&lt;/code&gt; blocks script access to the cookie, and &lt;code&gt;SameSite&lt;/code&gt; controls specified cross-site sending behavior. They address different risks and do not replace one another; &lt;code&gt;SameSite=None&lt;/code&gt; requires &lt;code&gt;Secure&lt;/code&gt; in modern browsers. For this site&amp;rsquo;s no-account content model, ask why a cookie is needed at all before broadening its scope. Redact cookie values from shared audit records.&lt;/p&gt;
&lt;h2 id="keep-a-bounded-audit-record"&gt;Keep a bounded audit record&lt;/h2&gt;
&lt;p&gt;Record the UTC time, client versions, requested/final URL, status, enforcing headers, TLS verification result and dates, relevant CAA lookup, and SSH record source. Distinguish observed success, unexpected results, and checks not performed. These several observations take more than “two commands” and cannot prove the whole service secure.&lt;/p&gt;
&lt;p&gt;Repeat the relevant checks after certificate, DNS, proxy, or deployment changes. Preserve prior results to identify drift, and test actual browser behavior separately when the claim depends on browser enforcement.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&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 processing and request destinations, checked 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, hostname verification, fatal verification errors, and presented chains, checked 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-based service identity and wildcard matching, checked 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;, relevant record-set search and issuance policy, checked 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;, host scope and policy processing, checked 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;, known-host lookup and fingerprint display, checked 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;, enforcing versus report-only policy and script directive fallback (working draft), checked 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 attributes and their limits, checked 2026-10-15.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item></channel></rss>