Skip to content

WireGuard VPN

Applies to CentOS Stream 9 & 10 / AlmaLinux 9.x & 10.x / Rocky Linux 9.x & 10.x

A VPN lets remote clients reach a private network — or route all their traffic through a trusted gateway — over an encrypted tunnel. On modern Enterprise Linux, WireGuard is the fastest path to a working tunnel: the kernel module ships in-tree, the configuration is a handful of lines, and the cryptography is fixed and modern. This guide walks through a road-warrior setup (remote clients dialing into a central server) and then covers IPsec/libreswan as an alternative for site-to-site deployments.

  • How to install wireguard-tools and generate key pairs on the server and each client
  • How to write a server wg0.conf with NAT masquerade and a matching client config
  • How to enable kernel IP forwarding and open the right firewalld ports
  • How to bring the tunnel up persistently with systemd and verify the handshake
  • When to reach for IPsec/libreswan instead
  • A server reachable on a public IP (or a forwarded UDP port) running EL 9 or EL 10
  • sudo/root access on the server and each client
  • firewalld and NetworkManager active (the defaults on these distributions)
  • EPEL configured only if wireguard-tools is not in your base repositories

WireGuard creates a virtual network interface (wg0) backed by a small set of static keys. Each side holds a private key and shares its public key with the other. Peers are identified solely by their public key, and AllowedIPs decides which tunnel addresses (and, for full-tunnel clients, which destinations) are routed through the interface. There are no user accounts, certificates, or negotiation phases to debug — if the keys and AllowedIPs line up and UDP can flow, the tunnel forms.

In this guide the server uses the tunnel subnet 10.10.0.0/24, with the server at 10.10.0.1 and the first client at 10.10.0.2.

The kernel module is in-tree on modern EL kernels, so you only need the userspace tooling.

Install the WireGuard CLI tools
$ sudo dnf install wireguard-tools

Run this on the server and again on each client. The commands write a private key and derive its public key in one shot.

Generate a private/public key pair
$ umask 077
$ wg genkey | tee privatekey | wg pubkey > publickey

umask 077 ensures the private key is created without group or world access. Inspect the two files:

View the generated keys
$ cat privatekey
$ cat publickey

Each side needs its own config. The server config defines the interface plus one [Peer] block per client; the client config defines its own interface plus a single [Peer] block pointing back at the server.

Create /etc/wireguard/wg0.conf on the server. Replace <SERVER_PRIVATE_KEY> with the server’s private key and <CLIENT_PUBLIC_KEY> with the client’s public key. Set PostUp/PostDown so the server masquerades client traffic out of its public interface (eth0 below — adjust to match your NIC).

/etc/wireguard/wg0.conf (server)
[Interface]
Address = 10.10.0.1/24
PrivateKey = <SERVER_PRIVATE_KEY>
ListenPort = 51820
PostUp = firewall-cmd --zone=public --add-masquerade
PostUp = firewall-cmd --direct --add-rule ipv4 nat POSTROUTING 0 -s 10.10.0.0/24 -o eth0 -j MASQUERADE
PostDown = firewall-cmd --zone=public --remove-masquerade
PostDown = firewall-cmd --direct --remove-rule ipv4 nat POSTROUTING 0 -s 10.10.0.0/24 -o eth0 -j MASQUERADE
[Peer]
# First client
PublicKey = <CLIENT_PUBLIC_KEY>
AllowedIPs = 10.10.0.2/32

On the server, AllowedIPs is a routing/whitelist setting: it lists exactly which tunnel addresses belong to that peer. Give each client a unique /32. Lock the file down:

Restrict permissions on the server config
$ sudo chmod 600 /etc/wireguard/wg0.conf

The server only routes client traffic if the kernel forwards packets between interfaces. Set this persistently with a drop-in under /etc/sysctl.d/.

Enable IPv4 (and IPv6) forwarding
$ sudo tee /etc/sysctl.d/99-wireguard-forward.conf <<'EOF'
net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding = 1
EOF
$ sudo sysctl --system

Confirm it is active:

Verify the forwarding setting
$ sysctl net.ipv4.ip_forward
Expected output
net.ipv4.ip_forward = 1

WireGuard listens on UDP 51820. Open that port and enable masquerade so client traffic can NAT out of the server.

Allow WireGuard through firewalld
$ sudo firewall-cmd --permanent --add-port=51820/udp
$ sudo firewall-cmd --permanent --add-masquerade
$ sudo firewall-cmd --reload

