SmartestHome/proxy/README.md

137 lines
6.3 KiB
Markdown

# 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=<IDENTITY_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".