Pi bundle: Kvitt on port 443 through NPM (D-124)
Build Kvitt images / build (push) Successful in 2h51m36s
Build Kvitt images / build (push) Successful in 2h51m36s
KVITT_PUBLIC_URL=https://${KVITT_PUBLIC_HOST} with no port; the health check through NPM prints the port-less URL; publish-apk.sh's summary drops :8443. README: no new router rule (the existing external 443 forward to the Home Assistant Pi serves Kvitt, NPM selects by name), NPM listens on 443, the fallback, operations and APK-abuse notes, and every :8443 URL. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
This commit is contained in:
+21
-17
@@ -165,10 +165,10 @@ Copying the key to the Pi (§10.7 step 7, mode 0440, group 10001) is done by `pi
|
||||
| Owner's Mac | `infra/build-images.sh [tag]` (fallback image build), `infra/publish-apk.sh` (release APK into `infra/pi/out/apk/`) | The owner (or an agent, for the APK build only) |
|
||||
| Kvitt Pi, `/opt/kvitt/` | `compose.yaml`, `.env`, `secrets/`, `import/`, `apk/`, and the scripts `deploy.sh`, `bootstrap.sh`, `backup-now.sh`, `install-apk.sh` (copied there by `install.sh`) | The owner, in their own SSH session |
|
||||
| Home Assistant Pi | Nginx Proxy Manager (NPM): a proxy host for `<KVITT_PUBLIC_HOST>` with its own Let's Encrypt certificate, TLS on 443, plain HTTP to `<KVITT_PI_HOST>:8080` over the LAN (D-120) | The owner, in NPM's web UI (step 6) |
|
||||
| Router (ASUS) | External TCP 8443 to the Home Assistant Pi's 443 (NPM); external 80 already reaches NPM for Let's Encrypt | The owner (step 7) |
|
||||
| Router (ASUS) | No new rule: the existing external TCP 443 forward to the Home Assistant Pi (NPM, for Home Assistant) serves Kvitt too, NPM picks the proxy host by name; external 80 already reaches NPM for Let's Encrypt; nothing is forwarded to the Kvitt Pi (D-124) | The owner checks it (step 7) |
|
||||
|
||||
Stack (`pi/compose.yaml`, project name `kvitt`): `server` (publishes `8080:8080` on the Pi's LAN interfaces, the
|
||||
only published port; `KVITT_PUBLIC_URL=https://${KVITT_PUBLIC_HOST}:8443`, `KVITT_TRUSTED_PROXY=${HA_HOST}`) and
|
||||
only published port; `KVITT_PUBLIC_URL=https://${KVITT_PUBLIC_HOST}` (default port 443, D-124), `KVITT_TRUSTED_PROXY=${HA_HOST}`) and
|
||||
`backup`; volumes `data`, `backups`; network `kvitt`; every service `restart: unless-stopped`, json-file logs
|
||||
10 MB x 3, `TZ=Europe/Stockholm`. Images are `${KVITT_REGISTRY}/kvitt-server|kvitt-backup:${KVITT_TAG}` with
|
||||
`KVITT_REGISTRY=gitea.lan/jacobh2` (D-104; the names the workflow pushes). Caddy and cert-sync were removed by
|
||||
@@ -385,7 +385,7 @@ starts and waits. If an earlier bundle with Caddy and cert-sync was installed, i
|
||||
`kvitt_certs` and `kvitt_caddy_data`) and says so. Expected end: `server: healthy`, a `docker compose ps` table with
|
||||
`server` (ports `0.0.0.0:8080->8080/tcp`) and `backup`, the line `GET http://localhost:8080/health: {"status":"ok",...}`,
|
||||
then "Not reachable through Nginx Proxy Manager yet" (expected on the first install, before step 6) or
|
||||
`GET https://<KVITT_PUBLIC_HOST>:443/health (via NPM at <HA_HOST>): {"status":"ok",...}`, then "Kvitt is running.".
|
||||
`GET https://<KVITT_PUBLIC_HOST>/health (via NPM at <HA_HOST>:443): {"status":"ok",...}`, then "Kvitt is running.".
|
||||
Then `rm -r ~/kvitt-pi/secrets` (the script reminds you). Running `install.sh` again is safe. Re-running
|
||||
`install.sh` keeps the `KVITT_TAG` that `deploy.sh` set; change the tag only with `deploy.sh <tag>`.
|
||||
|
||||
@@ -396,7 +396,8 @@ HTTP over the home LAN to the Kvitt Pi's port 8080 (plain HTTP between the two P
|
||||
sure `<KVITT_PUBLIC_HOST>` points at the home's public address (your DNS or DuckDNS page shows the same address as
|
||||
`icehack.asuscomm.com`). Let's Encrypt's HTTP-01 challenge reaches NPM through external port 80, which already
|
||||
works: it is how NPM got the certificate for Home Assistant. Serving Kvitt under a path of `icehack.asuscomm.com`
|
||||
(for example `/kvitt/`) is not supported.
|
||||
(for example `/kvitt/`) is not supported. NPM listens on the default HTTPS port 443 for every proxy host and
|
||||
picks the host by the name the client asks for, so Kvitt and Home Assistant share that port (D-124).
|
||||
|
||||
Open NPM's web UI (Home Assistant, Settings, Add-ons, Nginx Proxy Manager, Open Web UI), then Hosts, Proxy Hosts,
|
||||
Add Proxy Host.
|
||||
@@ -509,13 +510,14 @@ the connection comes from 10/8, 172.16/12, 192.168/16 (and the CDN ranges it dow
|
||||
LAN can choose its own client address for the rate limits. Internet clients arrive with their public address (the
|
||||
router's forward keeps the source address), so their `X-Real-IP` is ignored.
|
||||
|
||||
### 7. Router forward
|
||||
### 7. Router: no new rule
|
||||
|
||||
ASUS router web UI, WAN, Virtual Server / Port Forwarding: add a rule: Service Name `Kvitt`, Protocol TCP, External
|
||||
Port `8443`, Internal Port `443` (NPM's HTTPS port; if the add-on's Network settings map `443/tcp` to another host
|
||||
port, use that one), Internal IP Address `<HA_HOST>`; Apply. No rule forwards to the Kvitt Pi (remove one to
|
||||
`<KVITT_PI_HOST>` if you made it for the Caddy version). Then on the phone with Wi-Fi off, open
|
||||
`https://<KVITT_PUBLIC_HOST>:8443/health` in Chrome: it shows `"status":"ok"` without a certificate warning (yes/no).
|
||||
No new rule (D-124): external TCP 443 already reaches the Home Assistant Pi (NPM) for Home Assistant, and NPM picks
|
||||
Kvitt's proxy host by name on the same port. Verify it exists: ASUS router web UI, WAN, Virtual Server / Port
|
||||
Forwarding: the list has a TCP rule with External Port `443` and Internal IP Address `<HA_HOST>` (yes/no). Nothing is
|
||||
forwarded to the Kvitt Pi: remove a rule to `<KVITT_PI_HOST>` if you made one for the Caddy version, and a rule with
|
||||
External Port `8443` if you made one for the earlier NPM version (D-120). Then on the phone with Wi-Fi off, open
|
||||
`https://<KVITT_PUBLIC_HOST>/health` in Chrome: it shows `"status":"ok"` without a certificate warning (yes/no).
|
||||
|
||||
### 8. First backup and bootstrap
|
||||
|
||||
@@ -526,9 +528,10 @@ On the Pi:
|
||||
/opt/kvitt/bootstrap.sh # first install only: prints the admin invite code, link and expiry
|
||||
```
|
||||
|
||||
The invite link starts with `https://<KVITT_PUBLIC_HOST>:8443/i/`. The release app opens such links and connects to
|
||||
that host only once an Android build carries the new host name (SPEC §8.5; until then the app knows
|
||||
`icehack.asuscomm.com`): tell the orchestrator before pairing a release build.
|
||||
The invite link starts with `https://<KVITT_PUBLIC_HOST>/i/` (no port). The release app opens such links and connects to
|
||||
that host only once its Android build carries the host name (`kvitt.publicHost`, D-121) and the default port 443
|
||||
(D-124; builds made for port 8443 do not match a link without a port): tell the orchestrator before pairing a
|
||||
release build.
|
||||
|
||||
### 9. Deploying a new version
|
||||
|
||||
@@ -552,8 +555,8 @@ copying the bundle again (step 4, without the secrets) and running `./install.sh
|
||||
### Fallback (SPEC §10.5.3): when NPM is down
|
||||
|
||||
* NPM or the Home Assistant Pi down means Kvitt is unreachable from outside, and also from home over
|
||||
`https://<KVITT_PUBLIC_HOST>:8443` (that path goes through NPM too). The server on the Kvitt Pi keeps running and
|
||||
nothing there needs to change. The phones keep working on their full local copy and sync when NPM is back.
|
||||
`https://<KVITT_PUBLIC_HOST>` (that path goes through NPM too); Home Assistant is then unreachable from outside as
|
||||
well (same NPM, same port 443). The server on the Kvitt Pi keeps running and nothing there needs to change. The phones keep working on their full local copy and sync when NPM is back.
|
||||
* On the home Wi-Fi only **debug builds** can reach the server directly, at `http://<KVITT_PI_HOST>:8080` (AC-2.8
|
||||
allows `http://` to private LAN addresses in debug builds only). **Release builds require HTTPS with a publicly
|
||||
trusted certificate (SPEC §8.5) and cannot use that address: there is no LAN fallback for the release app.** A
|
||||
@@ -580,12 +583,13 @@ copying the bundle again (step 4, without the secrets) and running `./install.sh
|
||||
| Memory | `docker stats --no-stream` (server RSS under 100 MB with both phones syncing) |
|
||||
| Disk | `df -h /` and `docker system df`; SQLite a few MB, pictures under 1 MB, APKs about 10 MB each since R8, 5 kept |
|
||||
| Image digests deployed | `tail -n 8 /opt/kvitt/deployed-digests` |
|
||||
| Client IPs are real (once, after the router forward) | Open `https://<KVITT_PUBLIC_HOST>:8443/health` on the phone with Wi-Fi off, then within a minute on the Kvitt Pi `sudo apt install -y conntrack` (once) and `sudo conntrack -L -p tcp --orig-port-dst 8080`: in every line the first `src=` is `<HA_HOST>` and the second half (from the second `src=`) has `dst=<HA_HOST>`, no `172.*` there, so the server sees NPM's address and trusts its `X-Forwarded-For`. If the second half shows a `172.*` address instead, every internet client shares one per-IP limit (SPEC §8.6): tell the orchestrator |
|
||||
| Client IPs are real (once, after step 7) | Open `https://<KVITT_PUBLIC_HOST>/health` on the phone with Wi-Fi off, then within a minute on the Kvitt Pi `sudo apt install -y conntrack` (once) and `sudo conntrack -L -p tcp --orig-port-dst 8080`: in every line the first `src=` is `<HA_HOST>` and the second half (from the second `src=`) has `dst=<HA_HOST>`, no `172.*` there, so the server sees NPM's address and trusts its `X-Forwarded-For`. If the second half shows a `172.*` address instead, every internet client shares one per-IP limit (SPEC §8.6): tell the orchestrator |
|
||||
| Restore | `infra/RESTORE.md` |
|
||||
|
||||
**APK downloads (security review L-3, D-119).** `GET /apk/files/<name>` is public (the landing page links it) and
|
||||
about 11 MB per response; the server allows 10 per minute per client IP. If one address still fetches
|
||||
`/apk/files/` over and over (NPM's access log), remove the router forward (step 7) until it stops.
|
||||
`/apk/files/` over and over (NPM's access log), disable Kvitt's proxy host in NPM (Hosts, Proxy Hosts, the row's
|
||||
menu, Disable) until it stops; the router's 443 forward also serves Home Assistant, so leave it in place.
|
||||
|
||||
### Owner decisions (open)
|
||||
|
||||
|
||||
@@ -10,9 +10,9 @@ KVITT_REGISTRY=gitea.lan/jacobh2
|
||||
# or the short git SHA that infra/build-images.sh printed.
|
||||
KVITT_TAG=CHANGE_ME_TAG
|
||||
# Kvitt's own public host name (D-120), owner-chosen: a domain or DuckDNS name pointing at the home's public address.
|
||||
# NPM holds its Let's Encrypt certificate; the router forwards external 8443 to NPM's 443. Invite links and the
|
||||
# landing page use https://<KVITT_PUBLIC_HOST>:8443 (KVITT_PUBLIC_URL). Not icehack.asuscomm.com: that name is
|
||||
# NPM's proxy host for Home Assistant.
|
||||
# NPM holds its Let's Encrypt certificate and serves it on the default HTTPS port 443, which the router already
|
||||
# forwards to NPM for Home Assistant (D-124). Invite links and the landing page use https://<KVITT_PUBLIC_HOST>
|
||||
# (KVITT_PUBLIC_URL, no port). Not icehack.asuscomm.com: that name is NPM's proxy host for Home Assistant.
|
||||
KVITT_PUBLIC_HOST=CHANGE_ME_KVITT_PUBLIC_HOST
|
||||
# LAN IPv4 address of the Home Assistant Pi, where Nginx Proxy Manager terminates TLS and forwards to this Pi's port
|
||||
# 8080 (D-120). The server trusts X-Forwarded-For only from this address (KVITT_TRUSTED_PROXY), so it must be the
|
||||
|
||||
@@ -30,7 +30,7 @@ services:
|
||||
environment:
|
||||
TZ: Europe/Stockholm
|
||||
KVITT_DATA_DIR: /data
|
||||
KVITT_PUBLIC_URL: https://${KVITT_PUBLIC_HOST:?set KVITT_PUBLIC_HOST in .env}:8443
|
||||
KVITT_PUBLIC_URL: https://${KVITT_PUBLIC_HOST:?set KVITT_PUBLIC_HOST in .env}
|
||||
# NPM's LAN address: only a request whose TCP peer is this address may set X-Forwarded-For, whose last entry
|
||||
# is then the client IP of the rate limits (SPEC §8.5, §8.6, D-120). An IP literal (install.sh checks it).
|
||||
KVITT_TRUSTED_PROXY: ${HA_HOST:?set HA_HOST in .env}
|
||||
|
||||
+5
-4
@@ -6,7 +6,8 @@
|
||||
KVITT_DIR=${KVITT_DIR:-/opt/kvitt}
|
||||
# The server's port, published on the Pi's LAN interfaces; Nginx Proxy Manager forwards to it (D-120).
|
||||
SERVER_URL=http://localhost:8080
|
||||
# Nginx Proxy Manager's HTTPS port on the Home Assistant Pi (the router forwards external 8443 to it, D-120).
|
||||
# Nginx Proxy Manager's HTTPS port on the Home Assistant Pi, the default HTTPS port that Kvitt's public URL uses
|
||||
# (the router's existing external 443 forward reaches it, D-124).
|
||||
NPM_HTTPS_PORT=443
|
||||
|
||||
say() { printf '%s\n' "$*"; }
|
||||
@@ -76,11 +77,11 @@ print_proxy_health() {
|
||||
local out ha_host host
|
||||
ha_host=$(env_value HA_HOST)
|
||||
host=$(env_value KVITT_PUBLIC_HOST)
|
||||
if out=$(curl -fsS --max-time 15 --resolve "$host:$NPM_HTTPS_PORT:$ha_host" "https://$host:$NPM_HTTPS_PORT/health" 2>&1); then
|
||||
say "GET https://$host:$NPM_HTTPS_PORT/health (via NPM at $ha_host): $out"
|
||||
if out=$(curl -fsS --max-time 15 --resolve "$host:$NPM_HTTPS_PORT:$ha_host" "https://$host/health" 2>&1); then
|
||||
say "GET https://$host/health (via NPM at $ha_host:$NPM_HTTPS_PORT): $out"
|
||||
return 0
|
||||
fi
|
||||
say "GET https://$host:$NPM_HTTPS_PORT/health (via NPM at $ha_host) failed: $out"
|
||||
say "GET https://$host/health (via NPM at $ha_host:$NPM_HTTPS_PORT) failed: $out"
|
||||
return 1
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ KVITT_PUBLIC_HOST="$(sed -n 's/^KVITT_PUBLIC_HOST=//p' "$env_file" | tail -n 1)"
|
||||
[[ "$KVITT_PUBLIC_HOST" != icehack.asuscomm.com ]] \
|
||||
|| { echo "publish-apk.sh: KVITT_PUBLIC_HOST cannot be icehack.asuscomm.com, NPM's proxy host for Home Assistant (D-120)." >&2; exit 1; }
|
||||
|
||||
echo "Release $version_name (version_code $version_code) for https://$KVITT_PUBLIC_HOST:8443"
|
||||
echo "Release $version_name (version_code $version_code) for https://$KVITT_PUBLIC_HOST"
|
||||
|
||||
PATH="/opt/homebrew/opt/openjdk@25/bin:$PATH" tools/gradle-serial.sh :app:assembleRelease \
|
||||
"-Pkvitt.versionCode=$version_code" "-Pkvitt.versionName=$version_name" "-Pkvitt.publicHost=$KVITT_PUBLIC_HOST"
|
||||
@@ -94,7 +94,7 @@ done | sort -n -r | tail -n +6 | while read -r old; do
|
||||
done
|
||||
|
||||
echo "Staged in infra/pi/out/apk/: $file_name ($size_bytes bytes, sha256 $sha256) and latest.json."
|
||||
echo "Built for the release server https://$KVITT_PUBLIC_HOST:8443 (KVITT_PUBLIC_HOST in infra/pi/.env)."
|
||||
echo "Built for the release server https://$KVITT_PUBLIC_HOST (KVITT_PUBLIC_HOST in infra/pi/.env)."
|
||||
echo "Copy to the Pi and install (infra/README.md, Publishing an APK):"
|
||||
echo " scp infra/pi/out/apk/latest.json infra/pi/out/apk/$file_name infra/pi/out/apk/kvitt-$version_code.mapping.txt <ssh user>@<Kvitt Pi address>:/opt/kvitt/apk/"
|
||||
echo " ssh <ssh user>@<Kvitt Pi address> /opt/kvitt/install-apk.sh"
|
||||
|
||||
Reference in New Issue
Block a user