svos-dev: the service worker fetches its hash-pinned INVENTORY with credentials omitted. Exactly those files plus /sw.js are now served before basic_auth (GET/HEAD, exact files). The list is generated from the release's sw.js by svos-front-inventory, which hash-checks each file and validates before reload. Gated static routes follow the web/README hosting contract: missing assets 404 instead of index.html, other document requests get index.html, everything else 404, unversioned files no-cache. acceptance-matrix.sh: 35/35 live; 16 FAIL against the previous config (positive control). git added to the nh3-svos playbook (kernel's remote check). Live backup drill: quiesced backup ~1-2 s outage; a refused freeze does NOT abort vzdump (unfrozen backup, job reported successful); both images restore-mounted read-only and verified.
93 lines
4.1 KiB
Caddyfile
93 lines
4.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
|
|
}
|
|
|
|
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
|
|
|
|
# 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
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
}
|