VPN mini-HOWTO: WireGuard, OpenVPN and IPsec on Linux

Updated November 2026

A VPN joins machines or networks over the Internet through an encrypted tunnel. On Linux today the simplest choice is WireGuard, which is built into the kernel and needs a dozen lines of configuration. OpenVPN is the choice when you need TCP, certificates or very wide client support, and IPsec (strongSwan) when the other end is a firewall appliance or another vendor’s gateway.

The classic VPN mini-HOWTO ran PPP over SSH, which nobody should do any more. The Linux Documentation Project keeps a related classic VPN HOWTO at tldp.org. This page covers current tools as of November 2026: OpenVPN 2.6 (Debian 13) and 2.7 (upstream), strongSwan 6.

Which VPN to use

Linux VPN options
WireGuardOpenVPNstrongSwan (IPsec/IKEv2)
TransportUDP only (one port)UDP or TCP (one port)UDP 500 and 4500, ESP
AuthenticationPublic keys per peerX.509 certificates, optional user/passwordCertificates, pre-shared keys, EAP
Setup effortLowMedium (needs a small PKI)Medium to high
Best forYour own servers, laptops and phonesRestrictive networks that only allow TCP; existing OpenVPN fleetsSite-to-site links with routers and firewalls from other vendors; built-in IKEv2 clients

WireGuard

Each WireGuard peer has a key pair. A peer’s AllowedIPs works both ways: it is the list of addresses that may arrive from that peer, and the routes sent into the tunnel towards it. There is no “connection” to start; packets flow as soon as both sides know each other’s public key and one side knows where to find the other (Endpoint).

# Debian / Ubuntu
sudo apt install wireguard-tools
# Fedora / RHEL / Alma / Rocky
sudo dnf install wireguard-tools

Make a key pair on each machine. umask 077 keeps the private key unreadable to other users:

umask 077
wg genkey | tee privatekey | wg pubkey > publickey

Server: /etc/wireguard/wg0.conf

[Interface]
Address = 10.20.0.1/24
ListenPort = 51820
PrivateKey = <server-private-key>

[Peer]
# alice-laptop
PublicKey = <client-public-key>
AllowedIPs = 10.20.0.2/32

Client: /etc/wireguard/wg0.conf on the laptop

[Interface]
Address = 10.20.0.2/24
PrivateKey = <client-private-key>

[Peer]
PublicKey = <server-public-key>
Endpoint = vpn.example.com:51820
AllowedIPs = 10.20.0.0/24
PersistentKeepalive = 25

Fill in your own keys. Replace each placeholder with the contents of the privatekey or publickey file made by wg genkey on that machine: the server file holds the server’s private key and the client’s public key, the client file holds the client’s private key and the server’s public key. A private key never leaves the machine it was made on.

PersistentKeepalive = 25 sends a small packet every 25 seconds so that a home router’s NAT keeps the path open; the server does not need it. Each client gets its own [Peer] block on the server, with its own /32 address.

Bring it up

sudo chmod 600 /etc/wireguard/wg0.conf
sudo wg-quick up wg0
sudo wg show
sudo systemctl enable --now wg-quick@wg0     # start at boot (run wg-quick down wg0 first)

wg show on the server prints something like:

interface: wg0
  public key: <server-public-key>
  private key: (hidden)
  listening port: 51820

peer: <client-public-key>
  allowed ips: 10.20.0.2/32

Once the client has sent traffic, the peer also shows an endpoint, a latest handshake time and transfer counters. No handshake means the UDP port is blocked or a key is wrong. Open the port on the server: sudo firewall-cmd --permanent --add-service=wireguard and sudo firewall-cmd --reload, or sudo ufw allow 51820/udp.

Send all traffic through the tunnel

To use the server as the client’s Internet exit, set AllowedIPs = 0.0.0.0/0, ::/0 on the client, and add DNS = 10.20.0.1 if the server runs a resolver (wg-quick applies it by calling resolvconf, so a resolvconf program must be installed). wg-quick then installs policy-routing rules (with ip rule) so that the tunnel’s own packets still leave through the normal gateway. The server must forward and masquerade the traffic; see the IP Masquerade HOWTO for net.ipv4.ip_forward and the nftables or firewalld NAT rules.

