How To

Install OpenWrt on Proxmox VE as a VM Router

Two virtual NICs, one isolated bridge, and a LAN subnet that does not collide with the network upstream. Get those three right and OpenWrt makes a very capable router VM, with a firewall, DHCP, DNS and the full package feed on a few hundred megabytes of RAM. Get the subnet wrong and the VM boots fine, then quietly fails to resolve a single hostname.

Original content from computingforgeeks.com - post 147050

This guide installs OpenWrt on Proxmox VE from the official UEFI disk image: a dedicated LAN bridge, qm create with the image imported in one step, the serial console, the LAN subnet fix, LuCI access, root filesystem expansion, the QEMU guest agent, and a client VM routed through it. OpenWrt 25.12 replaced opkg with apk, so every package command here uses the new manager. The whole build was run on Proxmox VE 8.4 with OpenWrt 25.12.5 in September 2026, and the same qm options are documented for Proxmox VE 9.

Plan the OpenWrt on Proxmox network layout

OpenWrt on x86 assigns roles by interface order: the first NIC (eth0) becomes the LAN bridge and the second (eth1) becomes the WAN DHCP client. The board file on a fresh VM says exactly that:

{ "lan": { "device": "eth0", "protocol": "static" }, "wan": { "device": "eth1", "protocol": "dhcp" } }

So the VM gets net0 on a new bridge with no physical ports (the private LAN your other VMs join) and net1 on vmbr0, which faces your existing network and gets an address from its DHCP server. That is the layout used throughout:

Proxmox NICBridgeInside OpenWrtRoleAddress in this lab
net0vmbr1 (no ports)eth0 in br-lanLAN, DHCP server192.168.10.1/24
net1vmbr0 (uplink)eth1WAN, DHCP client192.168.1.149/24 from the home router

If you plan to pass a physical NIC through instead of using virtio bridges, the card choice matters more than anything in this guide. Our list of 10GbE NICs that behave in Proxmox covers which chipsets pass through cleanly. If you would rather keep the router off the hypervisor entirely, our picks for dedicated firewall appliance hardware cover the boxes built for it.

Create the LAN bridge on Proxmox

A bridge with no ports behaves like an unplugged switch that only VMs connect to. Create it from the node shell, then apply the pending network change:

pvesh create /nodes/$(hostname)/network --iface vmbr1 --type bridge --autostart 1 --comments "OpenWrt LAN"
pvesh set /nodes/$(hostname)/network

The second command runs the same ifreload as the Apply Configuration button in the web UI. It applies every pending network change on the node, so check the node’s Network panel for unrelated pending edits first. With nothing else pending, vmbr0 is left untouched. Proxmox writes this stanza to /etc/network/interfaces:

auto vmbr1
iface vmbr1 inet manual
	bridge-ports none
	bridge-stp off
	bridge-fd 0
#OpenWrt LAN

The same thing is available in the GUI under the node, System, Network, Create, Linux Bridge, leaving Bridge ports empty. If vmbr1 already exists on your node, pick another free name and use it in the qm create command below.

Download the OpenWrt x86 image

The OpenWrt x86 target ships several disk images. The OpenWrt x86 documentation recommends ext4-combined-efi unless you have a reason to pick otherwise, and that is the right call for a VM:

ImageFirmwareRoot filesystemTrade-off
generic-ext4-combined-efiUEFI (OVMF)Read-write ext4, can be grownNo failsafe mode or factory reset
generic-ext4-combinedBIOS (SeaBIOS)Read-write ext4, can be grownSame, for legacy boot
generic-squashfs-combined-efiUEFI (OVMF)Read-only squashfs plus overlayFactory reset works, under 100 MB for packages, extroot not supported

Losing factory reset costs little on Proxmox, because a VM snapshot does the same job better. Detect the latest stable release from the OpenWrt GitHub releases API, then download the image and its checksum file on the Proxmox node:

