From e21ca30cd8ec68355374cb8ab6071d2a4466be7b Mon Sep 17 00:00:00 2001 From: Derek Reynolds Date: Tue, 11 Aug 2026 12:51:36 -0700 Subject: [PATCH] Harden the demo's systemd unit and generalise the deploy notes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The unit I shipped had only NoNewPrivileges and PrivateTmp, weaker than the other services on the box. This is a public unauthenticated network service, so confine it: read-only filesystem except its own log directory, no device or kernel access, a syscall filter, and a 1G ceiling above the app's own connection caps. MemoryDenyWriteExecute is deliberately omitted — it breaks V8's JIT. Verified in place: sandbox active, logs still writable, no denials, and the public endpoint still serves brotli SSE over h2. The README's deploy section now documents the hardening rather than one machine's hostnames and paths. Co-Authored-By: Claude Opus 5 (1M context) --- demo/README.md | 42 ++++++++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/demo/README.md b/demo/README.md index 831bb98..c131245 100644 --- a/demo/README.md +++ b/demo/README.md @@ -163,22 +163,36 @@ dashboard can never inflate the numbers it reports. ## Deployment -Runs on an exe.dev VM (`htmlwire`), Ubuntu 24.04, Node 24 at `/opt/node`, -managed by `htmlwire.service`. The platform proxy terminates TLS and forwards -to `127.0.0.1:8000`, so the app speaks plain HTTP while the browser still gets -`https://` and `wss://` — which is what makes the WebSocket pane work in -production where the local self-signed cert failed. - -```bash -# redeploy -rsync -az --delete --exclude node_modules --exclude tmp --exclude results \ - demo package.json htmlwire.exe.xyz:~/htmlwire/ -ssh htmlwire.exe.xyz 'cd ~/htmlwire && npm install --omit=dev && sudo systemctl restart htmlwire' - -ssh htmlwire.exe.xyz 'sudo journalctl -u htmlwire -f' # logs -ssh exe.dev share set-private htmlwire # close it back up +Runs behind a TLS-terminating proxy that forwards to `127.0.0.1:8000`, so the +app speaks plain HTTP while the browser still gets `https://` and `wss://` — +which is what makes the WebSocket pane work in production, where the local +self-signed cert failed. + +The app binds **loopback only**; nothing but the proxy can reach it. Because it +is a public service with no authentication, the systemd unit confines it: + +```ini +NoNewPrivileges=yes +PrivateTmp=yes +PrivateDevices=yes +ProtectSystem=strict +ProtectHome=read-only +ReadWritePaths=/srv/htmlwire/logs # the only writable path +ProtectKernelTunables=yes +ProtectKernelModules=yes +ProtectControlGroups=yes +RestrictSUIDSGID=yes +RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX +SystemCallFilter=@system-service +MemoryMax=1G # above the app's own connection caps +TasksMax=256 ``` +`MemoryDenyWriteExecute` is deliberately absent — it breaks V8's JIT and the +process will not start with it. + +Redeploy is an rsync of `demo/` plus `systemctl restart`. + ### Why this host and not Cloudflare Workers The exe.dev proxy is transparent to both compression schemes, verified on the -- knotmirror 0.0.1