137 lines
6.3 KiB
Markdown
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".
|