For most of the homelab's life, there wasn't a firewall.
The ISP router did what ISP routers do — NAT out, block unsolicited inbound, don't ask hard questions. That's not zero security. It's just not intent-driven security. Which was fine, because the rack ran things I wrote for myself, and nobody on the internet was paying attention to my home connection.
Then I stood up an Akash provider.
The segmentation piece walked through what changed inside the rack when Akash landed — VLANs, ACLs, east-west policy. This piece is the parallel story of what changed at the edge. OPNsense on a dedicated box, ISP terminating on the WAN interface, all internal traffic on a single trunked LAN interface, running for 18 months as the only thing between the rack and the internet.
I'll cover the origin, the hardware, the split of labour between OPNsense and the Nexus 3548P, the four war stories that taught me the platform, and the four things I still haven't done that I know I should.
The last section is the point of the piece. Retrospectives that only list what worked are marketing. Retrospectives that admit what's still on the list are useful.
// no firewall, then one firewall
Before Akash, the perimeter was the ISP router.
Not zero security. Consumer-grade routers do block unsolicited inbound by default — that's been true of every ISP-supplied box shipped in the last twenty years. But the operative word is default. There was no policy. The perimeter existed because the ISP router existed. Anything more specific — logging, per-service inbound rules, outbound restriction, awareness of what was actually crossing — didn't exist because I hadn't asked for it.
That worked for a rack running things I wrote for me, running when I was watching. When Akash landed, the calculus changed. The Akash provider accepts inbound tenant deployments at an ingress that is by design publicly discoverable on-chain. Trusting the ISP router to be the only thing between that ingress and my workstation stopped being defensible once the provider was live and taking inbound deployments.
The natural sequence, once I was building for that: segment first, then police. The segmentation piece covered the segment step. This is the police step.
OPNsense went in on a dedicated box. ISP terminated on its WAN interface. Every internal VLAN — management, workload VLANs, storage VLAN — trunked into a single LAN interface. The rack now had two policy-driven layers separating it from the internet: the segmentation inside, and OPNsense at the edge. Neither trusts the other by default.
// why bare metal, not a VM
The obvious cheap answer would have been an OPNsense VM on Proxmox. It would have run fine. I didn't do that. Three reasons, in the order I actually thought about them.
Compute belongs to workloads. The Proxmox hosts exist to run DePIN infrastructure — Akash, storage, harvesters, validators. Every VM slot I give to a perimeter firewall is a slot I don't give to something earning. Bare metal at the edge means the servers stay full of what they're for.
The perimeter is a different animal from the compute. The firewall shouldn't share a fate with the hypervisor. If Proxmox goes sideways — kernel panic, storage-driver bug, upgrade gone wrong — the perimeter should be uninvolved. Bare metal enforces that separation physically, not just conceptually.
ISP terminates here. OPNsense is the box the ISP handoff talks to. That relationship is cleaner when exactly one physical device owns it, and when that device isn't a guest on a hypervisor whose primary job is something else entirely.
The hardware, as it exists in the rack today:
model: dell optiplex 7080 SFF
cpu: intel core i7-10700 (8c/16t)
memory: 32 gb
storage: SATA SSD, built-in bay
nics: 1× onboard intel i219 (WAN)
4× intel i350 add-in card (LAN + 3 spares)
os: opnsense 25.7 (freebsd base)
form: SFF, rack shelf
The i7-10700 is overkill for what OPNsense currently asks of it. This rack doesn't push enough traffic to stress an 8-core Comet Lake at the edge. But headroom is what enables the still-on-the-list items — IDS at line rate needs cores, and a CARP secondary needs a matched-pair budget to be defensible.
Between OPNsense and pfSense: open-source posture and the UI. The plugin ecosystem sealed it.
// the split of labour
The segmentation piece established the split. Sharpening it for this piece:
The Nexus 3548P handles east-west. Inter-VLAN routing at line rate on the switching ASIC. Workload-to-workload isolation lives on the Nexus, enforced by ACLs on each SVI.
OPNsense handles north-south. WAN, NAT, port-forwards, outbound NAT posture, inbound perimeter policy. Anything that crosses the boundary between the rack and the internet.
Two rulesets, two devices, two audit surfaces. Both stay simpler.
Interface layout at the OPNsense end:
WAN interface → ISP handoff, single link, dhcp from provider
LAN interface → tagged trunk to nexus, carries all internal vlans
spare ports → available for CARP secondary or dedicated management
One physical LAN interface is enough because the workload VLANs are tagged on the trunk. OPNsense sees each VLAN as a virtual interface on top of the physical LAN port and can write rules against each independently — inbound and outbound, per-VLAN, without the physical port count constraining what's possible.
The spare ports on the add-in card matter for what's coming. When the CARP secondary goes in, pfsync between the pair needs its own link. That's what one of the spares becomes.
// what the rack does with NAT
OPNsense defaults to Automatic outbound NAT: one masquerade rule per LAN interface, everything else inferred. That's what most homelab OPNsense builds run. It's the correct default. It also isn't what this rack runs.
This rack runs Manual outbound NAT. Every workload VLAN has an explicit outbound rule referencing a named network alias. The pattern:
alias: workload-a-net → 10.x.x.0/24
rule: source workload-a-net → NAT to WAN interface address
Two reasons that's worth the extra config surface.
Explicit is better than inferred. When you're staring at a firewall trying to work out whether workload X can reach the internet, seeing an explicit named rule beats reasoning about Automatic-mode inference every time. The rule either exists or it doesn't. There's no "well, in Automatic mode it would generate…" middle ground.
Aliases are reusable. The same workload-a-net alias that appears in the outbound NAT rule can be referenced in inbound rules, port-forwards, floating rules — anywhere the subnet needs to appear. One object, one place to update. When a workload's subnet changes — or when it moves — one alias edit propagates everywhere.
The trade-off: Manual mode means new workloads don't get NAT for free. Adding a workload means adding an alias, adding a rule, and applying. That friction is a feature. It means every workload on the wire had a moment where someone explicitly decided how it talks to the internet. Nothing gets to the outside by accident.
// four things OPNsense taught me
Four incidents from 18 months in. Each taught a different class of lesson.
1. The saved config that wasn't the applied config.
A static route to a workload subnet, correctly configured in OPNsense — and workloads on that subnet couldn't reach the internet. Firewall rules checked out. NAT checked out. The routing table on the L3 switch was correct. The route was in the OPNsense config screen but not in the OPNsense routing table.
Pending changes had been staged and never applied. Clicking Apply activated the route and connectivity came back immediately.
Lesson: saved is not applied. OPNsense stages changes to firewall, NAT, and routing configs, and requires an explicit Apply to activate them. The running config can silently drift from the staged one if you forget. Any troubleshooting session that starts with "but the config looks right" should also check whether pending changes are waiting.
2. The subnet OPNsense wouldn't accept as a NAT source.
Configuring outbound NAT for a workload VLAN. Enter the workload subnet as the source. OPNsense refuses: "not a valid source IP address or alias."
The subnet was valid. The rule editor just wouldn't accept it in raw form.
The fix: create a named network alias containing the subnet, then reference the alias in the rule. Alias workload-a-net containing 10.x.x.0/24, and the rule takes it without complaint.
Lesson: OPNsense treats aliases as first-class objects. Some rule fields insist on aliases even when a raw CIDR is technically valid. This isn't a bug — it's the OPNsense-idiom answer to "how do we make firewall rules maintainable at scale." Once the reflex to reach for aliases is in place, everything gets more consistent.
3. The firewall rule that wouldn't edit.
An inbound WAN rule showed up in the firewall ruleset but the edit UI wouldn't let me change it. It looked like a normal rule. It just wouldn't respond to edits.
The rule was auto-generated by a NAT port-forward. OPNsense's port-forwards have a Filter rule association setting that automatically creates the corresponding firewall rule and manages it as a dependency of the port-forward. Direct edits either don't apply or get overwritten the next time the port-forward is touched.
Fix: either edit the port-forward's own rule fields (which flow through to the generated firewall rule), or set the association to "None" and manage the firewall rule separately.
Lesson: know which rules OPNsense controls and which you control. Not every rule in the ruleset is an independent object. Assuming otherwise costs you an afternoon.
4. The package update that wouldn't complete.
A routine package and firmware update ran into a repository / package-consistency issue and stopped mid-upgrade. Recovering the box required diagnosing the state of the pending upgrade rather than just retrying. Endpoint landed at OPNsense 25.7.3_7 eventually.
The firewall stayed on-line during the diagnosis. That was luck. I hadn't taken a config backup before the update, and I hadn't confirmed console access. If the update had left the box in an unusable state, I would have been rebuilding OPNsense from memory.
Lesson: before touching updates, capture a config backup and confirm console access. Both take five minutes. Neither had happened. Both are habits now.
// what I still haven't done
Four gaps. In the order I'd actually tackle them if I started this weekend.
1. Backups first.
The single most embarrassing gap. OPNsense's config is an XML export that can be committed to Git, uploaded to cloud storage, or scheduled to a network share via the built-in backup module. None of that has happened here.
Backups are first not because they're the most urgent to have, but because they're the cheapest to get. Ten minutes of config setup and I have a restorable state. Every subsequent gap-closing effort is safer once this is in place. The next OPNsense update — and it'll happen — will be less nerve-wracking with a known-good config sitting in a repo somewhere.
2. HA second.
Would I make the same bare-metal call again? Absolutely. Would I have planned CARP into the initial build from day one? Also absolutely.
Retrofitting HA to a running perimeter is harder than designing it in. CARP virtual IPs need to be planned around the WAN handoff. pfsync between peers needs a dedicated link. Both are easier to reason about with a matched pair of boxes than with an active production box and a second box being brought into a pair. The next OptiPlex is the plan. It's a project, not a config change.
3. Logging third.
OPNsense's logs currently live on the box and roll over. That's fine for "check the last hour's blocked traffic." It isn't fine for anything else. The natural target is either a Loki/Grafana stack in the rack or Graylog on a dedicated VM.
Logging goes before IDS/IPS because IDS/IPS generates the logs. Having nowhere for them to land defeats the point.
4. IDS/IPS last.
Suricata on the LAN interface, ET Open ruleset, IDS mode first (observation) before IPS mode (enforcement). Deliberately last, because the false-positive load is real, the tuning effort is real, and the payoff without steps 1–3 is limited. IDS/IPS without a config backup, without HA, and without a place to send logs would just add fragility instead of catching it.
The order is deliberate. The temptation with lists like this is to lead with the exciting-sounding gap. Backups aren't exciting. They're the load-bearing gap that everything else depends on.
// closing
The perimeter has held for 18 months. Zero incidents I've traced to the firewall itself. Two of the four war stories above (the un-applied config, the un-editable rule) taught me how the UI thinks. Two (the alias requirement, the package update) taught me how the platform thinks. All four made me more useful, not less.
The next 18 months aren't going to be about new capability. They're going to be about closing the four gaps. A backup piece. A CARP-HA piece. A logging-to-something-real piece. Eventually an IDS/IPS piece, once the first three are in place to catch what it finds.
If you're running a homelab without a firewall today because the ISP router is fine, you're where I was two years ago. The trigger to move isn't a security incident — those don't announce themselves. The trigger is any workload you'd rather not have sharing a box with your workstation, or any inbound traffic you can't defend by hand-waving at NAT.
If you already have OPNsense running: check whether your config is backed up. That's the single highest-leverage thing you can do this week.
Top comments (0)