For a quick test, bring the interface up by hand. wg-quick reads /etc/wireguard/wg0.conf and configures the interface, addresses, routes, and the PostUp rules in one step.

Start the tunnel manually
$ sudo wg-quick up wg0

To stop it again:

Stop the tunnel
$ sudo wg-quick down wg0

Once it works, make it persistent with the bundled systemd template unit so the tunnel comes back after a reboot:

Enable the tunnel at boot
$ sudo systemctl enable --now wg-quick@wg0

Run the same enable command (with each host’s own config) on both the server and the clients.

Check tunnel state with wg show. A successful connection reports a recent handshake and a non-zero transfer count.

Inspect the tunnel
$ sudo wg show
Expected output (server side, one connected client)
interface: wg0
public key: <SERVER_PUBLIC_KEY>
private key: (hidden)
listening port: 51820
peer: <CLIENT_PUBLIC_KEY>
endpoint: 203.0.113.55:48124
allowed ips: 10.10.0.2/32
latest handshake: 18 seconds ago
transfer: 1.21 KiB received, 980 B sent

From the client, ping the server’s tunnel address to confirm end-to-end connectivity:

Test connectivity from the client
$ ping -c 3 10.10.0.1

If you see a latest handshake and traffic counters increase, the tunnel is up.

  1. Install wireguard-tools on the server and every client.

  2. Generate a key pair on each host with wg genkey | tee privatekey | wg pubkey > publickey.

  3. Write the server /etc/wireguard/wg0.conf with the server private key and one [Peer] block (client public key, AllowedIPs = 10.10.0.2/32).

  4. Write each client /etc/wireguard/wg0.conf with its private key and a [Peer] block (server public key, Endpoint, and AllowedIPs).

  5. Enable IP forwarding on the server via /etc/sysctl.d/ and sudo sysctl --system.

  6. Open 51820/udp and add masquerade in firewalld, then reload.

  7. Run sudo systemctl enable --now wg-quick@wg0 on every host and verify with sudo wg show.

WireGuard is the right default for most road-warrior and modern site-to-site links. Reach for IPsec when you need interoperability with appliances and clouds that speak standard IKEv2, or when policy mandates an IPsec-only VPN. On Enterprise Linux the supported implementation is libreswan.

Install libreswan
$ sudo dnf install libreswan
$ sudo systemctl enable --now ipsec

libreswan supports both site-to-site tunnels (two gateways joining their subnets) and road-warrior setups (remote clients dialing into a gateway, typically over IKEv2). Connections are defined in /etc/ipsec.conf and /etc/ipsec.d/*.conf, with secrets in /etc/ipsec.secrets. The firewall must allow UDP 500 and 4500 plus the ESP protocol; SELinux ships with the policy libreswan needs.

Open IPsec ports in firewalld
$ sudo firewall-cmd --permanent --add-service=ipsec
$ sudo firewall-cmd --reload

No handshake at all. This is almost always a connectivity or key issue. Work through:

  • UDP port reachability. Confirm 51820/udp is open on the server (sudo firewall-cmd --list-ports) and on any upstream router/cloud security group. WireGuard is UDP-only — a TCP-only rule will not work.
  • IP forwarding. If the handshake succeeds but clients cannot reach the wider network, re-check sysctl net.ipv4.ip_forward returns 1 on the server.
  • AllowedIPs mismatch. On the server, each peer’s AllowedIPs must include that client’s tunnel /32. On the client, AllowedIPs must include the subnets you expect to reach. A missing range silently drops the traffic.
  • Clock skew. WireGuard’s handshake is time-sensitive; a badly wrong system clock on either side breaks it. Ensure chronyd is running and synced (chronyc tracking).
  • Wrong public key or endpoint. Double-check you exchanged public (not private) keys, and that the client Endpoint points at the server’s reachable address and port.

Routing or NAT problems. If clients connect but cannot browse the internet through the server (full tunnel), confirm masquerade is enabled (sudo firewall-cmd --query-masquerade) and that the PostUp masquerade rule references the server’s actual outbound interface name.

DNS leaks. With AllowedIPs = 0.0.0.0/0, set a DNS entry in the client [Interface] that points to a resolver reachable through the tunnel. Without it, queries may still go to the client’s local DNS server, leaking which sites you visit even though web traffic flows through the VPN.