mkdir -p /root/openwrt && cd /root/openwrt
VER=$(curl -s https://api.github.com/repos/openwrt/openwrt/releases/latest | grep -m1 '"tag_name"' | sed -E 's/.*"v([^"]+)".*/\1/')
echo "${VER}"

It printed 25.12.5 at the time of testing. Fetch the image and verify it before doing anything else with it:

IMG=openwrt-${VER}-x86-64-generic-ext4-combined-efi.img.gz
wget -q -O "${IMG}" "https://downloads.openwrt.org/releases/${VER}/targets/x86/64/${IMG}"
wget -q -O sha256sums "https://downloads.openwrt.org/releases/${VER}/targets/x86/64/sha256sums"
sha256sum -c sha256sums --ignore-missing
gunzip -k "${IMG}"

The checksum passes and the compressed 14 MB download unpacks to a 120 MiB raw disk image:

openwrt-25.12.5-x86-64-generic-ext4-combined-efi.img.gz: OK

Older guides warn that gunzip exits with “trailing garbage ignored” on OpenWrt images. The 25.12.5 image unpacked with exit code 0, so any script with set -e works without a workaround.

Create the OpenWrt VM with qm

Three values change per host: the VM ID, the storage the disk lands on, and the path to the unpacked image. Set them in the same shell as the download step, because IMAGE is built from ${VER}:

export VM_ID=$(pvesh get /cluster/nextid)
export STORAGE=local-lvm
export IMAGE=/root/openwrt/openwrt-${VER}-x86-64-generic-ext4-combined-efi.img
echo "VM ${VM_ID} on ${STORAGE} from ${IMAGE}"

Run pvesm status if you are unsure of your storage name. Our node used a ZFS pool named zfs-pool, so the outputs below show that. The whole VM, including the disk import, is one command:

qm create ${VM_ID} --name openwrt --ostype l26 --machine q35 --bios ovmf --cpu host \
  --cores 1 --memory 256 --scsihw virtio-scsi-single \
  --efidisk0 ${STORAGE}:1,efitype=4m,pre-enrolled-keys=0 \
  --scsi0 ${STORAGE}:0,import-from=${IMAGE} \
  --net0 virtio,bridge=vmbr1 --net1 virtio,bridge=vmbr0 \
  --serial0 socket --vga serial0 --boot order=scsi0 --onboot 1

The import-from option copies the raw image straight into a new VM disk, so there is no separate qm importdisk step and no guessing the disk name to attach afterwards. The import ends with:

transferred 120.3 MiB of 120.3 MiB (100.00%)
scsi0: successfully created disk 'zfs-pool:vm-9420-disk-1,size=126123520'

A few of those flags are deliberate. pre-enrolled-keys=0 leaves Secure Boot off, because the OpenWrt image does not boot with the standard keys enrolled (the exact error is covered below). --vga serial0 sends the console to a serial port, which gives you copy and paste in the terminal instead of a noVNC screen. --cpu host replaces the kvm64 model that older guides use, and 256 MB of RAM is plenty for routing (free inside the idle guest reported about 38 MB used).

The image only has a 104 MB root partition, so grow the virtual disk now. The partition itself is expanded later from inside OpenWrt:

qm disk resize ${VM_ID} scsi0 1G

Confirm the NIC order before first boot, since swapping net0 and net1 puts the DHCP server on your uplink:

qm config ${VM_ID} | grep -E '^(net|scsi0|efidisk0|bios)'

LAN on vmbr1, WAN on vmbr0, and the disk now at 1G:

bios: ovmf
efidisk0: zfs-pool:vm-9420-disk-0,efitype=4m,pre-enrolled-keys=0,size=1M
net0: virtio=BC:24:11:02:55:3D,bridge=vmbr1
net1: virtio=BC:24:11:56:9C:42,bridge=vmbr0
scsi0: zfs-pool:vm-9420-disk-1,size=1G

Boot OpenWrt and open the serial console

Start the VM and attach to its serial port from the node shell:

qm start ${VM_ID}
qm terminal ${VM_ID}

Proxmox prints starting serial terminal on interface serial0 (press Ctrl+O to exit). OVMF hands off to the OpenWrt kernel, which logs Secure boot disabled, and about 16 seconds after qm start the console shows “Please press Enter to activate this console”. Press Enter and you land in a root shell with no password. Check the release first:

cat /etc/openwrt_release

The tested build, running kernel 6.12.94:

DISTRIB_ID='OpenWrt'
DISTRIB_RELEASE='25.12.5'
DISTRIB_REVISION='r33051-f5dae5ece4'
DISTRIB_TARGET='x86/64'
DISTRIB_ARCH='x86_64'
DISTRIB_DESCRIPTION='OpenWrt 25.12.5 r33051-f5dae5ece4'
DISTRIB_TAINTS=''

Error: “BdsDxe: failed to load Boot0001 … Access Denied”

This is what the console shows when the EFI disk was created with pre-enrolled-keys=1, which is the default when you add an EFI disk in the Proxmox web UI. OVMF refuses to start the unsigned OpenWrt bootloader and stops at BdsDxe: No bootable option or device was found. Delete the EFI disk and recreate it with pre-enrolled-keys=0, or untick Pre-Enroll keys in the GUI.

Set a root password straight away. LuCI and SSH both log in with it:

passwd

Change the OpenWrt LAN subnet before it clashes

OpenWrt’s LAN defaults to 192.168.1.1/24, and so do a large share of home and ISP routers. When the WAN side gets its DHCP lease from a 192.168.1.0/24 network, the VM ends up with two routes to the same subnet and the LAN route wins. On our first boot the WAN picked up 192.168.1.149 from the home router at 192.168.1.1:

ip route

Both interfaces claim the subnet, and the upstream gateway’s address is now OpenWrt’s own LAN address:

default via 192.168.1.1 dev eth1  src 192.168.1.149
192.168.1.0/24 dev br-lan scope link  src 192.168.1.1
192.168.1.0/24 dev eth1 scope link  src 192.168.1.149 

Pinging 1.1.1.1 still works, which is what makes this confusing. Everything that needs a name or the upstream LAN fails:

OpenWrt default LAN and WAN on the same 192.168.1.0/24 subnet breaking DNS and apk

Error: “server can’t find downloads.openwrt.org: REFUSED”

The WAN lease names 192.168.1.1 as the upstream DNS server, but that address now belongs to OpenWrt’s own br-lan. dnsmasq notices and drops it, logging ignoring nameserver 192.168.1.1 - local interface in logread, which leaves it with no upstream at all. It is the subnet clash again, not a DNS setting.

Error: “wget: Failed to send request: Operation not permitted” during apk update

With no working DNS, apk update fails every repository with ERROR: wget: exited with error 4 and ends on 8 unavailable, 0 stale; 182 distinct packages available, meaning only the packages baked into the image are visible. Hosts on the upstream 192.168.1.0/24 network are unreachable too (a ping to one of them lost 100% of packets).

The fix is to move the LAN to a subnet nothing upstream uses. One detail catches people out: on 25.12 the default LAN address is stored in CIDR form with no separate netmask option, so setting ipaddr=192.168.10.1 on its own gives br-lan a 255.255.255.255 mask and no LAN route. Include the prefix length:

uci set network.lan.ipaddr='192.168.10.1/24'
uci commit network
service network reload

Check the stored config and the routing table again:

uci show network.lan
ip route

Each subnet now has its own interface, and the DHCP server on br-lan hands out 192.168.10.x addresses:

network.lan=interface
network.lan.device='br-lan'
network.lan.proto='static'
network.lan.ipaddr='192.168.10.1/24'
network.lan.ip6assign='60'
default via 192.168.1.1 dev eth1  src 192.168.1.149
192.168.1.0/24 dev eth1 scope link  src 192.168.1.149
192.168.10.0/24 dev br-lan scope link  src 192.168.10.1 

Name resolution works immediately, and the package index now loads in full:

apk update

All eight feeds download this time:

OK: 11241 distinct packages available

If your upstream network is already 192.168.10.0/24, pick something else, such as 10.10.10.1/24. The only rule is that LAN and WAN must not overlap.

Allow LuCI and SSH from your network

The default firewall drops unsolicited input on the WAN zone, which is correct for a router facing the internet. In a Proxmox homelab the “WAN” is usually your own trusted LAN, and managing the router from a VM on vmbr1 gets old fast. This rule opens SSH and the web UI to one upstream subnet only:

uci add firewall rule
uci set firewall.@rule[-1].name='Allow-Mgmt-Home'
uci set firewall.@rule[-1].src='wan'
uci set firewall.@rule[-1].src_ip='192.168.1.0/24'
uci set firewall.@rule[-1].proto='tcp'
uci set firewall.@rule[-1].dest_port='22 80 443'
uci set firewall.@rule[-1].target='ACCEPT'
uci commit firewall
service firewall restart

Change src_ip to your upstream subnet. Skip this rule entirely if the WAN faces a modem or a public IP, and manage the router from the LAN side instead. From a machine on the home network, confirm LuCI answers on the WAN address:

curl -s -o /dev/null -w "%{http_code}\n" http://192.168.1.149/cgi-bin/luci/

A 403 is the expected answer here. LuCI serves its login form with a 403 status until you authenticate, so it proves the firewall rule works rather than signalling a block. A timeout would mean the rule is missing.

Log in to the LuCI web interface

LuCI ships in the release image, so there is nothing to install. Browse to the WAN address (or 192.168.10.1 from a LAN client) and sign in as root with the password you set:

OpenWrt LuCI login page on a Proxmox VE VM

The first visit to Status, Overview opens a dialog from Attended Sysupgrade asking whether to check online for firmware upgrades every time the page loads. Either choice is saved as attendedsysupgrade.client.login_check_for_upgrades and the dialog does not return. We enabled it, then confirmed the pending change with Save & Apply:

OpenWrt LuCI check online for firmware upgrades dialog

The overview confirms the VM details: QEMU Q35 machine, the host i7-6700 passed through by --cpu host, 61 MB of 212 MB RAM in use with LuCI loaded, the WAN lease, and the DHCP lease of the client VM connected later in this guide:

OpenWrt 25.12 LuCI status overview running as a Proxmox VM

Network, Interfaces shows the result of the plan from the start: lan on br-lan at 192.168.10.1/24, and wan plus wan6 on eth1:

OpenWrt LuCI interfaces page showing LAN on br-lan and WAN on eth1

Network, Firewall, Traffic Rules lists the stock WAN rules with Allow-Mgmt-Home at the bottom, limited to 192.168.1.0/24 on ports 22, 80 and 443:

OpenWrt firewall traffic rule allowing LuCI and SSH from the home network

Software packages are managed under System, Software, which is the LuCI front end to apk on 25.12.

Expand the OpenWrt root filesystem

Resizing the Proxmox disk earlier added unallocated space, but OpenWrt still has its 104 MB root partition and a 98 MB filesystem. The OpenWrt expand root guide provides a script that grows both and survives a sysupgrade. Install its tools first:

apk add parted losetup resize2fs blkid

Download the script from the wiki and source it. The empty -U "" user agent is the wiki’s own instruction:

wget -U "" -O expand-root.sh "https://openwrt.org/_export/code/docs/guide-user/advanced/expand_root?codeblock=3"
. ./expand-root.sh

Sourcing it writes two boot scripts, /etc/uci-defaults/70-rootpt-resize and /etc/uci-defaults/80-rootfs-resize, and adds both to /etc/sysupgrade.conf so they run again after an upgrade. Start the resize:

sh /etc/uci-defaults/70-rootpt-resize

parted first asks to fix the GPT, because the backup header still sits at the old 120 MB end of the disk. The script answers that itself with Fixing, due to --fix. The VM then grows the partition and reboots, grows the filesystem on the next boot and reboots again. Both reboots together took about 40 seconds. Log back in and check:

parted -s /dev/sda unit MiB print
df -h /

Partition 2 now fills the 1 GB disk, and the root filesystem went from 98.3M to 990.5M:

OpenWrt root partition expanded to 1 GB on Proxmox

The wiki carries a warning worth repeating: do not use this script on an NVMe device, where it leaves the install unbootable with failed to execute /usr/libexec/login. That applies to bare metal with an NVMe boot drive. A Proxmox VM disk on virtio SCSI shows up as /dev/sda regardless of what the host storage is, so it is not affected.

Install the QEMU guest agent

With the guest agent running, Proxmox shows the router’s IP addresses on the VM summary page and can shut it down cleanly. Install it inside OpenWrt:

apk add qemu-ga

Back on the Proxmox node, enable the agent device. Proxmox adds the new virtio serial port only when the VM process starts fresh, so shut it down and start it again rather than rebooting from inside OpenWrt:

qm set ${VM_ID} --agent enabled=1
qm shutdown ${VM_ID} && qm start ${VM_ID}

After it boots, ask the agent what it sees:

qm guest cmd ${VM_ID} ping && qm guest cmd ${VM_ID} get-osinfo

The agent answers and reports the OpenWrt release:

{
   "id" : "openwrt",
   "kernel-release" : "6.12.94",
   "kernel-version" : "#0 SMP Mon Jun 29 12:59:20 2026",
   "machine" : "x86_64",
   "name" : "OpenWrt",
   "pretty-name" : "OpenWrt 25.12.5",
   "version" : "25.12.5",
   "version-id" : "25.12.5"
}

qm guest cmd ${VM_ID} network-get-interfaces returns eth1 with 192.168.1.149 and br-lan with 192.168.10.1, the same addresses LuCI shows.

Connect a VM to the OpenWrt LAN

Any VM or container whose NIC sits on vmbr1 is now behind the router. We cloned an Ubuntu 26.04 cloud-init template with its network set to DHCP and moved its NIC to the LAN bridge (VM 9421 in this lab). This form generates a new MAC address, which is fine for a fresh clone. For an existing VM whose netplan matches on MAC, pass the current one as virtio=BC:24:11:xx:xx:xx,bridge=vmbr1 so DHCP keeps working:

qm set 9421 --net0 virtio,bridge=vmbr1
qm start 9421

OpenWrt’s dnsmasq logs the handshake, and the lease appears in /tmp/dhcp.leases on the router:

daemon.info dnsmasq-dhcp[1]: DHCPREQUEST(br-lan) 192.168.10.223 bc:24:11:47:00:ed
daemon.info dnsmasq-dhcp[1]: DHCPACK(br-lan) 192.168.10.223 bc:24:11:47:00:ed ubuntu

Inside the client, check the address, route, DNS server and a real HTTPS request:

ip -4 -br a; ip route | head -2
resolvectl dns
ping -c2 -W2 1.1.1.1 | tail -2
curl -sI https://downloads.openwrt.org | head -1

The client took 192.168.10.223, uses OpenWrt as both gateway and resolver, and reaches the internet through the WAN NAT:

lo               UNKNOWN        127.0.0.1/8
eth0             UP             192.168.10.223/24 metric 100
default via 192.168.10.1 dev eth0 proto dhcp src 192.168.10.223 metric 100
192.168.10.0/24 dev eth0 proto kernel scope link src 192.168.10.223 metric 100
Global:
Link 2 (eth0): 192.168.10.1 fd9c:f246:b0a4::1
2 packets transmitted, 2 received, 0% packet loss, time 1002ms
rtt min/avg/max/mdev = 10.000/10.317/10.635/0.317 ms
HTTP/2 200

From here the VM behaves like any OpenWrt router: WireGuard, VLANs, adblock and SQM are all a package install away. GL.iNet builds its router firmware on OpenWrt, and our GL.iNet Brume 2 WireGuard gateway guide shows the same WireGuard gateway role on dedicated hardware. If VLANs are the next step, a managed switch with VLAN support lets you trunk tagged traffic into a VLAN-aware Proxmox bridge and split it inside OpenWrt.

Snapshot before you upgrade OpenWrt

The ext4 image has no factory reset, so the Proxmox snapshot is your undo button. Take one before any package upgrade or firmware change:

qm snapshot ${VM_ID} pre-upgrade --description "before apk upgrades"
qm listsnapshot ${VM_ID}

Both the root disk and the EFI vars disk are captured:

snapshotting 'drive-scsi0' (zfs-pool:vm-9420-disk-1)
snapshotting 'drive-efidisk0' (zfs-pool:vm-9420-disk-0)
`-> pre-upgrade                 2026-09-29 22:00:30     before apk upgrades
 `-> current                                            You are here!

We tested the undo path by creating a file after the snapshot and then rolling back. qm rollback ${VM_ID} pre-upgrade left the VM stopped, and after qm start the file was gone while the expanded 990.5M root filesystem stayed. Plan for that stop: the router, and everything behind it, is offline until it runs again, so add --start 1 to the rollback to boot it straight away. On ZFS storage Proxmox can only roll back to the most recent snapshot, so delete newer ones first or keep just one pre-upgrade snapshot at a time.

Keep a config backup outside the VM as well, because a snapshot does not help if the storage it lives on fails. OpenWrt builds the archive itself:

sysupgrade --create-backup /tmp/backup-openwrt.tar.gz

The 8.3K archive holds everything under /etc/config (network, firewall, dhcp, uhttpd and the rest), the dropbear host keys, and /etc/passwd plus /etc/shadow with the root password hash. Copy it off the router with scp and store it like any other secret. Restoring it on a fresh VM from this guide rebuilds the router in minutes.

For moving to a newer OpenWrt release, the x86 wiki lists sysupgrade, LuCI Attended Sysupgrade, owut and the Firmware Selector as supported paths, and the expand root guide says the scripts it saved in /etc/sysupgrade.conf run again after an upgrade to re-expand the root partition. The x86 page also warns about a data-loss bug (issue #20426) on systems with more than one block device, so read it before you add a second virtual disk to this VM. If this router is one piece of a bigger small-hardware build, our tested roundup of lightweight Linux distros for IoT and old hardware puts OpenWrt’s idle footprint next to nine other minimal systems. A small fanless mini PC for a Proxmox homelab runs this VM alongside everything else.

Keep reading

UFW Firewall Commands with Examples on Ubuntu 26.04 / 24.04 Security UFW Firewall Commands with Examples on Ubuntu 26.04 / 24.04 PHP2Shell (CVE-2026-109618, CVE-2026-109619): Check and Patch PHP Php PHP2Shell (CVE-2026-109618, CVE-2026-109619): Check and Patch PHP PHP2Shell on End-of-Life PHP 8.1, 8.0 and 7.4 (WHMCS, ionCube) Php PHP2Shell on End-of-Life PHP 8.1, 8.0 and 7.4 (WHMCS, ionCube) Homelab and Sysadmin Gifts by Budget: $25, $50, $100 and $250+ Networking Homelab and Sysadmin Gifts by Budget: $25, $50, $100 and $250+ Best Pentesting Hardware for Kali Linux in 2026 Security Best Pentesting Hardware for Kali Linux in 2026 Set Up FreeIPA Server and Enroll Linux Clients on Rocky Linux 10 Rocky Linux Set Up FreeIPA Server and Enroll Linux Clients on Rocky Linux 10

Leave a Comment

Press ESC to close