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.
92 lines
3.8 KiB
Python
Executable File
92 lines
3.8 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""svos-front-inventory: regenerate the SVOS front door's credential-free shell paths from the live release.
|
|
|
|
eshpfi configs/svos-front/, installed at /usr/local/sbin/svos-front-inventory on nh3-svos. Run as root after every
|
|
SVOS release (after /opt/svos/current moves), or the service worker's install breaks: its INVENTORY names hashed
|
|
asset files, and only those exact paths (plus /sw.js) are served before Basic auth (svos-dev, 2026-10-05).
|
|
|
|
What it does:
|
|
1. Reads INVENTORY from /opt/svos/current/static/sw.js (a JSON object of path -> sha256).
|
|
2. Refuses unless every path is a plain absolute file path and every file exists with exactly that sha256.
|
|
3. Writes /etc/caddy/svos-public-inventory (one Caddyfile `path` line, root:caddy 0640) atomically.
|
|
4. Validates the whole Caddyfile as the caddy user with the unit's EnvironmentFile (no secret on any argv),
|
|
restores the previous inventory file if validation fails, and otherwise reloads caddy-svos.
|
|
--check: steps 1-2 only, plus a diff against the installed file; changes nothing.
|
|
"""
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
STATIC = Path("/opt/svos/current/static")
|
|
OUT = Path("/etc/caddy/svos-public-inventory")
|
|
SAFE = re.compile(r"^/[A-Za-z0-9._-]+(/[A-Za-z0-9._-]+)*$")
|
|
VALIDATE = ["systemd-run", "--quiet", "--wait", "--pipe", "--collect", "-p", "User=caddy", "-p", "Group=caddy",
|
|
"-p", "EnvironmentFile=/etc/caddy/env",
|
|
"-p", "Environment=XDG_DATA_HOME=/var/lib/caddy XDG_CONFIG_HOME=/var/lib/caddy",
|
|
"/usr/local/bin/caddy", "validate", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"]
|
|
|
|
|
|
def die(msg):
|
|
sys.exit(f"svos-front-inventory: {msg}")
|
|
|
|
|
|
def inventory():
|
|
src = (STATIC / "sw.js").read_text(encoding="utf-8")
|
|
m = re.search(r"^const INVENTORY = (\{.*?\});$", src, re.M)
|
|
if not m:
|
|
die("no `const INVENTORY = {...};` line in sw.js")
|
|
inv = json.loads(m.group(1))
|
|
if not inv:
|
|
die("INVENTORY is empty")
|
|
for path, digest in inv.items():
|
|
if not SAFE.match(path) or ".." in path.split("/"):
|
|
die(f"unsafe inventory path {path!r}")
|
|
f = STATIC / path.lstrip("/")
|
|
if not f.is_file() or f.is_symlink():
|
|
die(f"{path} is not a regular file in {STATIC}")
|
|
actual = hashlib.sha256(f.read_bytes()).hexdigest()
|
|
if actual != digest:
|
|
die(f"{path} sha256 {actual} != INVENTORY {digest}")
|
|
return sorted(inv)
|
|
|
|
|
|
def render(paths):
|
|
release = os.path.realpath(STATIC.parent)
|
|
return (f"# GENERATED by /usr/local/sbin/svos-front-inventory from {release}/static/sw.js; do not edit.\n"
|
|
f"# Exact credential-free shell paths (hash-checked), plus /sw.js.\n"
|
|
f"path {' '.join(paths + ['/sw.js'])}\n")
|
|
|
|
|
|
def main():
|
|
new = render(inventory())
|
|
old = OUT.read_text(encoding="utf-8") if OUT.exists() else ""
|
|
if "--check" in sys.argv[1:]:
|
|
print("inventory ok;", "installed file is current" if old == new else "installed file DIFFERS:\n" + new)
|
|
return
|
|
if old == new:
|
|
print("unchanged; nothing to do")
|
|
return
|
|
tmp = OUT.with_name(OUT.name + ".new")
|
|
tmp.write_text(new, encoding="utf-8")
|
|
shutil.chown(tmp, "root", "caddy")
|
|
os.chmod(tmp, 0o640)
|
|
backup = OUT.with_name(OUT.name + ".prev")
|
|
if OUT.exists():
|
|
shutil.copy2(OUT, backup)
|
|
os.replace(tmp, OUT)
|
|
if subprocess.run(VALIDATE).returncode != 0:
|
|
if backup.exists():
|
|
os.replace(backup, OUT)
|
|
die("caddy validate FAILED; previous inventory restored, caddy not reloaded")
|
|
subprocess.run(["systemctl", "reload", "caddy-svos"], check=True)
|
|
print("inventory updated and caddy-svos reloaded:\n" + new)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|