# proxy The household's TLS front door: one HTTPS entry point, an HTTP→HTTPS redirect, and a single hostname instead of a fistful of `http://192.168.x.x:80xx` URLs. Caddy, because it does automatic certificate management with a two-line config and `tls internal` gives a working local CA with **no external dependency at all** — which matters when `docs/network-integration.md` §1 forbids exposing anything to the WAN, and therefore forbids the usual HTTP-01 challenge. ## Why this exists, concretely It isn't hygiene. Three specific things were broken or unsafe without it. ### 1. The admin panel's token travels in the URL `admin.html?api=…&token=` carries a credential that grants full administrative access to the person registry **and** to device-access grants — the things that decide whether a smart lock opens. Over plain HTTP that token is readable by anything on the smart-home VLAN, which is a VLAN deliberately full of cheap IoT hardware (see `network-integration.md` §3's own reasoning about why that segment is treated as untrusted). TLS doesn't fix the token being *in a URL* — it's still in browser history and the address bar. It fixes it being on the wire in cleartext, which is the part that scales to "anyone who ever joins this network". ### 2. The registration camera cannot work over plain HTTP `getUserMedia` requires a **secure context**. On `http://192.168.x.x:8098`, `navigator.mediaDevices` is simply `undefined` in Chromium and Firefox — so `register.html`'s camera never starts. Worse, it failed *silently*. The call site is: ```js navigator.mediaDevices?.getUserMedia({…}).then(…).catch(…) ``` Optional chaining short-circuits the **whole chain**, not just the property access — so when `mediaDevices` is undefined the expression evaluates to `undefined` and neither `.then` nor `.catch` ever runs. No error, no "Camera unavailable" message, no photo, no explanation. Registration quietly proceeds without the audit photo it was supposed to capture. (`register.js` now also handles this defensively and says so out loud, but HTTPS is what actually makes the camera work.) ### 3. Mixed content would break the admin panel anyway Serving the page over HTTPS while its `?api=` still pointed at `http://…:8097` would have every API call blocked by the browser as mixed content. So the API has to be reachable over the same origin — which is why this proxies both the static frontend *and* the API, rather than just putting a certificate in front of a static file server. ## What it routes | Path | To | Notes | |---|---|---| | `/` | `identity-web` | the dashboard and registration pages | | `/admin.html` | `identity-web` | the admin panel | | `/api/identity/*` | `identity:8097` | prefix stripped before forwarding | | `/api/pantry/*` | `pantry-vision:8095` | prefix stripped; only if that service is enabled | | `http://…` | → `https://…` | permanent redirect, all paths | Everything else in the stack (Home Assistant, Grocy, Frigate, Portainer…) keeps its own port and is untouched. This is the front door for **this repo's own services**, not a household-wide gateway — bringing HA's own auth and websockets behind a proxy is a separate decision with its own failure modes, and it isn't needed to fix any of the three problems above. ## Certificates Two modes, set by `proxy.tls` in `CoreSystemConfig.json`: ### `internal` (default) Caddy runs its own CA and issues itself a certificate. Zero configuration, zero external dependencies, works on a network with no internet at all. **The catch**: browsers don't trust that CA until you install its root. Until you do, you get an interstitial warning — clickable on a laptop, but genuinely a problem on a **kiosk**, where a full-screen certificate interstitial is not something anyone can dismiss with no keyboard. That's why the kiosk images still use plain HTTP by default, and why moving them over is a documented follow-up rather than something done for you. Export the root once the proxy has started: ```sh tools/export-proxy-ca.sh # writes proxy/ca/root.crt from the running container ``` Then install it on the machines that need it (on Debian: copy to `/usr/local/share/ca-certificates/` and run `update-ca-certificates`; Firefox and Chromium each have their own store as well). ### `custom` Point `proxy.cert_file` / `proxy.key_file` at a certificate you already have. This is the better option if you own a domain — and **you can get a real, publicly-trusted certificate for a LAN-only host without exposing anything**, via a DNS-01 challenge (certbot or acme.sh with your DNS provider's API). Nothing is port-forwarded; the challenge is answered in DNS. Then no CA needs installing anywhere, and kiosks can move to HTTPS with no interstitial. ## Configure ```json "proxy": { "enabled": true, "hostname": "home.example.lan", "tls": "internal", "cert_file": "", "key_file": "" } ``` `hostname` is what the certificate is issued for and what you type in the browser. It needs to resolve to the container host — either a DNS record on your own resolver (OPNsense's Unbound has an override for exactly this) or a `hosts` entry on the machines that use it. An IP address works for `tls internal` too, if you'd rather not touch DNS. ## Manual verification still outstanding 1. **Never run.** No Caddy container has been started, no certificate issued, no request proxied. The Caddyfile is generated and its structure is checked by tests, but that is not the same as Caddy having parsed it. 2. **`handle_path`'s prefix stripping is written from Caddy's documented behaviour**, not observed. If `/api/identity/people` arrives at the identity container as `/api/identity/people` rather than `/people`, that directive is the thing to look at. 3. **Nothing tests the HTTP→HTTPS redirect end to end** — Caddy does this automatically for any site with TLS, which is exactly the kind of "the tool handles it" assumption worth confirming once with `curl -I`. 4. **The `custom` mode's file paths are mounted, not validated.** A wrong path fails at Caddy startup, which is visible in `docker logs caddy` and nowhere else. 5. **Whether a kiosk's Chromium accepts the internal CA** once installed into the image's trust store is unverified — Chromium keeps its own NSS store on Linux, and "installed the CA system-wide" does not always mean "Chromium trusts it".