diff --git a/TODO.md b/TODO.md index 75f5713..2ec1cd0 100644 --- a/TODO.md +++ b/TODO.md @@ -18,7 +18,9 @@ dotfiles; `LICENSE` is the only mandated file still missing. Every external reference in the repo is now pinned by cryptographic hash (or, for the wrangler CLI install, an exact version), and the Hugo that builds the published site is a deliberate pinned version rather than whatever the base image's package repo -serves. +serves. The site now ships a Cloudflare Pages `_headers` file, so its response +security headers are declared in the repo instead of being whatever the edge +defaults to — unverified in production until the next deploy. # Next Step @@ -28,6 +30,30 @@ remaining policy scaffold is otherwise complete. # Completed Steps +- 2026-08-09: added `static/_headers` so Cloudflare Pages serves baseline + response security headers (closes #14). Hugo copies `static/` verbatim into + `public/`, which is the deploy root Pages reads the file from; this is the + first root-level `static/` in the repo, and the built tree confirms it unions + with the theme's rather than shadowing it — `public/css/style.css` and + `public/index.html` are byte-identical to the previous build and the static + file count goes 1 to 2. The live "before" was measured, not assumed: + Cloudflare already sends `X-Content-Type-Options` and `Referrer-Policy` by + default, so the substance here is `Strict-Transport-Security`, + `Content-Security-Policy`, `X-Frame-Options` and `Permissions-Policy`. The CSP + is `default-src 'none'` with `style-src 'unsafe-inline'`, which the built page + supports exactly: it has no script, img, link, iframe, form or media element + and no `style=`/`on*=` attribute, only the one inline `<style>` block + `baseof.html` fills by `readFile`. Verified in a headless Chrome against a + local server that parses the committed `_headers` and applies it as real + response headers: zero CSP violations, the inlined stylesheet parses to 17 + rules and the computed body padding, tagline colour and link colour all come + from the theme CSS, framing the page from another origin is refused by + `frame-ancestors 'none'` (consistent with `X-Frame-Options: DENY`), and all + five named outbound links still navigate with status 200. HSTS carries neither + `preload` nor `includeSubDomains`: `www.lora.vegas` is the only other name in + DNS and it is served by this same Pages project, so this file sets HSTS on its + responses directly, and `includeSubDomains` would instead bind every future + subdomain for a year with no way to walk it back inside the max-age window - 2026-08-09: restructured `README.md` into the canonical section set (closes #11): a Description first line, then Getting Started, Entrypoints, Rationale, Design, TODO, License, Author. The non-standard About / Contributing / @@ -178,6 +204,14 @@ remaining policy scaffold is otherwise complete. here (#20) - Move the deploy container to a pinned node 22 so the wrangler pin can advance past 4.86.0 (#21) +- After the next deploy, confirm the `_headers` file actually took effect: + `curl -sSI https://lora.vegas/` must show `strict-transport-security` or + `content-security-policy`. Cloudflare Pages silently ignores a malformed + `_headers`, and checking `x-content-type-options` would pass either way + because the edge sends it regardless (#14) +- Decide the HSTS `includeSubDomains` and `preload` posture for `lora.vegas`. + Both are owner calls: neither can be walked back inside the max-age window, + and `includeSubDomains` binds hostnames this repo does not control (#14) - Sync the reformat of `REPO_POLICIES.md` back upstream to `prompts` so the canonical copy is clean under the shared prettier settings and future syncs are a straight byte copy diff --git a/static/_headers b/static/_headers new file mode 100644 index 0000000..40dfac7 --- /dev/null +++ b/static/_headers @@ -0,0 +1,42 @@ +# Cloudflare Pages response headers. +# +# Hugo copies static/ verbatim into public/, so this file lands at the +# deploy output root, which is where Pages reads it from. Pages consumes +# the file rather than publishing it. Values here override what +# Cloudflare would otherwise send. +# +# Every value below was checked against the built public/index.html, not +# copied from a template. That page loads nothing: no script, img, link, +# iframe, form, video, audio, object or embed element, no style= or on*= +# attribute. It has exactly one inline