The scenario
You just provisioned a new VPS or set up a home server, and you need a secure tunnel to your desktop. You found a tutorial that says to run wg-quick up wg0, but your terminal throws a dependency error or the interface stays down. You know sudo and dnf, but WireGuard's configuration format looks like a mix of INI syntax and networking jargon. You need a clear path from package installation to a working tunnel without guessing.
What WireGuard actually does
WireGuard is a virtual network interface. It sits between your application layer and your routing table. When you send traffic through it, the kernel encrypts the packets, wraps them in UDP, and ships them to a remote peer. The remote peer decrypts, unwraps, and routes the payload. It works like a dedicated Ethernet cable that happens to run over the internet, but the cable only exists when both ends agree on the cryptographic keys.
Unlike older VPN stacks that rely on complex state machines and multiple protocol handshakes, WireGuard keeps the state minimal. It uses pre-shared keys for authentication and a lightweight key exchange for encryption. The result is a tunnel that establishes in milliseconds and drops packets silently when the other side goes offline. The kernel handles the heavy lifting, which means lower latency and less CPU overhead.
Read the architecture before you start typing commands. Understanding how the interface maps to your routing table saves hours of debugging later.
Install and generate keys
Fedora ships WireGuard in the main repositories. You do not need third-party repos or manual compilation. Run the package manager to pull in the userspace tools and the kernel module.
sudo dnf install wireguard-tools -y # pulls the CLI tools and registers systemd unit templates
modprobe wireguard # loads the kernel driver into the running system
lsmod | grep wireguard # confirms the module is active before you write any config
WireGuard relies on asymmetric cryptography. Each endpoint needs a private key and a public key. The private key stays on the machine. The public key travels to the peer. Generate both in a single command and store them where only root can read them.
sudo mkdir -p /etc/wireguard/keys # isolates sensitive material from the main config directory
sudo wg genkey | sudo tee /etc/wireguard/keys/privatekey | sudo wg pubkey | sudo tee /etc/wireguard/keys/publickey # generates the pair and pipes the public key out
sudo chmod 600 /etc/wireguard/keys/privatekey # restricts access to root only
Keep the private key file secure. If an attacker reads it, they can impersonate your machine and decrypt all traffic. The public key can be shared openly. Store the public key somewhere you can paste it into the remote peer's configuration later.
Back up the key directory before you proceed. Lost keys mean rebuilding the tunnel from scratch.
Write the configuration
WireGuard reads configuration files from /etc/wireguard/. The format follows a simplified INI structure. You define the local interface, then list each peer you want to connect to. Each peer block contains their public key, the IP addresses you are allowed to route through them, and their network endpoint.
Create the configuration file for a client tunnel. Replace the placeholder values with your actual keys and server details.
[Interface]
PrivateKey = <YOUR_PRIVATE_KEY> # paste the content of /etc/wireguard/keys/privatekey here
Address = 10.0.0.2/24 # assigns a virtual IP to this end of the tunnel
ListenPort = 51820 # tells the kernel which UDP port to monitor for incoming packets
[Peer]
PublicKey = <PEER_PUBLIC_KEY> # paste the remote server's public key here
AllowedIPs = 0.0.0.0/0 # routes all outbound traffic through this peer
Endpoint = <PEER_IP>:51820 # the public IP and port of the remote server
PersistentKeepalive = 25 # sends a heartbeat every 25 seconds to keep NAT mappings alive
Save the file as /etc/wireguard/wg0.conf. The filename dictates the interface name. wg0.conf creates wg0. wg1.conf creates wg1. Do not edit files in /usr/lib/. System updates will overwrite them. Always place custom configurations in /etc/.
Verify the file permissions before starting the service. WireGuard refuses to load configs readable by other users.
sudo chmod 600 /etc/wireguard/wg0.conf # ensures only root can read the private key inside
sudo ls -l /etc/wireguard/wg0.conf # confirms the permissions match the security requirement
Start the tunnel and verify
The wg-quick wrapper handles the heavy lifting. It reads the config file, applies the interface settings, configures the firewall rules, and brings the interface up. It also registers a systemd service automatically, so the tunnel survives reboots.
sudo wg-quick up wg0 # parses the config and applies the interface and peer settings
wg show wg0 # displays the interface details and peer handshake status
If the command succeeds, you will see the local listening port, the assigned IP, and the peer block with a latest handshake timestamp. The timestamp proves the key exchange completed and the tunnel is live.
Verify that traffic actually routes through the tunnel. Ping the remote peer's virtual IP or run a DNS lookup that should resolve through the tunnel.
ping -c 3 10.0.0.1 # tests connectivity to the server's tunnel IP
curl -s ifconfig.me # confirms your public IP changed to the server's address
Enable the systemd service so the tunnel starts automatically on boot.
sudo systemctl enable wg-quick@wg0.service # registers the tunnel for automatic startup
sudo systemctl start wg-quick@wg0.service # starts it immediately without reloading the config manually
Run systemctl status wg-quick@wg0.service before you assume it is working. The status output shows recent log lines and the active state in one view.
Check the routing table after the tunnel comes up. Missing routes are the fastest way to break connectivity.
Routing, NAT, and keepalives
WireGuard does not automatically change your default route unless you tell it to. The AllowedIPs = 0.0.0.0/0 directive in the config tells the kernel to send all traffic through the tunnel. If you only want specific subnets to route through the VPN, replace 0.0.0.0/0 with the exact CIDR ranges you need. Split tunneling reduces latency for local traffic and prevents your server from seeing unnecessary DNS queries.
NAT traversal requires the PersistentKeepalive setting. Home routers and cloud firewalls drop idle UDP connections after a few minutes. The keepalive sends a small encrypted packet every 25 seconds to keep the mapping alive. Without it, your tunnel will work for a while, then silently fail until you restart the interface.
If you are running the server side, you must enable IP forwarding in the kernel. The server needs to route packets between the tunnel interface and the physical network interface.
# /etc/sysctl.d/99-wireguard.conf
net.ipv4.ip_forward = 1 # allows the kernel to forward packets between interfaces
net.ipv6.conf.all.forwarding = 1 # enables forwarding for IPv6 tunnel traffic
Apply the setting immediately and verify it took effect.
sudo sysctl -p /etc/sysctl.d/99-wireguard.conf # loads the forwarding rules into the running kernel
cat /proc/sys/net/ipv4/ip_forward # confirms the value is 1 before testing traffic
Restart the firewall after changing routing rules. Runtime and persistent configurations diverge quickly if you forget to reload.
Common pitfalls and error messages
WireGuard fails silently when keys mismatch. You will see the interface come up, but the peer block will show latest handshake: (none). The tunnel exists, but no data passes through it. Check the public key in the config against the actual key file on the remote machine. A single wrong character breaks the handshake.
Firewall rules are the second most common blocker. Fedora's default firewall drops incoming UDP traffic on non-standard ports. If you are running the server, open the port in firewalld.
sudo firewall-cmd --permanent --add-port=51820/udp # adds the port to the persistent zone
sudo firewall-cmd --reload # applies the change to the running firewall
If you see Error: RTNETLINK answers: File exists when running wg-quick up, the interface is already active. Bring it down first.
sudo wg-quick down wg0 # clears the interface and removes the routing table entries
sudo wg-quick up wg0 # reinitializes the tunnel cleanly
SELinux rarely blocks WireGuard, but if you get permission denied errors on the config file, check the context. WireGuard expects wireguard_conf_t on files in /etc/wireguard/. Restore the default context if you moved files around.
sudo restorecon -v /etc/wireguard/wg0.conf # resets the SELinux label to the package default
When the service fails to start, you will see this in the journal:
wg-quick[1234]: # wg setconf wg0 /dev/fd/63
wg-quick[1234]: rt-netlink answers: File exists
wg-quick[1234]: # ip link set mtu 1420 up dev wg0
wg-quick[1234]: Error: RTNETLINK answers: File exists
The error means the interface is already up or a previous crash left stale routing entries. Flush the interface and try again.
Read the actual error before guessing. journalctl -xeu wg-quick@wg0.service shows the exact failure point with explanatory text.
When to use WireGuard versus other tools
Use WireGuard when you need a fast, low-overhead tunnel for point-to-point or small site-to-site connections. Use OpenVPN when you require TCP fallback, complex certificate management, or legacy client support. Use systemd-networkd with wg-quick when you want declarative network configuration that integrates with Fedora's default networking stack. Stick to the default wg-quick wrapper unless you are writing custom routing scripts or managing hundreds of peers programmatically.
Test the tunnel in a disposable VM before applying it to production. A misconfigured default route can lock you out of SSH.