
A VPN that takes three evenings to configure is a VPN nobody wants to maintain. That is why WireGuard keeps winning arguments in sysadmin Slack channels. The configuration is a few dozen lines, the codebase is small enough to audit, and the data path lives inside the kernel, so there is no userspace daemon to babysit.
Fedora 44 is a good host for it. The release shipped on April 28, 2026 with kernel 6.19 and has since moved forward through normal updates. WireGuard has been in the mainline kernel since 5.6, so no DKMS module, no out-of-tree repository, and no rebuilds after each kernel update. You install a small tools package and write a config file. That is the whole install.
The details are where people lose time, though. A missing masquerade rule gives you a tunnel that connects but carries no traffic. Forgetting IP forwarding gives you the same symptom. A strict MTU, a cloud security group that blocks UDP, or an SELinux label on the wrong directory each cost an hour. This guide covers the full path: installing the tools, generating keys properly, building a server and a client, wiring up firewalld, and tuning the result. It ends with a troubleshooting section built from the failures that show up most often in production.
The examples assume a Fedora 44 server with a public IP and one or more clients, such as a laptop, a phone, or another Linux box. If you are a Fedora 44 desktop user who only wants to connect to an existing VPN, skip ahead to the NetworkManager section.
What WireGuard Actually Is (and Why It Behaves Differently)
WireGuard is a layer 3 VPN. It creates a virtual network interface, usually wg0, and routes IP packets through an encrypted UDP tunnel. Each peer is identified by a public key, and each peer declares which IP ranges it is allowed to send from. That declaration is called AllowedIPs, and it does double duty as both a routing table and an access control list.
That detail confuses people coming from OpenVPN or IPsec. There is no “connection” in the traditional sense. There is no handshake you can watch fail in a log file. The interface is simply up, and packets are encrypted and sent when they match a peer’s AllowedIPs. If nothing matches, nothing is sent, and nothing complains.
Two consequences follow. First, WireGuard is silent by design, so debugging relies on wg show, tcpdump, and counters rather than log messages. Second, it is stateless from the server’s perspective until a peer talks, which is why a client behind NAT needs PersistentKeepalive. Keep both in mind. They explain most of the “it should work but doesn’t” tickets.
Prerequisites and Planning
Before touching a terminal, decide a few things. These choices are painful to change after clients are deployed.
- VPN subnet: Pick a private range that will not collide with home networks or office LANs. Avoid
192.168.0.0/24and192.168.1.0/24, because every consumer router uses them. Something like10.8.0.0/24or10.66.66.0/24is safer. - UDP port: The convention is 51820. Any UDP port works. Changing it does not add real security, but it can help on networks that throttle well-known ports.
- Routing model: Decide between full tunnel (all client traffic goes through the server) and split tunnel (only specific subnets do). Full tunnel needs NAT on the server. Split tunnel often does not.
- Public endpoint: A static IP or a stable DNS name. If the server IP changes, clients with a hard-coded IP break silently.
- Root or sudo access: Every command below needs it.
On cloud providers such as AWS, Hetzner, or DigitalOcean, also open UDP 51820 in the provider’s firewall or security group. The OS firewall is only half the picture, and this is the most common reason a freshly built tunnel never handshakes.
Step 1: Update Fedora 44 and Install the Tools
Start from a current system. Fedora moves fast, and a stale kernel or an old firewalld can produce odd behavior.
sudo dnf upgrade --refresh -y
If the upgrade pulled in a new kernel, reboot before continuing. It is a minor annoyance now and a mystery later.
Now install the userspace tooling:
sudo dnf install -y wireguard-tools qrencode firewalld
wireguard-tools provides the wg and wg-quick commands. qrencode is optional but very handy, because it turns a client config into a QR code that a phone can scan. firewalld is already present on most Fedora installs, but the explicit install does no harm.
Confirm the kernel side is available:
wg --version
modinfo wireguard | head -3
If modinfo prints module details, you are done with the “installation” in the strict sense. Nothing else needs compiling. On Fedora there is no wireguard-dkms or kmod-wireguard to worry about, and anyone telling you otherwise is quoting a 2018 tutorial.
A note for people who run other distributions: on Ubuntu or Debian the equivalent is sudo apt install wireguard, and on AlmaLinux or Rocky you install epel-release first and then wireguard-tools. The config files in this guide work unchanged across all of them. Only the package manager differs.
Step 2: Enable IP Forwarding
If the server will route client traffic to the internet or to another network, the kernel must be allowed to forward packets. It is off by default.
Use a drop-in file rather than editing /etc/sysctl.conf. It is cleaner and survives package updates:
sudo tee /etc/sysctl.d/99-wireguard.conf > /dev/null <<'EOF'
net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding = 1
EOF
sudo sysctl --system
Verify:
sysctl net.ipv4.ip_forward
The output should read net.ipv4.ip_forward = 1. The IPv6 line only matters if you plan to tunnel IPv6. Leaving it on is harmless, but if you do not use IPv6 at all, you can drop that line and keep the surface smaller.
Step 3: Generate Keys the Right Way
WireGuard uses Curve25519 key pairs. Each machine needs its own private key, and the other side only ever sees the public key. Never reuse a key pair across machines, and never paste a private key into a ticket or chat.
Create the config directory with strict permissions first:
sudo mkdir -p /etc/wireguard
sudo chmod 700 /etc/wireguard
Generate the server keys in a subshell with a restrictive umask so the private key is never world readable, even briefly:
cd /etc/wireguard
sudo sh -c 'umask 077; wg genkey | tee server_private.key | wg pubkey > server_public.key'
Do the same for each client, preferably on the client itself. Generating client keys on the server and shipping the private key over the network works, but it means the server briefly held a secret it never needed. On a small team that tradeoff is acceptable. In a regulated environment it is not.
Check the result:
sudo ls -l /etc/wireguard
sudo cat /etc/wireguard/server_public.key
The private key files should show -rw------- and be owned by root.
Optional: Add a Preshared Key
WireGuard supports an extra symmetric key per peer pair. It adds a layer of protection against a future attacker with a quantum computer who recorded your traffic today. It costs almost nothing:
wg genpsk
Generate one per peer and include it as PresharedKey in both the server’s [Peer] block and the client’s [Peer] block. Teams with long data retention concerns tend to adopt this routinely.
Step 4: Write the Server Configuration
Identify the server’s outbound network interface. You will need it for NAT:
ip -o -4 route show to default
On a typical Fedora 44 cloud image, you will see something like eth0 or ens3. On a physical box it may be enp1s0. The interface name matters for the NAT rules in the next step, so write it down.
Create /etc/wireguard/wg0.conf:
sudo nano /etc/wireguard/wg0.conf
[Interface]
Address = 10.8.0.1/24
ListenPort = 51820
PrivateKey = <SERVER_PRIVATE_KEY>
[Peer]
# laptop-godet
PublicKey = <CLIENT_PUBLIC_KEY>
AllowedIPs = 10.8.0.2/32
A few comments on why it looks this way.
Address assigns the server the first host in the VPN subnet. ListenPort fixes the UDP port so firewall rules stay predictable. The server’s [Peer] section uses a /32 in AllowedIPs, because the server should accept only that exact address from that client. A wider range would let a compromised client spoof other VPN addresses.
Notice what is absent: no Endpoint on the server side. The server learns each client’s endpoint from incoming handshakes, which is what lets roaming laptops keep working when they change networks.
I deliberately did not use SaveConfig = true, even though many tutorials include it. With that flag, wg-quick rewrites the config file on shutdown, which can wipe out comments and any manual edits made while the interface was up. In a configuration-managed environment (Ansible, Puppet, Salt) it also creates drift. Keep the file as the single source of truth and use wg set only for temporary tests.
Lock the file down:
sudo chmod 600 /etc/wireguard/wg0.conf
Replace the placeholders with the actual key contents. A quick way to avoid typos is to print the key and paste it in your editor, not retype it.
Step 5: Configure firewalld and NAT on Fedora
Fedora ships firewalld by default, and it is the right tool here. Fighting it with hand-written iptables rules is a recurring source of conflicts, particularly since firewalld uses an nftables backend on modern Fedora.
Enable and start it:
sudo systemctl enable --now firewalld
Open the WireGuard port and enable masquerading:
sudo firewall-cmd --permanent --add-port=51820/udp
sudo firewall-cmd --permanent --add-masquerade
sudo firewall-cmd --reload
This is the approach used in the community guides for current Fedora releases. Masquerade on the zone makes traffic from VPN clients appear to come from the server’s public IP, which is what full-tunnel clients need.
Some guides put these commands in PostUp and PostDown lines inside wg0.conf. That works, but it ties firewall state to the tunnel’s lifecycle, and calling firewall-cmd from inside a hook can race with firewalld startup at boot. Persistent firewalld rules avoid that class of problem entirely.
A Tighter Zone-Based Setup
The public zone is a blunt instrument. If the server does anything else, a dedicated zone for the tunnel interface is cleaner:
sudo firewall-cmd --permanent --new-zone=wireguard
sudo firewall-cmd --permanent --zone=wireguard --add-interface=wg0
sudo firewall-cmd --permanent --zone=wireguard --add-masquerade
sudo firewall-cmd --permanent --zone=wireguard --set-target=ACCEPT
sudo firewall-cmd --reload
You still keep UDP 51820 open on the external zone. The wireguard zone then controls what VPN clients can reach. Change --set-target=ACCEPT to default and add specific services if you want to limit clients to, say, SSH and a database port.
A caveat here. Interface-to-zone binding for wg0 can behave unexpectedly if the interface does not exist when firewalld loads. In practice, firewalld picks it up when wg-quick brings the interface up, but if you see the interface land in the default zone after a reboot, a reload after the tunnel starts fixes it. Some admins add a small systemd drop-in to handle this.
Step 6: Start the Tunnel and Make It Persistent
Bring it up manually first, to see errors in your terminal:
sudo wg-quick up wg0
sudo wg show
You should see the interface, the public key, the listening port, and your peer. There will be no handshake line yet, because the client has not connected.
If everything looks good, enable the systemd unit:
sudo systemctl enable --now wg-quick@wg0
sudo systemctl status wg-quick@wg0
The unit name includes the config file name without the extension. A file called office.conf becomes wg-quick@office. That naming scheme is what makes running multiple tunnels on one host easy.
If the wg-quick up call already activated the interface, enable --now might complain that it is already up. Run sudo wg-quick down wg0 first and then enable the service, so systemd owns the state.
Step 7: Configure a Linux Client
On the client, the install is identical:
sudo dnf install -y wireguard-tools # Fedora, Alma, Rocky
sudo apt install -y wireguard # Debian, Ubuntu
Generate keys on the client, then send only the public key to the server admin (or yourself):
sudo mkdir -p /etc/wireguard && sudo chmod 700 /etc/wireguard
cd /etc/wireguard
sudo sh -c 'umask 077; wg genkey | tee client_private.key | wg pubkey > client_public.key'
Create /etc/wireguard/wg0.conf on the client:
[Interface]
Address = 10.8.0.2/24
PrivateKey = <CLIENT_PRIVATE_KEY>
DNS = 1.1.1.1
[Peer]
PublicKey = <SERVER_PUBLIC_KEY>
Endpoint = vpn.example.com:51820
AllowedIPs = 0.0.0.0/0, ::/0
PersistentKeepalive = 25
Each line has a reason.
AllowedIPs = 0.0.0.0/0, ::/0 makes this a full tunnel. All IPv4 and IPv6 traffic goes through the server. For split tunneling, replace it with the networks you actually want, for instance 10.8.0.0/24, 192.168.50.0/24. If you have no IPv6 connectivity on the server, tunneling ::/0 can black-hole IPv6 traffic on the client, so remove it in that case. That one trips up many people.
PersistentKeepalive = 25 sends a tiny packet every 25 seconds so NAT devices keep the mapping alive. Clients behind home routers or mobile carriers need it. A server with a public IP does not.
DNS tells wg-quick to configure resolvers when the tunnel comes up. On Fedora this relies on systemd-resolved being active, since wg-quick calls resolvconf. If you see resolvconf: command not found, check that systemd-resolved is installed and running and that the compatibility command is present. Otherwise remove the DNS line and manage DNS yourself.
Start the client tunnel:
sudo wg-quick up wg0
Now go back to the server and make sure the client’s public key is in the server’s [Peer] block with AllowedIPs = 10.8.0.2/32. Reload without dropping existing sessions:
sudo wg syncconf wg0 <(sudo wg-quick strip wg0)
This applies peer changes live. Restarting the unit would work, but it briefly disconnects everyone, which is rude on a shared server.
Step 8: Verify the Tunnel End to End
Never declare victory after wg-quick up. Test in layers.
From the client:
ping -c 3 10.8.0.1
sudo wg show
curl -s https://ifconfig.me
The ping confirms the encrypted path. The wg show output should list a latest handshake within the last couple of minutes and non-zero transfer counters in both directions. The curl result should display the server’s public IP if you are running a full tunnel.
From the server:
sudo wg show
sudo tcpdump -ni any udp port 51820
If packets arrive in tcpdump but no handshake shows up, the keys are mismatched. If nothing arrives, the problem is upstream, in a cloud firewall, NAT, or a wrong endpoint. That single distinction cuts troubleshooting time in half.
Phones and Tablets: The QR Code Shortcut
Typing a 44-character key on a phone is nobody’s idea of fun. Build a client config on the server and render it:
qrencode -t ansiutf8 < /etc/wireguard/phone.conf
Scan it from the official WireGuard app on Android or iOS. Treat that terminal output as sensitive, since it contains a private key. Clear your scrollback afterwards and avoid running it inside a shared tmux session.
Using NetworkManager Instead of wg-quick
Fedora Workstation users often prefer to have the VPN appear in the desktop network menu. NetworkManager supports WireGuard natively, so you can import an existing config:
sudo nmcli connection import type wireguard file /etc/wireguard/wg0.conf
nmcli connection up wg0
Servers should stay with wg-quick and systemd. Desktops benefit from NetworkManager because it handles suspend and resume, Wi-Fi changes, and per-connection autoconnect. Mixing both on the same machine, with the same interface name, creates a fight over wg0. Pick one.
SELinux on Fedora 44: Leave It On
SELinux is enforcing by default, and WireGuard rarely needs any special treatment. That is a good reason to resist the old reflex of running setenforce 0.
Where trouble does appear is file context. If you copy configs from another location with mv rather than cp, they can retain the wrong label. Restore it:
sudo restorecon -Rv /etc/wireguard
If something is still blocked, read the actual denial instead of guessing:
sudo ausearch -m avc -ts recent
sudo journalctl -t setroubleshoot --since "10 minutes ago"
Disabling SELinux to fix a VPN is the sysadmin equivalent of removing a smoke detector because it beeped.
Troubleshooting WireGuard on Fedora 44
Silent failures are the norm, so a methodical checklist beats guessing.
The Interface Is Up but There Is No Handshake
Run sudo wg show on both sides.
- Cause: Wrong public key on one side. Fix: Compare the keys character by character. A trailing character or a swapped key is common.
- Cause: UDP blocked upstream. Fix: Check the cloud security group, the router’s port forward, and
sudo firewall-cmd --list-all. Confirm withtcpdumpon the server. - Cause: Wrong endpoint or port. Fix: Test DNS resolution of the hostname and confirm the port in both configs matches.
- Cause: Client behind strict NAT. Fix: Add
PersistentKeepalive = 25.
Handshake Works, but No Internet Through the Tunnel
- Cause: IP forwarding off. Fix:
sysctl net.ipv4.ip_forwardmust return 1. - Cause: Masquerade missing. Fix:
sudo firewall-cmd --list-alland look formasquerade: yes. - Cause: Wrong outbound interface in custom NAT rules. Fix: Re-check
ip routeoutput. - Cause:
::/0routed with no IPv6 on the server. Fix: Remove::/0from the client’sAllowedIPs.
DNS Does Not Resolve After Connecting
- Cause:
resolvconfmissing orsystemd-resolvedinactive. Fix:systemctl status systemd-resolved, then checkresolvectl statusfor thewg0link. - Cause: DNS server unreachable through the tunnel. Fix: Point to a resolver the server can reach, or run a local resolver such as Unbound on the server’s VPN address.
“RTNETLINK answers: Operation not supported” or “Unknown device type”
- Cause: The WireGuard module is unavailable. This shows up in containers or minimal virtualization (some OpenVZ or LXC setups). Fix: Run it on a proper VM or enable the module on the host. Check with
lsmod | grep wireguardandsudo modprobe wireguard.
“Address already in use”
- Cause: Another process or a NetworkManager connection owns the port or the interface name. Fix:
sudo ss -ulpn | grep 51820and stop the other service. Run only one management method per interface.
Slow Speeds or Stalled Large Transfers
- Cause: MTU mismatch. Fix: Lower MTU to 1380 or 1280 and retest.
- Cause: CPU saturated by softirq. Fix: Check
mpstat, and enable NIC multiqueue. - Cause: Upstream bandwidth cap. Fix: Test the raw link with
iperf3outside the tunnel.
Tunnel Does Not Start at Boot
- Cause: The service was never enabled or it starts before the network is ready. Fix:
systemctl is-enabled wg-quick@wg0. For hostname-based endpoints, add a drop-in withAfter=network-online.targetandWants=network-online.target. Reports from Silverblue users show that firewalld ordering can also matter.