feat(backend): server hardening — timeouts, security headers, trusted-proxy client IP #56

Merged
clawbot merged 1 commits from feat/backend-hardening into next 2026-09-21 15:05:27 +02:00
Collaborator

Implements #19.

Timeouts. http.Server now sets ReadHeaderTimeout (5s) and IdleTimeout (60s) beside the existing ReadTimeout/WriteTimeout, as named constants in http.go. The change is limited to those two fields and their constants so it rebases trivially against the parallel work in #22.

Security headers. New SecurityHeaders middleware sets HSTS (max-age=31536000; includeSubDomains), CSP, X-Frame-Options: DENY, X-Content-Type-Options: nosniff, Referrer-Policy: no-referrer, and a restrictive Permissions-Policy. Because the backend is a JSON API with no HTML surface, the CSP is the tightest possible: default-src 'none'; frame-ancestors 'none'. It is registered before CORS in SetupRoutes, so the headers are set on the response before the CORS handler can short-circuit a preflight — verified by reading the ordering: SecurityHeaders writes headers, then calls next (CORS).

Client IP. Forwarded headers are honoured only when the direct peer is in a trusted-proxy allowlist. X-Forwarded-For (left-most valid entry) is preferred, then X-Real-IP; an untrusted peer's headers are ignored and the direct peer is logged. The set is TRUSTED_PROXIES, defaulting to loopback (127.0.0.1/32, ::1/128) plus RFC1918 — loopback included because nginx shares the container. Parsing uses net/netip; no new dependency.

Unit tests cover both IP directions and the header set.

Disclosure: host golangci-lint version-skews (built for go1.25, a file needs go1.26) and panics, so the authoritative backend gate is the Dockerfile.backend build (runs make fmt-check, make lint, make test under the pinned linter) — green. Root make check green.

Model: opus-4-8

Implements https://git.eeqj.de/sneak/netwatch/issues/19. **Timeouts.** `http.Server` now sets `ReadHeaderTimeout` (5s) and `IdleTimeout` (60s) beside the existing `ReadTimeout`/`WriteTimeout`, as named constants in `http.go`. The change is limited to those two fields and their constants so it rebases trivially against the parallel work in https://git.eeqj.de/sneak/netwatch/issues/22. **Security headers.** New `SecurityHeaders` middleware sets HSTS (`max-age=31536000; includeSubDomains`), CSP, `X-Frame-Options: DENY`, `X-Content-Type-Options: nosniff`, `Referrer-Policy: no-referrer`, and a restrictive `Permissions-Policy`. Because the backend is a JSON API with no HTML surface, the CSP is the tightest possible: `default-src 'none'; frame-ancestors 'none'`. It is registered before CORS in `SetupRoutes`, so the headers are set on the response before the CORS handler can short-circuit a preflight — verified by reading the ordering: `SecurityHeaders` writes headers, then calls `next` (CORS). **Client IP.** Forwarded headers are honoured only when the direct peer is in a trusted-proxy allowlist. `X-Forwarded-For` (left-most valid entry) is preferred, then `X-Real-IP`; an untrusted peer's headers are ignored and the direct peer is logged. The set is `TRUSTED_PROXIES`, defaulting to loopback (`127.0.0.1/32`, `::1/128`) plus RFC1918 — loopback included because nginx shares the container. Parsing uses `net/netip`; no new dependency. Unit tests cover both IP directions and the header set. Disclosure: host `golangci-lint` version-skews (built for go1.25, a file needs go1.26) and panics, so the authoritative backend gate is the `Dockerfile.backend` build (runs `make fmt-check`, `make lint`, `make test` under the pinned linter) — green. Root `make check` green. Model: opus-4-8
clawbot added 1 commit 2026-09-21 14:50:17 +02:00
Add ReadHeaderTimeout and IdleTimeout to the http.Server (named
constants beside the existing timeouts) to close the slowloris and
idle-keep-alive gaps.

Add a SecurityHeaders middleware setting HSTS, a JSON-API CSP
(default-src 'none'; frame-ancestors 'none'), X-Frame-Options: DENY,
X-Content-Type-Options: nosniff, Referrer-Policy, and Permissions-Policy.
It is registered before CORS so the headers ride on preflight responses.

Resolve the client IP from X-Forwarded-For / X-Real-IP only when the
direct peer is in a trusted-proxy allowlist, defaulting to loopback plus
RFC1918 and configurable via TRUSTED_PROXIES; an untrusted peer's
forwarded headers are ignored and the direct peer is logged. Uses
net/netip; no new dependency.

Model: opus-4-8
clawbot self-assigned this 2026-09-21 14:50:46 +02:00
clawbot added the needs-review label 2026-09-21 14:50:46 +02:00
Author
Collaborator

Reviewed independently against the definition of done on #19 (including the loopback amendment) and gated on the PR head rebased onto current next: every item is met, the forced backend Docker build (fmt-check, lint, test under the pinned linter) is green, and the security headers are present on both a live preflight OPTIONS and a normal GET.

Verdict: PASS

Model: opus-4-8

Reviewed independently against the definition of done on https://git.eeqj.de/sneak/netwatch/issues/19 (including the loopback amendment) and gated on the PR head rebased onto current `next`: every item is met, the forced backend Docker build (fmt-check, lint, test under the pinned linter) is green, and the security headers are present on both a live preflight `OPTIONS` and a normal `GET`. Verdict: PASS Model: opus-4-8
clawbot merged commit f3895789d2 into next 2026-09-21 15:05:27 +02:00
clawbot deleted branch feat/backend-hardening 2026-09-21 15:05:27 +02:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/netwatch#56