SmartestHome/docs/network-integration.md

14 KiB

Network integration — OPNsense, VLANs, and whether to port-forward anything

Short answer: don't port-forward any of this to the WAN. Nothing in this repo needs to be reachable from the public internet, and forwarding it would trade a huge amount of security for very little. If you want to reach the household stack while away from home, the answer is a VPN back into your own network, not opening ports on the firewall. The rest of this document explains why, and how to segment things internally with VLANs so a compromised IoT device (cameras especially — see Phase 5/20's "zero WAN egress" guardrail) can't reach the rest of your network either.

This is guidance, not automation — nothing under this repo touches your OPNsense config, same convention as this project's HA-integration catalog entries (docs/project-plan.md §2): you apply this by hand in the OPNsense web UI.

1. Why no port forward

Every custom service this repo builds (identity, pantry-vision, transit, admin-canvas, digest-web/admin-web/pantry-web/identity-web, chores, trash-calendar) is a small stdlib-http.server/nginx process, bearer-token gated where it needs to be, but none of it was built or hardened with "reachable from the raw internet" as a threat model — no rate limiting, no WAF, no DDoS/abuse handling, no security audit. Home Assistant, Grocy, Frigate, Node-RED, Portainer, Netdata, and every other off-the-shelf piece of the stack are the same story: capable, actively maintained software, but not written or configured here with public exposure in mind. Port-forwarding any of them turns "a bug in one of these" into "a bug reachable by the entire internet, scanning for it constantly." None of them need to be reachable from the internet in the first place — every real use case (checking the dashboard from work, registering a guest while out, planning a trip) is solved just as well by a VPN.

2. Remote access: WireGuard, not forwarded HTTP ports

OPNsense ships a WireGuard implementation (Instances/Peers under VPN → WireGuard, built in since 22.1 — no separate package needed on current OPNsense). Set up one instance on the firewall and a peer per device you want remote access from (your phone, a laptop). The only thing that ever needs forwarding is one UDP port for WireGuard itself (commonly 51820, but pick anything free) — and that's a categorically different exposure than forwarding this stack's HTTP services directly: WireGuard silently drops any packet that isn't from an already-paired, cryptographically-authenticated peer, so an internet-wide scanner sees nothing to attack at all, versus a live HTTP endpoint answering every request that reaches it. Once connected, your phone/laptop is (virtually) on your LAN/VLAN and reaches identity/pantry-vision/HA/etc. at their normal LAN IPs — no per-service forwarding, no bearer tokens exposed to the WAN, nothing else to configure network- side per new service this repo adds later.

If you don't need remote access at all, skip this section entirely — every service in this stack works purely on the LAN with zero WAN configuration.

2.1 Split tunnel — route the smart-home VLAN, nothing else

AllowedIPs on the client profile is both the route table and WireGuard's crypto ACL, so it alone decides what enters the tunnel. Route the smart-home VLAN and leave everything else on the phone's own connection:

[Peer]
AllowedIPs = 192.168.30.0/24    # the smart-home VLAN, adjust to your real subnet
# NOT 0.0.0.0/0 (sends all your traffic home for no reason)
# NOT 192.168.0.0/16 — see the collision warning below

Never route the whole 192.168.0.0/16. Practically every café, hotel and hotspot LAN lives on 192.168.0.0/24 or 192.168.1.0/24; routing the entire /16 makes the phone send that network's own gateway down the tunnel, and you lose connectivity on that Wi-Fi altogether. Route only your actual VLAN prefix — and if your home LAN is itself on 192.168.1.0/24, put the smart-home VLAN somewhere unlikely (192.168.73.0/24, say) so the clash can't arise in the first place.

