# SVOS front door on nh3-svos (eshpfi configs/svos-front/), 2026-10-05. Prime: mesh-only, svos.phasefinal.com. # - Listens on the tailnet address (100.64.0.9:443) AND, since 2026-10-08 (Prime: "serve it on the LAN too"), on the # LAN address (10.100.50.43:443), so non-mesh internal clients work; the internal AdGuards answer 10.100.50.43, the # headscale MagicDNS records still answer 100.64.0.9. No internet listener and no :80 redirect server. # - Cert: Let's Encrypt via Cloudflare DNS-01 with a phasefinal.com-only token (env CF_API_TOKEN, /etc/caddy/env). # - Identity: HTTP Basic auth (Prime 2026-10-05: "Basic auth is fine, saved in browser is fine"; mesh-only because we # don't host externally). One account, `Vuong` (Prime is how SVOS addresses him). argon2id hash in # /etc/caddy/svos-basic-auth.accounts (root:caddy 0640, NOT in git); plaintext vaulted nh3-svos/basic-auth-vuong. # The WhoIs/forward_auth gate that preceded this was retired the same day (every mesh node is login `pfi`). # - Client-supplied identity/forwarding headers are deleted BEFORE auth. After auth, Tailscale-User-Login is set to # the authenticated user ({http.auth.user.id}) and Authorization is deleted: the kernel's human path (INV-U1-06) # refuses any request that carries an Authorization header, and the app has no business seeing the password. # reverse_proxy sets X-Forwarded-Proto=https and X-Forwarded-For itself; Host passes through; Origin untouched. # - App side (svos-dev): kernel 127.0.0.1:8400 (/api/* with the prefix stripped, /api/health is the kernel's), # connector 127.0.0.1:8403 (/health, /verbs/ping|pins|pin|unpin, /openapi.json), static UI /opt/svos/current/static. { email lkraven@lkraven.com auto_https disable_redirects admin localhost:2019 # Hosting bounds (svos-dev 2026-10-05, svoperatingsystem web/README.md Hosting contract): request headers within 5 s, # the whole request body within 15 s. NO write/response deadline: /api/stream (SSE) must stay open. Measured on a # replica 2026-10-05 before deploy: SSE 25 s and a 20 s slow response survive on h1 and h2; a stalled body is cut at # 15 s (h1 504, h2 502); unfinished headers are closed at 5 s. servers { timeouts { read_header 5s read_body 15s } } } https://svos.phasefinal.com:443 { bind 100.64.0.9 10.100.50.43 tls { dns cloudflare {env.CF_API_TOKEN} resolvers 1.1.1.1 8.8.8.8 } route { # Whole families by prefix (Caddy header-delete wildcard; tested 2026-10-05: case-insensitive, and # X-Forwardedness / Tailscale-Other survive), plus the named strays outside them. request_header -Tailscale-User-* request_header -X-Forwarded-* request_header -Tailscale-Headers-Info request_header -X-Real-IP request_header -Forwarded request_header -X-SVOS-Peer # Request bodies capped at exactly 1 MiB (1,048,576 B, = the kernel dispatcher's own cap) on every route; # only /api/* and the connector read bodies at all. Over the cap -> 413. Responses are not limited. request_body { max_size 1MiB } # Credential-free offline shell (svos-dev 2026-10-05): the service worker fetches its hash-pinned INVENTORY # with credentials:'omit', so exactly those files plus /sw.js are served BEFORE Basic auth, GET/HEAD only, # exact files (no fallback). Public build bytes only; no API or connector path. The path list is GENERATED # from the live release's sw.js by /usr/local/sbin/svos-front-inventory, which must run after every release. @public_shell { method GET HEAD import /etc/caddy/svos-public-inventory } handle @public_shell { @revalidate path /sw.js /index.html /manifest.webmanifest /favicon.svg /icons/* header @revalidate Cache-Control no-cache root * /opt/svos/current/static file_server } # Voice (U12, app-voice on 127.0.0.1:8402; Prime 2026-10-08 "voice on"). BEFORE basic_auth on purpose: the browser # calls /voice/* with `Authorization: Bearer ` (web/src/lib/voice.ts), minted by a signed kernel open # that itself goes through /api behind Basic auth. Basic auth here would reject the bearer, and the kernel's # Authorization strip below would drop it. The lease IS the auth; the app refuses /voice without it. The /voice # prefix is preserved upstream, Authorization passes untouched, no buffering (streamed audio), cancellation # reaches the listener (client disconnect closes the upstream). Engines stay private (fv-ml1 :8300, irv-ml1 :8198). @voice path /voice /voice/* handle @voice { reverse_proxy 127.0.0.1:8402 { flush_interval -1 } } # Previous release's hashed assets across a rollout (svos-dev 2026-10-05): open workers restore evicted inventory # credential-free. Generated by the same tool: a matcher + handle rooted in THAT release's static dir, or # comments only when there is no previous release. import /etc/caddy/svos-public-previous basic_auth argon2id SVOS { import /etc/caddy/svos-basic-auth.accounts } request_header Tailscale-User-Login {http.auth.user.id} request_header -Authorization handle_path /api/* { reverse_proxy 127.0.0.1:8400 } @connector path /health /verbs/ping /verbs/pins /verbs/pin /verbs/unpin /openapi.json handle @connector { reverse_proxy 127.0.0.1:8403 } # Static routes per svoperatingsystem web/README.md "Hosting contract" (gated): # / and /status -> index.html; the static route space -> exact files, a missing file is 404 (never index.html); # other HTML document requests -> index.html (the app renders Page not found); everything else -> 404. @shell_doc path / /status handle @shell_doc { header Cache-Control no-cache root * /opt/svos/current/static rewrite * /index.html file_server } @static path /assets/* /icons/* /favicon.svg /manifest.webmanifest /index.html /sw.js handle @static { @revalidate path /sw.js /index.html /manifest.webmanifest /favicon.svg /icons/* header @revalidate Cache-Control no-cache root * /opt/svos/current/static file_server } @other_doc expression header({'Sec-Fetch-Dest': 'document'}) || header({'Accept': '*text/html*'}) handle @other_doc { header Cache-Control no-cache root * /opt/svos/current/static rewrite * /index.html file_server } handle { respond 404 } } } # app-terminal's own origin (svos-dev 2026-10-07, U8 + U10c "a pane needs its own origin"). The interface embeds this # page in a cross-origin iframe; the browser derives its URL from the registry's base_url # (instance.toml: base_url = "https://svos-terminal.phasefinal.com"), so the kernel's signed calls (/health, /verbs/*) # also arrive here, from the box itself, and pass to the connector on 127.0.0.1:8404. # - NO Basic auth and no identity header: Chromium cancels Basic prompts for cross-origin frames and subresources, so a # gate here kills the pane. The lease is the page's auth (GET / without one is a waiting page by ruling A105; # /terminal/session|stream|close refuse without the bearer); /verbs/* and /health refuse without the kernel's # Ed25519 signature. As defence in depth, /verbs/* and /health are refused unless they come from the box itself. # - The app sets Content-Security-Policy: frame-ancestors https://svos.phasefinal.com (its frame_ancestors config); # it passes through unmodified. /terminal/stream is a long-lived ndjson stream: no buffering (flush_interval -1) # and no write/idle deadline (none is set anywhere in this file). Same 1 MiB body cap. # - Mesh-only like the main site: headscale MagicDNS extra record -> 100.64.0.9; nh3-svos resolves it via /etc/hosts. (svos_terminal_upstream) { reverse_proxy 127.0.0.1:8404 { flush_interval -1 } } https://svos-terminal.phasefinal.com:443 { bind 100.64.0.9 10.100.50.43 tls { dns cloudflare {env.CF_API_TOKEN} resolvers 1.1.1.1 8.8.8.8 } route { request_header -Tailscale-User-* request_header -X-Forwarded-* request_header -Tailscale-Headers-Info request_header -X-Real-IP request_header -Forwarded request_header -X-SVOS-Peer request_body { max_size 1MiB } # The kernel's signed calls come from the box itself (its source on the mesh address); anything else is refused. @not_from_box { path /verbs/* /health not remote_ip 100.64.0.9 10.100.50.43 127.0.0.1 } handle @not_from_box { respond 403 } # Exactly the paths the connector serves (svos-dev 2026-10-07), method-pinned; everything else is 404. @page { method GET HEAD path / /assets/* } handle @page { import svos_terminal_upstream } @lease_get { method GET path /terminal/session /terminal/stream } handle @lease_get { import svos_terminal_upstream } @lease_close { method POST path /terminal/close } handle @lease_close { import svos_terminal_upstream } @verbs { method POST path /verbs/* } handle @verbs { import svos_terminal_upstream } @health { method GET path /health } handle @health { import svos_terminal_upstream } handle { respond 404 } } }