Files
esh-pfi-infrastructure/configs/svos-front/Caddyfile
T
vh 1210b4a0d5 feat(svos-front,backups): SVOS hosting bounds and previous-release assets; backup check flags fs-freeze failures and covers nh3-pve-2
Front door: request bodies capped at 1 MiB, headers within 5 s, body within 15 s, no response deadlines (SSE stays open). Validated live: 413 over the cap, stalled body cut at 15 s, signed ping and an SSE stream unaffected. svos-front-inventory also serves the previous release's hashed assets credential-free from that release's static dir, hash-checked. Backup check: reads the logs of vzdump tasks PVE records as OK and reports ERROR lines; an fs-freeze/thaw error is an application-consistency failure, new verdict DEGRADED (exit 4), wired into the alert wrapper. nh3-pve-2 added to the node and namespace lists (its job results were never read). The svos drill task is acknowledged by UPID; BACKUP_IGNORE_ACKS=1 counts it (positive control: exit 4).
2026-10-05 20:22:16 -07:00

113 lines
5.1 KiB
Caddyfile

# SVOS front door on nh3-svos (eshpfi configs/svos-front/), 2026-10-05. Prime: mesh-only, svos.phasefinal.com.
# - Listens ONLY on the tailnet address (100.64.0.9:443). There is no LAN or 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
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
}
# 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
}
}
}