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
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
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 next2026-09-21 15:05:27 +02:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Implements #19.
Timeouts.
http.Servernow setsReadHeaderTimeout(5s) andIdleTimeout(60s) beside the existingReadTimeout/WriteTimeout, as named constants inhttp.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
SecurityHeadersmiddleware sets HSTS (max-age=31536000; includeSubDomains), CSP,X-Frame-Options: DENY,X-Content-Type-Options: nosniff,Referrer-Policy: no-referrer, and a restrictivePermissions-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 inSetupRoutes, so the headers are set on the response before the CORS handler can short-circuit a preflight — verified by reading the ordering:SecurityHeaderswrites headers, then callsnext(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, thenX-Real-IP; an untrusted peer's headers are ignored and the direct peer is logged. The set isTRUSTED_PROXIES, defaulting to loopback (127.0.0.1/32,::1/128) plus RFC1918 — loopback included because nginx shares the container. Parsing usesnet/netip; no new dependency.Unit tests cover both IP directions and the header set.
Disclosure: host
golangci-lintversion-skews (built for go1.25, a file needs go1.26) and panics, so the authoritative backend gate is theDockerfile.backendbuild (runsmake fmt-check,make lint,make testunder the pinned linter) — green. Rootmake checkgreen.Model: opus-4-8
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 preflightOPTIONSand a normalGET.Verdict: PASS
Model: opus-4-8