The web-security series treats security settings as contracts with scope and failure modes. This tutorial runs those contracts end to end, read-only, against a live host, and tells you what each answer means. It does not change the server; it inspects what the server will tell anyone. Use www.xbcatm.com below and substitute your own host throughout.

1. Start with the headers the edge advertises

The first contract is what a browser is told to allow. Fetch the headers with check-http-headers.sh (or any read-only fetch) and read the actual policy:

$ curl -s -D - -o /dev/null https://www.xbcatm.com/
HTTP/2 200
content-security-policy: default-src 'self'; base-uri 'self'; form-action 'none'; frame-ancestors 'none'; object-src 'none'; script-src 'none'; style-src 'self'; img-src 'self' data:
strict-transport-security: max-age=31536000; includeSubDomains
x-content-type-options: nosniff
x-frame-options: DENY

Each header is a claim about a specific failure mode. content-security-policy with script-src 'none' is the strongest for a static site, and security-headers explains why the policy should match the actual page. strict-transport-security with includeSubDomains is the transport promise, and hsts-rollout warns it must be rolled out in measured steps because it is sticky. nosniff stops MIME-type guessing. A header you intend but did not verify is a bug, not a feature.

Step 1 is static, but it is the contract a browser sees first. For a static site, script-src 'none' is both safe and honest: there is nothing to execute, so nothing runs. The important habit is to compare what you believe the policy is with what the edge actually sends. A proxy that strips or adds a header silently changes the contract; the read-only fetch catches that drift. If you use a tool like check-http-headers.sh, it reads the status and headers together, which keeps the policy and the reachability claim in the same record.

2. Confirm the identity is real, not a guess

The TLS identity starts with the leaf certificate. Read its exact subject, issuer, and validity dates:

$ echo | openssl s_client -connect www.xbcatm.com:443 -servername www.xbcatm.com 2>/dev/null \
  | openssl x509 -noout -subject -issuer -dates
subject=CN=www.xbcatm.com
issuer=C=US, O=Let's Encrypt, CN=YE1
notBefore=Aug 21 08:37:13 2026 GMT
notAfter=Nov 19 08:37:12 2026 GMT

The subject is the exact hostname, not a wildcard from an adjacent certificate, and notAfter is a concrete date. certificate-expiry treats that date as the release gate rather than a dashboard color. An operator should be able to quote notAfter from memory of the last audit, not discover it at renewal time.

3. Read the chain, not just the leaf

A leaf only has value inside a chain the client can walk to a trusted root. Ask the edge what chain it actually presents:

$ echo | openssl s_client -connect www.xbcatm.com:443 -servername www.xbcatm.com -showcerts 2>/dev/null | grep 'BEGIN CERT' | wc -l
4

With its leaf plus intermediates, that number is expected to be 4. The certificate-chain note explains why: the leaf must be signed by an intermediate you actually authorized, and the edge must send that intermediate so deep-validation clients succeed. Count of exactly one is a red flag on a public CA. A second reason to check the chain is that the identity you read in step 2 only proves the leaf; it does not prove the client can reach a root it trusts. The chain is what closes that gap. If the edge ever starts sending a different intermediate — say, a test CA one day — the count and the issuer of certificate 1 change before any element on the page does. Reading the chain is the cheapest way to notice that trust moved before it matters.

4. Trade a validator to prove freshness is intact

The conditional-requests note applies here too: a live edge should answer a 304 to a revalidation so caches and clients do not re-download. This is also evidence that caching is working as the headers promise:

$ curl -s -o /dev/null -w 'HTTP %{http_code}\n' -H 'If-None-Match: "dkx84jwth7nkc9e"' https://www.xbcatm.com/
HTTP 304

Because this site sticks to a script-free policy, the CSP never changes between releases; only the etag moves when bytes change.

5. Verify which CA is allowed to issue

TLS identity goes hand in hand with DNS policy. Confirm the CAA record authorizes the issuer you actually use, so only that CA can legally sign for the domain:

$ dig +short CAA www.xbcatm.com

dns-caa-records explains CAA as supplier of trust: it authorizes the issuer, making the certificate-chain audit meaningful. Without a CAA record, any CA can issue; with one, the intermediate from step 4 should trace back to an authorized issuer.

6. Check who holds the host key

The chain protects TLS, but another trust boundary is SSH access. A host key is the deployment record for the server you SSH into. ssh-host-key treats it as part of the record you pin before you trust a connection:

$ ssh-keygen -lf ~/.ssh/known_hosts | grep www.xbcatm.com

Confirm the fingerprint matches what you recorded when the host was added. A changed host key after a rebuild is either a legitimate reinstall or a reason to stop — this is exactly why host-key policy exists as a web-security boundary, not just a convenience.

Even a no-script static site can set cookies; if it does, their scope is a security claim. cookie-scope says a cookie’s host, path, transport, and same-site boundaries should be narrower than the application’s reach. Audit that no cookie is set for a broader scope than needed, and prefer Secure; HttpOnly; SameSite for anything sensitive.

The audit as a checklist

SignalRead it asNotes
CSP script-src 'none'no-script policy is livesecurity-headers
HSTS includeSubDomainstransport promise is stickyhsts-rollout
certificate subject+datesidentity is a concrete datecertificate-expiry
chain shows 4 certsintermediates are sentcertificate-chain
304 on revalidationcache validator worksconditional
CAA authorizedonly CA can issuedns-caa
host key footprint matchesSSH trust is a recordssh-host-key

Each signal was a note; together they are a single checklist. You add all these in under a minute with two read-only commands, leaving the server untouched.

Typical-procedure notes

This audit belongs in a scheduled check, not only at release. If you already have a set of checks, add the two openssl lines and the header fetch to them. Keep the outputs in a file named by date so a later comparison can show drift. That is how a static site stays evidence-first even when nothing is changing.

Sources