ducoterra 47b7b4fc23
Build and Push Container / build-and-push (push) Successful in 23s
phase: 03_nginx_security_headers
**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`.
2026-10-23 00:07:13 -04:00
2026-10-23 00:07:13 -04:00
2026-05-27 21:52:36 -04:00
2026-10-23 00:07:13 -04:00
2026-10-22 23:14:49 -04:00
2026-06-03 10:17:52 -04:00
2026-10-23 00:07:13 -04:00
2026-10-23 00:07:13 -04:00
2026-10-23 00:07:13 -04:00
2026-10-22 23:14:49 -04:00

Developer Homepage

A static portfolio site (vanilla HTML/CSS/JS + a fake terminal easter egg) packaged as an nginx container and deployed with Podman Quadlet.

Build & Run with Podman

Build the image

podman build -t homepage:latest .

Run the container

The image runs build.sh itself and copies the hashed dist/ output into the nginx html root, so the built site is already inside the image:

podman run --rm -p 0.0.0.0:8080:8080 homepage:latest

To iterate on src/ without rebuilding the image, build locally and bind-mount the built output over the html root — mount dist/, never src/, because index.html requests content-hashed filenames:

./build.sh
podman run --rm -p 0.0.0.0:8080:8080 -v ./dist:/usr/share/nginx/html:z homepage:latest

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, serves index.html with no-cache, and caches the hashed assets as immutable for a year.

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 includes 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

./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 lives with the deployment host's config. Install it under ~/.config/containers/systemd/ (copy it from wherever the host's unit files are kept):

mkdir -p ~/.config/containers/systemd
cp <deploy-config>/homepage.container ~/.config/containers/systemd/

It must publish host port 8080 and use the image CI publishes: gitea.reeseapps.com/services/homepage (main on every push to main, plus one tag per release).

Quadlet is a systemd generator: it turns homepage.container into a homepage.service unit when systemd regenerates units, so manage it under the .service name:

systemctl --user daemon-reload
systemctl --user start homepage.service
systemctl --user status homepage.service

Auto-start on boot comes from the unit's [Install] section (e.g. WantedBy=default.target), which the generator applies while generating — the Quadlet services are transient, so systemctl enable has no effect on them; re-run systemctl --user daemon-reload after editing the unit. For a user-mode unit that must come up without a login session, enable lingering once: loginctl enable-linger $USER.

Development

Source files live in src/: index.html, style.css, script.js and the flat terminal modules (terminal.js, terminal-commands.js, terminal-vim.js, terminal-achievements.js). Keep every .js file flat in src/ — build.sh only hashes root-level files.

npm install        # installs the pinned @playwright/test dev dependency
./build.sh         # src/ -> dist/ (content-hashed asset names)
npm test           # Playwright E2E suite against dist/ on 127.0.0.1:8123

The fake terminal performs no real network I/O. curl and wget render deterministic simulated output built by simulatedCurlOutput() / simulatedWgetOutput() in src/terminal-commands.js, which echo the requested URL and say they are simulated (audit finding M-2, Phase 02) — add new command output the same canned way. The E2E suite guards this: one test records every request outside the test origin while running curl and wget, so a real network call reintroduced into src/ fails the suite. That test derives the allowed origin from baseURL in playwright.config.js, so changing the port there needs no test edit.

Always build before testing: the suite exercises the built artifact in dist/, never src/. npm test starts (or reuses) its own static server, so no manual server is needed. Rebuild the image (podman build) after changing src/.

Requires Node.js 20+ (the @playwright/test engine floor) and the Chromium revision cached for the pinned Playwright version — run npx playwright install chromium if it is missing, and see AGENTS.md before bumping the pin.

The validation gate the phased pipeline runs per task is .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.

S
Description
No description provided
Readme
2.5 MiB
Languages
JavaScript 52.3%
HTML 25.1%
CSS 18.4%
Shell 3.6%
Dockerfile 0.3%
Other 0.3%