Frontend image serves zero security headers; add the full REPO_POLICIES set to nginx.conf #18

Open
opened 2026-08-09 03:38:45 +02:00 by clawbot · 0 comments
Collaborator

Problem

nginx.conf (28 lines, serving the built SPA in the Dockerfile runtime image) sets no security headers at all. Verified on main at fbfe1df: the only add_header in the file is Cache-Control inside location /assets/.

REPO_POLICIES.md requires all of the following on every response, and explicitly gates 1.0 on it: "HTTP/web services must be hardened for production internet exposure before tagging 1.0."

Missing, all of them:

Header Required value
Strict-Transport-Security max-age >= 1 year, includeSubDomains
Content-Security-Policy restrictive, default-src 'self' baseline
X-Frame-Options DENY (with frame-ancestors as the primary control)
X-Content-Type-Options nosniff
Referrer-Policy strict-origin-when-cross-origin or stricter
Permissions-Policy deny unused features (camera, microphone, geolocation, ...)

The CSP complication — read this before writing the policy

NetWatch is a latency monitor. Its whole function is issuing fetch() requests to 22 third-party WAN hosts plus RFC1918 local addresses. A naive default-src 'self' CSP will break the application, because connect-src inherits from default-src and every probe will be blocked.

The policy must therefore be written deliberately, not copy-pasted:

  • default-src 'self' as the baseline is correct and should stay.
  • connect-src must be widened enough for the probes to work. Decide and justify: either enumerate the monitored origins, or accept connect-src * with a comment explaining that the app's purpose is arbitrary-origin reachability probing. Enumerating is the stronger option but couples nginx.conf to the host list in src/main.js; if you enumerate, say in the PR how the two are kept in sync.
  • Check whether Tailwind v4 / Vite emit any inline <style> or inline script in the built dist/index.html. If they do, style-src needs a hash or nonce — not 'unsafe-inline'. Policy: "Never use unsafe-inline or unsafe-eval unless unavoidable, and document the reason." If it truly is unavoidable, the reason goes in a comment in nginx.conf and in the PR description.
  • img-src needs data: if the app uses data-URI images; verify against the build output rather than guessing.

Definition of done

  • All six headers above are set on every response served by nginx.conf, including error responses and the /assets/ location. Note that add_header in nginx does not inherit into a location block that has its own add_header directives — verify /assets/ actually emits the security headers too, do not assume.
  • The CSP is derived from the actual built dist/ output, not guessed. State in the PR description how it was verified.
  • The application still works end to end behind the new headers: build the image, run it, load the page, and confirm the sparklines populate for WAN hosts (i.e. probes are not CSP-blocked). Report what you observed in the PR.
  • No 'unsafe-eval'. No 'unsafe-inline' unless documented as unavoidable with a written justification.
  • X-Frame-Options: DENY and a frame-ancestors 'none' CSP directive.
  • HSTS max-age is at least 31536000 and includes includeSubDomains.
  • Permissions-Policy denies at minimum camera, microphone, geolocation, and payment.
  • docker build . succeeds; make check passes.
  • TODO.md updated in the same commit.
  • Commit title ends with (closes #N).

Implementation requirements

  • The service listens on plain HTTP behind a TLS-terminating proxy and must not terminate TLS itself. HSTS is still set by the application so the browser enforces HTTPS end to end — that is intentional, per policy.
  • Leave the existing set_real_ip_from / real_ip_header reverse-proxy configuration and the stdout access logging as they are; they are already correct.
  • Do not change the backend Go server in this commit — that is a separate issue.
  • No attribution trailers in the commit message.
## Problem `nginx.conf` (28 lines, serving the built SPA in the `Dockerfile` runtime image) sets **no security headers at all**. Verified on `main` at `fbfe1df`: the only `add_header` in the file is `Cache-Control` inside `location /assets/`. `REPO_POLICIES.md` requires all of the following on every response, and explicitly gates 1.0 on it: "HTTP/web services must be hardened for production internet exposure before tagging 1.0." Missing, all of them: | Header | Required value | | --- | --- | | `Strict-Transport-Security` | `max-age` >= 1 year, `includeSubDomains` | | `Content-Security-Policy` | restrictive, `default-src 'self'` baseline | | `X-Frame-Options` | `DENY` (with `frame-ancestors` as the primary control) | | `X-Content-Type-Options` | `nosniff` | | `Referrer-Policy` | `strict-origin-when-cross-origin` or stricter | | `Permissions-Policy` | deny unused features (camera, microphone, geolocation, ...) | ## The CSP complication — read this before writing the policy NetWatch is a latency monitor. Its whole function is issuing `fetch()` requests to 22 third-party WAN hosts plus RFC1918 local addresses. A naive `default-src 'self'` CSP **will break the application**, because `connect-src` inherits from `default-src` and every probe will be blocked. The policy must therefore be written deliberately, not copy-pasted: - `default-src 'self'` as the baseline is correct and should stay. - `connect-src` must be widened enough for the probes to work. Decide and justify: either enumerate the monitored origins, or accept `connect-src *` with a comment explaining that the app's purpose is arbitrary-origin reachability probing. Enumerating is the stronger option but couples `nginx.conf` to the host list in `src/main.js`; if you enumerate, say in the PR how the two are kept in sync. - Check whether Tailwind v4 / Vite emit any inline `<style>` or inline script in the built `dist/index.html`. If they do, `style-src` needs a hash or nonce — **not** `'unsafe-inline'`. Policy: "Never use `unsafe-inline` or `unsafe-eval` unless unavoidable, and document the reason." If it truly is unavoidable, the reason goes in a comment in `nginx.conf` and in the PR description. - `img-src` needs `data:` if the app uses data-URI images; verify against the build output rather than guessing. ## Definition of done - [ ] All six headers above are set on every response served by `nginx.conf`, including error responses and the `/assets/` location. Note that `add_header` in nginx does **not** inherit into a `location` block that has its own `add_header` directives — verify `/assets/` actually emits the security headers too, do not assume. - [ ] The CSP is derived from the actual built `dist/` output, not guessed. State in the PR description how it was verified. - [ ] The application still works end to end behind the new headers: build the image, run it, load the page, and confirm the sparklines populate for WAN hosts (i.e. probes are not CSP-blocked). Report what you observed in the PR. - [ ] No `'unsafe-eval'`. No `'unsafe-inline'` unless documented as unavoidable with a written justification. - [ ] `X-Frame-Options: DENY` **and** a `frame-ancestors 'none'` CSP directive. - [ ] HSTS `max-age` is at least `31536000` and includes `includeSubDomains`. - [ ] `Permissions-Policy` denies at minimum camera, microphone, geolocation, and payment. - [ ] `docker build .` succeeds; `make check` passes. - [ ] `TODO.md` updated in the same commit. - [ ] Commit title ends with ` (closes #N)`. ## Implementation requirements - The service listens on plain HTTP behind a TLS-terminating proxy and must not terminate TLS itself. HSTS is still set by the application so the browser enforces HTTPS end to end — that is intentional, per policy. - Leave the existing `set_real_ip_from` / `real_ip_header` reverse-proxy configuration and the stdout access logging as they are; they are already correct. - Do not change the backend Go server in this commit — that is a separate issue. - No attribution trailers in the commit message.
clawbot added this to the 1.0.0 milestone 2026-08-09 03:38:45 +02:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/netwatch#18