Skip to main content
RunBook Academy

← All labs in OPNsense

Lab · advanced · ~75 min

Lab: Build a DMZ and configure port forwarding

B · Nested virtualisationC · Simulation

Objectives

  • Add a third interface for the DMZ with a public address
  • Place a service in the DMZ and verify it is reachable from the WAN side via port forward
  • Lock down the DMZ host's outbound traffic to specific destinations only
  • Verify the floating rule that automatically allows the inbound traffic
  • Audit the NAT and rule tables for the new flow

Prerequisites

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, or python3 -m http.server is 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:

  1. A NAT rdr (redirect) rule on the WAN interface mapping port 8080 to the DMZ host.
  2. A pass rule on the WAN interface permitting the inbound traffic.
  3. 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 -sr shows 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.
  • tcpdump shows 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.

Deliverables

  • · A DMZ interface with a public address on a separate subnet
  • · A web server in the DMZ reachable from the WAN
  • · A port forward rule and its associated firewall rule
  • · A locked-down DMZ-to-LAN deny rule with logging
  • · A live capture of the port-forwarded flow

Verification status

Last reviewed
2026-08-14
Executed end to end
not yet run on hardware

The commands and configuration here have been reviewed against the verified software versions, but nobody has run this lab start to finish on a system meeting its prerequisites. Treat the Expected Outcome as the intended result rather than an observed one, and keep the Cleanup section to hand.