Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28d99e89d0 |
@@ -72,6 +72,7 @@ RUN addgroup -g 1000 -S netwatch && \
|
||||
# conf.d; bin/entrypoint.sh says how.
|
||||
RUN rm /etc/nginx/conf.d/default.conf
|
||||
COPY nginx.conf /etc/nginx/templates/netwatch.conf.template
|
||||
COPY security-headers.conf /etc/nginx/security-headers.conf
|
||||
COPY --from=frontend /app/dist /usr/share/nginx/html
|
||||
COPY --from=builder /src/netwatch-server /usr/local/bin/netwatch-server
|
||||
COPY bin/entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||
|
||||
@@ -188,6 +188,8 @@ only inside the container, on `127.0.0.1:8081`. The image:
|
||||
reverse proxies named in `TRUSTED_PROXIES`, and by default from none
|
||||
- Sends access logs to stdout
|
||||
- Caches static assets with immutable headers
|
||||
- Sends the security headers `REPO_POLICIES.md` requires on every response, as
|
||||
`security-headers.conf` sets them, in place of the backend's own
|
||||
- Stores reports in `DATA_DIR`, `/data/reports` by default, on the `/data`
|
||||
volume. The backend runs as user `netwatch` (uid 1000), so a directory
|
||||
bind-mounted at `/data` must be writable by uid 1000
|
||||
|
||||
@@ -23,6 +23,13 @@ latest run passes.
|
||||
|
||||
# Completed Steps
|
||||
|
||||
- 2026-09-29: nginx sends the security headers `REPO_POLICIES.md` requires on
|
||||
every response (issue #18), including errors, `/assets/` and what it passes on
|
||||
from the backend, whose own copies it drops so each header goes out once. They
|
||||
live in `security-headers.conf`, which `nginx.conf` includes. The content
|
||||
security policy allows no inline script or style, so the status dot's grey in
|
||||
`src/main.js` is now a class; `connect-src` is `*` because probed hosts
|
||||
redirect to others, and the browser checks each redirect against it
|
||||
- 2026-09-29: the request log is bounded (issue #60): the method, URL, protocol,
|
||||
`User-Agent`, `Referer`, request ID (which chi takes from the client's
|
||||
`X-Request-Id` header) and client address it writes are each cut to 128 bytes,
|
||||
|
||||
+3
-1
@@ -104,7 +104,9 @@ this server. The image's entrypoint, `bin/entrypoint.sh`, starts the server as
|
||||
user `netwatch` (uid 1000) with `BIND_ADDRESS=127.0.0.1` and `PORT=8081`, so
|
||||
only nginx reaches it, and with `TRUSTED_PROXIES=127.0.0.1/32`, so it takes the
|
||||
client address nginx passes on and no other. `DATA_DIR` is `/data/reports`, on
|
||||
the `/data` volume, which `netwatch` owns.
|
||||
the `/data` volume, which `netwatch` owns. nginx replaces the security headers
|
||||
this server sets with those in the root `security-headers.conf`, so those are
|
||||
what clients of the image see.
|
||||
|
||||
The container's own `TRUSTED_PROXIES` goes to nginx instead: IP addresses or
|
||||
CIDRs, separated by commas, of the reverse proxies in front of the container.
|
||||
|
||||
+16
@@ -8,6 +8,11 @@ server {
|
||||
# Keep the nginx version out of the Server header and error pages.
|
||||
server_tokens off;
|
||||
|
||||
# The security headers, on every response. An add_header in a
|
||||
# location drops every add_header from here, so a location with one
|
||||
# of its own includes this file again.
|
||||
include /etc/nginx/security-headers.conf;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
@@ -32,6 +37,7 @@ server {
|
||||
location /assets/ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
include /etc/nginx/security-headers.conf;
|
||||
}
|
||||
|
||||
# netwatch-server, the Go backend, runs in the same container and
|
||||
@@ -45,6 +51,16 @@ server {
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# netwatch-server sets the same security headers on its own
|
||||
# responses. Its copies are dropped so that each header goes out
|
||||
# once, as security-headers.conf sets it.
|
||||
proxy_hide_header Strict-Transport-Security;
|
||||
proxy_hide_header Content-Security-Policy;
|
||||
proxy_hide_header X-Frame-Options;
|
||||
proxy_hide_header X-Content-Type-Options;
|
||||
proxy_hide_header Referrer-Policy;
|
||||
proxy_hide_header Permissions-Policy;
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://127.0.0.1:8081;
|
||||
}
|
||||
|
||||
@@ -64,13 +64,15 @@ main() {
|
||||
# nginx.conf is a template: the image renders it over its own
|
||||
# default.conf, with the same port and limit bin/entrypoint.sh uses.
|
||||
# The empty file it includes trusts no proxy, as bin/entrypoint.sh
|
||||
# writes it when TRUSTED_PROXIES is unset.
|
||||
# writes it when TRUSTED_PROXIES is unset. nginx.conf also includes
|
||||
# the security headers, so the page runs under the shipped policy.
|
||||
docker run -d --rm --name "$SERVER" \
|
||||
--network "$NETWORK" --network-alias netwatch \
|
||||
-e PORT=8080 -e NGINX_ENVSUBST_FILTER='^PORT$' \
|
||||
-v "$ROOT/dist:/usr/share/nginx/html:ro" \
|
||||
-v "$ROOT/nginx.conf:/etc/nginx/templates/default.conf.template:ro" \
|
||||
-v /dev/null:/etc/nginx/trusted-proxies.conf:ro \
|
||||
-v "$ROOT/security-headers.conf:/etc/nginx/security-headers.conf:ro" \
|
||||
"$SERVER_IMAGE" > /dev/null
|
||||
|
||||
# The image's own entrypoint already exposes CDP on 9222 and passes
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# The security headers REPO_POLICIES.md requires on every response.
|
||||
# nginx.conf includes this file, which Dockerfile copies to
|
||||
# /etc/nginx/security-headers.conf. always sends each header on error
|
||||
# responses too.
|
||||
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
|
||||
# Scripts and styles load only from the page's own origin. Inline ones
|
||||
# are blocked, style attributes in markup included, so style elements
|
||||
# through classes or element.style. data: images are for the favicon
|
||||
# in index.html. connect-src is * because the browser checks each probe in
|
||||
# src/main.js against it, and also every redirect the probe follows,
|
||||
# and several of those hosts redirect to others; a list of hosts here
|
||||
# would block those probes. It also covers the reports the page sends
|
||||
# to its own origin.
|
||||
add_header Content-Security-Policy "default-src 'self'; connect-src *; img-src 'self' data:; object-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'" always;
|
||||
|
||||
add_header X-Frame-Options DENY always;
|
||||
add_header X-Content-Type-Options nosniff always;
|
||||
|
||||
# The probed hosts are not told where the page is served from.
|
||||
add_header Referrer-Policy no-referrer always;
|
||||
|
||||
add_header Permissions-Policy "accelerometer=(), camera=(), display-capture=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), midi=(), payment=(), usb=()" always;
|
||||
+1
-1
@@ -716,7 +716,7 @@ function hostRowHTML(host, index, showPin = true) {
|
||||
${pinBtn}
|
||||
<div class="w-[420px] flex-shrink-0 grid grid-cols-[minmax(0,1fr)_auto] items-center">
|
||||
<div class="flex items-center gap-2 min-w-[200px]">
|
||||
<div class="w-3 h-3 rounded-full flex-shrink-0" style="background-color: ${latencyHex(null)}"></div>
|
||||
<div class="w-3 h-3 rounded-full flex-shrink-0 bg-[#6b7280]"></div>
|
||||
<span class="font-medium text-white truncate">${host.name}</span>
|
||||
</div>
|
||||
<div class="latency-value text-4xl font-bold tabular-nums text-right mt-3" data-host="${index}">
|
||||
|
||||
Reference in New Issue
Block a user