WireGuard VPN Server on OpenWrt
Guide for OpenWrt 25.12 (GL.iNet Flint 2 / GL-MT6000, vanilla OpenWrt)
Remote access to the homelab (192.168.1.0/24) without exposing any service to the internet. The only thing reachable from outside is a single UDP port that responds exclusively to valid WireGuard handshakes. Uses DuckDNS to track the dynamic WAN IP.
Prerequisites:
- Vanilla OpenWrt 25.12+ (apk package manager)
- A public (non-CGNAT) WAN IP
- SSH access to the router as root
Step 1: Verify You Have a Public IP
From any LAN machine:
curl -s ifconfig.me
On the router:
ifstatus wan | grep '"address"'
What this does:
curl -s ifconfig.me– Asks an external service what IP your traffic appears fromifstatus wan– Shows the address actually assigned to the router’s WAN interface
Result: If both IPs match, you have a public IP and can run a VPN server. If the WAN address is in 100.64.0.0/10 or a private range while curl shows something different, you are behind CGNAT and inbound WireGuard will not work (use Tailscale instead).
Step 2: Create a DuckDNS Hostname
- Sign in at duckdns.org
- Add a subdomain (e.g.
example.duckdns.org) - Note the account token shown at the top of the page
Security note: The token is the only credential protecting the record. Treat it as a secret; it can be regenerated via the account icon → “regen token”.
Step 3: Install the DDNS Client on the Router
apk update
apk add luci-app-ddns ca-certificates
What this does:
luci-app-ddns– LuCI page for DDNS; pulls inddns-scriptsandddns-scripts-services(which contains the duckdns.org provider definition) as dependenciesca-certificates– CA bundle so the update runs over verified HTTPS
Step 4: Configure DDNS via UCI
Replace example with your subdomain and YOUR_TOKEN with your DuckDNS token:
uci set ddns.duckdns=service
uci set ddns.duckdns.enabled='1'
uci set ddns.duckdns.service_name='duckdns.org'
uci set ddns.duckdns.lookup_host='example.duckdns.org'
uci set ddns.duckdns.domain='example'
uci set ddns.duckdns.password='YOUR_TOKEN'
uci set ddns.duckdns.use_ipv6='0'
uci set ddns.duckdns.ip_source='network'
uci set ddns.duckdns.ip_network='wan'
uci set ddns.duckdns.use_https='1'
uci set ddns.duckdns.cacert='/etc/ssl/certs'
uci set ddns.duckdns.check_interval='10'
uci set ddns.duckdns.check_unit='minutes'
uci set ddns.duckdns.force_interval='72'
uci set ddns.duckdns.force_unit='hours'
uci commit ddns
/etc/init.d/ddns enable
/etc/init.d/ddns restart
What this does:
service_name='duckdns.org'– Selects the provider definition from the services filedomain='example'– DuckDNS expects the bare subdomain, not the full hostnamepassword='YOUR_TOKEN'– The DuckDNS token goes in the password fieldlookup_host– Hostname used to check what DNS currently answersip_source='network'/ip_network='wan'– Reads the IP directly from the WAN interface instead of an external detection servicecheck_interval10 minutes – How often the IP is compared against DNSforce_interval72 hours – Push an update even when nothing changed, so the record never goes stale
Verify:
cat /var/log/ddns/duckdns.log
Look for DDNS Provider answered: OK near the end.
Step 5: Install WireGuard
apk add luci-proto-wireguard kmod-wireguard wireguard-tools qrencode
What this does:
kmod-wireguard– The kernel module that does the actual encryptionwireguard-tools– Thewgandwg genkeyuserspace utilitiesluci-proto-wireguard– Lets LuCI and netifd manage WireGuard interfacesqrencode– Renders client configs as QR codes for the mobile app (terminal output only on OpenWrt; PNG output is compiled out)
Verify:
modinfo wireguard | head -3
wg --version
Step 6: Generate the Server Keypair
mkdir -p /etc/wireguard
chmod 700 /etc/wireguard
umask 077
wg genkey | tee /etc/wireguard/server.key | wg pubkey > /etc/wireguard/server.pub
What this does:
umask 077– Ensures every file created is readable by root only (mode 600)wg genkey– Generates the private keytee– Writes the private key toserver.keywhile passing it on towg pubkey– Derives the matching public key intoserver.pub
Result: server.key stays on the router and is never shared. server.pub goes into every client config.
Step 7: Create the WireGuard Interface and Firewall Rules
VPN subnet used here: 10.10.10.0/24 (router = 10.10.10.1). Deliberately outside 192.168.x.x so it never collides with the LAN or with whatever network a client is physically on (hotel/guest WiFi is very often 192.168.0.x or 192.168.1.x). Listen port 48222 is arbitrary; a nonstandard port just reduces scanner noise in logs.
First confirm the first firewall zone is lan (the commands below reference it positionally):
uci show firewall.@zone[0].name
Then:
uci set network.wg0=interface
uci set network.wg0.proto='wireguard'
uci set network.wg0.private_key="$(cat /etc/wireguard/server.key)"
uci set network.wg0.listen_port='48222'
uci add_list network.wg0.addresses='10.10.10.1/24'
uci add firewall rule
uci set firewall.@rule[-1].name='Allow-WireGuard'
uci set firewall.@rule[-1].src='wan'
uci set firewall.@rule[-1].dest_port='48222'
uci set firewall.@rule[-1].proto='udp'
uci set firewall.@rule[-1].target='ACCEPT'
uci add_list firewall.@zone[0].network='wg0'
uci commit network
uci commit firewall
/etc/init.d/network restart
/etc/init.d/firewall restart
What this does:
network.wg0– Defines the WireGuard interface with the server key, port, and tunnel addressAllow-WireGuardrule – Opens UDP 48222 from WAN for the handshakeadd_list firewall.@zone[0].network='wg0'– Puts wg0 in the lan zone, so VPN clients are treated like LAN devices and can reach192.168.1.x. Without this, clients handshake successfully but all their traffic is dropped
Note: /etc/init.d/network restart may print Command failed: Not found. This can be cosmetic; verify actual state instead of retrying blindly.
Verify:
wg show
ip addr show wg0
Expect wg0 listening on 48222 with address 10.10.10.1/24. If wg0 is missing, run ifup wg0 and check again.
Step 8: Add a Client (Peer)
Each device gets its own keypair and the next free /32 (first client 10.10.10.2, second 10.10.10.3, and so on). Example for a phone:
umask 077
wg genkey | tee /etc/wireguard/phone.key | wg pubkey > /etc/wireguard/phone.pub
uci add network wireguard_wg0
uci set network.@wireguard_wg0[-1].description='phone'
uci set network.@wireguard_wg0[-1].public_key="$(cat /etc/wireguard/phone.pub)"
uci add_list network.@wireguard_wg0[-1].allowed_ips='10.10.10.2/32'
uci commit network
ifup wg0
What this does:
wireguard_wg0section – Registers a peer on the wg0 interfaceallowed_ips='10.10.10.2/32'– Server-side, this means “packets from this peer must come from this address, and traffic to this address goes to this peer”. One address per deviceifup wg0– Reloads the interface so the new peer is active
Verify:
wg show
The peer should be listed with its allowed IPs.
Step 9: Generate the Client Configuration
cat > /etc/wireguard/phone.conf << EOF
[Interface]
PrivateKey = $(cat /etc/wireguard/phone.key)
Address = 10.10.10.2/32
DNS = 192.168.1.1
[Peer]
PublicKey = $(cat /etc/wireguard/server.pub)
Endpoint = example.duckdns.org:48222
AllowedIPs = 192.168.1.0/24, 10.10.10.0/24
PersistentKeepalive = 25
EOF
What this does:
DNS = 192.168.1.1– Clients resolve via the router (keeps LAN ad-blocking active over VPN)Endpoint– The DuckDNS hostname plus the listen portAllowedIPs(client-side) – The routes sent through the tunnel. As written this is a split tunnel: only homelab traffic uses the VPN; everything else uses the client’s normal connection. Use0.0.0.0/0instead for a full tunnel (all traffic routed home; useful on untrusted WiFi, costs battery and home upload bandwidth)PersistentKeepalive = 25– Keeps NAT mappings alive so the tunnel stays reachable from behind mobile carrier NAT
Step 10: Provision a Phone
Render the config as a QR code and scan it with the official WireGuard app (+ → Scan from QR code):
qrencode -t utf8 < /etc/wireguard/phone.conf
If the terminal QR will not scan (dark themes and line spacing distort it), generate a PNG on a Linux box instead. OpenWrt’s qrencode cannot output PNG, and the router’s dropbear has no SFTP server, so modern scp needs the -O flag:
scp -O root@192.168.1.1:/etc/wireguard/phone.conf /tmp/phone.conf
qrencode -t png -o /tmp/phone-wg.png < /tmp/phone.conf
rm /tmp/phone.conf
Open the PNG full screen, scan it, then delete it (it encodes the private key):
rm /tmp/phone-wg.png
Step 11: Provision a Linux Client
Copy the config while still on the LAN, then:
sudo apt update && sudo apt install wireguard
sudo mv /tmp/mint.conf /etc/wireguard/wg0.conf
sudo chmod 600 /etc/wireguard/wg0.conf
Bring the tunnel up and down manually:
sudo wg-quick up wg0
sudo wg-quick down wg0
Alternative: import into NetworkManager for a tray toggle (nmcli connection import type wireguard file /etc/wireguard/wg0.conf). Use one mechanism or the other, not both, or they will conflict over the interface.
Step 12: Test From Outside the LAN
With the client on mobile data or a hotspot (not home WiFi):
sudo wg show
What to look for:
latest handshakea few seconds old – The tunnel is establishedtransfershowing received bytes climbing – Traffic is flowing both ways (sent climbing while received stays 0 means the handshake is failing)
Then open a LAN service in the browser, e.g. http://192.168.1.60:8096 for Jellyfin. Services are reached by their normal LAN addresses; the 10.10.10.x addresses only identify tunnel endpoints.
Step 13: Cleanup and Backup
Client private keys are only needed on the client. Once provisioned, remove them from the router (peers in UCI store only public keys):
rm /etc/wireguard/phone.key /etc/wireguard/phone.conf
Take a config backup and pull it off the router (/tmp is RAM-backed and vanishes on reboot):
sysupgrade -b /tmp/backup-$(date +%Y%m%d).tar.gz
From a LAN or VPN-connected machine:
scp -O root@192.168.1.1:/tmp/backup-20260719.tar.gz ~/
Security Notes
- Nothing on the LAN is exposed to the internet. The only open port is UDP 48222, and it responds only to valid handshakes from registered peer keys; to a port scanner it looks closed
- The backup archive contains the server private key and the DuckDNS token. Store it as a secret, never in a git repository
- The DuckDNS token ends up in plaintext in
/etc/config/ddns(root-readable only). This is standard for ddns-scripts. Note that a hijacked DDNS hostname cannot impersonate the server, since WireGuard clients authenticate the server by its public key; the worst case is denial of service - If a client device is lost or wiped, remove its peer section from UCI (or regenerate the keypair and update the peer’s public key). The remaining peers are unaffected
- Avoid restarting the network or firewall services while managing the router over the VPN; you will cut your own session and need LAN access to recover
