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: DENYand 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
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.
Problem
nginx.conf(28 lines, serving the built SPA in theDockerfileruntime image) sets no security headers at all. Verified onmainatfbfe1df: the onlyadd_headerin the file isCache-Controlinsidelocation /assets/.REPO_POLICIES.mdrequires 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:
Strict-Transport-Securitymax-age>= 1 year,includeSubDomainsContent-Security-Policydefault-src 'self'baselineX-Frame-OptionsDENY(withframe-ancestorsas the primary control)X-Content-Type-OptionsnosniffReferrer-Policystrict-origin-when-cross-originor stricterPermissions-PolicyThe 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 naivedefault-src 'self'CSP will break the application, becauseconnect-srcinherits fromdefault-srcand 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-srcmust be widened enough for the probes to work. Decide and justify: either enumerate the monitored origins, or acceptconnect-src *with a comment explaining that the app's purpose is arbitrary-origin reachability probing. Enumerating is the stronger option but couplesnginx.confto the host list insrc/main.js; if you enumerate, say in the PR how the two are kept in sync.<style>or inline script in the builtdist/index.html. If they do,style-srcneeds a hash or nonce — not'unsafe-inline'. Policy: "Never useunsafe-inlineorunsafe-evalunless unavoidable, and document the reason." If it truly is unavoidable, the reason goes in a comment innginx.confand in the PR description.img-srcneedsdata:if the app uses data-URI images; verify against the build output rather than guessing.Definition of done
nginx.conf, including error responses and the/assets/location. Note thatadd_headerin nginx does not inherit into alocationblock that has its ownadd_headerdirectives — verify/assets/actually emits the security headers too, do not assume.dist/output, not guessed. State in the PR description how it was verified.'unsafe-eval'. No'unsafe-inline'unless documented as unavoidable with a written justification.X-Frame-Options: DENYand aframe-ancestors 'none'CSP directive.max-ageis at least31536000and includesincludeSubDomains.Permissions-Policydenies at minimum camera, microphone, geolocation, and payment.docker build .succeeds;make checkpasses.TODO.mdupdated in the same commit.(closes #N).Implementation requirements
set_real_ip_from/real_ip_headerreverse-proxy configuration and the stdout access logging as they are; they are already correct.