<?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>Tutorials · XB Field Notes</title><link>https://www.xbcatm.com/tutorials/</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://www.xbcatm.com/tutorials/index.xml" rel="self" type="application/rss+xml"/><item><title>Ship a static site and prove the edge agrees</title><link>https://www.xbcatm.com/tutorials/ship-a-static-release/</link><pubDate>Tue, 11 Aug 2026 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/tutorials/ship-a-static-release/</guid><description>A complete release walkthrough from build to public-edge confirmation, with the checks that turn a staged tree into a published, verified artifact.</description><content:encoded>&lt;p&gt;A release is complete when the public edge agrees with the files you chose — not when the final upload command exits zero. This tutorial turns that sentence into a repeatable procedure using the checks the release-and-recovery notes describe. Everything before the last phase is safe to run locally; the only remote step is the read-only probe at the end.&lt;/p&gt;
&lt;p&gt;The workflow has four phases that each produce evidence the next phase consumes: &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;.&lt;/p&gt;
&lt;p&gt;Before you begin, make three facts explicit so a failure is cheap: how to run the build (&lt;code&gt;make build&lt;/code&gt;), which single directory the live site serves, and how to rename that directory back to the previous tree. Writing those three answers down first turns the whole procedure from a hope into a script with a visible rollback. The rest of the tutorial assumes you can answer them.&lt;/p&gt;
&lt;h2 id="1-build-a-single-reproducible-artifact"&gt;1. Build a single, reproducible artifact&lt;/h2&gt;
&lt;p&gt;Start from a clean control tree, not from whatever is currently on the server. The &lt;code&gt;small-systems&lt;/code&gt; note argues for boundaries you can name; a static build needs only a handful. Run the project target:&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;$ make build
&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; Pages │ 130 │ 128
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Static files │ 42 │ 42
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Total in 171 ms
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The build writes a fingerprint into &lt;code&gt;data/buildinfo.toml&lt;/code&gt; — the source commit and build time — which the home page renders. Keeping that fingerprint inside the artifact lets a later reader prove which source tree produced the bytes, so an audit does not have to guess.&lt;/p&gt;
&lt;h2 id="2-verify-against-the-offline-artifact-contract"&gt;2. Verify against the offline artifact contract&lt;/h2&gt;
&lt;p&gt;Before touching the live host, run the checks that need no network. These confirm the artifact is internally consistent on its own:&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;$ make check
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;validated 257 HTML documents; internal targets and script-free contract are intact
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The verifier validates every generated SVG, walks every internal link and fragment, enforces the no-JavaScript contract, and verifies the downloadable checksums. It fails on a broken artifact rather than on a slow network — the property &lt;code&gt;reliable-defaults&lt;/code&gt; calls making the main path boring.&lt;/p&gt;
&lt;h2 id="3-name-every-byte-in-a-manifest"&gt;3. Name every byte in a manifest&lt;/h2&gt;
&lt;p&gt;A release claim must be able to say exactly which bytes changed and then prove they are the ones served. Hash the artifact into a sorted manifest:&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;cd&lt;/span&gt; public &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; find . -type f &lt;span class="p"&gt;|&lt;/span&gt; sort &lt;span class="p"&gt;|&lt;/span&gt; sed &lt;span class="s1"&gt;&amp;#39;s#^./##&amp;#39;&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; xargs sha256sum &amp;gt; /tmp/release.sha256
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ grep robots.txt /tmp/release.sha256
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;robots.txt f6ad425cbc898cef00d4b62bef6b17dc627de9ee0c910b31dcc1ee4d054f3d24
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;release-manifest&lt;/code&gt; notes name every published byte so the &amp;ldquo;edge agrees&amp;rdquo; claim can be checked byte-for-byte. Keep this manifest for the whole release window; you will compare it against the served tree after the swap.&lt;/p&gt;
&lt;h2 id="4-publish-by-switching-complete-directories"&gt;4. Publish by switching complete directories&lt;/h2&gt;
&lt;p&gt;Do not overwrite a live tree file-by-file. Stage a full tree beside the existing one, then switch the single directory name Caddy serves — the &lt;code&gt;atomic-switch&lt;/code&gt; note calls this complete-directory publication:&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;$ rsync -a public/ /srv/www/example-site.new/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ mv /srv/www/example-site.new /srv/www/example-site
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Because the serve root is one directory name, the swap is instant and nothing is ever half-written. The old tree is still there, so rollback is a single rename in the other direction (&lt;code&gt;reliable-defaults&lt;/code&gt;).&lt;/p&gt;
&lt;h2 id="5-confirm-the-public-edge-agrees-with-what-you-staged"&gt;5. Confirm the public edge agrees with what you staged&lt;/h2&gt;
&lt;p&gt;Before trusting the swap, prove the served status and headers match the artifact. The &lt;code&gt;read-only-release-smoke-tests&lt;/code&gt; and &lt;code&gt;read-public-edge&lt;/code&gt; notes both make this the final gate — run the helper and read its output:&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;$ ./downloads/check-http-headers.sh 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;content-type: text/html; charset=utf-8
&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A &lt;code&gt;200&lt;/code&gt; with the correct CSP and content type is the first agreement. The second agreement is bytes: hash the served tree and compare to the manifest from step 3. When they match, the edge is the tree you staged. If the served HTML differs, stop — you are looking at the wrong tree.&lt;/p&gt;
&lt;h2 id="6-confirm-the-certificate-window-before-you-finish"&gt;6. Confirm the certificate window before you finish&lt;/h2&gt;
&lt;p&gt;The release is not done until the certificate is healthy too. &lt;code&gt;check-tls-expiry&lt;/code&gt; reads the validity window without breaking the TLS session:&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;$ ./downloads/check-tls-expiry.sh www.xbcatm.com 30
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;subject=CN=www.xbcatm.com
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;issuer=C=US, O=Let&amp;#39;s Encrypt, CN=YE1
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;notBefore=Aug 21 08:37:13 2026 GMT
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;notAfter=Nov 19 08:37:12 2026 GMT
&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;certificate remains valid for at least 30 day(s)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A near-expiry certificate turns a &amp;ldquo;successful&amp;rdquo; release into an imminent incident. The &lt;code&gt;certificate-expiry&lt;/code&gt; note and the release checklist both make the TLS window part of the release condition rather than a later surprise. Read &lt;code&gt;notAfter&lt;/code&gt; as a concrete date, not a dashboard color.&lt;/p&gt;
&lt;h2 id="7-mind-the-dns-cache-window"&gt;7. Mind the DNS cache window&lt;/h2&gt;
&lt;p&gt;If your release changes DNS records (a host change, a CAA update, or a new edge), remember the change propagates on a schedule you do not own. The &lt;code&gt;dns-change-window&lt;/code&gt; note shows why a rollback must include the cache-aware window, not just the record. A release that looks complete locally can still be mid-propagation at a remote resolver for as long as the old TTL.&lt;/p&gt;
&lt;h2 id="8-back-up-a-restore-path-not-just-the-bytes"&gt;8. Back up a restore path, not just the bytes&lt;/h2&gt;
&lt;p&gt;Publishing is the other side of recovery. Before a release, verify there is a restore rehearsal — the &lt;code&gt;backup-rehearsal&lt;/code&gt; note stresses that a backup is only useful after you have restored from it once. This release tutorial assumes the period &lt;code&gt;restore&lt;/code&gt; step already works, because the recovery half of the series is what makes publishing reversible instead of risky.&lt;/p&gt;
&lt;h2 id="9-record-the-outcome-a-late-operator-can-audit"&gt;9. Record the outcome a late operator can audit&lt;/h2&gt;
&lt;p&gt;When the swap is confirmed and the windows are checked, leave a factual record a future operator can trust: the commit, the manifest hash, and the two probe outputs. A &lt;code&gt;postmortem&lt;/code&gt; entry is more useful when it names facts — which bytes, which status, which dates — than when it asserts &amp;ldquo;it worked&amp;rdquo;. Put the same facts in the changelog so the site&amp;rsquo;s own evolution stays auditable.&lt;/p&gt;
&lt;h2 id="the-path-maps-to-the-series"&gt;The path maps to the series&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Phase&lt;/th&gt;
&lt;th&gt;Evidence&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Build&lt;/td&gt;
&lt;td&gt;buildinfo fingerprint&lt;/td&gt;
&lt;td&gt;small-systems, atomic-switch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Check&lt;/td&gt;
&lt;td&gt;offline verify passes&lt;/td&gt;
&lt;td&gt;reliable-defaults, release-checklist&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manifest&lt;/td&gt;
&lt;td&gt;SHA-256 of every file&lt;/td&gt;
&lt;td&gt;release-manifest, checksums&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Publish&lt;/td&gt;
&lt;td&gt;complete-directory swap&lt;/td&gt;
&lt;td&gt;atomic-switch, caddy-static-files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Verify&lt;/td&gt;
&lt;td&gt;HEAD parity + manifest hash&lt;/td&gt;
&lt;td&gt;read-only-smoke-tests, public-edge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cert&lt;/td&gt;
&lt;td&gt;validity window as date&lt;/td&gt;
&lt;td&gt;certificate-expiry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Record&lt;/td&gt;
&lt;td&gt;commit + probe output&lt;/td&gt;
&lt;td&gt;postmortem, changelog&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Each stage consumes the previous stage&amp;rsquo;s evidence. If any stage fails, you stop and the old tree remains the served one — the property that makes the whole procedure safe to practice.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/software/coreutils/manual/html_node/sha2-utilities.html"&gt;GNU Coreutils: sha2 utilities&lt;/a&gt;, per-file checksums and manifests, checked 2026-08-11.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pubs.opengroup.org/onlinepubs/9799919799/functions/rename.html"&gt;POSIX &lt;code&gt;rename()&lt;/code&gt;&lt;/a&gt;, complete-directory publication, checked 2026-08-11.&lt;/li&gt;
&lt;li&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 selection and swap, checked 2026-08-11.&lt;/li&gt;
&lt;li&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;, certificate dates, checked 2026-08-11.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Read a live edge the way a client does</title><link>https://www.xbcatm.com/tutorials/http-edge-walkthrough/</link><pubDate>Fri, 07 Aug 2026 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/tutorials/http-edge-walkthrough/</guid><description>A full tour of the HTTP contracts a static site promises, with the same read-only curl and openssl commands you can run against any endpoint.</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. By the end you will have reproduced, on any live static edge, the checks those notes describe — without changing its state.&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. Every command below is read-only: it reads headers, requests metadata, or asks for a specific range; none of them writes to the origin.&lt;/p&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 origin has no transactions, so most mutating methods are rejected explicitly. Ask the edge what it allows:&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 -s -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;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 the honest answer. The &lt;code&gt;http-method-semantics&lt;/code&gt; note explains why this is the contract a static origin should keep: every method is either supported with documented semantics or rejected with a status that says so. The dangerous failure mode is a proxy or imported handler that converts a rejected method into a silent 200. If your edge ever answers 200 to &lt;code&gt;POST&lt;/code&gt;, the promise is no longer visible.&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 -s -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; bids a browser not to guess the type. Each is evidence, not decoration, and together they say: this edge tells you exactly its policy before you trust the body.&lt;/p&gt;
&lt;h2 id="3-confirm-head-equals-get-but-carries-no-body"&gt;3. Confirm HEAD equals GET but carries no body&lt;/h2&gt;
&lt;p&gt;HEAD must be safe and return the same metadata as GET, without a body. Compare the HEAD &lt;code&gt;Content-Length&lt;/code&gt; (the bytes GET would return) with what you actually READ:&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 -sI 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 -s 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. If a proxy strips or rewrites the HEAD &lt;code&gt;Content-Length&lt;/code&gt;, a pair of checks like this catches it. This is exactly why &lt;code&gt;check-http-headers.sh&lt;/code&gt; reads status and headers together with curl and why the release smoke tests compare HEAD before and after a swap.&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 -s -o /dev/null -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 206 bytes=100
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;206&lt;/code&gt; with 100 bytes for a 100-byte range is the honest result. The &lt;code&gt;http-range-requests&lt;/code&gt; note explains how the server selects which bytes and why a strong validator is required. If a server answers &lt;code&gt;200&lt;/code&gt; with the full body to any range instead, that is a different, more expensive contract — and it tells you the origin does not track the byte identity you asked for.&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 stored copy is current only if the validator still matches. Replay your ETag conditionally:&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 -s -o /dev/null -w &amp;#39;HTTP %{http_code}\n&amp;#39; -H &amp;#39;If-None-Match: &amp;#34;dkx84jwth7nkc9e&amp;#34;&amp;#39; https://www.xbcatm.com/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;HTTP 304
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;304 Not Modified&lt;/code&gt; is a small header-only reply that tells you the copy you asked about is still the answer. That is the &lt;code&gt;conditional-requests&lt;/code&gt; contract: validation decides whether a stored copy is fresh, while &lt;code&gt;http-cache-headers&lt;/code&gt; describes two clocks — freshness from the cache headers, and correctness from the validator. A &lt;code&gt;304&lt;/code&gt; is never an error; it is the cheap way to say &amp;ldquo;you already have the right bytes&amp;rdquo;. When the ETag changes, the next revalidation returns a fresh &lt;code&gt;200&lt;/code&gt;.&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 -s -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 -s -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;The plain GET returned 15890 bytes; the gzip negotiation returned 4368 for the same content. 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 serves a mismatch.&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 -s -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 -s 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;A host that answers its own hosts across robots, sitemap, and the HTTP headers is one whose operator has read the public edge before publishing it.&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 it proves&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; -&amp;gt; 405&lt;/td&gt;
&lt;td&gt;origin rejects unsupported methods&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;header names the real bytes&lt;/td&gt;
&lt;td&gt;content-type-and-nosniff&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HEAD == GET Content-Length&lt;/td&gt;
&lt;td&gt;safe, head parity&lt;/td&gt;
&lt;td&gt;head-parity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Range 0-99 -&amp;gt; 206/100&lt;/td&gt;
&lt;td&gt;exact byte selection&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;validator decides freshness&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;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-08-07.&lt;/li&gt;
&lt;li&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-08-07.&lt;/li&gt;
&lt;li&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-08-07.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://curl.se/docs/manpage.html"&gt;curl manual&lt;/a&gt;, read-only inspection flags, checked 2026-08-07.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Audit the trust of a public edge</title><link>https://www.xbcatm.com/tutorials/audit-a-public-edge/</link><pubDate>Thu, 06 Aug 2026 00:00:00 +0000</pubDate><guid>https://www.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 security settings as contracts with scope and failure modes. This tutorial runs those contracts end to end, read-only, against a live host, and tells you what each answer means. It does not change the server; it inspects what the server will tell anyone. Use &lt;code&gt;www.xbcatm.com&lt;/code&gt; below and substitute your own host throughout.&lt;/p&gt;
&lt;h2 id="1-start-with-the-headers-the-edge-advertises"&gt;1. Start with the headers the edge advertises&lt;/h2&gt;
&lt;p&gt;The first contract is what a browser is told to allow. Fetch the headers with &lt;code&gt;check-http-headers.sh&lt;/code&gt; (or any read-only fetch) and read the actual policy:&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 -s -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;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;Each header is a claim about a specific failure mode. &lt;code&gt;content-security-policy&lt;/code&gt; with &lt;code&gt;script-src 'none'&lt;/code&gt; is the strongest for a static site, and &lt;code&gt;security-headers&lt;/code&gt; explains why the policy should match the actual page. &lt;code&gt;strict-transport-security&lt;/code&gt; with &lt;code&gt;includeSubDomains&lt;/code&gt; is the transport promise, and &lt;code&gt;hsts-rollout&lt;/code&gt; warns it must be rolled out in measured steps because it is sticky. &lt;code&gt;nosniff&lt;/code&gt; stops MIME-type guessing. A header you intend but did not verify is a bug, not a feature.&lt;/p&gt;
&lt;p&gt;Step 1 is static, but it is the contract a browser sees first. For a static site, &lt;code&gt;script-src 'none'&lt;/code&gt; is both safe and honest: there is nothing to execute, so nothing runs. The important habit is to compare what you &lt;em&gt;believe&lt;/em&gt; the policy is with what the edge actually sends. A proxy that strips or adds a header silently changes the contract; the read-only fetch catches that drift. If you use a tool like &lt;code&gt;check-http-headers.sh&lt;/code&gt;, it reads the status and headers together, which keeps the policy and the reachability claim in the same record.&lt;/p&gt;
&lt;h2 id="2-confirm-the-identity-is-real-not-a-guess"&gt;2. Confirm the identity is real, not a guess&lt;/h2&gt;
&lt;p&gt;The TLS identity starts with the leaf certificate. Read its exact subject, issuer, and validity dates:&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;$ echo | openssl s_client -connect www.xbcatm.com:443 -servername www.xbcatm.com 2&amp;gt;/dev/null \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | openssl x509 -noout -subject -issuer -dates
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;subject=CN=www.xbcatm.com
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;issuer=C=US, O=Let&amp;#39;s Encrypt, CN=YE1
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;notBefore=Aug 21 08:37:13 2026 GMT
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;notAfter=Nov 19 08:37:12 2026 GMT
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The subject is the exact hostname, not a wildcard from an adjacent certificate, and &lt;code&gt;notAfter&lt;/code&gt; is a concrete date. &lt;code&gt;certificate-expiry&lt;/code&gt; treats that date as the release gate rather than a dashboard color. An operator should be able to quote &lt;code&gt;notAfter&lt;/code&gt; from memory of the last audit, not discover it at renewal time.&lt;/p&gt;
&lt;h2 id="3-read-the-chain-not-just-the-leaf"&gt;3. Read the chain, not just the leaf&lt;/h2&gt;
&lt;p&gt;A leaf only has value inside a chain the client can walk to a trusted root. Ask the edge what chain it actually presents:&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;$ echo | openssl s_client -connect www.xbcatm.com:443 -servername www.xbcatm.com -showcerts 2&amp;gt;/dev/null | grep &amp;#39;BEGIN CERT&amp;#39; | wc -l
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;4
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;With its leaf plus intermediates, that number is expected to be &lt;code&gt;4&lt;/code&gt;. The &lt;code&gt;certificate-chain&lt;/code&gt; note explains why: the leaf must be signed by an intermediate you actually authorized, and the edge must send that intermediate so deep-validation clients succeed. Count of exactly one is a red flag on a public CA. A second reason to check the chain is that the identity you read in step 2 only proves the leaf; it does not prove the client can reach a root it trusts. The chain is what closes that gap. If the edge ever starts sending a different intermediate — say, a test CA one day — the count and the &lt;code&gt;issuer&lt;/code&gt; of certificate 1 change before any element on the page does. Reading the chain is the cheapest way to notice that trust moved before it matters.&lt;/p&gt;
&lt;h2 id="4-trade-a-validator-to-prove-freshness-is-intact"&gt;4. Trade a validator to prove freshness is intact&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;conditional-requests&lt;/code&gt; note applies here too: a live edge should answer a &lt;code&gt;304&lt;/code&gt; to a revalidation so caches and clients do not re-download. This is also evidence that caching is working as the headers promise:&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 -s -o /dev/null -w &amp;#39;HTTP %{http_code}\n&amp;#39; -H &amp;#39;If-None-Match: &amp;#34;dkx84jwth7nkc9e&amp;#34;&amp;#39; https://www.xbcatm.com/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;HTTP 304
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Because this site sticks to a script-free policy, the CSP never changes between releases; only the &lt;code&gt;etag&lt;/code&gt; moves when bytes change.&lt;/p&gt;
&lt;h2 id="5-verify-which-ca-is-allowed-to-issue"&gt;5. Verify which CA is allowed to issue&lt;/h2&gt;
&lt;p&gt;TLS identity goes hand in hand with DNS policy. Confirm the CAA record authorizes the issuer you actually use, so only that CA can legally sign for the domain:&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;$ dig +short CAA www.xbcatm.com
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;dns-caa-records&lt;/code&gt; explains CAA as supplier of trust: it authorizes the issuer, making the certificate-chain audit meaningful. Without a CAA record, any CA can issue; with one, the intermediate from &lt;code&gt;step 4&lt;/code&gt; &lt;em&gt;should&lt;/em&gt; trace back to an authorized issuer.&lt;/p&gt;
&lt;h2 id="6-check-who-holds-the-host-key"&gt;6. Check who holds the host key&lt;/h2&gt;
&lt;p&gt;The chain protects TLS, but another trust boundary is SSH access. A host key is the deployment record for the server you SSH into. &lt;code&gt;ssh-host-key&lt;/code&gt; treats it as part of the record you pin before you trust a connection:&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;$ ssh-keygen -lf ~/.ssh/known_hosts | grep www.xbcatm.com
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Confirm the fingerprint matches what you recorded when the host was added. A changed host key after a rebuild is either a legitimate reinstall or a reason to stop — this is exactly why host-key policy exists as a web-security boundary, not just a convenience.&lt;/p&gt;
&lt;h2 id="7-keep-cookie-scope-narrower-than-the-app"&gt;7. Keep cookie scope narrower than the app&lt;/h2&gt;
&lt;p&gt;Even a no-script static site can set cookies; if it does, their scope is a security claim. &lt;code&gt;cookie-scope&lt;/code&gt; says a cookie&amp;rsquo;s host, path, transport, and same-site boundaries should be narrower than the application&amp;rsquo;s reach. Audit that no cookie is set for a broader scope than needed, and prefer &lt;code&gt;Secure; HttpOnly; SameSite&lt;/code&gt; for anything sensitive.&lt;/p&gt;
&lt;h2 id="the-audit-as-a-checklist"&gt;The audit as a checklist&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;Read it as&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CSP &lt;code&gt;script-src 'none'&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no-script policy is live&lt;/td&gt;
&lt;td&gt;security-headers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HSTS &lt;code&gt;includeSubDomains&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;transport promise is sticky&lt;/td&gt;
&lt;td&gt;hsts-rollout&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;certificate subject+dates&lt;/td&gt;
&lt;td&gt;identity is a concrete date&lt;/td&gt;
&lt;td&gt;certificate-expiry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;chain shows &lt;code&gt;4&lt;/code&gt; certs&lt;/td&gt;
&lt;td&gt;intermediates are sent&lt;/td&gt;
&lt;td&gt;certificate-chain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;304&lt;/code&gt; on revalidation&lt;/td&gt;
&lt;td&gt;cache validator works&lt;/td&gt;
&lt;td&gt;conditional&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CAA authorized&lt;/td&gt;
&lt;td&gt;only CA can issue&lt;/td&gt;
&lt;td&gt;dns-caa&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;host key footprint matches&lt;/td&gt;
&lt;td&gt;SSH trust is a record&lt;/td&gt;
&lt;td&gt;ssh-host-key&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Each signal was a note; together they are a single checklist. You add all these in under a minute with two read-only commands, leaving the server untouched.&lt;/p&gt;
&lt;h2 id="typical-procedure-notes"&gt;Typical-procedure notes&lt;/h2&gt;
&lt;p&gt;This audit belongs in a scheduled check, not only at release. If you already have a set of checks, add the two &lt;code&gt;openssl&lt;/code&gt; lines and the header fetch to them. Keep the outputs in a file named by date so a later comparison can show drift. That is how a static site stays evidence-first even when nothing is changing.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc9110.html"&gt;RFC 9110 — HTTP Semantics&lt;/a&gt;, validators and conditional requests, checked 2026-08-06.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc6797.html"&gt;RFC 6797 — HSTS&lt;/a&gt;, transport policy, checked 2026-08-06.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc5280.html"&gt;RFC 5280 — X.509 PKI&lt;/a&gt;, certificate chains, checked 2026-08-06.&lt;/li&gt;
&lt;li&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;, chain and identity inspection, checked 2026-08-06.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://linux.die.net/man/1/dig"&gt;dig(1)&lt;/a&gt;, DNS record lookup, checked 2026-08-06.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item></channel></rss>