This lab builds a triple-homed firewall with a dedicated DMZ interface, places a public-facing service in the DMZ, and configures the port-forward and firewall rules that make it reachable from the WAN. The DMZ is the segment where services that must face the Internet live, and the discipline of the lab is that the DMZ is permitted only what it needs — to serve the public service and to be monitored, nothing more.
The compounding error in this kind of deployment is opening the DMZ wide because “the service needs to be reachable”. The service is reachable on the specific port and protocol; the DMZ as a whole remains isolated from the LAN and from the Internet except for the routes the operator has explicitly authored.
Objective
By the end of this lab you can:
- Add a third interface to OPNsense and address it as the DMZ.
- Place a web server in the DMZ with a public-facing address.
- Configure a port forward and its associated firewall rule.
- Lock down the DMZ host’s outbound traffic to specific destinations.
- Verify the inbound flow with a packet capture and the firewall log.
Requirements
- The OPNsense baseline from the previous lab (LAN rules and VLANs).
- A third NIC on the firewall for the DMZ (or a VLAN configured as the DMZ interface).
- A web server VM or container that can be placed in the DMZ
—
nginx,caddy, orpython3 -m http.serveris enough for the verification. - A WAN-side client that can reach the firewall’s WAN address on the chosen port.
The DMZ interface in production connects to a public-facing subnet. In a lab, that subnet can be a virtual network with a fake “public” prefix and an upstream router providing NAT/Internet. The point is the topology, not the prefixes.
Tasks
Task 1: Add the DMZ interface
In the GUI: Interfaces → Other Types → VLAN (or use a
dedicated NIC if you have a third one). Add the DMZ interface
on a separate subnet — for the lab, 192.168.50.1/24 is
fine. Enable the interface, set a static address, and click
Save.
Verify from the console:
# Substitute your own value before running:
# DMZ_IF is the DMZ device as ifconfig(8) names it: a spare NIC
# (igb2) or the VLAN you just created (vlan0.50).
DMZ_IF=igb2
ifconfig "$DMZ_IF" | grep inet
An interface without an address is the most common side effect of forgetting the “enable” toggle on the interface assignment page.
Task 2: Place a service in the DMZ
On a VM or container attached to the DMZ network, configure the address:
# Substitute the DMZ-facing NIC on that host before running:
DMZ_NIC=eth1
sudo ip addr add 192.168.50.10/24 dev "$DMZ_NIC"
sudo ip route add default via 192.168.50.1
Start a web server:
mkdir -p /tmp/dmz-web && cd /tmp/dmz-web
echo "DMZ web server — running" > index.html
python3 -m http.server 8080
Verify from the firewall:
curl -s http://192.168.50.10:8080/
The server should respond. A failure means the host’s route is wrong or the DMZ interface is not yet active.
Task 3: Configure the port forward
In the GUI: Firewall → NAT → Port Forward. Add a rule:
- Interface: WAN
- Protocol: TCP
- Destination: WAN address
- Destination port: 8080 (lab choice — avoid 80/443 in case the upstream router has those mapped)
- Redirect target IP: 192.168.50.10
- Redirect target port: 8080
- Description: DMZ web forward (lab)
Save, then Apply changes. The associated firewall rule is auto-created on the WAN interface — confirm by going to Firewall → Rules → WAN and finding the new pass rule that matches the port forward.
Task 4: Verify the port forward from the WAN side
From a WAN-side client (or from the upstream lab router):
# Substitute your own value before running:
# WAN_ADDR is the firewall's WAN address.
WAN_ADDR=203.0.113.10
curl -sI --connect-timeout 5 "http://$WAN_ADDR:8080/"
A 200 OK means the port forward is working. A connection refused means the firewall rule is missing. A connection timeout means the NAT is not yet applied.
If the lab is running on a private nested network, “WAN side” may be the hypervisor’s upstream router. The verification works as long as the WAN-side source is on a different subnet than the DMZ.
Task 5: Verify the floating rule and the auto-added rule
From the firewall:
pfctl -sr | grep -E '8080|192.168.50'
The output should show:
- A NAT rdr (redirect) rule on the WAN interface mapping port 8080 to the DMZ host.
- A pass rule on the WAN interface permitting the inbound traffic.
- A pass rule on the DMZ interface (or a floating rule) permitting the established traffic from the WAN source.
The third rule is often missed in the GUI. The pass rule on the WAN interface is the gate; the DMZ-side rule is the return path. Both must be present.
Task 6: Lock down the DMZ host’s outbound traffic
The DMZ host can reach the Internet. That is the threat model. From the DMZ host, the operator cannot allow unrestricted egress because a compromised web server becomes a launchpad for outbound attacks (DDoS, malware distribution, C2 callbacks).
In the GUI, Firewall → Rules → DMZ, add:
- Allow DMZ host to WAN for the specific outbound traffic the service needs (DNS, NTP, package updates).
- Block and log DMZ host to LAN net (the operator’s production network).
- Block and log DMZ host to RFC 1918 private networks.
- Default deny.
The DMZ host can reach the WAN for the limited set of services it needs, and is blocked from reaching internal networks. The associated deny rules must be logged.
Verify:
# From the DMZ host
curl -sI --connect-timeout 5 https://example.com # should succeed
ping -c 2 192.168.20.1 # should fail (LAN gateway)
The first works because the DMZ is allowed egress for HTTP. The second fails because the DMZ is denied access to the LAN subnet.
Task 7: Capture the inbound flow
On the firewall, start a capture on the DMZ interface:
# DMZ_IF: the DMZ device as ifconfig(8) names it (Task 1).
DMZ_IF=igb2
tcpdump -ni "$DMZ_IF" 'tcp port 8080' -c 10
From the WAN-side client, hit the port forward. The capture should show the SYN arriving at the DMZ host with the WAN source as the destination. The destination MAC is the firewall’s MAC; the destination IP is the firewall’s WAN address; the destination port is 8080. The packet is translated inside the firewall and emerges on the DMZ interface with the destination IP rewritten to the DMZ host.
If the capture shows the SYN but no SYN-ACK, the DMZ host is not listening, or the DMZ host’s gateway is not the firewall.
Task 8: Audit the ruleset
configctl filter show rules | grep -E 'DMZ|WAN'
configctl nat show rules | grep -E '8080|192.168.50'
The port forward should be in the NAT table with the
correct destination port and target. The firewall rule
should be in the WAN rule set. The DMZ-side deny rules
should be in the DMZ rule set. A forward that works but
doesn’t appear in pfctl -sr is a GUI hallucination — the
runtime is the truth.
Validation
- The DMZ interface has an address and is reachable from the firewall.
- The web server in the DMZ responds to a direct hit on its address.
- The port forward from the WAN side reaches the DMZ web server.
pfctl -srshows the NAT rdr and the matching firewall rule.- The DMZ host’s outbound traffic to the LAN is blocked and logged.
- The DMZ host’s outbound traffic to the WAN is allowed for the specific services configured.
Expected Result
You have a triple-homed firewall: WAN (public), LAN (internal), DMZ (public-facing services). The DMZ host is reachable from the WAN via the port forward. The DMZ host cannot reach the LAN. The DMZ host has limited egress to the WAN. The runtime ruleset matches the GUI model.
Troubleshooting
- The port forward times out from the WAN side. The associated firewall rule is missing. Look on Firewall → Rules → WAN — there should be a pass rule on the WAN interface matching the port forward. Add it manually if the GUI did not auto-create.
- The DMZ host cannot reach the WAN for outbound. The outbound NAT does not cover the DMZ subnet. Check Firewall → NAT → Outbound and add the DMZ subnet to the automatic outbound NAT or build a manual rule.
- The DMZ host can reach the LAN. The DMZ-side deny rule is missing or shadowed. Add the explicit deny with logging.
tcpdumpshows the SYN arriving but the DMZ host never receives it. The DMZ host’s default gateway is not the firewall. Correct the host’s routing.
Cleanup
Snapshot the configuration:
configctl backup download
# Save as opnsense-baseline-dmz.xml
To remove the DMZ:
# GUI: Interfaces → [DMZ] → disable
# Firewall → NAT → Port Forward → delete the port forward
# Firewall → Rules → DMZ → delete the rules
# Firewall → Rules → WAN → delete the auto-created rule
If the DMZ was a VLAN, also delete the VLAN and the assignment. The restore path is the snapshot from before the lab.
What you learned
- A port forward is two things: a NAT translation and a firewall rule. Both must be present; the operator who ships the NAT without the rule ships a silent failure.
- The DMZ is not a less-secure LAN; it is a different security posture. Inbound from the WAN is allowed on specific ports; outbound is locked down; the LAN is unreachable.
- The discipline of “explicit allow, default deny, log the deny” applies to the DMZ as much as to the LAN.
- The float between the WAN pass rule and the DMZ return rule is the small space where asymmetric paths hide. Both rules must be authored.