feat: add Cloudflare tunnel hosting

This commit is contained in:
Jet 2026-05-28 14:50:07 -07:00
parent e6c1b82679
commit 23e087ae4b
No known key found for this signature in database
15 changed files with 839 additions and 30 deletions

113
docs/hosting.md Normal file
View file

@ -0,0 +1,113 @@
# Hosting
Noisebell is hosted with DigitalOcean as the public service and observability hub, the Raspberry Pi on Tailscale, and Cloudflare Tunnel as the only intended public HTTP entry point.
## Public Routes
| Hostname | Target | Access |
|---|---|---|
| `noisebell.extremist.software` | cache public routes | public via Cloudflare Tunnel |
| `rss-noisebell.extremist.software` | RSS service | public via Cloudflare Tunnel |
| `grafana-noisebell.extremist.software` | Grafana | public via Cloudflare Tunnel, Grafana login required |
`discord.noisebell.extremist.software` is not routed through the tunnel. The Discord bot stays local to the DigitalOcean host and receives cache webhooks at `127.0.0.1`.
## Private Routes
| Route | Purpose |
|---|---|
| `jet@noisebell-do` | DO administration over Tailscale SSH |
| `pi@noisebell-pi` or `pi@100.66.45.36` | Pi administration over Tailscale SSH |
| `noisebell-do:3000/webhook` | Pi state-change webhook to cache |
| `noisebell-pi:80` | DO cache polling and Pi app metrics |
| `noisebell-pi:8090` | cache to Pi Home Assistant relay |
| `noisebell-pi:9100` | Pi node exporter metrics |
| `noisebell-do:3100` | Pi journal shipping to Loki |
## DigitalOcean Firewall
NixOS firewall policy is in `hosts/noisebell-do/configuration.nix`.
SSH is Tailscale-only because no public TCP SSH port is opened and `tailscale0` is trusted. Direct public HTTP and HTTPS stay open while `services.noisebell-public-gateway.enable = false` so an accidental pre-tunnel deploy does not break the existing site. Once the Cloudflare Tunnel module is enabled, public TCP ports close and all public web access enters through the tunnel.
After the tunnel is verified, mirror that policy in the DigitalOcean cloud firewall: close public inbound `22`, `80`, and `443`; keep outbound open; keep Tailscale UDP reachable as needed.
## Cloudflare Tunnel
The tunnel module is `hosts/noisebell-do/public-gateway.nix`. It defines ingress for the three public hostnames and returns `404` for everything else.
The module reconciles Cloudflare through the API when enabled:
- creates a locally-managed tunnel named `noisebell-do` if it does not exist
- writes the tunnel credentials JSON to `/var/lib/noisebell-public-gateway/credentials.json`
- upserts proxied CNAME records for the public hostnames
- starts `cloudflared` with local Nix-managed ingress rules
Create two age secrets before enabling the module:
```sh
cd secrets
agenix -e cloudflare-api-token.age
nix shell nixpkgs#openssl -c openssl rand -base64 32
agenix -e cloudflare-tunnel-secret.age
```
Paste the generated base64 value into `cloudflare-tunnel-secret.age`.
Set `services.noisebell-public-gateway.accountId` and `services.noisebell-public-gateway.zoneId` in `hosts/noisebell-do/configuration.nix`, then flip `services.noisebell-public-gateway.enable = true`.
Required Cloudflare API token scopes:
| Resource | Scope |
|---|---|
| Account | `Cloudflare Tunnel:Edit` or `Cloudflare One Connector: cloudflared:Edit` |
| Zone | `DNS:Edit` for `extremist.software` |
The token should be restricted to the Noisebell Cloudflare account and the `extremist.software` zone.
## Grafana
Grafana listens on `127.0.0.1:3030` on the DO host. Public access is through `https://grafana-noisebell.extremist.software/` and requires the Grafana login form.
The admin user is `admin`. The password is generated on the DO host at first start and stored in `/var/lib/grafana/admin_password`.
```sh
ssh jet@noisebell-do sudo cat /var/lib/grafana/admin_password
```
Two dashboards are provisioned from `hosts/noisebell-do/observability.nix`:
| Dashboard | Audience | Datasources |
|---|---|---|
| `Noisebell Full Debug` | authenticated operators | Prometheus and Loki |
| `Noisebell Public` | anyone with the shared link | Prometheus only |
Use Grafana externally shared dashboards for the public-safe view. `noisebell-grafana-public-dashboard.service` creates or refreshes the public share on boot/deploy with a deterministic token.
The public-safe URL is:
```text
https://grafana-noisebell.extremist.software/public-dashboards/6e6f69736562656c6c7075626c696330
```
The RSS and Grafana hostnames use one label under `extremist.software` so they are covered by Cloudflare Universal SSL. Nested names like `grafana.noisebell.extremist.software` require additional Cloudflare certificate setup.
The helper script `scripts/share-grafana-public-dashboard jet@noisebell-do` can still be used to repair or print that URL manually. The public dashboard intentionally avoids Loki/raw journal panels and uses stored Prometheus queries only.
## Pi Hardening
`scripts/deploy-pios-pi.sh` configures the Raspberry Pi OS host. It now uses `NOISEBELL_ENDPOINT_URL=http://noisebell-do:3000/webhook` by default, so state changes go to the cache over Tailscale instead of the public domain.
The deploy script applies a persistent firewall service, `noisebell-tailscale-only-firewall.service`, that drops non-Tailscale TCP traffic to `22`, `80`, `8090`, and `9100`. Existing SSH sessions survive because established connections are allowed. New SSH, app, relay, and node exporter access must use Tailscale.
Deploy the Pi over Tailscale after the first bootstrap:
```sh
HOME_ASSISTANT_BASE_URL=http://10.21.0.43:8123 scripts/deploy-pios-pi.sh pi@100.66.45.36
```
Override the cache webhook only if needed:
```sh
NOISEBELL_CACHE_WEBHOOK_URL=http://noisebell-do:3000/webhook scripts/deploy-pios-pi.sh pi@100.66.45.36
```