<?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>Notes · XB Field Notes</title><link>https://www.xbcatm.com/notes/</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/notes/index.xml" rel="self" type="application/rss+xml"/><item><title>A maintenance timer should leave its last result visible</title><link>https://www.xbcatm.com/notes/a-maintenance-timer-should-leave-its-last-result-visible/</link><pubDate>Tue, 04 Aug 2026 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/notes/a-maintenance-timer-should-leave-its-last-result-visible/</guid><description>Pair a oneshot service with a timer, bounded runtime, and journal evidence.</description><content:encoded>&lt;p&gt;Cron-like syntax is not the whole contract for a maintenance task. The command needs a clear service identity, a bounded runtime, and evidence that distinguishes “not scheduled” from “ran and failed”. A systemd timer and oneshot service keep those concerns separate.&lt;/p&gt;
&lt;h2 id="define-the-work-as-a-service"&gt;Define the work as a service&lt;/h2&gt;
&lt;p&gt;Use an explicit user and absolute command path:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# /etc/systemd/system/site-check.service&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;[Unit]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Read-only public site check&lt;/span&gt;
&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;&lt;span class="k"&gt;[Service]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;oneshot&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;User&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;sitecheck&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;ExecStart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/local/bin/check-public-site&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;TimeoutStartSec&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;2min&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;TimeoutStartSec&lt;/code&gt; prevents a network or subprocess wait from occupying the schedule indefinitely. The helper should return non-zero when its contract fails and must not modify the service it checks.&lt;/p&gt;
&lt;h2 id="schedule-the-service"&gt;Schedule the service&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# /etc/systemd/system/site-check.timer&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;[Unit]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Run the public site check every hour&lt;/span&gt;
&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;&lt;span class="k"&gt;[Timer]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;OnCalendar&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;hourly&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Persistent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;RandomizedDelaySec&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;5min&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Unit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;site-check.service&lt;/span&gt;
&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;&lt;span class="k"&gt;[Install]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;WantedBy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;timers.target&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;Persistent=true&lt;/code&gt; can run a missed calendar event after the machine returns. &lt;code&gt;RandomizedDelaySec&lt;/code&gt; spreads routine work across a window; it is not appropriate when the task must run at an exact instant.&lt;/p&gt;
&lt;h2 id="validate-before-enabling"&gt;Validate before enabling&lt;/h2&gt;
&lt;p&gt;Inspect the calendar expression, then start the service manually:&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;systemd-analyze calendar hourly
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;systemctl start site-check.service
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;systemctl status site-check.service
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;journalctl -u site-check.service -n &lt;span class="m"&gt;50&lt;/span&gt; --no-pager
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Enable the timer only after the service succeeds with the intended user, environment, paths, and network access:&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;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; --now site-check.timer
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;systemctl list-timers site-check.timer
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="alert-on-the-service-result"&gt;Alert on the service result&lt;/h2&gt;
&lt;p&gt;The timer firing is not success. Monitor the oneshot service&amp;rsquo;s exit status or journal result. Keep stdout concise and send detailed artifacts to a bounded path if they are needed for debugging.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man5/systemd.timer.5.html"&gt;systemd.timer(5)&lt;/a&gt;, calendar timers, persistence, and delay behavior, checked 2026-08-04.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man7/systemd.time.7.html"&gt;systemd.time(7)&lt;/a&gt;, calendar expression syntax, checked 2026-08-04.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man5/systemd.service.5.html"&gt;systemd.service&lt;/a&gt;, oneshot service semantics, checked 2026-08-04.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Restart policies need a rate limit</title><link>https://www.xbcatm.com/notes/restart-policies-need-a-rate-limit/</link><pubDate>Mon, 03 Aug 2026 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/notes/restart-policies-need-a-rate-limit/</guid><description>Let systemd recover transient failures without hiding a permanent configuration error.</description><content:encoded>&lt;p&gt;Automatic restart is useful for a long-running service, but an unbounded loop can turn a clear failure into a noisy one. The unit should describe both the normal process lifecycle and the amount of recovery that is acceptable.&lt;/p&gt;
&lt;h2 id="track-setup-failures"&gt;Track setup failures&lt;/h2&gt;
&lt;p&gt;For a long-running process, &lt;code&gt;Type=exec&lt;/code&gt; makes failures to execute the configured program visible during start-up rather than treating a fork as success. Pair it with an explicit &lt;code&gt;ExecStart&lt;/code&gt; and the least-privileged &lt;code&gt;User&lt;/code&gt; that can perform the work.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;[Service]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;exec&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;User&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;site&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;ExecStart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/srv/site/bin/server&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Restart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;on-failure&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;RestartSec&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;5s&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;Restart=on-failure&lt;/code&gt; is a recovery policy, not a health check. If the process stays alive while serving bad responses, systemd will not know that the site is broken; an external smoke test still has a job.&lt;/p&gt;
&lt;h2 id="bound-the-loop"&gt;Bound the loop&lt;/h2&gt;
&lt;p&gt;Systemd applies start-rate limiting through &lt;code&gt;StartLimitIntervalSec=&lt;/code&gt; and &lt;code&gt;StartLimitBurst=&lt;/code&gt;. Choose values that allow a short transient outage but stop a crash loop from consuming the host&amp;rsquo;s resources. When the limit is hit, the journal should contain the evidence needed to distinguish “restarted successfully” from “gave up after repeated failure”.&lt;/p&gt;
&lt;p&gt;After changing a unit, verify the loaded configuration and inspect recent logs:&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;systemctl daemon-reload
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;systemctl restart example.service
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;systemctl status --no-pager example.service
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;journalctl -u example.service -n &lt;span class="m"&gt;80&lt;/span&gt; --no-pager
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Do not use a restart policy to conceal missing files, invalid permissions, or a bad environment. Fix the first failure, then test the recovery path.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man5/systemd.service.5.html"&gt;systemd.service&lt;/a&gt;, service type, restart, and start-rate limiter, checked 2026-08-03.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man5/systemd.unit.5.html"&gt;systemd.unit&lt;/a&gt;, common unit sections and drop-in semantics, checked 2026-08-03.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>A release checklist for a static site</title><link>https://www.xbcatm.com/notes/a-release-checklist-for-a-static-site/</link><pubDate>Thu, 30 Jul 2026 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/notes/a-release-checklist-for-a-static-site/</guid><description>A compact pre-publish check for generated HTML, links, assets, and response headers.</description><content:encoded>&lt;p&gt;A static site is simple to publish, but simplicity does not remove the need for a release check. The useful checks are small enough to run on every release and specific enough to fail for a real reason.&lt;/p&gt;
&lt;h2 id="build-a-clean-artifact"&gt;Build a clean artifact&lt;/h2&gt;
&lt;p&gt;Use a pinned generator version and build into a disposable output directory:&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;hugo version
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;hugo --gc --minify --destination public
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The exact command depends on the project, but the important property is that the output is reproducible from the tracked source. Do not edit generated HTML by hand after the build.&lt;/p&gt;
&lt;h2 id="check-the-artifact-not-only-the-source"&gt;Check the artifact, not only the source&lt;/h2&gt;
&lt;p&gt;Inspect the generated tree and verify that expected entry points exist:&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;test&lt;/span&gt; -s public/index.html
&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; -s public/404.html
&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; -s public/robots.txt
&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; -s public/sitemap.xml
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;find public -type f -print &lt;span class="p"&gt;|&lt;/span&gt; sort
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then search for accidental placeholders, local paths, and development-only hosts:&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;rg -n &lt;span class="s1"&gt;&amp;#39;localhost|127\.0\.0\.1|TODO|example\.com|/Users/&amp;#39;&lt;/span&gt; public --glob &lt;span class="s1"&gt;&amp;#39;!notes/a-release-checklist-for-a-static-site/**&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;An intentional example in an article should be clearly fenced as an example. A host name or private path in a link is usually a release mistake.&lt;/p&gt;
&lt;h2 id="exercise-the-public-routes"&gt;Exercise the public routes&lt;/h2&gt;
&lt;p&gt;Serve the artifact locally and request the paths a visitor can reach:&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;hugo server --bind 127.0.0.1 --port &lt;span class="m"&gt;1313&lt;/span&gt; --disableFastRender
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -I http://127.0.0.1:1313/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -I http://127.0.0.1:1313/notes/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -I http://127.0.0.1:1313/resources/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -I http://127.0.0.1:1313/missing-page/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Confirm that the 404 route returns a 404 status in the production server. A pretty error page with a 200 status still breaks monitoring and caches.&lt;/p&gt;
&lt;h2 id="record-the-result"&gt;Record the result&lt;/h2&gt;
&lt;p&gt;Keep the source commit, generator version, and checksum of each downloadable resource in the release record. This makes it possible to compare a live file with the source that produced it without relying on memory.&lt;/p&gt;
&lt;p&gt;The downloadable version of this checklist is in the &lt;a href="https://www.xbcatm.com/resources/"&gt;Resources&lt;/a&gt; section.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://gohugo.io/commands/hugo/"&gt;Hugo command reference&lt;/a&gt;, generator flags for a clean production build, checked 2026-07-30.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://curl.se/docs/manpage.html"&gt;curl manual&lt;/a&gt;, HTTP verification and header inspection options, checked 2026-07-30.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.xbcatm.com/resources/"&gt;Static-site release checklist&lt;/a&gt;, downloadable companion resource.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>SSH host keys are part of the deployment record</title><link>https://www.xbcatm.com/notes/ssh-host-keys-are-part-of-the-deployment-record/</link><pubDate>Tue, 28 Jul 2026 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/notes/ssh-host-keys-are-part-of-the-deployment-record/</guid><description>Keep first contact explicit and reject unexpected host-key changes.</description><content:encoded>&lt;p&gt;An SSH private key authenticates a client; it does not tell the client which server it reached. The server&amp;rsquo;s host key and the local &lt;code&gt;known_hosts&lt;/code&gt; database provide that second half of the identity check.&lt;/p&gt;
&lt;h2 id="separate-first-contact-from-rotation"&gt;Separate first contact from rotation&lt;/h2&gt;
&lt;p&gt;With &lt;code&gt;StrictHostKeyChecking=yes&lt;/code&gt;, SSH refuses unknown hosts and changed keys. That is the safest default for a fixed production endpoint, provided the expected key is installed through a trusted channel. &lt;code&gt;accept-new&lt;/code&gt; is a narrower convenience for a fleet where new hosts are expected: it accepts a never-seen key but still refuses a changed key.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-sshconfig" data-lang="sshconfig"&gt;Host production-site
HostName www.example.test
User deploy
IdentityFile ~/.ssh/site_ed25519
StrictHostKeyChecking yes
UserKnownHostsFile ~/.ssh/known_hosts
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Do not “fix” a warning by adding &lt;code&gt;StrictHostKeyChecking=no&lt;/code&gt; to a deployment script. That converts a useful signal into silent acceptance. If a host is rebuilt, record the replacement fingerprint and review why the old key is no longer valid.&lt;/p&gt;
&lt;h2 id="make-the-fingerprint-reviewable"&gt;Make the fingerprint reviewable&lt;/h2&gt;
&lt;p&gt;Obtain the fingerprint from the host owner or console before the first connection, then compare it with what the client sees. A key change should have a ticket, maintenance note, or other explanation that another operator can inspect.&lt;/p&gt;
&lt;p&gt;The same rule applies to automation: the known-hosts file is configuration with security consequences, not disposable cache data. Back it up with the deployment record, but do not publish private keys or copied credentials with the site artifact.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://man.openbsd.org/ssh_config"&gt;OpenBSD &lt;code&gt;ssh_config&lt;/code&gt;&lt;/a&gt;, &lt;code&gt;StrictHostKeyChecking&lt;/code&gt; and &lt;code&gt;UserKnownHostsFile&lt;/code&gt;, checked 2026-07-28.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man.openbsd.org/ssh-keygen"&gt;OpenSSH &lt;code&gt;ssh-keygen&lt;/code&gt; manual&lt;/a&gt;, fingerprint inspection, checked 2026-07-28.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>The advantage of small systems</title><link>https://www.xbcatm.com/notes/the-advantage-of-small-systems/</link><pubDate>Mon, 27 Jul 2026 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/notes/the-advantage-of-small-systems/</guid><description>A system is easier to trust when its important behavior fits inside a single mental model.</description><content:encoded>&lt;p&gt;Small does not mean unfinished. It means each component has a clear job, dependencies are deliberate, and the path from request to result remains visible.&lt;/p&gt;
&lt;h2 id="visibility-is-a-feature"&gt;Visibility is a feature&lt;/h2&gt;
&lt;p&gt;When operators can name every boundary, they can also identify which evidence is missing. Logs become more useful, alerts become more specific, and routine maintenance becomes less risky.&lt;/p&gt;
&lt;p&gt;For a static website the important boundaries are usually easy to list:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DNS points the hostname to the intended edge.&lt;/li&gt;
&lt;li&gt;TLS terminates at the intended proxy.&lt;/li&gt;
&lt;li&gt;The proxy serves one known output directory.&lt;/li&gt;
&lt;li&gt;The output contains only the files the site needs.&lt;/li&gt;
&lt;li&gt;A 404 response is handled by the site, not by an accidental upstream.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Writing this list down is more valuable than adding a dashboard that does not answer any of those questions.&lt;/p&gt;
&lt;h2 id="complexity-should-earn-its-place"&gt;Complexity should earn its place&lt;/h2&gt;
&lt;p&gt;A new layer is worthwhile when it removes more uncertainty than it creates. A build step that fingerprints assets can be useful when caching is important. A database is useful when content needs transactions. Neither is automatically an improvement for a small, mostly static site.&lt;/p&gt;
&lt;p&gt;The same rule applies to client-side JavaScript. If a page needs a search index or an interactive editor, add the smallest implementation that meets that requirement and update the content security policy deliberately. If the page is a document, plain HTML is often easier to inspect and more resilient.&lt;/p&gt;
&lt;h2 id="make-ownership-explicit"&gt;Make ownership explicit&lt;/h2&gt;
&lt;p&gt;Every file should have a reason to exist and a clear way to update it. A vendored theme should record its upstream version and license. A generated directory should be marked as generated. A resource download should say what it contains, when it was updated, and how its checksum was produced.&lt;/p&gt;
&lt;p&gt;These small statements prevent future operators from treating an accidental artifact as a supported interface.&lt;/p&gt;
&lt;h2 id="leave-room-for-the-next-change"&gt;Leave room for the next change&lt;/h2&gt;
&lt;p&gt;The best small system is not the one with the fewest lines. It is the one where the next change has a visible place to go. A notes site can start with Markdown, a static generator, and a web server; if it later needs search or comments, the new requirement can be evaluated on its own rather than assumed from the beginning.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://gohugo.io/getting-started/directory-structure/"&gt;Hugo directory structure&lt;/a&gt;, directory conventions for content and assets, checked 2026-07-27.&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; directive&lt;/a&gt;, static file serving defaults referenced by the design, checked 2026-07-27.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Security headers should describe the actual page</title><link>https://www.xbcatm.com/notes/security-headers-should-describe-the-actual-page/</link><pubDate>Thu, 23 Jul 2026 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/notes/security-headers-should-describe-the-actual-page/</guid><description>A compact policy is safer when it matches the resources a static page really loads.</description><content:encoded>&lt;p&gt;Security headers are not a substitute for secure code, but they make the browser enforce useful boundaries. The policy should describe the page that is actually served, not a generic template copied from another project.&lt;/p&gt;
&lt;h2 id="start-from-the-resource-graph"&gt;Start from the resource graph&lt;/h2&gt;
&lt;p&gt;If a site serves HTML, a local stylesheet, a favicon, and no executable JavaScript, a narrow policy can be understandable:&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The script-src &amp;rsquo;none&amp;rsquo; choice is only correct while the site does not require executable scripts. Adding search, comments, or an analytics tag changes the resource graph and should trigger a deliberate policy review.&lt;/p&gt;
&lt;h2 id="add-transport-and-framing-protections"&gt;Add transport and framing protections&lt;/h2&gt;
&lt;p&gt;For an HTTPS-only site, the following headers are common starting points:&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;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;span class="line"&gt;&lt;span class="cl"&gt;Referrer-Policy: strict-origin-when-cross-origin
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=()
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Only enable HSTS with includeSubDomains when every subdomain is prepared to serve HTTPS. A header is a contract with future deployments, not merely a score in a scanner.&lt;/p&gt;
&lt;h2 id="check-behavior-not-just-presence"&gt;Check behavior, not just presence&lt;/h2&gt;
&lt;p&gt;Request a normal page, a missing page, and a static asset. Error responses should carry the same important security headers as successful responses. Also confirm that compression and caching do not change the content type or expose an internal path.&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://www.xbcatm.com/resources/"&gt;Caddy example&lt;/a&gt; in this site is a starting point, not a universal policy. Adapt the host names, certificate handling, and application routes to your own deployment.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP"&gt;MDN: Content Security Policy guide&lt;/a&gt;, CSP directive names and syntax, checked 2026-07-23.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Strict-Transport-Security"&gt;MDN: &lt;code&gt;Strict-Transport-Security&lt;/code&gt;&lt;/a&gt;, HSTS directives and deployment cautions, checked 2026-07-23.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://caddyserver.com/docs/caddyfile/directives/header"&gt;Caddy &lt;code&gt;header&lt;/code&gt; directive&lt;/a&gt;, response header manipulation directives, checked 2026-07-23.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>robots.txt and sitemaps answer different questions</title><link>https://www.xbcatm.com/notes/robots.txt-and-sitemaps-answer-different-questions/</link><pubDate>Tue, 21 Jul 2026 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/notes/robots.txt-and-sitemaps-answer-different-questions/</guid><description>Keep crawler preferences, URL discovery, and access control separate.</description><content:encoded>&lt;p&gt;Two small files are often treated as if they were a security boundary. They are not. A &lt;code&gt;robots.txt&lt;/code&gt; file describes crawler preferences, while a sitemap gives search systems a list of URLs that belong to the site.&lt;/p&gt;
&lt;h2 id="what-robotstxt-can-and-cannot-do"&gt;What robots.txt can and cannot do&lt;/h2&gt;
&lt;p&gt;The rules are published at the origin&amp;rsquo;s &lt;code&gt;/robots.txt&lt;/code&gt; path and are read by cooperating crawlers. They do not protect a private directory, remove a URL from the network, or replace authentication. A route that must not be public needs server authorization or a network boundary.&lt;/p&gt;
&lt;p&gt;Keep the file boring and test its actual location:&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;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.example.test/sitemap.xml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The file should be UTF-8. If a deployment redirects it, verify the crawler behavior you intend rather than assuming every client follows the same chain.&lt;/p&gt;
&lt;h2 id="what-a-sitemap-adds"&gt;What a sitemap adds&lt;/h2&gt;
&lt;p&gt;A sitemap is an inventory, not a permission list. Use absolute URLs from one host, encode XML values correctly, and include only canonical pages that you are prepared to publish. Do not list a URL merely because it exists in a build directory.&lt;/p&gt;
&lt;p&gt;The protocol permits up to 50,000 URLs and 50 MB per sitemap file. A small site does not need an index file; one ordinary sitemap is easier to inspect. Validate it as XML and request it through the same HTTPS edge as a normal page.&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;curl -fsS https://www.example.test/robots.txt
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -fsS https://www.example.test/sitemap.xml &lt;span class="p"&gt;|&lt;/span&gt; xmllint --noout -
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If &lt;code&gt;xmllint&lt;/code&gt; is not installed, parse the XML in the same CI environment that publishes it. The important part is that the check runs before the file is replaced at the edge.&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/rfc9309.html"&gt;RFC 9309 — Robots Exclusion Protocol&lt;/a&gt;, robots rules, file location, and user-agent matching, checked 2026-07-21.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.sitemaps.org/protocol.html"&gt;Sitemaps Protocol&lt;/a&gt;, sitemap XML structure and discovery, checked 2026-07-21.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Reliable defaults beat clever recovery</title><link>https://www.xbcatm.com/notes/reliable-defaults-beat-clever-recovery/</link><pubDate>Mon, 20 Jul 2026 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/notes/reliable-defaults-beat-clever-recovery/</guid><description>A predictable first path prevents more incidents than a complicated fallback can repair.</description><content:encoded>&lt;p&gt;Small services often accumulate recovery logic before their normal path is fully understood. Every retry, alternate endpoint, and compatibility branch adds another state that must be observed and tested.&lt;/p&gt;
&lt;h2 id="make-the-common-path-boring"&gt;Make the common path boring&lt;/h2&gt;
&lt;p&gt;A useful default is explicit, easy to inspect, and safe to repeat. For a static site, that can be as simple as:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Build from a pinned toolchain.&lt;/li&gt;
&lt;li&gt;Publish one complete directory.&lt;/li&gt;
&lt;li&gt;Check the resulting HTML and assets.&lt;/li&gt;
&lt;li&gt;Keep the previous directory available for rollback.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The sequence is less impressive than a multi-stage deployment system, but its state is visible. When something fails, the operator can answer whether the problem is in the source, the build, the copy, or the web server.&lt;/p&gt;
&lt;h2 id="retry-only-a-bounded-operation"&gt;Retry only a bounded operation&lt;/h2&gt;
&lt;p&gt;Retries are useful when the operation is known to be transient and repeating it cannot create a second side effect. A download from an origin may be safe to retry; a database migration may not be. Treating every error as retryable hides the boundary that needs attention.&lt;/p&gt;
&lt;p&gt;When a retry is appropriate, record the attempt count and the final reason. A log entry such as &amp;ldquo;publish failed: checksum mismatch&amp;rdquo; is more actionable than a sequence of silent retries followed by a generic timeout.&lt;/p&gt;
&lt;h2 id="prefer-one-authoritative-state"&gt;Prefer one authoritative state&lt;/h2&gt;
&lt;p&gt;Configuration, runtime state, and documentation should agree about which directory is live and which command produces it. If a release can be copied to three different paths depending on who runs it, recovery becomes guesswork.&lt;/p&gt;
&lt;p&gt;For a static deployment, a short release record is enough:&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;release: 2026-07-20
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;source: git commit &amp;lt;commit-id&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;builder: Hugo 0.165.0
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;output: public/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;verification: links + headers + checksums
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The placeholder commit ID is deliberately resolved at release time instead of being invented in documentation.&lt;/p&gt;
&lt;h2 id="recovery-should-restore-a-known-state"&gt;Recovery should restore a known state&lt;/h2&gt;
&lt;p&gt;Recovery is easiest when it returns the service to the last known-good directory, not when it chooses among several partially built alternatives. Keep the previous output until the new output has passed its checks, then remove old copies according to a retention policy.&lt;/p&gt;
&lt;p&gt;The goal is not to eliminate failure. It is to make the failure small enough to explain and the recovery small enough to trust.&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;, section 9.2.2 on idempotent methods and retries, checked 2026-07-20.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.xbcatm.com/resources/"&gt;Static-site release checklist&lt;/a&gt;, local companion resource.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>A release manifest should name every published byte</title><link>https://www.xbcatm.com/notes/a-release-manifest-should-name-every-published-byte/</link><pubDate>Thu, 16 Jul 2026 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/notes/a-release-manifest-should-name-every-published-byte/</guid><description>Build a deterministic file manifest and compare it before and after transport.</description><content:encoded>&lt;p&gt;A static artifact is a directory, not one executable. Recording only the source commit or only the homepage checksum leaves gaps: a missing image, stale alias, or changed download can survive unnoticed. A release manifest should enumerate every regular file.&lt;/p&gt;
&lt;h2 id="generate-a-stable-list"&gt;Generate a stable list&lt;/h2&gt;
&lt;p&gt;Run the command from the artifact root so paths remain relative. Sort by path under a fixed locale:&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;find . -type f -exec sha256sum &lt;span class="o"&gt;{}&lt;/span&gt; + &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 -k2 &amp;gt; ../release.sha256
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The manifest is evidence about the artifact, so keep it outside the directory being measured unless the manifest itself is intentionally part of the release.&lt;/p&gt;
&lt;h2 id="verify-after-transport"&gt;Verify after transport&lt;/h2&gt;
&lt;p&gt;On the destination, verify individual files:&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; /srv/www/site.new
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sha256sum -c /path/to/release.sha256
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This catches changed bytes and missing paths. It does not report unexpected extra files, so compare the destination file list too:&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;find . -type f &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 &amp;gt; destination.files
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;awk &lt;span class="s1"&gt;&amp;#39;{print $2}&amp;#39;&lt;/span&gt; /path/to/release.sha256 &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 &amp;gt; expected.files
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;diff -u expected.files destination.files
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="add-an-aggregate-digest-for-release-records"&gt;Add an aggregate digest for release records&lt;/h2&gt;
&lt;p&gt;Hash the manifest itself to produce a short identifier for logs and deployment records:&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;sha256sum release.sha256
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The aggregate digest is only meaningful when the manifest generation rules are fixed: relative path form, sorting locale, hash algorithm, and whether generated metadata is included all belong in the procedure.&lt;/p&gt;
&lt;h2 id="do-not-confuse-integrity-with-authenticity"&gt;Do not confuse integrity with authenticity&lt;/h2&gt;
&lt;p&gt;SHA-256 proves that two byte sequences match. If an attacker can replace both the artifact and the manifest, the check does not establish who produced them. Artifact signing and trusted distribution are separate controls.&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;, checksum generation and checking, checked 2026-07-16.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man1/sha256sum.1.html"&gt;sha256sum(1)&lt;/a&gt;, command behavior and output format, checked 2026-07-16.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Redirects and error codes should preserve the story</title><link>https://www.xbcatm.com/notes/redirects-and-error-codes-should-preserve-the-story/</link><pubDate>Tue, 14 Jul 2026 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/notes/redirects-and-error-codes-should-preserve-the-story/</guid><description>Use permanent redirects and absence codes to describe what happened, not to hide a broken route.</description><content:encoded>&lt;p&gt;An HTTP status is part of the interface. A browser may render a friendly page, but a monitor, crawler, cache, and API client also need the status code and headers to tell the same story.&lt;/p&gt;
&lt;h2 id="permanent-moves-are-not-all-interchangeable"&gt;Permanent moves are not all interchangeable&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;308 Permanent Redirect&lt;/code&gt; communicates that the resource has a new permanent URI while preserving the request method. That is important for a client that sends &lt;code&gt;POST&lt;/code&gt;, &lt;code&gt;PUT&lt;/code&gt;, or another non-&lt;code&gt;GET&lt;/code&gt; method. A redirect from an old page to a new page is easy to test with &lt;code&gt;GET&lt;/code&gt;; a migration of an endpoint must also be tested with the method that real callers 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;curl -sS -D - -o /dev/null --max-redirs &lt;span class="m"&gt;0&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; https://old.example.test/api/item
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Check that &lt;code&gt;Location&lt;/code&gt; is absolute or correctly rooted, that the target is the intended host, and that the response is not silently downgraded to a temporary redirect. A &lt;code&gt;308&lt;/code&gt; is cacheable by default, so change it only with a deliberate migration plan.&lt;/p&gt;
&lt;h2 id="distinguish-missing-from-removed"&gt;Distinguish missing from removed&lt;/h2&gt;
&lt;p&gt;Use &lt;code&gt;404 Not Found&lt;/code&gt; when the server cannot find a current representation and the absence may be temporary or unknown. &lt;code&gt;410 Gone&lt;/code&gt; is stronger: it says the resource was intentionally removed and is unlikely to return. Both should have a body that helps a human, but neither should return &lt;code&gt;200&lt;/code&gt; just because a custom error page rendered successfully.&lt;/p&gt;
&lt;p&gt;The check belongs at the edge:&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;curl -sS -o /tmp/missing.html -w &lt;span class="s1"&gt;&amp;#39;%{http_code} %{url_effective}\n&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; https://www.example.test/a-route-that-does-not-exist
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Keep the response headers on the error path. Security headers, a useful content type, and a correct status are more valuable than a decorative error animation.&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;, sections 15 and 13, checked 2026-07-14.&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;, section 3, checked 2026-07-14.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://curl.se/docs/manpage.html"&gt;curl manual&lt;/a&gt;, status and redirect inspection flags, checked 2026-07-14.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Read the public edge in two commands</title><link>https://www.xbcatm.com/notes/read-the-public-edge-in-two-commands/</link><pubDate>Mon, 13 Jul 2026 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/notes/read-the-public-edge-in-two-commands/</guid><description>Run read-only probes against a live site and let the response headers and certificate dates confirm what you think is deployed.</description><content:encoded>&lt;p&gt;A deployment is complete when the public edge agrees with the files you chose, not when the last upload command exits zero. Two read-only helpers from the &lt;a href="https://www.xbcatm.com/resources/"&gt;operator kit&lt;/a&gt; make that agreement visible from any machine that can reach the host.&lt;/p&gt;
&lt;h2 id="inspect-the-response-headers"&gt;Inspect the response headers&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;check-http-headers.sh&lt;/code&gt; fetches one URL and prints the response line, the headers, and the final URL after redirects:&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;check-http-headers.sh https://www.xbcatm.com/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A recent read against this site produced:&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;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-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;last-modified: Mon, 24 Aug 2026 11:20:54 GMT
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;permissions-policy: accelerometer=(), camera=(), geolocation=(), gyroscope=(), microphone=(), payment=(), usb=()
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;referrer-policy: strict-origin-when-cross-origin
&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;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_STATUS 200
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;FINAL_URL https://www.xbcatm.com/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Two lines matter immediately: a &lt;code&gt;200&lt;/code&gt; status with a &lt;code&gt;FINAL_URL&lt;/code&gt; that matches the host you intended, and a &lt;code&gt;Content-Security-Policy&lt;/code&gt; that still says &lt;code&gt;script-src 'none'&lt;/code&gt;. If any of those differ from the policy you believe is live, the edge is not the tree you staged.&lt;/p&gt;
&lt;h2 id="inspect-the-certificate"&gt;Inspect the certificate&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;check-tls-expiry.sh HOST [DAYS]&lt;/code&gt; reads the public certificate for the hostname and enforces a minimum validity window:&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;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;A recent read printed:&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;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;Notice the subject is the exact hostname, not a wildcard from an adjacent site. &lt;code&gt;notAfter&lt;/code&gt; is a concrete date; if it is closer than the window you passed, the helper exits non-zero and your release check can treat that as a failing condition.&lt;/p&gt;
&lt;h2 id="use-both-around-a-release"&gt;Use both around a release&lt;/h2&gt;
&lt;p&gt;Once before and once after you swap directories:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Before: record &lt;code&gt;last-modified&lt;/code&gt;, the CSP, and the certificate dates as your baseline.&lt;/li&gt;
&lt;li&gt;Deploy the staged tree.&lt;/li&gt;
&lt;li&gt;After: run the same two commands and compare &lt;code&gt;last-modified&lt;/code&gt; and the checksums.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The two outputs together are a small but complete contract: the certificate is current, the policy is intact, and the bytes changing at the edge match what you published. That is the evidence a release claim is allowed to be.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://caddyserver.com/docs/automatic-https"&gt;Caddy Automatic HTTPS&lt;/a&gt;, automatic TLS certificate management, checked 2026-07-13.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP"&gt;MDN: Content Security Policy guide&lt;/a&gt;, CSP directive names and syntax, checked 2026-07-13.&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; documentation&lt;/a&gt;, SNI and certificate inspection options, checked 2026-07-13.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>A release is not complete until the public edge agrees</title><link>https://www.xbcatm.com/notes/a-release-is-not-complete-until-the-public-edge-agrees/</link><pubDate>Thu, 09 Jul 2026 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/notes/a-release-is-not-complete-until-the-public-edge-agrees/</guid><description>Compare the generated artifact with the responses a visitor can actually receive.</description><content:encoded>&lt;p&gt;Static publishing removes application runtime complexity, but it does not remove the boundary between a generated directory and a public response. A release check should cover both.&lt;/p&gt;
&lt;p&gt;&lt;img loading="lazy" src="https://www.xbcatm.com/diagrams/release-path.svg" alt="A small static release path" /&gt;
&lt;/p&gt;
&lt;h2 id="start-from-a-clean-artifact"&gt;Start from a clean artifact&lt;/h2&gt;
&lt;p&gt;Build with the pinned generator into a clean destination. Record the source commit, generator version, output file list, and checksums of files that users download. A clean build catches stale files that a copy-over deployment can accidentally preserve.&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;make verify
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;find public -type f -print &lt;span class="p"&gt;|&lt;/span&gt; sort
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The file list is evidence, not decoration. If a route disappears, the diff should explain whether the source was removed or the generator failed to emit it.&lt;/p&gt;
&lt;h2 id="exercise-three-response-classes"&gt;Exercise three response classes&lt;/h2&gt;
&lt;p&gt;Request one HTML page, one static resource, and one missing path. Inspect status, content type, cache policy, and security 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;curl -fsS -D - -o /dev/null https://www.example.test/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -fsS -D - -o /dev/null https://www.example.test/downloads/check-http-headers.sh
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -sS -D - -o /dev/null https://www.example.test/does-not-exist
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The third command intentionally does not use &lt;code&gt;-f&lt;/code&gt;: a 404 is the expected result and should still be captured. Compare the public body with the generated &lt;code&gt;404.html&lt;/code&gt; and verify that the status remains 404.&lt;/p&gt;
&lt;h2 id="keep-rollback-simple"&gt;Keep rollback simple&lt;/h2&gt;
&lt;p&gt;Do not delete the previous directory until the new one has passed the checks. A rollback should select a known-good directory, not reconstruct one from memory during an incident. The &lt;a href="https://www.xbcatm.com/resources/"&gt;release checklist&lt;/a&gt; and &lt;a href="https://www.xbcatm.com/resources/"&gt;HTTP header check&lt;/a&gt; are deliberately small enough to run from a clean shell.&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;, status and representation semantics, checked 2026-07-09.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://curl.se/docs/manpage.html"&gt;curl manual&lt;/a&gt;, response inspection options, checked 2026-07-09.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.xbcatm.com/resources/"&gt;Static-site release checklist&lt;/a&gt;, local companion resource.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>A postmortem should leave a fact record</title><link>https://www.xbcatm.com/notes/a-postmortem-should-leave-a-fact-record/</link><pubDate>Tue, 07 Jul 2026 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/notes/a-postmortem-should-leave-a-fact-record/</guid><description>Structure an incident write-up around captured output so the review thread stays on evidence instead of memory.</description><content:encoded>&lt;p&gt;A postmortem earns trust when its claims can be re-checked. That means every phase names the command that produced the fact, not a paraphrase of what someone remembers. The four phases below keep a write-up reviewable at any later time.&lt;/p&gt;
&lt;h2 id="detect-the-symptom-with-evidence"&gt;Detect: the symptom, with evidence&lt;/h2&gt;
&lt;p&gt;Start with the smallest verifiable symptom and the command that showed it. A capture is better than a prose description:&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;$ 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-security-policy: ... script-src &amp;#39;none&amp;#39; ...
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;last-modified: Mon, 24 Aug 2026 11:20:54 GMT
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If the symptom is that a header changed, the output showing the previous policy and the new one belongs side by side. If it is a failure, include the exit status and the error line.&lt;/p&gt;
&lt;h2 id="scope-how-wide-and-how-you-know"&gt;Scope: how wide, and how you know&lt;/h2&gt;
&lt;p&gt;State the blast radius with evidence, not adjectives. Fix the boundary you can inspect:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Which hosts or paths reply with the same symptom? One &lt;code&gt;check-http-headers.sh&lt;/code&gt; per candidate host answers this.&lt;/li&gt;
&lt;li&gt;Does the cache differ from origin? Compare &lt;code&gt;Vary&lt;/code&gt; and validators.&lt;/li&gt;
&lt;li&gt;Which rollback window applies? Use the TTL that was in force before the change.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="fix-the-change-and-its-intended-effect"&gt;Fix: the change and its intended effect&lt;/h2&gt;
&lt;p&gt;Write the corrective action so it can be replayed. Prefer the smallest change that restores the contract, and name what the post-fix run should show:&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="c1"&gt;# new policy line expected in the response:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;script-src &lt;span class="s1"&gt;&amp;#39;none&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="verify-the-post-incident-run"&gt;Verify: the post-incident run&lt;/h2&gt;
&lt;p&gt;Repeat the detection command after the change and show that the observed output now matches the intended contract. A postmortem without this final section is a story; with it, it is a record that approves its own future re-check.&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;, response metadata and conditional requests, checked 2026-07-07.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.xbcatm.com/resources/"&gt;artifact HTTP header check&lt;/a&gt;, read-only inspection used throughout this record.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Range requests should prove which bytes were selected</title><link>https://www.xbcatm.com/notes/range-requests-should-prove-which-bytes-were-selected/</link><pubDate>Mon, 06 Jul 2026 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/notes/range-requests-should-prove-which-bytes-were-selected/</guid><description>Verify partial responses, Content-Range, and the behavior of unsatisfiable byte ranges.</description><content:encoded>&lt;p&gt;Range requests let a client ask for part of a representation. Media players, resumable download tools, and diagnostic clients use them, but a server that advertises or returns ranges incorrectly can produce silent corruption instead of a clean failure.&lt;/p&gt;
&lt;h2 id="establish-the-complete-size"&gt;Establish the complete size&lt;/h2&gt;
&lt;p&gt;Fetch headers for the complete representation and record its length and validator:&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;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://www.example.test/downloads/archive.tar.gz
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -sS -D full.headers -o /dev/null &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;awk &lt;span class="s1"&gt;&amp;#39;tolower($1) ~ /^(content-length:|etag:|last-modified:|accept-ranges:)$/ { print }&amp;#39;&lt;/span&gt; full.headers
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;Accept-Ranges: bytes&lt;/code&gt; is an explicit signal, but absence of the header does not by itself prove that range requests are unsupported. Exercise the request.&lt;/p&gt;
&lt;h2 id="request-a-known-interval"&gt;Request a known interval&lt;/h2&gt;
&lt;p&gt;Ask for the first 16 bytes and keep both headers and body:&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;curl -sS -D range.headers -o range.body &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="s1"&gt;&amp;#39;Range: bytes=0-15&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;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;wc -c range.body
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;awk &lt;span class="s1"&gt;&amp;#39;tolower($1) ~ /^(content-range:|content-length:|etag:)$/ || /^HTTP\// { print }&amp;#39;&lt;/span&gt; range.headers
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A successful byte-range response uses &lt;code&gt;206 Partial Content&lt;/code&gt;. &lt;code&gt;Content-Range&lt;/code&gt; should identify &lt;code&gt;0-15&lt;/code&gt; and the complete representation length, while the body should contain exactly 16 bytes.&lt;/p&gt;
&lt;h2 id="test-an-impossible-range"&gt;Test an impossible range&lt;/h2&gt;
&lt;p&gt;Request bytes beyond the known end:&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;curl -sS -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; -H &lt;span class="s1"&gt;&amp;#39;Range: bytes=999999999-&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;For an unsatisfiable byte range, &lt;code&gt;416 Range Not Satisfiable&lt;/code&gt; and a &lt;code&gt;Content-Range&lt;/code&gt; value such as &lt;code&gt;bytes */12345&lt;/code&gt; make the actual length visible. A &lt;code&gt;200&lt;/code&gt; response may be valid when the server ignores the range, but it must return the complete representation rather than a mislabeled fragment.&lt;/p&gt;
&lt;h2 id="keep-validators-in-the-story"&gt;Keep validators in the story&lt;/h2&gt;
&lt;p&gt;Resumed downloads can use &lt;code&gt;If-Range&lt;/code&gt; with a strong validator or date. If the representation changed, the safe response is the full current representation, not bytes spliced from two versions. Test this behavior before calling a download resumable.&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#name-range-requests"&gt;RFC 9110 — HTTP Semantics&lt;/a&gt;, range units, partial responses, and &lt;code&gt;Content-Range&lt;/code&gt;, checked 2026-07-06.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://curl.se/docs/manpage.html"&gt;curl manual&lt;/a&gt;, &lt;code&gt;--range&lt;/code&gt; and header options, checked 2026-07-06.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>HTTP methods are a promise about side effects</title><link>https://www.xbcatm.com/notes/http-methods-are-a-promise-about-side-effects/</link><pubDate>Thu, 02 Jul 2026 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/notes/http-methods-are-a-promise-about-side-effects/</guid><description>The HTTP method names what a request may change; a static origin can answer most of them without pretending to have a stateful contract.</description><content:encoded>&lt;p&gt;A request method is a promise about side effects, not just a verb you pass to a client. When the origin is a static file tree, most of that promise is already settled before the first request: the tree has no transactions, so methods that would mutate state have nothing safe to mutate.&lt;/p&gt;
&lt;h2 id="the-method-is-part-of-the-contract"&gt;The method is part of the contract&lt;/h2&gt;
&lt;p&gt;HTTP method semantics are defined in &lt;a href="https://www.rfc-editor.org/rfc/rfc9110.html"&gt;RFC 9110&lt;/a&gt;. GET and HEAD must be safe: they promise a client that the request does not change server state. PUT, POST, and DELETE are not safe by default; they may change state. A server that cannot honor the promise should say so with an explicit status, not with a guessed body.&lt;/p&gt;
&lt;p&gt;For a static site the useful distinctions are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GET&lt;/strong&gt; returns the representation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HEAD&lt;/strong&gt; returns the same response metadata without the body.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OPTIONS&lt;/strong&gt; discloses what the origin will accept.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;POST / PUT / DELETE / PATCH&lt;/strong&gt; have no meaning for an immutable file tree and should be rejected.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="what-a-caddy-static-origin-actually-answers"&gt;What a Caddy static origin actually answers&lt;/h2&gt;
&lt;p&gt;Point &lt;code&gt;curl&lt;/code&gt; at the public edge and the method boundary is easy to read. An &lt;code&gt;OPTIONS&lt;/code&gt; request against a site that only serves files returns &lt;code&gt;405 Method Not Allowed&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ curl -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;A &lt;code&gt;405&lt;/code&gt; with an &lt;code&gt;Allow&lt;/code&gt; header naming the supported methods is the correct, explicit reject. Compare this with a dynamic origin that happens to ignore the body: an imported handler can turn a universal non-promise into silent success, which hides the actual contract.&lt;/p&gt;
&lt;h2 id="head-should-equal-get-except-for-the-body"&gt;HEAD should equal GET except for the body&lt;/h2&gt;
&lt;p&gt;Because HEAD is safe and body-free, its metadata should match the equivalent GET. &lt;code&gt;Content-Length&lt;/code&gt; still describes the bytes a GET would return. On this site both agree:&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;HEAD content-length: 15890
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;GET bytes read : 15890
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If a proxy strips or rewrites &lt;code&gt;Content-Length&lt;/code&gt; on HEAD, a checker that compares the two will catch it. This is the parity the &lt;code&gt;head-request-parity&lt;/code&gt; note and the &lt;code&gt;check-http-headers.sh&lt;/code&gt; helper both rely on.&lt;/p&gt;
&lt;h2 id="what-to-do-when-a-client-sends-a-mutating-method"&gt;What to do when a client sends a mutating method&lt;/h2&gt;
&lt;p&gt;Answer explicitly. Return &lt;code&gt;405 Method Not Allowed&lt;/code&gt; with an &lt;code&gt;Allow&lt;/code&gt; header listing the safe methods, rather than falling through to a page that looks successful. The HTTP foundations path depends on a single obvious answer at the edge: every method is either supported with its documented semantics or rejected with a status that says so.&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;, method definitions, safe/idempotent methods, and &lt;code&gt;405&lt;/code&gt;, checked 2026-07-02.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Methods"&gt;MDN: HTTP request methods&lt;/a&gt;, method semantics and &lt;code&gt;405&lt;/code&gt; behavior, checked 2026-07-02.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://curl.se/docs/manpage.html"&gt;curl manual&lt;/a&gt;, &lt;code&gt;-X&lt;/code&gt;, &lt;code&gt;-o&lt;/code&gt;, and &lt;code&gt;-w&lt;/code&gt; inspection options, checked 2026-07-02.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Validators decide whether a cached copy is still the answer</title><link>https://www.xbcatm.com/notes/validators-decide-whether-a-cached-copy-is-still-the-answer/</link><pubDate>Tue, 30 Jun 2026 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/notes/validators-decide-whether-a-cached-copy-is-still-the-answer/</guid><description>ETags and Last-Modified let a cache and an origin agree on whether a stored copy is current, saving the body on every revalidation.</description><content:encoded>&lt;p&gt;A cache does not re-download a document every time it wants to know whether its copy is current. Instead it sends a validator back to the origin and lets the origin answer with a small &lt;code&gt;304 Not Modified&lt;/code&gt;, or with the new body. Validation is the second half of HTTP caching, and validators are the keys that make it cheap.&lt;/p&gt;
&lt;h2 id="a-validator-is-a-currentness-answer-you-can-compare"&gt;A validator is a currentness answer you can compare&lt;/h2&gt;
&lt;p&gt;A validator is metadata that changes when the representation changes. HTTP defines two families:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Strong validators (ETag)&lt;/strong&gt; identify a specific version of the bytes. Comparing them is reliable.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Weak validators (Last-Modified)&lt;/strong&gt; are timestamps with second granularity; they can miss two writes in the same second.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;An origin should send at least one validator. Caddy serves files with both &lt;code&gt;ETag&lt;/code&gt; and &lt;code&gt;Last-Modified&lt;/code&gt; by default, so the public edge of this site returns both:&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;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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="conditional-requests-trade-a-header-for-a-possible-304"&gt;Conditional requests trade a header for a possible 304&lt;/h2&gt;
&lt;p&gt;A client asks with &lt;code&gt;If-None-Match&lt;/code&gt; (or a validator it already holds) and the origin compares it to the current one. When they match, the origin confirms it without resending 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 %{http_code}\n&amp;#39; -H &amp;#39;If-None-Match: dkx84jwth7nkc9e&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;span class="line"&gt;&lt;span class="cl"&gt;$ # size_download == 0 on a revalidation hit
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;When they differ, or no validator was sent, the origin answers &lt;code&gt;200&lt;/code&gt; with the fresh body. A &lt;code&gt;304&lt;/code&gt; is not a failure: it is the cheaper way to say the cached copy is still valid.&lt;/p&gt;
&lt;h2 id="strong-and-weak-are-not-interchangeable"&gt;Strong and weak are not interchangeable&lt;/h2&gt;
&lt;p&gt;Use the strong ETag for conditional &lt;code&gt;GET&lt;/code&gt; so the comparison is authoritative. Weak validators only promise that a body has stayed semantically similar, which is why range requests prefer strong ones. The &lt;code&gt;http-range-requests&lt;/code&gt; note covers byte selection; this note covers &amp;ldquo;is my stored copy still the answer&amp;rdquo;.&lt;/p&gt;
&lt;h2 id="what-changes-when-you-republish"&gt;What changes when you republish&lt;/h2&gt;
&lt;p&gt;When the tree changes, the ETag for a page changes too, so caches that revalidate fetch the new body. If a release updates bytes without touching &lt;code&gt;Last-Modified&lt;/code&gt;, the ETag still catches the change, which is why the release manifest records byte checksums rather than only file mtimes.&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, &lt;code&gt;ETag&lt;/code&gt;, and conditional request fields, checked 2026-06-30.&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, &lt;code&gt;If-None-Match&lt;/code&gt;, and &lt;code&gt;304&lt;/code&gt;, checked 2026-06-30.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Conditional_requests"&gt;MDN: HTTP conditional requests&lt;/a&gt;, 304 and validator behavior, checked 2026-06-30.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Compression changes the selected HTTP representation</title><link>https://www.xbcatm.com/notes/compression-changes-the-selected-http-representation/</link><pubDate>Mon, 29 Jun 2026 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/notes/compression-changes-the-selected-http-representation/</guid><description>Treat content encoding, validators, and Vary as one observable response contract.</description><content:encoded>&lt;p&gt;Compression saves transfer bytes, but it also creates another representation of a resource. A client announces acceptable content codings with &lt;code&gt;Accept-Encoding&lt;/code&gt;; the server describes the applied coding with &lt;code&gt;Content-Encoding&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="make-negotiation-visible"&gt;Make negotiation visible&lt;/h2&gt;
&lt;p&gt;When a response can change according to &lt;code&gt;Accept-Encoding&lt;/code&gt;, include that request field in &lt;code&gt;Vary&lt;/code&gt;. This tells shared caches that a compressed response and an identity response are not interchangeable cache entries.&lt;/p&gt;
&lt;p&gt;Compare the two paths instead of checking only a browser request:&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;curl -sS -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; -H &lt;span class="s1"&gt;&amp;#39;Accept-Encoding: identity&amp;#39;&lt;/span&gt; https://www.example.test/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -sS --compressed -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; https://www.example.test/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Inspect &lt;code&gt;Content-Encoding&lt;/code&gt;, &lt;code&gt;Vary&lt;/code&gt;, &lt;code&gt;Content-Length&lt;/code&gt; when present, and the validator. A server can use different entity tags for differently encoded representations; what matters is that conditional requests validate the same selected representation consistently.&lt;/p&gt;
&lt;h2 id="compress-appropriate-content"&gt;Compress appropriate content&lt;/h2&gt;
&lt;p&gt;Text formats such as HTML, CSS, JSON, XML, and SVG usually benefit from compression. Formats that are already compressed might not. Caddy&amp;rsquo;s &lt;code&gt;encode&lt;/code&gt; directive negotiates supported encodings and normally avoids encoding a response without an appropriate content type or minimum size.&lt;/p&gt;
&lt;p&gt;Do not add compression solely to satisfy a score. Measure a representative response, confirm that the edge emits the intended headers, and verify that downloadable files retain the correct content type and checksum after transfer decoding.&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;, content codings and &lt;code&gt;Vary&lt;/code&gt;, checked 2026-06-29.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://caddyserver.com/docs/caddyfile/directives/encode"&gt;Caddy &lt;code&gt;encode&lt;/code&gt; directive&lt;/a&gt;, content encoding selection and negotiation, checked 2026-06-29.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://curl.se/docs/manpage.html"&gt;curl manual&lt;/a&gt;, &lt;code&gt;--compressed&lt;/code&gt;, checked 2026-06-29.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Cache headers are a contract with two clocks</title><link>https://www.xbcatm.com/notes/cache-headers-are-a-contract-with-two-clocks/</link><pubDate>Thu, 25 Jun 2026 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/notes/cache-headers-are-a-contract-with-two-clocks/</guid><description>Choose freshness and revalidation rules from how often a representation can change.</description><content:encoded>&lt;p&gt;Caching is not a single switch. A response has a freshness decision made by a cache and a validation conversation with the origin. Mixing those two ideas produces either stale content or needless requests.&lt;/p&gt;
&lt;h2 id="pick-the-change-model-first"&gt;Pick the change model first&lt;/h2&gt;
&lt;p&gt;For an HTML document that may change after publication, &lt;code&gt;Cache-Control: no-cache&lt;/code&gt; is usually a more accurate starting point than &lt;code&gt;no-store&lt;/code&gt;. &lt;code&gt;no-cache&lt;/code&gt; permits storage but requires a cache to validate before reuse; &lt;code&gt;no-store&lt;/code&gt; asks caches not to store the response at all. The distinction matters when you want conditional requests and a cheap &lt;code&gt;304 Not Modified&lt;/code&gt; response.&lt;/p&gt;
&lt;p&gt;For a fingerprinted asset such as &lt;code&gt;app.4f2c.js&lt;/code&gt;, the URL changes when the bytes change. A long freshness lifetime can then be reasonable:&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;Cache-Control: public, max-age=31536000, immutable
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That policy is unsafe for a stable URL whose contents can be replaced in place. If the URL is stable, prefer a shorter lifetime and a validator such as an entity tag (&lt;code&gt;ETag&lt;/code&gt;) or a meaningful &lt;code&gt;Last-Modified&lt;/code&gt; value.&lt;/p&gt;
&lt;h2 id="inspect-both-paths"&gt;Inspect both paths&lt;/h2&gt;
&lt;p&gt;Make one ordinary request and one conditional request. The exact server flags vary, but the evidence you want is stable:&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;curl -sS -D - -o /dev/null https://www.example.test/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -sS -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; -H &lt;span class="s1"&gt;&amp;#39;If-None-Match: &amp;#34;copy-a-real-etag-from-the-first-response&amp;#34;&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; https://www.example.test/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Do not paste a made-up validator into a runbook. Copy the value from the first response or use a test fixture. A successful revalidation should return &lt;code&gt;304&lt;/code&gt; only when the server can prove that the selected representation has not changed.&lt;/p&gt;
&lt;h2 id="keep-intermediaries-visible"&gt;Keep intermediaries visible&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Age&lt;/code&gt;, &lt;code&gt;Vary&lt;/code&gt;, and the cache-control directives explain why two clients can receive different results. If a response varies by &lt;code&gt;Accept-Encoding&lt;/code&gt;, language, or another request field, that dimension belongs in &lt;code&gt;Vary&lt;/code&gt;; otherwise a shared cache can reuse the wrong representation.&lt;/p&gt;
&lt;p&gt;The useful release check is not “the scanner found a cache header”. It is “the header matches the replacement policy, and a conditional request behaves as documented”.&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/rfc9111.html"&gt;RFC 9111 — HTTP Caching&lt;/a&gt;, sections 4–5, checked 2026-06-25.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc9110.html"&gt;RFC 9110 — HTTP Semantics&lt;/a&gt;, sections 8 and 13, checked 2026-06-25.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control"&gt;MDN: &lt;code&gt;Cache-Control&lt;/code&gt;&lt;/a&gt;, directive semantics and defaults, checked 2026-06-25.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>HSTS should be rolled out with a recovery plan</title><link>https://www.xbcatm.com/notes/hsts-should-be-rolled-out-with-a-recovery-plan/</link><pubDate>Tue, 23 Jun 2026 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/notes/hsts-should-be-rolled-out-with-a-recovery-plan/</guid><description>Increase max-age only after HTTPS, redirects, and every covered subdomain are ready.</description><content:encoded>&lt;p&gt;HTTP Strict Transport Security tells a supporting browser to use HTTPS for a host during a cached period. That persistence is the feature and the risk: removing the header does not immediately erase policy already stored by clients.&lt;/p&gt;
&lt;h2 id="verify-the-https-path-first"&gt;Verify the HTTPS path first&lt;/h2&gt;
&lt;p&gt;Before sending HSTS, confirm that the canonical hostname serves a valid certificate and that HTTP redirects to the intended HTTPS URL without depending on user input:&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;curl -sS -D - -o /dev/null --max-redirs &lt;span class="m"&gt;0&lt;/span&gt; http://www.example.test/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -sS -D - -o /dev/null https://www.example.test/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Check representative deep routes and error pages. HSTS cannot repair a certificate error because the browser requires a valid HTTPS connection before accepting the policy.&lt;/p&gt;
&lt;h2 id="start-with-a-short-max-age"&gt;Start with a short max-age&lt;/h2&gt;
&lt;p&gt;A staged rollout limits the recovery window while configuration is new:&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;Strict-Transport-Security: max-age=300
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;After observing the site and renewal path, increase the duration deliberately. Record the date, chosen value, and rollback conditions. A final policy might use a year, but the number is a commitment rather than a scanner score.&lt;/p&gt;
&lt;h2 id="treat-includesubdomains-as-a-separate-decision"&gt;Treat includeSubDomains as a separate decision&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;includeSubDomains&lt;/code&gt; extends policy to every subdomain. Inventory names first, including old services, delegated zones, and hostnames used only on internal networks. One HTTP-only descendant is enough to make the option disruptive for clients that received the parent policy.&lt;/p&gt;
&lt;h2 id="preload-is-not-ordinary-header-configuration"&gt;Preload is not ordinary header configuration&lt;/h2&gt;
&lt;p&gt;Browser preload programs can distribute policy outside the normal response cycle. Their requirements and removal delays are stricter than setting a header. Do not request preload merely because a scanner recommends it; confirm long-term ownership of the domain and all covered names.&lt;/p&gt;
&lt;h2 id="verify-the-public-response"&gt;Verify the public response&lt;/h2&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;curl -sS -D - -o /dev/null https://www.example.test/ &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;|&lt;/span&gt; awk &lt;span class="s1"&gt;&amp;#39;tolower($1) == &amp;#34;strict-transport-security:&amp;#34; { print }&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Test the canonical public edge, not only an origin port. Proxies may add, remove, or duplicate the header.&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/rfc6797.html"&gt;RFC 6797 — HTTP Strict Transport Security&lt;/a&gt;, policy processing and scope, checked 2026-06-23.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Strict-Transport-Security"&gt;MDN: &lt;code&gt;Strict-Transport-Security&lt;/code&gt;&lt;/a&gt;, directives and deployment cautions, checked 2026-06-23.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>HEAD should describe the same representation as GET</title><link>https://www.xbcatm.com/notes/head-should-describe-the-same-representation-as-get/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/notes/head-should-describe-the-same-representation-as-get/</guid><description>Compare HEAD and GET metadata without confusing a missing body with a different resource.</description><content:encoded>&lt;p&gt;The &lt;code&gt;HEAD&lt;/code&gt; method asks for the response a server would send to &lt;code&gt;GET&lt;/code&gt;, without transferring the response content. It is useful for checks that need status, validators, type, or length, but only when the metadata remains aligned with the real &lt;code&gt;GET&lt;/code&gt; path.&lt;/p&gt;
&lt;h2 id="compare-a-normalized-header-set"&gt;Compare a normalized header set&lt;/h2&gt;
&lt;p&gt;Dynamic headers such as &lt;code&gt;Date&lt;/code&gt; will differ between requests. Compare the fields that describe the selected representation:&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;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://www.example.test/downloads/manual.pdf
&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;curl -sS -I &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; &amp;gt; head.headers
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -sS -D get.headers -o /dev/null &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;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;for&lt;/span&gt; file in head.headers get.headers&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&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;== %s\n&amp;#39;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$file&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; awk &lt;span class="s1"&gt;&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; /^HTTP\// ||
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; tolower($1) ~ /^(content-type:|content-length:|content-encoding:|cache-control:|etag:|last-modified:|vary:)$/ { print }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; &amp;#39;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$file&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="k"&gt;done&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The two requests should select the same status and representation metadata. Compression negotiation also belongs in the comparison: send the same &lt;code&gt;Accept-Encoding&lt;/code&gt; value to both requests.&lt;/p&gt;
&lt;h2 id="do-not-use-head-as-the-only-release-check"&gt;Do not use HEAD as the only release check&lt;/h2&gt;
&lt;p&gt;A server can synthesize plausible headers while the file is missing, unreadable, or truncated. Keep one small &lt;code&gt;GET&lt;/code&gt; in the smoke test and verify its body or checksum. &lt;code&gt;HEAD&lt;/code&gt; reduces transfer cost; it does not prove the payload.&lt;/p&gt;
&lt;h2 id="check-redirects-explicitly"&gt;Check redirects explicitly&lt;/h2&gt;
&lt;p&gt;Without &lt;code&gt;--location&lt;/code&gt;, &lt;code&gt;curl -I&lt;/code&gt; reports the first response. That is useful when testing redirect policy. With &lt;code&gt;--location&lt;/code&gt;, it reports the final chain, which is useful when testing the destination. Record which question the command answers:&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;curl -sS -I --max-redirs &lt;span class="m"&gt;0&lt;/span&gt; https://old.example.test/manual
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -sS -I --location https://old.example.test/manual
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="treat-method-specific-failures-as-configuration-defects"&gt;Treat method-specific failures as configuration defects&lt;/h2&gt;
&lt;p&gt;A static page that returns &lt;code&gt;200&lt;/code&gt; to &lt;code&gt;GET&lt;/code&gt; and &lt;code&gt;405&lt;/code&gt; or &lt;code&gt;404&lt;/code&gt; to &lt;code&gt;HEAD&lt;/code&gt; usually exposes a proxy or routing inconsistency. Fix the method handling instead of teaching monitors to ignore it, unless the endpoint contract deliberately disallows &lt;code&gt;HEAD&lt;/code&gt;.&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#name-head"&gt;RFC 9110 — HTTP Semantics&lt;/a&gt;, HEAD method semantics, checked 2026-06-22.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://curl.se/docs/manpage.html"&gt;curl manual&lt;/a&gt;, &lt;code&gt;--head&lt;/code&gt;, &lt;code&gt;--include&lt;/code&gt;, and redirect options, checked 2026-06-22.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item></channel></rss>