Two client-side settings that are easy to get wrong together:

  • Android "always-on VPN": on. "Block connections without VPN" (lockdown): OFF. Lockdown drops everything not traversing the tunnel, which with a split tunnel kills all non-192.168.x traffic on the device.
  • Leave DNS = unset. Reach services by LAN IP (http://192.168.30.x:8090 for ntfy). No internal DNS needed, and your DNS queries don't get pulled home while you're out.

This is just §3's rule 4 (route WireGuard peers into the smart-home VLAN, not Trusted LAN) expressed on the client side.

2.2 Decided: ntfy stays LAN-only, no DMZ, no port forward

identity's arrival notifications (Phase 6b — "tell me when someone gets home") were the first thing in this project with a real argument for WAN exposure, so the alternatives were weighed properly and rejected:

Option Verdict
ntfy stays in the compose stack, reached over WireGuard Chosen. identity → ntfy is a container-to-container call that never reaches OPNsense at all — no firewall rule, no DMZ, no certificates. ntfy's Android app does instant delivery straight to a LAN server, so it works at home with nothing configured and away over the split tunnel above
ntfy in a DMZ, port-forwarded Rejected. Buys only "notifications without the VPN connected," at the cost of a public listener, TLS/reverse-proxy upkeep, and mandatory ntfy auth — an exposed topic leaks "the house is empty right now", which is materially worse than the usual leak of a self-hosted service
ntfy in a DMZ, reached from the smart-home VLAN by NAT reflection (hairpin) instead of an inter-VLAN rule Rejected. It does avoid a new rule — the smart-home VLAN's existing blanket outbound-WAN allow already covers traffic to your own public IP — but it makes a purely internal notification path depend on the ISP and external DNS being up, so two devices metres apart stop talking during a WAN outage. CGNAT breaks it outright, and the rule it avoids (smart-home → DMZ:443, one host, one port, outbound) was the safe direction anyway; DMZ → smart-home stays denied either way

The household uses no Apple devices, which removes the one genuine forcing function for exposure: ntfy's iOS app can only be woken by Apple's APNs, so a self-hosted server would need upstream-base-url relaying through ntfy.sh — WAN egress through a third party, in an otherwise fully-local stack. Android needs none of that. Revisit this table only if an Apple device ever joins the household.

3. VLAN segmentation — not a DMZ, a blast-radius boundary

A traditional DMZ exists to host something the WAN needs to reach. Nothing here needs that (§1), so "do I need a DMZ" isn't really the right question — the useful question is whether an IoT device on this network should be able to reach your laptop, NAS, or anything else you actually care about if it's ever compromised. Zigbee/RuView nodes, ESPHome BLE proxies, and especially the Tapo cameras (Phase 20 — IP cameras have a genuinely bad industry-wide security track record) are the class of device this matters most for. The fix is a dedicated VLAN, not a DMZ.

A reasonable split for this project's device inventory:

VLAN What goes on it Internet access Reaches trusted LAN?
Trusted LAN (existing) Your own laptops/desktops, phones (when not on the VPN) Yes
Smart-home VLAN The container host, kiosks/thin-clients/touch panels, RuView nodes, ESPHome BLE proxies, Zigbee coordinator Yes (needed: Ollama model pulls if not fully pre-cached, container image pulls, NTP, and any digest-engine ingestion sources — mail/Telegram/Discord/news feeds/FRED/Stooq all reach out to real external APIs, see digest-engine/README.md) No, by default — see below
Camera VLAN Tapo pan/tilt cameras (Phase 20), the existing peephole cam (Phase 5) No — block entirely No

Rules to actually write in OPNsense (Firewall → Rules → <VLAN interface>):

  1. Inter-VLAN default-deny. OPNsense's default "allow all outbound, block inbound from other interfaces" behavior already gets you most of the way — just don't add a blanket "Smart-home VLAN → Trusted LAN, any/any" rule. Add narrow allow rules only for what's actually needed (e.g. if you want to browse to Home Assistant from a trusted-LAN laptop without a VPN, that's a Trusted LAN → Smart-home VLAN rule scoped to port 8123, not the reverse direction).
  2. Camera VLAN: block outbound to the WAN entirely, and block Camera VLAN → every other VLAN except the one narrow path Frigate needs (RTSP/API from the container host's own interface, since Frigate is what actually pulls camera streams — the camera itself never needs to initiate anything toward the container host, only receive the stream pull, so even that path can often be a single allow rule scoped to the container host's IP and the camera's RTSP port). This is the same invariant this project's own testing checklist already states for the peephole cam (docs/project-plan.md's Phase 5 guardrail, "are cameras verified to have zero WAN egress?") — it applies identically to every Tapo camera added in Phase 20.
  3. Smart-home VLAN → Trusted LAN: deny by default, add narrow exceptions only if you have a real reason (e.g. Nextcloud/CalDAV living on a trusted-LAN machine rather than inside this stack — then just that one host:port, not the whole VLAN).
  4. WireGuard peers land wherever you configure the tunnel's allowed-IPs to route — typically you'd route a WireGuard peer into the Smart-home VLAN (so your phone, once connected, can reach identity/pantry-vision/HA the same way it would sitting on that VLAN at home), not into Trusted LAN.

None of this is automated by anything in this repo — the container host's own network interface/VLAN tagging, and every rule above, is configured by hand in OPNsense, same "nothing under this repo builds the HA/network side" convention as the rest of this project's integration catalog entries.

4. Full port inventory (LAN-only — none of these get forwarded, see §1)

Every port setup-container-host.sh publishes, current as of Phase 20. ENABLE_* column shows which are opt-in vs. always-on with the base stack.

Port Service Always on? Auth
8123 Home Assistant Yes HA's own login
1883 Mosquitto (MQTT) Yes MQTT username/password (mosquitto/config)
8080 zigbee2mqtt frontend Yes none by default — VERIFY you've set frontend.auth if this VLAN isn't fully trusted
1880 Node-RED Yes Node-RED's own login (if enabled)
3000 Homepage dashboard ENABLE_HOMEPAGE (default on) none
8090 ntfy ENABLE_NTFY (default on) ntfy's own auth (if configured) — off by default, treat the topic name as the only barrier until you set one. Carries identity's arrival notifications ("X just got home"), so an exposed topic leaks when the house is empty — this is the reason §2.2 keeps it LAN-only. Reach it from a phone at http://<container-host>:8090, at home or over the split tunnel
9000 Portainer ENABLE_PORTAINER (default on) Portainer's own login
445 Gallery SMB share ENABLE_GALLERY_SMB (default off) SMB username/password you set (GALLERY_SMB_USERNAME/PASSWORD)
9925 Mealie ENABLE_MEALIE (default off) Mealie's own login
5000, 8554, 8555 Frigate (web UI, RTSP, WebRTC) Yes (Phase 5+) Frigate's own login for the web UI; RTSP/WebRTC unauthenticated on the LAN by Frigate's own design — this is exactly why the camera VLAN's isolation (§3) matters, not the port itself
9283 Grocy Yes (Phase 1/7) Grocy's own login
8091 digest-web ENABLE_DIGEST_ENGINE none — read-only rendered output, no credentials in it by design
8094 admin-web ENABLE_ADMIN_CANVAS none — same reasoning as digest-web
8095 pantry-vision ENABLE_PANTRY_VISION bearer token (PANTRY_VISION_TOKEN)
8096 pantry-web ENABLE_PANTRY_VISION none
8097 identity ENABLE_IDENTITY bearer token (IDENTITY_TOKEN)
8098 identity-web ENABLE_IDENTITY none
8099 transit ENABLE_TRANSIT bearer token (TRANSIT_TOKEN)
8100 OpenTripPlanner (OTP) ENABLE_TRIP_PLANNING none — OTP has no built-in auth; this is why it's only ever called server-side by transit, never exposed to a kiosk browser directly
~8095 (VERIFY) Music Assistant ENABLE_MUSIC_ASSISTANT Music Assistant's own auth (if configured) — also flagged as an assumed/unverified port that collides with PANTRY_VISION_PORT, see docs/project-plan.md open decision #31 — resolve the actual port before relying on this table for it

admin-canvas (port 8092) is deliberately not in this table — it has no ports: mapping in the generated compose file at all, reachable only from other containers on the compose network (i.e. Home Assistant), by design.

On the LLM host (a separate machine — hosts/llm-host/)

Port Service Auth
11434 Ollama None whatsoever. Not a token, not a password — and the API is not read-only: it can pull and delete models as well as generate. Anyone who can reach this port can do all of it

That row is the strongest case in this table for §3's VLAN segmentation and §1's no- port-forward rule. It is also why the LLM host belongs on the smart-home VLAN rather than the trusted LAN: it accepts unauthenticated instructions from anything that can reach it, which is exactly the property you want blast-radius boundaries around.

5. What's still unverified here

This entire document was written against setup-container-host.sh's current ports: mappings, not tested against a real OPNsense instance — the VLAN/firewall rule guidance in §3 is standard practice for this class of network, not something run against your specific hardware/interface names. Confirm your OPNsense version actually ships WireGuard where you expect (VPN menu) before planning around it, and treat the port table in §4 as something to re-check against a live docker compose ps after deployment, not a permanent guarantee — a new ENABLE_* service added to the script later needs a new row here too.