<?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>Web security · XB Field Notes</title><link>https://hk.xbcatm.com/series/web-security/</link><description>Illustrated field notes on HTTP, DNS, TLS, Caddy, SSH, releases, and recovery.</description><generator>Hugo</generator><language>en-us</language><atom:link href="https://hk.xbcatm.com/series/web-security/index.xml" rel="self" type="application/rss+xml"/><item><title>Certificate expiry is a date, not a dashboard color</title><link>https://hk.xbcatm.com/notes/certificate-expiry-is-a-date-not-a-dashboard-color/</link><pubDate>Wed, 10 Jun 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/notes/certificate-expiry-is-a-date-not-a-dashboard-color/</guid><description>Check the certificate served for the real hostname and turn the remaining days into an actionable threshold.</description><content:encoded>&lt;p&gt;An HTTPS process can be healthy while the public certificate is close to expiry, issued for the wrong name, or served differently on another edge. A useful check asks the public endpoint for the certificate it would give to a real client.&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/tls-expiry.detail.en.svg" alt="A public hostname is sent as SNI to select a certificate. Observed subject, issuer, fingerprint, notBefore, and notAfter are evidence, not a validation result. Remaining time is compared with a service-specific threshold smaller than the normal certificate lifetime. Separate checks cover hostname, chain and trust store, notBefore, and revocation, with visible diagnostics. A pipeline can print a certificate while hiding verification errors, so its final exit code cannot establish TLS health." width="720" height="3050"&gt;&lt;figcaption&gt;Inspect the certificate a real client sees, then verify separately. Expiry alerts need a service-specific response threshold, handshake checks, renewal-failure alerts, and sampling of relevant edges and address families.&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="include-the-hostname"&gt;Include the hostname&lt;/h2&gt;
&lt;p&gt;TLS virtual hosting depends on the server name. Pass the hostname as SNI and inspect the peer certificate:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;www.example.test
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;openssl s_client -connect &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="s2"&gt;:443&amp;#34;&lt;/span&gt; -servername &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &amp;lt;/dev/null 2&amp;gt;/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; &lt;span class="p"&gt;|&lt;/span&gt; openssl x509 -noout -subject -issuer -dates -fingerprint -sha256
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The command is read-only. It does not renew anything and it does not prove that every intermediate certificate is accepted by every client. It does give you the not-before, not-after, subject, issuer, and fingerprint of the certificate presented for that hostname.&lt;/p&gt;
&lt;h2 id="alert-on-a-threshold"&gt;Alert on a threshold&lt;/h2&gt;
&lt;p&gt;Choose a threshold that leaves time for a failed renewal, a DNS problem, and a human response. The exact number belongs to the service&amp;rsquo;s recovery target; a universal “30 days” rule is not a substitute for that decision. The companion &lt;a href="https://hk.xbcatm.com/resources/"&gt;TLS expiry check&lt;/a&gt; prints the expiry and exits non-zero when the remaining interval is below the threshold you pass.&lt;/p&gt;
&lt;p&gt;Run the check from outside the host when possible. A local process may see a different listener, certificate file, or proxy path than a visitor on the public network.&lt;/p&gt;
&lt;h2 id="separate-observation-from-verification"&gt;Separate observation from verification&lt;/h2&gt;
&lt;p&gt;SNI selects a virtual host; it does not verify that the certificate matches that name. This pipeline prints a certificate even when &lt;code&gt;s_client&lt;/code&gt; reports verification problems, and stderr is suppressed. Use a separate hostname-and-chain verification check with visible diagnostics and a known trust store. Do not turn this inspection pipeline’s final exit code into a TLS-health verdict.&lt;/p&gt;
&lt;p&gt;The companion helper tests &lt;code&gt;notAfter&lt;/code&gt; with OpenSSL &lt;code&gt;-checkend&lt;/code&gt;; it does not establish hostname validity, trust, revocation status, or that &lt;code&gt;notBefore&lt;/code&gt; has arrived. Pair expiry alerts with handshake checks and renewal-failure alerts. Choose a threshold smaller than the certificate’s normal lifetime, especially for short-lived certificates, and sample each relevant edge/address family.&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-10-15.&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-10-15.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.openssl.org/3.0/man1/openssl-x509/"&gt;OpenSSL &lt;code&gt;x509&lt;/code&gt; documentation&lt;/a&gt;, certificate parsing, dates, and fingerprints, checked 2026-10-15.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>HSTS should be rolled out with a recovery plan</title><link>https://hk.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://hk.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;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/hsts-rollout.detail.en.svg" alt="A rollout rail requires HTTPS and redirect checks, a short example policy and observation of site and renewal before choosing a duration. A domain tree confines includeSubDomains to the sending host’s descendants. Separate preload and recovery panels explain stored-policy limits." width="720" height="2530"&gt;&lt;figcaption&gt;Extend HSTS only after observation. Decide descendant scope and preload separately. Recovery needs valid HTTPS and cannot instantly erase every stored or preloaded policy.&lt;/figcaption&gt;&lt;/figure&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="know-what-rollback-can-clear"&gt;Know what rollback can clear&lt;/h2&gt;
&lt;p&gt;A valid HTTPS response with &lt;code&gt;max-age=0&lt;/code&gt; clears that host’s learned policy only for clients that receive it. Removing the header or sending it over HTTP does not clear a cached policy. A child cannot opt out of a parent’s still-active &lt;code&gt;includeSubDomains&lt;/code&gt;, and clearing a parent does not delete policies independently learned for children.&lt;/p&gt;
&lt;p&gt;Scope follows the host that sends the header: policy from &lt;code&gt;www.example.test&lt;/code&gt; covers that host and, with &lt;code&gt;includeSubDomains&lt;/code&gt;, its descendants, not the sibling &lt;code&gt;api.example.test&lt;/code&gt; or the parent &lt;code&gt;example.test&lt;/code&gt;. Keep HTTPS working during recovery. Preload removal is a separate browser-update process, not an immediate response-header rollback.&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-10-15.&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-10-15.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hstspreload.org/"&gt;HSTS preload program&lt;/a&gt;, submission requirements and removal constraints, checked 2026-10-15.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Security headers should describe the actual page</title><link>https://hk.xbcatm.com/notes/security-headers-should-describe-the-actual-page/</link><pubDate>Thu, 23 Jul 2026 00:00:00 +0000</pubDate><guid>https://hk.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;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/security-layers.detail.en.svg" alt="The actual HTML, local stylesheet and favicon determine CSP resource allowances. Content-Security-Policy enforces; Report-Only reveals violations and needs separate reporting setup. Deliver CSP as a header: frame-ancestors is ignored in meta and has no default-src fallback, form-action needs its own directive, and style-src self alone does not allow inline styles. HSTS is learned from valid HTTPS responses; includeSubDomains at www.example.test covers that host and descendants, not example.test or api.example.test. Check normal, missing and asset responses and verify enforcement in a real browser. No browser test is claimed." width="720" height="3460"&gt;&lt;figcaption&gt;Treat headers as a deployment contract: match actual resources, distinguish enforced and report-only policy, and verify exact host scope and browser behavior.&lt;/figcaption&gt;&lt;/figure&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 &lt;code&gt;script-src 'none'&lt;/code&gt; 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://hk.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;p&gt;Deliver the CSP as a response header. &lt;code&gt;frame-ancestors&lt;/code&gt; is ignored in a meta policy and does not inherit &lt;code&gt;default-src&lt;/code&gt;; &lt;code&gt;form-action&lt;/code&gt; also needs its own directive. &lt;code&gt;style-src 'self'&lt;/code&gt; does not allow inline styles by itself, so test the generated theme, including dark mode. Keep &lt;code&gt;data:&lt;/code&gt; in &lt;code&gt;img-src&lt;/code&gt; only when the page actually needs data images.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Content-Security-Policy-Report-Only&lt;/code&gt; can reveal violations during a rollout, but does not enforce the policy; a reporting endpoint needs separate configuration. Verify enforcement in a real browser as well as checking headers. HSTS is learned from valid HTTPS responses; with &lt;code&gt;includeSubDomains&lt;/code&gt; it also applies to descendants of the host that sets it: a policy on &lt;code&gt;www.example.test&lt;/code&gt; does not cover the parent &lt;code&gt;example.test&lt;/code&gt; or sibling &lt;code&gt;api.example.test&lt;/code&gt;. Gradually increase the lifetime after checking that exact scope.&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;li&gt;&lt;a href="https://www.w3.org/TR/CSP3/"&gt;W3C Content Security Policy Level 3&lt;/a&gt;, response-header delivery, directive fallbacks, framing, and report-only policies; current working draft, checked 2026-10-15.&lt;/li&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;, valid HTTPS delivery and host/descendant matching in sections 8.1–8.3, checked 2026-10-15.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>SSH host keys are part of the deployment record</title><link>https://hk.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://hk.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;In public-key authentication, a client proves possession of its private key; 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;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/ssh-host-key.detail.en.svg" alt="Client private-key authentication and server host-key identity are separate checks. Compare the observed candidate with an independently trusted owner or console fingerprint before installation. yes refuses unknown and changed keys; accept-new accepts unseen keys without independent proof but refuses changes. Review rotations, keep private operational records, and do not treat ssh -G as a handshake." width="720" height="2300"&gt;&lt;figcaption&gt;SSH server identity depends on comparing the observed host key with a separately trusted reference. Strict checking rejects unknown or changed keys. Trust on first use and a network key scan do not independently authenticate first contact.&lt;/figcaption&gt;&lt;/figure&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. It permits connections despite changed host keys, subject to client restrictions; warnings may still be printed, but the intended fail-closed identity check has been weakened. 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;p&gt;&lt;code&gt;accept-new&lt;/code&gt; is trust on first use, not independent proof that the first server was genuine. &lt;code&gt;ssh-keyscan&lt;/code&gt; can collect a candidate public key, but fetching it over the same untrusted network does not authenticate it. Compare its fingerprint with the separately trusted value before installing it.&lt;/p&gt;
&lt;p&gt;Inspect the effective configuration without connecting, using a standalone copy of the example saved as &lt;code&gt;deploy.conf&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ssh -G -F ./deploy.conf production-site &lt;span class="p"&gt;|&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; grep -E &lt;span class="s1"&gt;&amp;#39;^(hostname|user|stricthostkeychecking|userknownhostsfile) &amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Expect the configured host, user, known-hosts path, and strict checking (&lt;code&gt;true&lt;/code&gt; or &lt;code&gt;yes&lt;/code&gt;, depending on client output). This parses configuration only; it does not test an actual handshake. For a nonstandard port, known-hosts entries normally use &lt;code&gt;[hostname]:port&lt;/code&gt;. Keep public-key fingerprints with the private operational release record, not the public website; hostnames can reveal internal topology even though host public keys are not secrets.&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;li&gt;&lt;a href="https://man.openbsd.org/ssh-keyscan"&gt;OpenSSH ssh-keyscan&lt;/a&gt;, unverified collected keys do not prevent man-in-the-middle attacks, checked 2026-10-15.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man.openbsd.org/ssh"&gt;OpenSSH ssh&lt;/a&gt;, -G prints evaluated client configuration without opening a session, checked 2026-10-15.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Cookie scope should be narrower than the application boundary</title><link>https://hk.xbcatm.com/notes/cookie-scope-should-be-narrower-than-the-application-boundary/</link><pubDate>Mon, 15 Jun 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/notes/cookie-scope-should-be-narrower-than-the-application-boundary/</guid><description>Review Domain, Path, Secure, HttpOnly, and SameSite as one browser storage contract.</description><content:encoded>&lt;p&gt;Cookies are attached to requests according to browser rules, not according to the server component that originally set them. A broad &lt;code&gt;Domain&lt;/code&gt; or &lt;code&gt;Path&lt;/code&gt; can expose state to unrelated routes or subdomains, while missing transport and script restrictions enlarge the damage from other failures.&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/cookie-scope.detail.en.svg" alt="Cookie scope begins with host-only versus Domain scope. Path selects request paths but is not authorization. Secure restricts transport without encrypting storage. SameSite controls cross-site context; None needs Secure in modern browsers. HttpOnly blocks document.cookie access but the browser still sends cookies. Supporting browsers require Secure, no Domain and Path=/ for __Host-. Deletion matches the name and domain/path scope, meets prefix and secure-origin requirements, and is separate from server session invalidation. Static-site checks include headers and browser storage." width="720" height="3300"&gt;&lt;figcaption&gt;Review the dimensions together, but keep their roles separate: where cookies are sent, who can read them through JavaScript, and which stored identity a deletion targets.&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="prefer-a-host-only-cookie"&gt;Prefer a host-only cookie&lt;/h2&gt;
&lt;p&gt;When &lt;code&gt;Domain&lt;/code&gt; is omitted, the cookie is scoped to the host that set it. Adding &lt;code&gt;Domain=example.test&lt;/code&gt; allows it to be sent to eligible subdomains too. Use the broader form only when multiple hosts genuinely share the same application and trust boundary.&lt;/p&gt;
&lt;p&gt;Inspect the response without printing real cookie values into logs. Header names are case-insensitive, so redaction must work for lowercase HTTP/2-style names 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;curl -sS -D - -o /dev/null https://app.example.test/login &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;
&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($0) ~ /^set-cookie:/ {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; sub(/^[^:]*:[^;]*/, &amp;#34;Set-Cookie: [value redacted]&amp;#34;)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; 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; }
&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&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="review-the-attributes-together"&gt;Review the attributes together&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Secure&lt;/code&gt; restricts cookie transmission to secure transport, normally HTTPS; it does not encrypt the stored value.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;HttpOnly&lt;/code&gt; prevents JavaScript access through &lt;code&gt;document.cookie&lt;/code&gt;; it does not stop the browser from sending the cookie.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SameSite&lt;/code&gt; influences cross-site requests and must match the application&amp;rsquo;s navigation or embedding needs.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Path&lt;/code&gt; controls when a cookie is sent, but it is not an authorization boundary between applications on the same host.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A cookie using &lt;code&gt;SameSite=None&lt;/code&gt; also needs &lt;code&gt;Secure&lt;/code&gt; in modern browsers. Avoid copying that pair into applications that do not require cross-site use.&lt;/p&gt;
&lt;p&gt;Same-site is broader than same-origin: sibling HTTPS subdomains can be same-site while remaining different origins. &lt;code&gt;SameSite&lt;/code&gt; therefore cannot replace the host boundary or the application’s CSRF defenses. Cookies are also not isolated by port, even with the &lt;code&gt;__Host-&lt;/code&gt; prefix.&lt;/p&gt;
&lt;h2 id="use-prefixes-when-their-constraints-fit"&gt;Use prefixes when their constraints fit&lt;/h2&gt;
&lt;p&gt;Cookie name prefixes such as &lt;code&gt;__Secure-&lt;/code&gt; and &lt;code&gt;__Host-&lt;/code&gt; let supporting browsers enforce additional requirements. A &lt;code&gt;__Host-&lt;/code&gt; cookie must use &lt;code&gt;Secure&lt;/code&gt;, omit &lt;code&gt;Domain&lt;/code&gt;, and use &lt;code&gt;Path=/&lt;/code&gt;, which makes the host-only intention explicit.&lt;/p&gt;
&lt;h2 id="test-deletion-with-the-same-scope"&gt;Test deletion with the same scope&lt;/h2&gt;
&lt;p&gt;Delete the same cookie name and domain/path scope using &lt;code&gt;Max-Age=0&lt;/code&gt; or a past &lt;code&gt;Expires&lt;/code&gt; value; not every attribute is part of its storage identity. Meet the prefix and secure-origin requirements when sending the deletion. Preserve whether &lt;code&gt;Domain&lt;/code&gt; was omitted: expire a host-only cookie without adding &lt;code&gt;Domain&lt;/code&gt;, and specify its original &lt;code&gt;Path&lt;/code&gt;. The current 6265bis draft includes the host-only flag in storage identity, so matching only the displayed domain string is insufficient as a portable deletion recipe. Verify deletion in the target browsers. A logout that clears only one scope can leave another active, and server-side sessions or tokens may also need invalidation.&lt;/p&gt;
&lt;h2 id="keep-static-sites-cookie-free"&gt;Keep static sites cookie-free&lt;/h2&gt;
&lt;p&gt;A genuinely static documentation site normally has no reason to set application cookies. Check representative public responses for &lt;code&gt;Set-Cookie&lt;/code&gt; and inspect browser storage after navigation, including any third-party scripts. A curl header check alone cannot detect JavaScript-set cookies or other browser storage; avoid adding storage the site does not need.&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/rfc6265.html"&gt;RFC 6265 — HTTP State Management Mechanism&lt;/a&gt;, cookie storage and scope, checked 2026-10-15.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-Cookie"&gt;MDN: &lt;code&gt;Set-Cookie&lt;/code&gt;&lt;/a&gt;, attributes and cookie prefixes, checked 2026-10-15.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://httpwg.org/http-extensions/draft-ietf-httpbis-rfc6265bis.html"&gt;HTTPWG cookie specification draft&lt;/a&gt;, SameSite and cookie-prefix requirements, checked 2026-10-15.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>CAA records should authorize the issuer you actually use</title><link>https://hk.xbcatm.com/notes/caa-records-should-authorize-the-issuer-you-actually-use/</link><pubDate>Wed, 17 Jun 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/notes/caa-records-should-authorize-the-issuer-you-actually-use/</guid><description>Inspect inherited CAA policy before restricting which certificate authorities may issue for a domain.</description><content:encoded>&lt;p&gt;Certification Authority Authorization records let a domain holder state which certificate authorities may issue certificates for the domain. They are useful only when the record matches the real renewal path and the operator understands how policy can be inherited from parent names.&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/caa-policy.detail.en.svg" alt="Follow aliases and walk upward to the first relevant nonempty CAA set, then stop without merging parents. Use the CA issuer identifier; issuewild overrides issue for wildcard requests. CAA constrains issuance, does not perform an HTTPS trust test or revoke existing certificates. Stage renewal changes while the current certificate is healthy." width="720" height="2164"&gt;&lt;figcaption&gt;CAA authorizes issuers at issuance time. Lookup stops at the first relevant nonempty record set; wildcard tags refine the rule. Existing certificates and HTTPS trust are separate boundaries. The displayed issuer is an example.&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="inspect-the-current-policy"&gt;Inspect the current policy&lt;/h2&gt;
&lt;p&gt;Query the exact hostname and its parent names:&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="k"&gt;for&lt;/span&gt; name in www.example.test example.test test&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;$name&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; dig +noall +answer CAA &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$name&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;These are placeholder names, queried through the configured resolver. An empty answer at the leaf does not necessarily mean there is no applicable policy: lookup follows aliases and stops at the first nonempty relevant CAA RRset while walking upward. It does not merge every parent’s restrictions. Inspect DNS status and authority information too; an empty answer-only display can hide &lt;code&gt;NXDOMAIN&lt;/code&gt; or &lt;code&gt;SERVFAIL&lt;/code&gt;. Compare authoritative answers with recursive results when diagnosing changes.&lt;/p&gt;
&lt;h2 id="match-the-cas-issuer-identifier"&gt;Match the CA’s issuer identifier&lt;/h2&gt;
&lt;p&gt;A record uses tags such as &lt;code&gt;issue&lt;/code&gt; or &lt;code&gt;issuewild&lt;/code&gt; and an issuer-domain value:&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;example.test. 3600 IN CAA 0 issue &amp;#34;letsencrypt.org&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The value is an authorization identifier, not the display name from a certificate. Confirm it in the certificate authority&amp;rsquo;s current documentation. Without &lt;code&gt;issuewild&lt;/code&gt;, &lt;code&gt;issue&lt;/code&gt; also governs wildcard issuance. When &lt;code&gt;issuewild&lt;/code&gt; is present, it takes precedence for wildcard requests; multiple records of the same tag grant alternatives rather than requiring every CA. CAA does not select a particular intermediate certificate.&lt;/p&gt;
&lt;h2 id="stage-the-dns-change-before-renewal"&gt;Stage the DNS change before renewal&lt;/h2&gt;
&lt;p&gt;Add or tighten CAA while the current certificate is healthy. Wait through the previous TTL, query authoritative and recursive answers, and run a staging or renewal test supported by the ACME client. Keep a rollback record ready.&lt;/p&gt;
&lt;p&gt;Do not add a restrictive record during an expiry incident unless the issuer path has already been verified. A syntactically correct but wrong policy can turn automatic renewal into a hard failure.&lt;/p&gt;
&lt;h2 id="observe-issuance-failures"&gt;Observe issuance failures&lt;/h2&gt;
&lt;p&gt;CAA checks happen at issuance time, not on every HTTPS request. Changing CAA neither revokes existing certificates nor proves that a historical issuance complied with the policy at that time. Certificate monitoring must therefore cover both current expiry and renewal attempts. Log the effective DNS answer with the ACME failure instead of recording only a generic authorization error.&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/rfc8659.html"&gt;RFC 8659 — DNS Certification Authority Authorization&lt;/a&gt;, CAA lookup and property semantics, checked 2026-10-15.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://letsencrypt.org/docs/caa/"&gt;Let&amp;rsquo;s Encrypt: CAA&lt;/a&gt;, issuer identifiers and operational guidance, checked 2026-10-15.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>A certificate chain is a path you should be able to print</title><link>https://hk.xbcatm.com/notes/a-certificate-chain-is-a-path-you-should-be-able-to-print/</link><pubDate>Mon, 08 Jun 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/notes/a-certificate-chain-is-a-path-you-should-be-able-to-print/</guid><description>Check the server’s certificate list, the client’s trust anchor, and the requested hostname as separate parts of TLS validation.</description><content:encoded>&lt;p&gt;A TLS client must build a valid certificate path to an anchor it already trusts, and verify the requested hostname. Printing certificates is a useful diagnostic, but does not establish either result by itself.&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/cert-chain.detail.en.svg" alt="A server-sent leaf and needed intermediates feed client path building. A separately configured trust anchor supplies trust. Requested hostname and certificate validity are additional checks; printing or counting certificates proves none of these alone." width="720" height="1740"&gt;&lt;figcaption&gt;The server supplies certificates; trust comes from the client’s anchor. Validate the path, requested hostname and validity period. Printed certificate counts cannot establish those results.&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="the-leaf-is-not-the-whole-story"&gt;The leaf is not the whole story&lt;/h2&gt;
&lt;p&gt;The endpoint presents the &lt;em&gt;leaf&lt;/em&gt; first. It contains the public key, validity period, and subject alternative names used for hostname checks. A public-CA deployment normally also sends the intermediates needed to build a path. Missing intermediates can break clients that do not already have them cached or cannot retrieve them.&lt;/p&gt;
&lt;p&gt;The self-signed root normally need not be sent: trust comes from the client’s configured anchors, not from a server including a certificate. A cross-signed certificate whose subject says “Root” can instead be an intermediate in the selected path. Certificate count alone is therefore not a completeness or trust test.&lt;/p&gt;
&lt;h2 id="read-the-chain-the-edge-actually-sends"&gt;Read the chain the edge actually sends&lt;/h2&gt;
&lt;p&gt;OpenSSL documents &lt;code&gt;s_client -showcerts&lt;/code&gt; as the server’s certificate list, not a verified chain. The earlier version of this note recorded this site snapshot:&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;certificate 0: 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; 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 1: CN=YE1 (intermediate)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; issuer : C=US, O=ISRG, CN=Root YE
&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 2/3: Root YE / ISRG Root … (CA certificates)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The original report counted four blocks. Its raw handshake log is not retained here, and this review did not reproduce the live handshake; treat the values as a historical report, not current edge evidence. Let’s Encrypt’s chain documentation does describe YE1 and a cross-signed Root YE path, so those names alone are not evidence of an invalid chain. Which certificate terminates trust depends on the client’s trust store and path building.&lt;/p&gt;
&lt;h2 id="keep-issuance-authorization-separate"&gt;Keep issuance authorization separate&lt;/h2&gt;
&lt;p&gt;Intermediates let a CA keep its root key offline and separate issuance operations. Revoking an intermediate can invalidate dependent leaf paths and may require replacement certificates; it does not preserve every leaf automatically.&lt;/p&gt;
&lt;p&gt;CAA authorizes a CA’s documented issuer-domain identifier at issuance time. It does not pin an intermediate, change browser trust, or prove that an existing certificate complied with historical CAA. The &lt;code&gt;dns-caa-records&lt;/code&gt; note covers that separate policy check.&lt;/p&gt;
&lt;h2 id="count-for-inventory-validate-for-trust"&gt;Count for inventory, validate for trust&lt;/h2&gt;
&lt;p&gt;The earlier inventory command was:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ echo | openssl s_client -connect www.xbcatm.com:443 -servername www.xbcatm.com -showcerts 2&amp;gt;/dev/null | grep &amp;#39;BEGIN CERT&amp;#39; | wc -l
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;4
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This is read-only network traffic, but it suppresses diagnostics and the final count can succeed even when an earlier command failed. Do not use &lt;code&gt;4&lt;/code&gt;, or any other fixed count, as a health threshold. A leaf-only result warrants checking the expected issuer path rather than declaring every one-certificate chain invalid.&lt;/p&gt;
&lt;p&gt;For actual validation, configure hostname checking and failure on verification errors with OpenSSL’s &lt;code&gt;-verify_hostname&lt;/code&gt; and &lt;code&gt;-verify_return_error&lt;/code&gt; options, retain diagnostics, and record the trust store and client version. Also test the clients you support; one OpenSSL trust store cannot establish compatibility with all browsers.&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/rfc8446.html"&gt;RFC 8446 — TLS 1.3&lt;/a&gt;, section 4.4.2 certificate-list order and optional trust-anchor omission, checked 2026-10-15.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc5280.html"&gt;RFC 5280 — X.509 PKI&lt;/a&gt;, section 6 trust-anchor input and path validation, checked 2026-10-15.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://letsencrypt.org/docs/faq/"&gt;Let&amp;rsquo;s Encrypt FAQ&lt;/a&gt;, certificate compatibility overview, checked 2026-10-15.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://letsencrypt.org/certificates/"&gt;Let&amp;rsquo;s Encrypt: Chains of Trust&lt;/a&gt;, YE1 and cross-signed Root YE paths, checked 2026-10-15.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.openssl.org/3.0/man1/openssl-s_client/"&gt;OpenSSL &lt;code&gt;s_client&lt;/code&gt;&lt;/a&gt;, presented-list and verification options, checked 2026-10-15.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc8659.html"&gt;RFC 8659 — CAA&lt;/a&gt;, issuance authorization rather than client trust, checked 2026-10-15.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Audit the trust of a public edge</title><link>https://hk.xbcatm.com/tutorials/audit-a-public-edge/</link><pubDate>Thu, 06 Aug 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/tutorials/audit-a-public-edge/</guid><description>A read-only walkthrough that inspects security headers, TLS identity, certificate chain, host keys, CAA, and cookie scope the way a careful operator would.</description><content:encoded>&lt;p&gt;The web-security series treats settings as contracts with scope and failure modes. This walkthrough inspects a host you are authorized to query. The HTTP, TLS, and DNS requests are read-only observations, although they still generate logs and traffic. SSH checks read your own trust record; the deployment host may differ from the website hostname.&lt;/p&gt;
&lt;p&gt;Use a disposable local working directory for captured output. No output shown here is a fresh measurement of &lt;code&gt;www.xbcatm.com&lt;/code&gt;, and a passing command proves only the property it actually checks.&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/tutorial-security.detail.en.svg" alt="Independent security evidence lanes distinguish SNI from verification, expiry from trust, variable presented chains, HSTS host scope, applicable CAA issuance policy, local SSH records, cookie risk controls and body ETag limits. The observations do not certify the whole service secure." width="720" height="4260"&gt;&lt;figcaption&gt;Each observation supports a bounded claim. Header presence, certificate dates and local records cannot combine into a whole-service safety badge.&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="1-start-with-the-policy-the-edge-advertises"&gt;1. Start with the policy the edge advertises&lt;/h2&gt;
&lt;p&gt;Fetch a GET response without silently following redirects, then inspect its status and headers:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;www.xbcatm.com
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;https://&lt;/span&gt;&lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="s2"&gt;/&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -sS --max-time &lt;span class="m"&gt;20&lt;/span&gt; -D headers.txt -o body.html &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cat headers.txt
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The repository&amp;rsquo;s intended policy includes the following illustrative fields:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Content-Security-Policy: default-src &amp;#39;self&amp;#39;; base-uri &amp;#39;self&amp;#39;; form-action &amp;#39;none&amp;#39;; frame-ancestors &amp;#39;none&amp;#39;; object-src &amp;#39;none&amp;#39;; script-src &amp;#39;none&amp;#39;; style-src &amp;#39;self&amp;#39;; img-src &amp;#39;self&amp;#39; data:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Strict-Transport-Security: max-age=31536000; includeSubDomains
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;X-Content-Type-Options: nosniff
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;X-Frame-Options: DENY
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;script-src 'none'&lt;/code&gt; is a strict choice for this script-free site, not a universal recipe for every static site. The browser enforces CSP; a report-only header does not block execution. Read the complete policy for more-specific script directives, and compare normal pages and error routes. &lt;code&gt;nosniff&lt;/code&gt; constrains MIME handling, notably script/style destinations, but does not certify the body&amp;rsquo;s media type.&lt;/p&gt;
&lt;p&gt;HSTS is learned from valid HTTPS responses and applies to the host that sent it. A policy received from &lt;code&gt;www.example.com&lt;/code&gt; with &lt;code&gt;includeSubDomains&lt;/code&gt; covers descendants of that name, not its sibling &lt;code&gt;api.example.com&lt;/code&gt; or parent &lt;code&gt;example.com&lt;/code&gt;. Inventory the actual scope before adopting a long duration. A proxy adding or stripping headers changes the browser contract, which is why artifact checks and edge checks are separate.&lt;/p&gt;
&lt;h2 id="2-verify-tls-identity-before-reading-certificate-dates"&gt;2. Verify TLS identity before reading certificate dates&lt;/h2&gt;
&lt;p&gt;SNI selects the intended virtual host; it does not verify the certificate name. Make verification failure fatal and explicitly request hostname validation. Use an up-to-date trust store appropriate to the clients under test.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;openssl s_client -connect &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="s2"&gt;:443&amp;#34;&lt;/span&gt; -servername &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -verify_hostname &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; -verify_return_error -showcerts &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;lt;/dev/null &amp;gt; tls.txt 2&amp;gt; tls.log
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Continue only if the command succeeded; preserve tls.log on failure.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;openssl x509 -in tls.txt -noout -subject -issuer -dates -ext subjectAltName
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Run these as separate checked steps, or under a shell setup that stops on failure. Hiding stderr or piping into a successful &lt;code&gt;x509&lt;/code&gt; command can conceal an unsuccessful handshake. Inspect the DNS names in &lt;code&gt;subjectAltName&lt;/code&gt;; the displayed subject CN is not sufficient for modern service-identity validation. Some library versions retain legacy CN fallback, so explicitly check that the expected SAN is present. A valid matching wildcard may be legitimate; an unrelated certificate is not.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;notAfter&lt;/code&gt; gives the expiry time. It does not prove the chain is trusted, the hostname matches, or revocation was checked. Record the date and the measured safety window rather than relying on memory. OpenSSL&amp;rsquo;s result describes its configured trust store and checks, not every browser&amp;rsquo;s behavior.&lt;/p&gt;
&lt;h2 id="3-read-the-presented-chain-without-inventing-a-fixed-count"&gt;3. Read the presented chain without inventing a fixed count&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;-showcerts&lt;/code&gt; output contains certificates sent by the server, not a declaration that they form a verified chain. This command displays their count only:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;grep -c &lt;span class="s1"&gt;&amp;#39;BEGIN CERTIFICATE&amp;#39;&lt;/span&gt; tls.txt
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;There is no required count of four. The server normally supplies the leaf and necessary intermediate certificates; a root usually belongs in the client&amp;rsquo;s trust store and need not be sent. Chain length varies with the CA and chosen path. One certificate warrants investigating whether intermediates are needed, but a count alone is neither success nor failure.&lt;/p&gt;
&lt;p&gt;Use the actual verification result from step 2, then investigate issuer/SAN changes, missing intermediates, expiry, and trust-store differences. An intermediate rotation can be normal CA maintenance. A numerically unchanged chain can still be wrong, and the count command does not automatically compare anything with an expected value.&lt;/p&gt;
&lt;h2 id="4-revalidate-the-captured-representation"&gt;4. Revalidate the captured representation&lt;/h2&gt;
&lt;p&gt;Caching is a separate contract, not evidence of TLS identity. Capture a current ETag and use it on a request selecting the same representation, as in the &lt;a href="https://hk.xbcatm.com/tutorials/http-edge-walkthrough/"&gt;HTTP walkthrough&lt;/a&gt;. A matching conditional GET can return 304; a deployment between requests can legitimately produce 200. An old hardcoded validator is not a reusable test fixture for a changing site.&lt;/p&gt;
&lt;p&gt;A no-script policy does not mean CSP never changes. Security headers can change independently of HTML, and a body ETag does not fingerprint every response header. Record the actual policy again after release.&lt;/p&gt;
&lt;h2 id="5-find-the-relevant-caa-policy"&gt;5. Find the relevant CAA policy&lt;/h2&gt;
&lt;p&gt;CAA constrains issuance by compliant certificate authorities. It is not certificate pinning and does not retroactively invalidate an already issued certificate. Start by inspecting the queried name and its ancestors; retain response status so a DNS failure is not mistaken for an empty answer.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;dig www.xbcatm.com CAA +noall +comments +answer
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;dig xbcatm.com CAA +noall +comments +answer
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;An empty answer at &lt;code&gt;www&lt;/code&gt; does not mean issuance is unrestricted. RFC 8659 searches upward until the first relevant nonempty CAA set, following aliases during each lookup; continue through applicable ancestors if necessary. Inspect &lt;code&gt;issue&lt;/code&gt;, and &lt;code&gt;issuewild&lt;/code&gt; when wildcard issuance is involved. An &lt;code&gt;iodef&lt;/code&gt; report address alone does not restrict issuance. If no restricting policy applies, CAA adds no issuer restriction, but normal domain validation and CA rules still apply.&lt;/p&gt;
&lt;p&gt;Compare the CA&amp;rsquo;s documented CAA identifier with the relevant policy, not merely the leaf&amp;rsquo;s human-readable issuer CN. Today&amp;rsquo;s CAA result is evidence about today&amp;rsquo;s DNS policy, not proof of what existed when the certificate was issued.&lt;/p&gt;
&lt;h2 id="6-check-the-deployment-hosts-recorded-ssh-identity"&gt;6. Check the deployment host&amp;rsquo;s recorded SSH identity&lt;/h2&gt;
&lt;p&gt;Use the actual SSH destination, including the bracketed host/port form for a non-default port. &lt;code&gt;ssh-keygen -F&lt;/code&gt; can find hashed known-host entries that a text grep of the hostname misses.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;deploy_host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;deploy.example.test
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ssh-keygen -F &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$deploy_host&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; -f &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.ssh/known_hosts&amp;#34;&lt;/span&gt; &amp;gt; host-record.txt
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ssh-keygen -lf host-record.txt
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Compare the fingerprint with an independently trusted record, such as the provider console or a verified administrator channel. This reads the local record; it does not prove which key the remote host presents now. A network key scan alone would not authenticate that key either. Investigate a changed key before updating trust; a rebuild is one possible explanation, not automatic permission to accept it.&lt;/p&gt;
&lt;h2 id="7-keep-cookie-scope-no-broader-than-necessary"&gt;7. Keep cookie scope no broader than necessary&lt;/h2&gt;
&lt;p&gt;Inspect all &lt;code&gt;Set-Cookie&lt;/code&gt; fields in the captured response and any relevant redirect or application route. No cookie on one page does not establish that the entire site never sets cookies. A host-only cookie omits &lt;code&gt;Domain&lt;/code&gt;; adding a parent Domain broadens the hosts receiving it. &lt;code&gt;Path&lt;/code&gt; controls delivery but is not a security boundary against same-origin code.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Secure&lt;/code&gt; restricts transport, &lt;code&gt;HttpOnly&lt;/code&gt; blocks script access to the cookie, and &lt;code&gt;SameSite&lt;/code&gt; controls specified cross-site sending behavior. They address different risks and do not replace one another; &lt;code&gt;SameSite=None&lt;/code&gt; requires &lt;code&gt;Secure&lt;/code&gt; in modern browsers. For this site&amp;rsquo;s no-account content model, ask why a cookie is needed at all before broadening its scope. Redact cookie values from shared audit records.&lt;/p&gt;
&lt;h2 id="keep-a-bounded-audit-record"&gt;Keep a bounded audit record&lt;/h2&gt;
&lt;p&gt;Record the UTC time, client versions, requested/final URL, status, enforcing headers, TLS verification result and dates, relevant CAA lookup, and SSH record source. Distinguish observed success, unexpected results, and checks not performed. These several observations take more than “two commands” and cannot prove the whole service secure.&lt;/p&gt;
&lt;p&gt;Repeat the relevant checks after certificate, DNS, proxy, or deployment changes. Preserve prior results to identify drift, and test actual browser behavior separately when the claim depends on browser enforcement.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://fetch.spec.whatwg.org/#x-content-type-options-header"&gt;WHATWG Fetch Standard&lt;/a&gt;, nosniff processing and request destinations, checked 2026-10-15.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://docs.openssl.org/3.0/man1/openssl-s_client/"&gt;OpenSSL &lt;code&gt;s_client&lt;/code&gt;&lt;/a&gt;, SNI, hostname verification, fatal verification errors, and presented chains, checked 2026-10-15.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc9525.html"&gt;RFC 9525: Service Identity in TLS&lt;/a&gt;, SAN-based service identity and wildcard matching, checked 2026-10-15.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc8659.html"&gt;RFC 8659: CAA&lt;/a&gt;, relevant record-set search and issuance policy, checked 2026-10-15.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc6797.html"&gt;RFC 6797: HSTS&lt;/a&gt;, host scope and policy processing, checked 2026-10-15.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://man.openbsd.org/ssh-keygen"&gt;OpenSSH &lt;code&gt;ssh-keygen&lt;/code&gt;&lt;/a&gt;, known-host lookup and fingerprint display, checked 2026-10-15.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://www.w3.org/TR/CSP3/"&gt;W3C: CSP Level 3&lt;/a&gt;, enforcing versus report-only policy and script directive fallback (working draft), checked 2026-10-15.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-Cookie"&gt;MDN: Set-Cookie&lt;/a&gt;, cookie attributes and their limits, checked 2026-10-15.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item></channel></rss>