Skip to content

Setting up a VPN with OpenVPN

The WireGuard lab needed exactly two keypairs and a handful of configuration lines before a tunnel was up. The VPN and tunneling protocols article described OpenVPN's TLS-based approach as the third family in that comparison — this lab is where that difference in configuration weight becomes concrete rather than theoretical: a real certificate authority, real server and client certificates, and considerably more setup before the first packet crosses the tunnel.

Goal and end result

By the end of this lab, you'll have built a small certificate authority, issued a server certificate and a client certificate from it, and brought up an OpenVPN tunnel authenticated by those certificates — the same TLS handshake this course already covered in depth, here authenticating a VPN tunnel instead of a web request.

Topology

+------------------+                     +------------------+
|   vpn-server       |                    |   vpn-client       |
|   tun0: 10.8.0.1    |<---- OpenVPN ----->|   tun0: 10.8.0.6    |
|   real: 192.168.100.10                  |   real: 192.168.100.11
+------------------+                     +------------------+

Requirements

  • Two Linux hosts (VMs or containers) that can already reach each other, with NET_ADMIN capability for the tun interface OpenVPN creates.
  • The openvpn and easy-rsa packages, installed on the server side; openvpn alone is enough on the client.

Safety note

Everything generated in this lab — the certificate authority, the server and client certificates — exists only for this exercise and should never be reused anywhere real; a CA private key that authorizes both this test setup and anything meant to be trusted in production is a single point of failure that undermines the trust model entirely. On a real deployment, the CA's private key is the single most sensitive piece of material in the whole setup, since whoever holds it can issue a certificate that any client or server in this VPN will trust.

Step 1: build a small certificate authority

apt-get install -y openvpn easy-rsa
make-cadir /etc/openvpn/easy-rsa
cd /etc/openvpn/easy-rsa
./easyrsa init-pki
./easyrsa build-ca nopass
Notice
------
CA creation complete. Your new CA certificate is at:
* /etc/openvpn/easy-rsa/pki/ca.crt

Real build-ca output includes a long run of . and + characters while OpenSSL generates entropy for the key — harmless noise, abridged here for readability. nopass skips protecting the CA's own private key with a passphrase, which is acceptable for this throwaway lab CA and a genuinely bad idea for a CA meant to last — a stolen, unprotected CA key lets an attacker issue certificates this VPN will trust indefinitely, with no way to detect the forgery from the certificate alone.

Step 2: issue a server certificate and a client certificate

./easyrsa build-server-full server nopass
./easyrsa build-client-full client1 nopass
Notice
------
Certificate created at:
* /etc/openvpn/easy-rsa/pki/issued/server.crt

Notice
------
Certificate created at:
* /etc/openvpn/easy-rsa/pki/issued/client1.crt

Both certificates are signed by the same CA generated in step 1 — exactly the chain of trust this course already covered for HTTPS, applied here to authenticating a VPN's two ends rather than a website. Generate Diffie-Hellman parameters, needed for the key exchange, and a static key used for an additional layer of authentication on every packet:

./easyrsa gen-dh
openvpn --genkey secret ta.key

Step 3: the server configuration

# /etc/openvpn/server.conf
port 1194
proto udp
dev tun

ca /etc/openvpn/easy-rsa/pki/ca.crt
cert /etc/openvpn/easy-rsa/pki/issued/server.crt
key /etc/openvpn/easy-rsa/pki/private/server.key
dh /etc/openvpn/easy-rsa/pki/dh.pem
tls-auth /etc/openvpn/easy-rsa/ta.key 0

server 10.8.0.0 255.255.255.0
keepalive 10 120
persist-tun
verb 3

Compare this directly against WireGuard's entire client-side config from the previous lab — four lines there covered a private key, an address, a peer's public key, and an endpoint. Here, a working server config already references five separate files (a CA certificate, a server certificate, a server key, Diffie-Hellman parameters, and a static auth key) before a single client has connected. tls-auth ... 0 adds an extra HMAC signature to every packet using the static key from step 2, so a packet that doesn't carry a valid signature from that shared key gets dropped before OpenVPN even attempts a TLS handshake against it — a defense specifically against unauthenticated scanning and denial-of-service traffic aimed at the OpenVPN port.

Step 4: the client configuration

