Add phased-execution planning structure
Build and Push Container / build-and-push (push) Successful in 11s
Build and Push Container / build-and-push (push) Successful in 11s
Introduce .agents/ (PLAN.md with locked architectural anchors, phase roadmap under phases/todo/) and AGENTS.md rules for agents working in the repo. Queues the pending phases: fix history XSS, simulate fetch commands, and nginx security headers.
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
# Phase 03 — Security headers on every location (audit M-4)
|
||||
|
||||
**Story:** n/a
|
||||
**Context:** `.agents/PLAN.md` §3 (serving + edge), `.agents/remediation_plan.md`
|
||||
findings M-4 (headers) and L-2 (`server_tokens`, included as a one-liner in
|
||||
the same file)
|
||||
|
||||
## Objective
|
||||
Fix the nginx `add_header` inheritance trap: the `location = /index.html`
|
||||
and the hashed-asset location each define their own cache `add_header`
|
||||
directives, so they **lose** the server-level security headers (CSP,
|
||||
X-Frame-Options, X-Content-Type-Options, Referrer-Policy). Extract the four
|
||||
security headers into a snippet and `include` it in **every** location, so
|
||||
every response class carries the full header set. Also set
|
||||
`server_tokens off;` (L-2) in the same pass.
|
||||
|
||||
## Dependencies
|
||||
— (none)
|
||||
|
||||
## Tasks
|
||||
1. `01_extract_header_snippet.md` — Create `nginx-security-headers.inc`, wire it into the Dockerfile, and `include` it in the server block and both header-defining locations (plus `server_tokens off;`).
|
||||
2. `02_add_header_verification.md` — Add `scripts/check-headers.sh` that builds and runs the real image and asserts all four headers on `/`, `/index.html`, and a hashed asset.
|
||||
|
||||
## Testing & Quality
|
||||
- Unit/integration: the container-based header check (task 02) is the
|
||||
integration test. It must assert all four headers on each of the three
|
||||
response classes.
|
||||
- Coverage: **>90%** on new/modified config (all three location paths
|
||||
exercised by the check script).
|
||||
- The Playwright E2E suite is unaffected (its webServer is plain python,
|
||||
not nginx) and must stay green.
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] `nginx-security-headers.inc` exists and holds the four `add_header …
|
||||
always;` lines
|
||||
- [ ] `nginx.conf` has **no** inline security `add_header` lines (only the
|
||||
`include`), and both header-defining locations `include` the snippet
|
||||
- [ ] `server_tokens off;` present in the `server` block
|
||||
- [ ] `scripts/check-headers.sh` runs the real image and prints
|
||||
`HEADER CHECK PASSED` (all four headers on `/`, `/index.html`, and a
|
||||
hashed asset)
|
||||
- [ ] `./build.sh && npm test` fully green
|
||||
- [ ] no behavior change in completed work (terminal, vim, achievements, menu)
|
||||
|
||||
**Out of scope (separate, unphased):** L-1 HSTS — that is an **edge/Caddy**
|
||||
responsibility (the container serves plain HTTP on 8080). Verify at the edge
|
||||
manually: `curl -sI https://reeseapps.com/ | grep -i strict-transport`. Do
|
||||
**not** add HSTS inside the 8080 container.
|
||||
@@ -0,0 +1,96 @@
|
||||
# Task 01 — Extract header snippet and include it in every location
|
||||
|
||||
**Phase:** `03_nginx_security_headers` · **Story:** n/a
|
||||
|
||||
## Objective
|
||||
Move the four security headers into a shared snippet and `include` it in
|
||||
**every** location, so no location can silently drop them by defining its own
|
||||
`add_header` (the nginx inheritance trap, audit M-4). Add `server_tokens off;`
|
||||
(audit L-2) in the same pass.
|
||||
|
||||
## Work
|
||||
1. Create `nginx-security-headers.inc` (repo root, next to `nginx.conf`) with
|
||||
exactly:
|
||||
```
|
||||
# Shared security headers — included by EVERY nginx location.
|
||||
# A location that defines its own add_header inherits none from the
|
||||
# server block, so each location includes this snippet directly.
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self';" always;
|
||||
```
|
||||
|
||||
2. `Dockerfile` — create the snippets dir and copy the file in. Change the
|
||||
existing `RUN rm … && apk add …` line to also `mkdir -p /etc/nginx/snippets`,
|
||||
and add a COPY right before the `COPY nginx.conf …` line:
|
||||
```dockerfile
|
||||
RUN rm /etc/nginx/conf.d/default.conf && \
|
||||
apk add --no-cache coreutils && \
|
||||
mkdir -p /etc/nginx/snippets
|
||||
|
||||
COPY nginx-security-headers.inc /etc/nginx/snippets/security-headers.inc
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
```
|
||||
|
||||
3. `nginx.conf` — rewrite to the exact content below. Key changes: the four
|
||||
inline `add_header` lines are gone from the `server` block; `server_tokens
|
||||
off;` is added; each of the three `location` blocks `include`s the snippet
|
||||
(so no location relies on header inheritance):
|
||||
```nginx
|
||||
server {
|
||||
listen 8080;
|
||||
server_name _;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# Hide the nginx version in the Server header (audit L-2)
|
||||
server_tokens off;
|
||||
|
||||
# Gzip compression
|
||||
gzip on;
|
||||
gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript;
|
||||
gzip_min_length 256;
|
||||
gzip_vary on;
|
||||
|
||||
# Never cache index.html so the latest HTML is always served
|
||||
location = /index.html {
|
||||
include /etc/nginx/snippets/security-headers.inc;
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||
add_header Pragma "no-cache";
|
||||
add_header Expires "0";
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
# Cache hashed assets forever (filename contains content hash)
|
||||
location ~* \.(css|js|jpeg|jpg|png|gif|ico|svg|woff2?)$ {
|
||||
include /etc/nginx/snippets/security-headers.inc;
|
||||
add_header Cache-Control "public, max-age=31536000, immutable";
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
location / {
|
||||
include /etc/nginx/snippets/security-headers.inc;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# Custom error pages
|
||||
error_page 404 /index.html;
|
||||
}
|
||||
```
|
||||
|
||||
## Testing & Quality
|
||||
- Unit/integration: config correctness is proven by the container check in
|
||||
task 02 (headers present on all three response classes) and by `nginx -t`
|
||||
(valid syntax, run implicitly by the container starting).
|
||||
- Coverage: **>90%** on the modified config (all three locations exercised).
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] `nginx-security-headers.inc` exists with the four `add_header … always;`
|
||||
lines
|
||||
- [ ] `nginx.conf` contains **no** inline security `add_header` lines and has
|
||||
`include /etc/nginx/snippets/security-headers.inc;` in all three
|
||||
locations, plus `server_tokens off;`
|
||||
- [ ] `Dockerfile` copies the snippet into the image
|
||||
- [ ] `./build.sh && npm test` still fully green (Playwright is unaffected)
|
||||
@@ -0,0 +1,87 @@
|
||||
# Task 02 — Add container-level header verification
|
||||
|
||||
**Phase:** `03_nginx_security_headers` · **Story:** n/a
|
||||
|
||||
## Objective
|
||||
Add a repeatable, container-based verification that the **real** image serves
|
||||
all four security headers on every response class (`/`, `/index.html`, and a
|
||||
hashed asset). This is the integration test for the M-4 fix and guards against
|
||||
future nginx config regressions.
|
||||
|
||||
## Work
|
||||
1. Create `scripts/check-headers.sh` (repo root; `mkdir -p scripts` first)
|
||||
with exactly:
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
# Verify every response class (/, /index.html, a hashed asset) carries the
|
||||
# full security-header set, using the REAL nginx image on a scratch port.
|
||||
# Requires a container runtime (podman or docker).
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
if command -v podman >/dev/null 2>&1; then RUNTIME=podman
|
||||
elif command -v docker >/dev/null 2>&1; then RUNTIME=docker
|
||||
else echo "ERROR: neither podman nor docker found" >&2; exit 2; fi
|
||||
|
||||
IMAGE="homepage-headercheck-$$"
|
||||
PORT=18099
|
||||
CID=""
|
||||
trap 'if [ -n "$CID" ]; then $RUNTIME rm -f "$CID" >/dev/null 2>&1 || true; fi' EXIT
|
||||
|
||||
echo ">> building image ($RUNTIME)..."
|
||||
$RUNTIME build -q -t "$IMAGE" .
|
||||
|
||||
echo ">> starting container on 127.0.0.1:$PORT..."
|
||||
CID="$($RUNTIME run -d --rm -p 127.0.0.1:$PORT:8080 "$IMAGE")"
|
||||
|
||||
for i in $(seq 1 30); do
|
||||
curl -sf -o /dev/null "http://127.0.0.1:$PORT/" && break
|
||||
sleep 0.5
|
||||
if [ "$i" -eq 30 ]; then echo "ERROR: container not ready" >&2; exit 1; fi
|
||||
done
|
||||
|
||||
ASSET="$(curl -s "http://127.0.0.1:$PORT/" \
|
||||
| grep -oE '(style|script|terminal)[a-z-]*\.[0-9a-f]{32}\.(css|js)' | head -n1 || true)"
|
||||
if [ -z "$ASSET" ]; then echo "ERROR: no hashed asset found in index.html" >&2; exit 1; fi
|
||||
|
||||
fail=0
|
||||
check_headers() {
|
||||
local path="$1" hdrs h
|
||||
hdrs="$(curl -sI "http://127.0.0.1:$PORT$path")"
|
||||
for h in "X-Frame-Options" "X-Content-Type-Options" "Referrer-Policy" "Content-Security-Policy"; do
|
||||
if ! printf '%s' "$hdrs" | grep -qi "^${h}:"; then
|
||||
echo "FAIL: ${h} missing on ${path}" >&2; fail=1
|
||||
fi
|
||||
done
|
||||
[ "$fail" -eq 0 ] && echo "OK: all security headers present on ${path}"
|
||||
}
|
||||
|
||||
echo ">> checking headers..."
|
||||
check_headers "/"
|
||||
check_headers "/index.html"
|
||||
check_headers "/${ASSET}"
|
||||
|
||||
if [ "$fail" -ne 0 ]; then echo "HEADER CHECK FAILED" >&2; exit 1; fi
|
||||
echo "HEADER CHECK PASSED"
|
||||
```
|
||||
Then `chmod +x scripts/check-headers.sh`.
|
||||
|
||||
2. Run it: `./scripts/check-headers.sh` — it must print
|
||||
`HEADER CHECK PASSED` with an `OK:` line for each of the three paths.
|
||||
|
||||
## Testing & Quality
|
||||
- Unit/integration: this script **is** the integration test — it builds the
|
||||
real image, serves it, and asserts all four headers on each response class.
|
||||
- Coverage: **>90%** on the modified nginx config (all three location paths
|
||||
asserted).
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] `scripts/check-headers.sh` exists and is executable
|
||||
- [ ] `./scripts/check-headers.sh` exits 0 and prints `HEADER CHECK PASSED`
|
||||
(with `OK:` lines for `/`, `/index.html`, and the hashed asset)
|
||||
- [ ] (negative check) temporarily removing one `include` from a location in
|
||||
`nginx.conf` makes the script FAIL for that path — confirms the check
|
||||
has teeth, then restore it
|
||||
- [ ] `./build.sh && npm test` fully green
|
||||
Reference in New Issue
Block a user