The firewall operating system of choice of network admins valuing open source products with a focus on security and strong community support is undoubtedly OPNsense. based on FreeBSD, it is extremely flexible while also easy to manage through the web UI, but getting an instance set up in a virtualized environment is prone to several pitfalls for newcomers.
Advantages of a virtualized deployment
Typically, router operating systems like OPNsense are installed on bare metal, often specialized networking hardware. While this provides best performance and predictability, it also requires you to have all the required hardware, physically manage network cables, switches and installation media.
Virtualization provides much more flexibility for infrastructure layout and network configuration, allowing operators to switch parts in and out or adjust them with just a few commands instead of dealing with physical hardware.
As an added bonus, KVM snapshots allow saving any router state you like and rolling back to it at any point, giving you complete freedom to play around with configurations and even breaking things without fear lockout or side effects.
While running OPNsense on KVM in production is easily possible, we will focus on an initial setup for testing (or to use as a base for your custom production deployment).
Defining networks
For our deployment, we will define two networks: a WAN and a LAN. The WAN will provide internet access and have libvirt provide dynamic IPs over DHCP. The LAN has no DHCP or internet access, acting as the network for internal machines protected by OPNsense.
The OPNsense instance will sit inbetween these two networks, passing internet access to the LAN machines and selectively forwarding specific ports or machines to machines on the WAN (your host machine in our case).
Let's start with the WAN network. Create a file opn-wan.xml:
<network>
<name>opn-wan</name>
<forward mode="nat"/>
<bridge name="virbr-opn-wan" stp="on" delay="0"/>
<ip address="10.10.10.1" netmask="255.255.255.0">
<dhcp>
<range start="10.10.10.251" end="10.10.10.254"/>
</dhcp>
</ip>
</network>This network makes a few important decisions: It sets the subnet to 10.10.10.0/24 and configures the host (the machine running KVM) to join the network using ip 10.10.10.1. It also enables DHCP to automatically assign IPs to vms on this network (the OPNsense instance), but specifically only from 10.10.10.251-254 (only 4 possible IPs). Since OPNsense will be the only machine ever receiving a DHCP IP, this will be fine, but we leave a few spares in case you want to run multiple instances to set up high availability later.
Most of the subnet is therefore kept as statically assigned IPs, which we will use later to define one-to-one nat mapping of "public" IPs to LAN machines.
Create and start the network:
virsh net-define opn-wan.xml
virsh net-start opn-wan
virsh net-autostart opn-wanThe second network configuration will be much shorter. Create a file opn-lan.xml:
<network>
<name>opn-lan</name>
<bridge name="virbr-opn-lan" stp="on" delay="0"/>
<ip address="192.168.1.254" netmask="255.255.255.0"/>
</network>Note the missing <forward> element to prevent direct internet access on this network. OPNsense will run a DHCP server on this network, so the KVM network has no DHCP service configured for it. Note that we still have to give the host machine an IP within the network, since libvirt has no option to run a DHCP client on bridge interfaces (you could run one manually, but would need to re-run it every time the bridge/network changes/restarts or the host reboots, making it impractical).
We chose the IP 192.168.1.254 for the host, since OPNsense will by default configure the LAN subnet as 192.168.1.0/24, giving itself IP 192.168.1.1 and also enabling DHCP for IPs 192.168.1.100-199. The host IP was chosen as the last in the subnet to prevent collisions with DHCP and static IP assignments you may create later.
Create and start the network:
virsh net-define opn-lan.xml
virsh net-start opn-lan
virsh net-autostart opn-lanYou should now see both networks when running:
virsh net-listIf one is missing or not marked as running and autostart enabled, carefully repeat the previous steps.
Creating the OPNsense vm
There are multiple different ways to get a virtual machine with OPNsense up and running, but we will focus on reproducibility and deployment speed. For this reason, we won't use the ISO dvd installer file and instead directly create a disk from the nano disk image instead:
wget https://pkg.opnsense.org/releases/25.7/OPNsense-25.7-nano-amd64.img.bz2
bunzip OPNsense-25.7-nano-amd64.img.bz2
qemu-img convert -f raw -O qcow2 OPNsense-25.7-nano-amd64.img opnsense.qcow2
qemu-img resize opnsense.qcow2 50GWe first download the OPNsense nano disk image using wget, decompress it and convert it to qcow2 format, then resize available disk space to 50GB, allowing for data to be written onto the virtual disk.
The wget url will become outdated over time; you can either decide to update the outdated instance after installation or head over to the OPNsense download page and manually fetch the latest image if you prefer - just make sure to pick the nano variant.
Now that we have a writable disk, create a vm from it:
virt-install --name opnsense \
--os-variant freebsd14.2 \
--vcpus 2 --memory 4096 \
--disk path=opnsense.qcow2,format=qcow2 \
--import --graphics none \
--network network=opn-wan \
--network network=opn-lanYou can adjust hardware specs like cpu or memory count, just make sure to stay within the recommended minimums of 1 core and 3gb memory to be safe. Also pay attention to the network order; changing it will affect the network adapter naming in later steps, so do not swap them in this command.
The machine will create and boot, showing text output on the terminal. During startup, you will see a message with a countdown like this:
Press any key to start the manual interface assignment: 5You should press ENTER here to configure the network adapters, as auto-configuration will likely pick the LAN adapter as WAN.
If you missed the message or didn't react fast enough, you can instead login as root with password opnsense and pick option Assign interfaces from the menu:
0) Logout 7) Ping host
1) Assign interfaces 8) Shell
2) Set interface IP address 9) pfTop
3) Reset the root password 10) Firewall log
4) Reset to factory defaults 11) Reload all services
5) Power off system 12) Update from console
6) Reboot system 13) Restore a backup
Enter an option: 1This will provide you with a series of prompts about the network configuration. Answer n to the questions about configuring LAGGs and VLANs, then type vtnet0 as the WAN interface name and vtnet1 as the LAN interface name. Leave all other questions blank and confirm with y at the end. It should look like this:
Do you want to configure LAGGs now? [y/N]: n
Do you want to configure VLANs now? [y/N]: n
Valid interfaces are:
vtnet0 52:54:00:f4:23:18 VirtIO Networking Adapter
vtnet1 52:54:00:a0:61:61 VirtIO Networking Adapter
If you do not know the names of your interfaces, you may choose to use
auto-detection. In that case, disconnect all interfaces now before
hitting 'a' to initiate auto detection.
Enter the WAN interface name or 'a' for auto-detection: vtnet0
Enter the LAN interface name or 'a' for auto-detection
NOTE: this enables full Firewalling/NAT mode.
(or nothing if finished): vtnet1
Enter the Optional interface 1 name or 'a' for auto-detection
(or nothing if finished):
The interfaces will be assigned as follows:
WAN -> vtnet0
LAN -> vtnet1
Do you want to proceed? [y/N]: yOnce done, you can now open a web browser in your KVM host and open the OPNsense web ui at https://192.168.1.1. You will receive a warning about the self-signed SSL certificate, allow an exception for the page and continue, then log in as user root with password opnsense.
As a final step, Navigate to System > Configuration > Wizard and start it by clicking the "Next" button. You can keep or adjust options as you like, but be careful to uncheck the Block RFC1918 Private Networks option on page 3 (titled "Network [WAN]"). A production deployment will not need this, but your "public" network (opn-wan) is a virtual bridge using private ip address range 10.x.x.x, which would block all incoming traffic from your host machine.
That's it, your OPNsense instance is now fully operational.
Deploying a dummy LAN machine
In order to test accessibility and network configuration in the coming sections, we need a dummy machine first. We will use a cloud image of ubuntu 25.04 "plucky puffin" here.
Download a disk image for it:
wget https://cloud-images.ubuntu.com/plucky/current/plucky-server-cloudimg-amd64.imgThe create a vm on the opn-lan network from it:
virt-install --name ubuntu --vcpus 1 --memory 1024 --boot uefi --disk path=ubuntu.qcow2,format=qcow2,size=50,backing_store=plucky-server-cloudimg-amd64.img --graphics none --network network=opn-lan --os-variant ubuntu24.04 --cloud-init root-password-generate=on,disable=onOn the first few lines of output, virsh will print the generated root account password. Copy it, you will need it after installation (don't copy the sample password below!):
Starting install...
Password for first root login is: 5bQxYndVRRwFYN4w
Installation will continue in 10 seconds (press Enter to skip)...When the server finished booting, log in as root with the password you copied earlier, then go through the password change prompts that show up.
Let's start a web server as a dummy web application:
apt update
apt install apache2 -yFinally, take note of the IP address for the ubuntu vm:
hostname -IIn this example, it was assigned IP 192.168.1.109 - yours will be different. Make sure to substitute the dummy IP with your real vm address in subsequent sections.
Open your web browser at the ip address you found, e.g. http://192.168.1.109 in this example, and confirm you see the default ubuntu apache2 landing page.
Port-forwarding LAN machine services
Port-forwarding is a very basic need of network setups using NAT protection, and OPNsense has excellent support for it. The goal of this example is to forward the webserver running port 80 of the ubuntu vm to port 8080 of the OPNsense public IP address, so it can be accessed from the WAN (the kvm host in this case).
On the OPNsense web ui, navigate to Firewall > NAT > Port Forward and click the button with the plus icon to add a new forwarding rule.
You can leave most values at their default values, except for:
Destination: change the select box from "Single host or Network" to "WAN address". You could also manually type your WAN ip here, but using "WAN address" will dynamically update if your dhcp ip changes, avoiding future headaches.Destination port range: this is the port you open on the OPNsense vm. Change the dropdown box from "HTTP" to "(other)", then type "8080" in BOTH text fields ("from:" and "to:").Redirect target IP: the ip of the LAN vm. for our dummy vm that's192.168.1.109, yours will be different. You should have noted this ip in the previous step; if you don't have it, navigate toServices>Dnsmasq DNS & DHCP>Leasesand find your VM ip in there.Redirect target port: this is the port on your LAN vm the service actually runs on. Select "HTTP" in the dropdown.
When done, click the "Save" button at the bottom, then click "Apply changes" button at the top right - don't forget this, your changes won't work unless they are applied after saving!
The forwarded port should now be working. From the kvm host, find the public IP address of the OPNsense instance with:
virsh net-dhcp-leases opn-wanYou should see only a single connected machine with a single assigned IP. For our example, it was 10.10.10.254, but yours may be different.
Open port 8080 for the IP you found in your web browser (like http://10.10.10.254:8080) and you should see the ubuntu vm's webserver landing page.
Mapping public alias IPs to LAN vms
A step from port forwarding, you can also map LAN vms to public IP addresses directly, exposing full access to them from the WAN.
Our networks are already set up to support this since we chose to leave most WAN IPs as a static pool not managed by DHCP. You can pick any free static IP and configure it as an alias for your OPNsense instance.
Navigate to Interfaces > Virtual IPs > Settings and click the button with the plus icon to add a new one. Pick an IP address from the static WAN pool (anything in range 10.10.10.2-250), we are using 10.10.10.15 in this example.
Type 10.10.10.15/32 into the "Network / Address" field, click "Save" and then click the "Apply" button at the bottom.
Next, head to Firewall > Rules > WAN and add a new rule. Leave "Source" at "any", but change "Destination" to your vm's IP address (NOT the alias address you just chose!). Our dummy vm is accessible at 192.168.1.109, yours will be different. Click "Save", then click "Apply changes" at the top.
As a last step, open Firewall > NAT > One-to-One and add a new mapping to map the public WAN ip to the LAN vms ip address.
Type your new alias IP in the "External network (Target)" field and the LAN vm's IP in the "Source / Internal" field, click "Save" and don't forget to click the "Apply" button at the bottom.
The dummy VM should now be reachable on the WAN IP address 10.10.10.15, allowing access to all ports without additional port forwarding. Open http://10.10.10.15 in your browser to confirm.