OpenVPN

OpenVPN uses TLS with certificates. Build a small certificate authority with Easy-RSA on a machine you trust, ideally not the VPN server:

# Debian / Ubuntu
sudo apt install openvpn easy-rsa
cp -r /usr/share/easy-rsa ~/easy-rsa
# Fedora / RHEL (EPEL): sudo dnf install openvpn easy-rsa
#   then copy /usr/share/easy-rsa/<version>/ instead
cd ~/easy-rsa
./easyrsa init-pki
./easyrsa build-ca
./easyrsa build-server-full server nopass
./easyrsa build-client-full alice nopass
openvpn --genkey tls-crypt tc.key

This creates pki/ca.crt, pki/issued/server.crt, pki/private/server.key and the same for alice. nopass leaves the server key unencrypted so the service can start unattended; keep the CA key (pki/private/ca.key) offline. tc.key is a shared key for tls-crypt, which encrypts and authenticates the control channel: it hides the certificates and makes OpenVPN traffic harder to identify, and packets made without the key are dropped.

Copy ca.crt, server.crt, server.key and tc.key to /etc/openvpn/server/ and create /etc/openvpn/server/server.conf:

port 1194
proto udp
dev tun
ca ca.crt
cert server.crt
key server.key
dh none
tls-crypt tc.key
topology subnet
server 10.8.0.0 255.255.255.0
keepalive 10 120
persist-tun
verb 3
explicit-exit-notify 1

dh none uses only elliptic-curve key exchange (OpenVPN 2.7 does this when dh is left out). server 10.8.0.0 255.255.255.0 gives clients addresses from 10.8.0.0/24.

sudo systemctl enable --now openvpn-server@server
sudo firewall-cmd --permanent --add-service=openvpn && sudo firewall-cmd --reload
journalctl -u openvpn-server@server -f

The unit name after @ is the config file name without .conf. A matching client file, alice.ovpn:

client
dev tun
proto udp
remote vpn.example.com 1194
resolv-retry infinite
nobind
persist-tun
remote-cert-tls server
ca ca.crt
cert alice.crt
key alice.key
tls-crypt tc.key
verb 3

remote-cert-tls server makes the client reject a certificate that was not issued for a server, which stops one client from impersonating the server. Run it with sudo openvpn --config alice.ovpn, or put it in /etc/openvpn/client/alice.conf and use openvpn-client@alice.

IPsec with strongSwan

strongSwan 6 is configured through swanctl and /etc/swanctl/swanctl.conf; the old ipsec.conf/stroke interface is no longer enabled by default. Install it with sudo apt install charon-systemd strongswan-swanctl on Debian and Ubuntu, or sudo dnf install strongswan on Fedora. This site-to-site example links 10.1.0.0/16 behind gateway A (203.0.113.1) with 10.2.0.0/16 behind gateway B (198.51.100.1), using IKEv2 and a pre-shared key. On gateway A:

connections {
    a-to-b {
        version = 2
        local_addrs = 203.0.113.1
        remote_addrs = 198.51.100.1
        local {
            auth = psk
            id = gw-a.example.com
        }
        remote {
            auth = psk
            id = gw-b.example.com
        }
        children {
            net-net {
                local_ts = 10.1.0.0/16
                remote_ts = 10.2.0.0/16
                start_action = trap
            }
        }
    }
}

secrets {
    ike-ab {
        id-a = gw-a.example.com
        id-b = gw-b.example.com
        secret = "<pre-shared-key>"
    }
}

Gateway B gets the mirror image (swap the addresses, IDs and subnets). start_action = trap brings the tunnel up when the first matching packet appears. Leaving out proposals and esp_proposals uses strongSwan’s default list of algorithms it considers safe. Replace <pre-shared-key> with a long random secret, the same on both gateways, for example from openssl rand -base64 24, or better, use certificates (auth = pubkey).

sudo systemctl enable --now strongswan
sudo swanctl --load-all
sudo swanctl --initiate --child net-net
sudo swanctl --list-sas
sudo firewall-cmd --permanent --add-service=ipsec && sudo firewall-cmd --reload

NetworkManager on desktops

Mistakes to avoid

Sources