**Phase 03 final verification — all green** - Verified (all tasks already in `complete/`): snippet/Dockerfile/nginx.conf wiring, `scripts/check-headers.sh`, guardrail teeth, restored config byte-identical. - Tests (full suite): `./build.sh` → OK; `npm test` → **46 passed, 0 failed**; `bash .agents/validate.sh` → **exit 0** (46/46 + `HEADER CHECK PASSED` + hygiene gates); no leftover podman images. - Completion criteria: - `nginx-security-headers.inc` with 4 `add_header … always;` lines — ✅ - `nginx.conf`: 0 inline security `add_header` (cache headers only), 3/3 locations `include` snippet — ✅ - `server_tokens off;` in server block — ✅ (live proof: `Server: nginx`, no version) - `./scripts/check-headers.sh` → `HEADER CHECK PASSED`, OK on `/`, `/index.html`, hashed asset — ✅ (also confirmed all 4 headers + correct Cache-Control on 404 fallback) - `./build.sh && npm test` fully green — ✅ - No behavior change (terminal/vim/achievements/menu suites) — ✅ - Negative check re-run: dropping one `include` → `FAIL … on /style.<hash>.css`, exit 1 — ✅ - Docs: README.md & AGENTS.md reviewed in full against actual state (commands, ports 8123/18099, header mechanism, CI tags vs `.gitea/workflows/build-push.yml`, Node ≥20 engine floor, innerHTML/neofetch exception at `terminal.js:487`, simulated curl/wget guards) — **verified consistent, no changes needed**. - Notable: check-headers.sh aborts at the first failing path via `set -e` before printing `HEADER CHECK FAILED` — README's wording matches this exactly (verified empirically). PLAN.md's "44 tests" count is stale (now 46) but PLAN.md is locked — left for the user. - No live-infra changes (no HSTS in container; edge/Caddy untouched). - Next pending phase: `04_huntington_role`.
This commit is contained in:
@@ -34,21 +34,42 @@ Visit `http://localhost:8080` in your browser.
|
||||
## Serving & configuration
|
||||
|
||||
`nginx.conf` is copied into the image as the site config. It listens on
|
||||
**8080 over plain HTTP**, serves the hashed build, declares the hardening
|
||||
headers (`X-Frame-Options`, `X-Content-Type-Options`, `Referrer-Policy`,
|
||||
`Content-Security-Policy`), serves `index.html` with `no-cache`, and caches the
|
||||
hashed assets as `immutable` for a year.
|
||||
**8080 over plain HTTP**, serves the hashed build, serves `index.html` with
|
||||
`no-cache`, and caches the hashed assets as `immutable` for a year.
|
||||
|
||||
Known gap (audit finding M-4, Phase 03): nginx does not merge `add_header`
|
||||
across configuration levels, so the `location` blocks that set their own headers
|
||||
drop the server-level security headers — measured against the built image, `/`,
|
||||
`/index.html` and a hashed asset currently get only the cache headers. Every
|
||||
`add_header` block must repeat the hardening headers until that phase lands.
|
||||
The four hardening headers (`X-Frame-Options`, `X-Content-Type-Options`,
|
||||
`Referrer-Policy`, `Content-Security-Policy`) live in
|
||||
`nginx-security-headers.inc`, which the Dockerfile copies to
|
||||
`/etc/nginx/snippets/security-headers.inc`. nginx does **not** merge
|
||||
`add_header` across configuration levels, so every `location` block `include`s
|
||||
that snippet instead of inheriting from the `server` block — otherwise the
|
||||
locations that set their own cache headers would silently drop the security
|
||||
headers (audit finding M-4, Phase 03). `server_tokens off;` hides the nginx
|
||||
version from the `Server` header (audit finding L-2). When adding a new
|
||||
`location`, `include` the snippet there too and keep the security headers out of
|
||||
`nginx.conf` itself.
|
||||
|
||||
TLS and HSTS are the job of the reverse proxy in front of the container
|
||||
(`reeseapps.com` terminates at the Caddy edge). Do **not** add
|
||||
`Strict-Transport-Security` or a `listen 443` block to `nginx.conf`.
|
||||
|
||||
### Verify the headers
|
||||
|
||||
```bash
|
||||
./scripts/check-headers.sh
|
||||
```
|
||||
|
||||
This is the integration test for the header fix: it builds the **real** image
|
||||
with podman (or docker), runs it on the scratch address `127.0.0.1:18099`, and
|
||||
asserts all four security headers on each response class — `/`, `/index.html`
|
||||
and the first hashed asset found in `index.html`. On success it prints one
|
||||
`OK:` line per path and `HEADER CHECK PASSED` (exit 0); a missing header prints
|
||||
`FAIL: <header> missing on <path>` and exits non-zero, aborting at the first
|
||||
failing path. No local `./build.sh` is needed — the image builds itself. The
|
||||
container is removed by the script; `.agents/validate.sh` removes the leftover
|
||||
`homepage-headercheck-*` image tag. Run it after **any** change to
|
||||
`nginx.conf`, `nginx-security-headers.inc` or the `Dockerfile`.
|
||||
|
||||
## Deploy with Quadlet
|
||||
|
||||
The `homepage.container` Quadlet unit is **not** tracked in this repository — it
|
||||
@@ -115,4 +136,7 @@ revision cached for the pinned Playwright version — run
|
||||
bumping the pin.
|
||||
|
||||
The validation gate the phased pipeline runs per task is `.agents/validate.sh`
|
||||
(full E2E suite plus repo-hygiene checks): `bash .agents/validate.sh`.
|
||||
(full E2E suite, the container security-header check from
|
||||
`scripts/check-headers.sh`, plus repo-hygiene checks): `bash
|
||||
.agents/validate.sh`. The header check is skipped with a loud `WARN:` line on a
|
||||
host with no container runtime; everything else still runs.
|
||||
|
||||
Reference in New Issue
Block a user