# 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. # U13 (2026-10-11): app-terminal now runs on nh3-dev, loopback-only. This origin reaches it through nh3-dev's seat-host # origin (fleet Caddy, allowlisted to this box's 10.100.50.43); Host must be the upstream's name or the fleet Caddy's # host matcher misses it. The old upstream was 127.0.0.1:8404 here. (svos_terminal_upstream) { reverse_proxy https://svos-seathost.nh3.phasefinal.com { header_up Host {upstream_hostport} 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 /seats/* 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 } # U13 fix (2026-10-12): app-terminal's registry base_url is this origin again (browsers frame panes from it), # so the kernel's signed seat-host calls (U5b) come through here too, from the box only (@not_from_box above). @seat_host { method POST path /seats/observe /seats/check /seats/launch /seats/retire-launch } handle @seat_host { import svos_terminal_upstream } handle { respond 404 } } } # The kernel's app origin (U13 B2, 2026-10-10, Prime-approved; svoperatingsystem U9a and docs/operations/u13-svos-edit.md # "Before the edit" 1). Apps that run OFF this box reach the kernel here: from the U13 edit, app-terminal, # app-postoffice (the bridge) and app-high-seat, all on nh3-dev. LAN only, because nh3-dev is not on the mesh; the # internal AdGuards answer svos-kernel.phasefinal.com -> 10.100.50.43 and no MagicDNS record exists. # - Upstream is the kernel's SECOND listener, `serve --app-listen 127.0.0.1:8410` (configs/svos-kernel/20-app-listen.conf): # a request there is never a human and loopback trust never applies, so every call authenticates with its own bearer. # NO Basic auth, and Authorization passes through untouched. # - Source allowlisted to nh3-dev (10.100.10.50); anything else gets 403 from Caddy. # - Paths, method-pinned, exactly svoperatingsystem docs/operations/u13-svos-edit.md item 1 at dc241f8 (every caller on # nh3-dev, read at source): the apps (bridge, terminal, app-miranda, app-high-seat) GET /auth/kernel-key, POST # /auth/enroll, POST /auth/proofs, POST /registry/apps/*/manifest, /verbs/*, POST /seats/enroll; Miranda's agent # connector GET /auth/whoami, /registry/apps, /registry/apps/*, /principals/*/grants, /seats, /events, # /artifacts/resolve (and /verbs/*). Anything else 404. B2's check: GET /auth/kernel-key without a credential is a # 401 from the KERNEL, not from Caddy. # - Client identity/forwarding headers are deleted as on the other sites; the same 1 MiB body cap; nothing streams. https://svos-kernel.phasefinal.com:443 { bind 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 } @not_nh3_dev not remote_ip 10.100.10.50 handle @not_nh3_dev { respond 403 } @app_get { method GET path /auth/kernel-key /auth/whoami /registry/apps /registry/apps/* /seats /events /artifacts/resolve } handle @app_get { reverse_proxy 127.0.0.1:8410 } @app_grants { method GET path_regexp ^/principals/[^/]+/grants$ } handle @app_grants { reverse_proxy 127.0.0.1:8410 } @app_post { method POST path /auth/enroll /auth/proofs /seats/enroll } handle @app_post { reverse_proxy 127.0.0.1:8410 } @app_manifest { method POST path_regexp ^/registry/apps/[^/]+/manifest$ } handle @app_manifest { reverse_proxy 127.0.0.1:8410 } @app_verbs path /verbs/* handle @app_verbs { reverse_proxy 127.0.0.1:8410 } handle { respond 404 } } }