# client1.ovpn
client
dev tun
proto udp
remote 192.168.100.10 1194
resolv-retry infinite
nobind
remote-cert-tls server

ca ca.crt
cert client1.crt
key client1.key
tls-auth ta.key 1

Copy ca.crt, client1.crt, client1.key, and ta.key onto the client alongside this file — every one of those files has to physically reach the client machine, unlike WireGuard's setup, where only a public key (not a private one, and not a separate certificate file) needed to move between peers. remote-cert-tls server matters specifically: without it, a compromised client certificate could be replayed to impersonate a server, since nothing would otherwise check that the certificate on the other end was actually issued with a server's role in mind. Note the tls-auth ... 1 here, the mirror of the server's ... 0 — the direction argument has to be opposite on each side for the HMAC to line up correctly.

Step 5: bring up the server, then connect the client

openvpn --config /etc/openvpn/server.conf --daemon
ip addr show tun0
3: tun0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UNKNOWN group default qlen 500
    inet 10.8.0.1 peer 10.8.0.2/32 scope global tun0

That peer 10.8.0.2/32 is worth noticing — unlike WireGuard's wg0, which showed a normal /24 subnet address in the previous lab, OpenVPN's default tun mode sets up each side of the tunnel as a point-to-point link with an explicit peer address, a detail this lab's server 10.8.0.0 255.255.255.0 directive handles automatically.

On the client:

openvpn --config client1.ovpn
Initialization Sequence Completed

That line is OpenVPN's own confirmation that the TLS handshake completed, the tunnel interface came up, and the client received its address from the server's 10.8.0.0/24 pool — the exact tunnel-interface pattern VPN basics already described generically, this time named tun0 (or tun1, tun2, and so on if another tunnel interface already exists on that machine) and brought up by OpenVPN specifically rather than wg0 by WireGuard. Check ip addr show on the client itself to see the exact address it was actually assigned — the server's own address stays fixed at 10.8.0.1, but the client's address from that pool is whatever the server hands out and isn't guaranteed to be the same on every run.

Step 6: confirm traffic actually flows

ping -c 4 10.8.0.1

Run from the client, addressing the server by its tunnel address:

PING 10.8.0.1 (10.8.0.1) 56(84) bytes of data.
64 bytes from 10.8.0.1: icmp_seq=1 ttl=64 time=0.612 ms
64 bytes from 10.8.0.1: icmp_seq=2 ttl=64 time=0.487 ms
64 bytes from 10.8.0.1: icmp_seq=3 ttl=64 time=0.501 ms
64 bytes from 10.8.0.1: icmp_seq=4 ttl=64 time=0.495 ms

--- 10.8.0.1 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3054ms

Troubleshooting

  • The client log shows repeated TLS Error: TLS handshake failed. Confirm both sides actually have the matching CA certificate and that the client's certificate hasn't expired or been revoked — a TLS failure here is the identical failure mode the TLS handshake article's own scenario walked through for HTTPS, just happening between a VPN client and server instead of a browser and a website.
  • The client connects, but tls-auth rejects every packet. Check the direction argument — it must be 0 on the server and 1 on the client (or omitted on both, in older configurations), never the same value on both sides.
  • Initialization Sequence Completed appears, but ping to the tunnel address times out. Confirm IP forwarding is enabled on the server (sysctl net.ipv4.ip_forward) if routing between the tunnel and a wider network is expected — for a direct server-to-client ping like this one, forwarding isn't required, but it's the first thing to check the moment a third host needs to be reached through the tunnel.

Cleanup

# on the client
killall openvpn
# on the server
killall openvpn
rm -rf /etc/openvpn/easy-rsa/pki
ip addr show tun0

An error stating the device doesn't exist confirms the tunnel interface is gone.

Evaluation criterion

You've completed this lab correctly if the client log shows Initialization Sequence Completed, a ping to the server's tunnel address succeeds, and you can explain — using the server and client config files from this lab — what happens if the tls-auth direction argument is set to the same value on both sides instead of opposite ones.

Both VPN labs so far connected exactly two machines directly. A more common real deployment puts something else in front of the actual application server entirely — accepting connections on its behalf and forwarding them onward, which is exactly the reverse proxy concept this course already introduced. The next lab builds one.

Sources