Skip to content

Network

A flat home network with OPNsense handling routing and firewalling. Everything sits on a single LAN — there is no VLAN segmentation today, which keeps the setup simple for a trusted home environment (the trade-off is discussed under Architecture decisions).

Topology

graph TD
    internet((Internet)) --> modem[Cable modem]
    modem --> opnsense[OPNsense VM<br/>router · firewall · DHCP · DNS]
    opnsense --> ap[TP-Link AP<br/>Wi-Fi]
    opnsense --> proxmox[Proxmox<br/>hypervisor]
    opnsense --> monitoring[monitoring<br/>Prometheus · Grafana · Loki]
    opnsense --> kontti[kontti<br/>media · AI · apps]
    opnsense --> nas[Synology DS920+<br/>NFS · backup target]
    opnsense --> ha[Home Assistant]
    tailscale[/Tailscale subnet router/] -.->|VPN| opnsense
    kontti -.->|NFS| nas
    monitoring -.->|scrape| kontti
    monitoring -.->|scrape| proxmox

OPNsense runs as a VM on Proxmox, so the router itself is part of the virtualised platform it protects — see the Proxmox and OPNsense pages.

Devices

Device Role
OPNsense (Proxmox VM) Router, firewall, DHCP, DNS
TP-Link AP Wi-Fi access point
Synology DS920+ Network storage
Proxmox Hypervisor
monitoring Monitoring server (Lenovo laptop)
kontti Application and media server
Home Assistant Smart home controller
Cable modem ISP uplink

Remote access is handled by Tailscale running as a subnet router on OPNsense.

Addressing and firewall design

The LAN is a single private subnet, and addresses split into two groups by intent:

  • Infrastructure and fixed devices — Proxmox, both uCore servers, the NAS, Home Assistant, and the IoT and ESPHome devices — get static DHCP reservations keyed to MAC address. Every one keeps a stable address that DNS host overrides, Prometheus scrape targets, Wake-on-LAN and Home Assistant's device integrations all depend on, without any of it being hard-coded on the machines themselves. The reservations live in one place (dnsmasq on OPNsense), so no address is pinned on the machines themselves.
  • Transient clients — phones and laptops — take dynamic leases from the general pool.

The firewall posture is deliberately minimal, because the attack surface is small by design. Nothing is port-forwarded from the internet: the WAN side is default-deny inbound, and the only way in is Tailscale — an encrypted tunnel rather than an open port. Internal services aren't exposed directly either; they sit behind HAProxy, which terminates TLS and is the single reachable front door for the web UIs, so individual service ports never need a firewall hole of their own.

How OPNsense itself is managed

The router used to be the one thing here that was configured only through its own web interface. Since September 2026 most of it is declared in the same proxmox repo as the hypervisor, driven over OPNsense's REST API rather than through the GUI or by editing its XML:

Playbook What it declares
opnsense_dns.yml 29 Unbound host overrides, plus the two forwards that hand tielinen.me and the reverse zone to dnsmasq
opnsense_haproxy.yml 26 published services, each as four linked objects — real server, host-header condition, backend pool, routing rule
opnsense_dhcp.yml 17 DHCP reservations, the dynamic pool, and the DHCP option that hands every client the filtering resolver
opnsense_shaper.yml The traffic shaper: 2 bandwidth pipes, 6 weighted queues, 10 rules

What stays in the GUI is a short and deliberate list: the HTTPS front end itself, the ACME certificate chain, firewall rules, interface assignment, and the Tailscale subnet router. The reasoning for the first two is on the OPNsense page.

Deletion is explicit rather than implied: none of these playbooks purge an entry just because it has dropped off a list, so an object disappears only when it is named in a separate absent list. With 29 host overrides fronting the reverse proxy, one forgotten row in a purge-on-absence model would break name resolution for the lot, so the possibility of drift is accepted in exchange.

What that leaves is a router most of which now has a reviewable definition — a diff to read before a change, and a list that can be read without logging in. The rest is still recovered rather than rebuilt, and that path runs at two levels:

  • The whole VM, via the nightly Proxmox vzdump job. This is the fast path, and it's the one that has actually been exercised — the restore test has recovered the OPNsense guest into a throwaway VMID with the network detached, so the path is proven rather than assumed.
  • The configuration on its own, exported from OPNsense to the Synology over SFTP. This is the layer that survives the VM being unrecoverable: a fresh OPNsense install plus that file reproduces the router without depending on the hypervisor's backup at all. It lands on the same NAS share, so it inherits the off-site replication to Backblaze along with everything else.

Two independent recovery paths at different granularities, which is the right shape when the thing being protected is the device every other device depends on — and the parts that are declared now rebuild from the repo on top of them.

The flat LAN

The honest limit of this design is the flat LAN itself. With no VLANs, firewall rules can't put a boundary between a compromised IoT device and the servers — they share one broadcast domain. That is the known, deliberate gap that would justify a managed switch and inter-VLAN rules if the threat model grew.

My experience

A flat network has been sufficient for my needs. The TP-Link AP covers Wi-Fi well enough for a small apartment. I prefer wired connections but haven't bothered running cables through the flat.

OPNsense used to be the one thing here I didn't manage with Ansible, because I hadn't found a collection for it I'd want to depend on. When I found one that looked good, I moved most of the router into the repo. Keeping settings as code documents them at the same time, which is most of why I wanted it — the DNS and HAProxy configuration is a lot clearer as a list in a file than spread across menus I have to remember to open.

Moving it there turned up a few stale settings and DHCP reservations I'd forgotten about, which was the useful part. Otherwise there wasn't much drama. The HTTPS front end and the ACME chain are still done by hand; I don't need to touch them often, and I may take them into Ansible too if the collection develops.

The Synology DS920+ has worked well as an NFS server and as a backup target that replicates to Backblaze B2. At some point I'll likely replace it with a different solution.