homelab-blueprint/docs/network/allow-caddy-to-apps.md
Brian Pooe fef521909b feat(forgejo): expose publicly at git.brianpooe.com with SSO-free access
Public repos are now browsable/clonable anonymously (HTTPS) and over SSH,
proxied through the Hetzner VPS. Unlike other public apps this is
deliberately not behind the oauth2-proxy/Zitadel gate, since Forgejo
already enforces its own per-repo visibility. Root redirects to the
.profile landing page. Documents the REQUIRE_SIGNIN_VIEW gotcha and the
Zitadel OIDC redirect_uri fix needed whenever FORGEJO_DOMAIN changes.
2026-07-19 18:34:13 +02:00

245 lines
10 KiB
Markdown

# Caddy Firewall Policies — Missing and Incorrect Rules
> [!NOTE]
> For the complete list of custom firewall rules, see [firewall-rules.md](firewall-rules.md).
Audit findings from the zone policy review. Apply these in UniFi
**Settings → Policy Engine → Policy Table** in the order shown. All
custom policies must be above the built-in `Block All Traffic` terminal
rule.
---
## 0. Immediate Cleanup — Redundant and Incorrect Rules
Sort these out before adding anything new.
### Delete — completely redundant
| Policy | Why |
|---|---|
| `Allow CADDY to Gateway DNS` | The Caddy Pi's `resolv.conf` points to Technitium at `10.0.10.5` (TRUSTED). `Allow CADDY to DNS` already covers that. The UDM Gateway DNS forwarder is not in use. |
### Keep — intra-zone rules that are actually required
UniFi's documentation implies intra-zone traffic is allowed by default, but in practice this does **not** apply to custom zones — removing these breaks intra-zone traffic.
| Policy | Why to keep |
|---|---|
| `Allow TRUSTED to TRUSTED Any` | Without this rule, devices within TRUSTED cannot reach each other (confirmed: removing it broke SSH to Technitium). |
| `Allow IoT to IoT Any` | Same — IoT intra-zone traffic requires an explicit allow for custom zones. |
### Fix — wrong action
| Policy | Problem | Fix |
|---|---|---|
| `Allow FAMILY to CADDY` | Action is set to **Block** instead of Allow. FAMILY devices cannot reach Caddy at all. | Edit the policy and change Action to **Allow**. |
### Review — direct pinholes that bypass Caddy
These work but contradict the network design (§7.3/7.4), which says MEDIA
and FAMILY should only reach internal services through Caddy. Once the
`ALLOW CADDY to arr stack` backend policy (§2 below) is in place, both
zones can reach Emby via `emby.yourdomain.com` through Caddy instead.
| Policy | Impact of keeping | Impact of removing |
|---|---|---|
| `Allow MEDIA to Emby` | Apple TV can connect directly to LXC — wider attack surface, bypasses Caddy logging and auth. | Apple TV must use `emby.yourdomain.com` via Caddy. Requires Caddy backend policy in place first. |
| `Allow FAMILY to EMBY` | Family devices bypass Caddy for Emby. | Family devices must use `emby.yourdomain.com` via Caddy. Requires Caddy backend policy in place first. |
Remove these only **after** the `ALLOW CADDY to arr stack` policy is
created and confirmed working.
---
## 1. Fix: Allow FAMILY to CADDY (currently set to Block)
The existing `Allow FAMILY to CADDY` policy has **Action = Block**. Edit
it and change the action to **Allow**.
| Field | Value |
|---|---|
| Name | `Allow FAMILY to CADDY` |
| Action | **Allow** (currently wrong — set to Block) |
| Source zone | `FAMILY` |
| Source IP | Any |
| Destination zone | `CADDY` |
| Destination IP | Any |
| Destination port | `443` |
| Protocol | TCP |
---
## 2. CADDY → TRUSTED Backend App Policies
Caddy at `10.0.15.5` cannot proxy to any backend apps in the TRUSTED zone
until these policies exist. Create one policy per row.
Every policy in this table uses:
- **Action:** Allow
- **Source zone:** `CADDY`
- **Source IP:** `10.0.15.5`
- **Destination zone:** `TRUSTED`
- **Protocol:** TCP
| Policy name | Destination IP | Destination port(s) | Purpose |
|---|---|---|---|
| `ALLOW CADDY to arr stack` | `10.0.10.20` | `5055,6767,7878,8000,8080,8096,8191,8282,8989,9696` | Arr-stack LXC (bazarr, radarr, gluetun, qbittorrent, emby, flaresolverr, sabnzbd, sonarr, prowlarr, seerr) |
| `ALLOW CADDY to Technitium UI` | `10.0.10.5` | `5380` | Technitium admin UI |
| `ALLOW CADDY to Proxmox` | `10.0.10.10` | `8006` | Proxmox VE |
| `ALLOW CADDY to PBS` | `10.0.10.8` | `8007` | Proxmox Backup Server |
| `ALLOW CADDY to LAN web devices` | `10.0.1.6` | `80` | Lite-8 PoE switch UI |
| `ALLOW CADDY to Beszel` | `10.0.10.13` | `8090` | Beszel monitoring |
| `ALLOW CADDY to Gramps` | `10.0.10.17` | `80` | Gramps Web |
| `ALLOW CADDY to Immich` | `10.0.10.15` | `2283` | Immich |
| `ALLOW CADDY to IT Tools` | `10.0.10.7` | `80` | IT Tools |
| `ALLOW CADDY to Paperless` | `10.0.10.11` | `8000` | Paperless-ngx |
| `ALLOW CADDY to YTD` | `10.0.10.14` | `3087` | Youtarr |
| `ALLOW CADDY to Bento PDF` | `10.0.10.6` | `8080` | Bento PDF |
| `ALLOW CADDY to Koffan` | `10.0.10.12` | `8080` | Koffan (LXC 111) |
| `ALLOW CADDY to Hermes` | `10.0.10.19` | `9119` | Hermes Agent dashboard (LXC 113) |
| `ALLOW CADDY to Forgejo` | `10.0.10.18` | `3000,22` | Forgejo web UI (LXC 114) and git-over-SSH for hosts pulling from raspberrypi's zone |
---
## 3. CADDY → IoT Backend Policies
Caddy cannot proxy to Home Assistant or Zigbee until these exist.
Every policy in this table uses:
- **Action:** Allow
- **Source zone:** `CADDY`
- **Source IP:** `10.0.15.5`
- **Destination zone:** `IoT`
- **Protocol:** TCP
| Policy name | Destination IP | Destination port(s) | Purpose |
|---|---|---|---|
| `ALLOW CADDY to Home Automation` | `10.0.30.6` | `8080,8123` | Zigbee2MQTT and Home Assistant |
| `ALLOW CADDY to Zigbee` | `10.0.30.5` | `80` | SLZB-06U Zigbee coordinator UI |
---
## 3a. IoT → CADDY Policies (reverse direction)
The Home Assistant host itself also needs to reach Caddy directly — it
pulls `homelab-blueprint` from Forgejo (`git.brianpooe.com`, proxied
through Caddy) to deploy its own docker-compose stack.
**Two independent layers enforce traffic into the Caddy host — both must
allow it, and both were found misconfigured while wiring this up:**
1. **UniFi zone-based firewall** (router-level, VLAN → VLAN):
| Policy name | Source zone | Source IP | Src. Port | Destination zone | Destination IP | Destination port(s) | Purpose |
|---|---|---|---|---|---|---|---|
| `Allow HA to Caddy` | `IoT` | `10.0.30.6` (Home Assistant IP) | `Any` | `CADDY` | `10.0.15.5` (Caddy IP) | `443` | git fetch/pull of homelab-blueprint from Forgejo |
Src. Port must be `Any`, not restricted to HA's own listening ports
(8080/8123) — this is outbound traffic *from* HA, so the source port is
a random ephemeral port, not the port HA listens on. Setting Src. Port
to a "Home Assistant" port object here was the original bug: the rule
never matched, so traffic fell through to `Block All Traffic` below it.
2. **`ufw` on the Caddy host itself** (10.0.15.5) — a second, separate
enforcement point the UniFi rule doesn't touch. It allow-lists inbound
80/443 per source subnet and did not include the IoT VLAN
(`10.0.30.0/24`):
```bash
sudo ufw allow from 10.0.30.0/24 to any port 80,443 proto tcp
```
## 3b. CADDY / IoT → Forgejo git-over-SSH (port 22)
Both raspberrypi (CADDY zone) and Home Assistant (IoT zone) clone
`homelab-blueprint` over SSH directly against the Forgejo LXC
(`10.0.10.18:22`), bypassing Caddy entirely — SSH can't be proxied
through Caddy (HTTP/S only), so this traffic goes straight to the TRUSTED
zone. `10.0.10.18` has no host-level firewall (`ufw` not installed,
`iptables INPUT` policy `ACCEPT`), so only the UniFi zone rule matters
here — no second enforcement layer to fight, unlike the Caddy host above.
Forgejo's git-SSH runs on the standard port 22 on this LXC, not the usual
`2222` (see `docker-compose-files/forgejo/README.md`) — the LXC's own
admin sshd was relocated to port **2200** (systemd `ssh.socket` override)
to free 22 up. Direct admin SSH to this LXC now needs `-p 2200`; `pct exec
114` from the Proxmox host is unaffected and still works normally.
| Policy name | Source zone | Source IP | Destination zone | Destination IP | Destination port(s) | Purpose |
|---|---|---|---|---|---|---|
| `ALLOW CADDY to Forgejo` (existing rule, port added) | `CADDY` | `10.0.15.5` | `TRUSTED` | `10.0.10.18` | `3000,22` | Forgejo web UI + git-over-SSH from raspberrypi |
| `Allow HA to Forgejo SSH` | `IoT` | `10.0.30.6` | `TRUSTED` | `10.0.10.18` | `22` | git-over-SSH from homeassistant |
`ufw status` on that host is the fastest way to check which subnets can
already reach it before assuming a UniFi rule is the problem.
## 3c. Public Exposure of Forgejo (Hetzner VPS)
Forgejo is also reachable from the public internet, at `git.brianpooe.com`
(HTTPS, public repos only — anonymous browsing/cloning, like GitHub) and
`ssh://git@forgejo.brianpooe.com` (SSH, requires a registered account key —
self-registration is disabled, so this is effectively you-only). Full detail
lives in `docker-compose-files/forgejo/README.md` ("Public Exposure") and,
on the VPS itself, `~/netbird-vps/docs/netbird-acl-playbook.md` and
`public-app-sso.md`. Summary of what changed on the LAN side of that path:
- **DNS is split-horizon**, not a new firewall rule: `git.brianpooe.com` and
`forgejo.brianpooe.com` are each their own single-name Technitium zone
(`A` record direct to `10.0.15.5` and `10.0.10.18` respectively) so LAN
clients resolve them locally while the public `*.brianpooe.com` wildcard
sends everyone else to the VPS. No UniFi policy changes were needed for
the HTTPS path — it already flows through the existing `ALLOW CADDY to
Forgejo` rule above.
- **The public SSH path is a new NetBird route**, not a UniFi rule: the VPS
NetBird peer previously only routed to the Pi Caddy (`10.0.15.5/32`).
A second `10.0.10.18/32` resource (routing peer `pi-caddy`, "Model 2" in
the playbook) was added so the VPS can reach Forgejo's git-SSH directly.
The VPS forwards its own (now-freed) public port 22 to `10.0.10.18:22`
via a `socat` container (`~/netbird-vps/ssh-forward/`) — the VPS's own
admin sshd was relocated to port 2200 to make room.
---
## 4. Public DNS Block Policies (per zone)
Without these, devices can bypass Technitium by querying public resolvers
(`8.8.8.8`, `1.1.1.1`, etc.) directly. Place each policy **above** the
broad `External` allow for its zone.
Every policy in this table uses:
- **Action:** Block
- **Source IP:** Any
- **Destination zone:** `External`
- **Destination IP:** Any
- **Protocol:** TCP/UDP
- **Destination port:** `53`
| Policy name | Source zone |
|---|---|
| `BLOCK FAMILY public DNS` | `FAMILY` |
| `BLOCK MEDIA public DNS` | `MEDIA` |
| `BLOCK IoT public DNS` | `IoT` |
| `BLOCK GUEST public DNS` | `GUEST` |
---
## Policy Order Reference
Within each zone's rule set, the evaluation order must be:
```
specific allows (DNS, DHCP, Caddy, app pinholes)
public DNS block (above the External allow)
External allow
Gateway DHCP allow
Gateway block
Block All Traffic ← built-in terminal rule
```
If a broad block rule sits above a specific allow, the block wins and the
intended traffic is dropped. Use **Reorder** in UniFi to verify positions
after adding new policies.