Andrew Mercer

OpenVPN Security Hardening

Part of the OpenVPN Comprehensive Guide.
The base configs in
openvpn-server-configuration.md and
openvpn-client-configuration.md already
include everything below — this doc is the "why," plus the pieces (CRL
testing, key rotation) that don't belong inline in a config-file walkthrough.

Run the daemon as an unprivileged user

# server.conf
user  openvpn
group openvpn
persist-key
persist-tun

OpenVPN needs root to create the tun device and set up initial routes, but
nothing after that — persist-key/persist-tun let it drop privileges
post-init without losing access to the files/interface it already opened.
Create the user/group if your package didn't:

sudo useradd --system --no-create-home --shell /usr/sbin/nologin openvpn

Control channel: use tls-crypt, not tls-auth

Both add an HMAC signature to control-channel packets so the server can
silently drop anything not signed with the shared key — this closes off
port-scanning, floods, and most attacks against the TLS stack itself before
a real handshake ever starts (the class of bug this defends against is
sometimes called "TLS stack hardening" in OpenVPN's own docs).

The difference: tls-auth only authenticates those packets; tls-crypt
additionally encrypts them, which hides certificate metadata (subject CN,
fingerprints) from passive observers during the handshake. There's no
compatibility reason to prefer tls-auth on a same-version client/server
pair, so use tls-crypt.

openvpn --genkey secret tls-crypt.key
# server.conf and client config — same key, same directive
tls-crypt /path/to/tls-crypt.key

(tls-auth needed a direction parameter — 0 on the server, 1 on the
client — because it's a plain HMAC in each direction. tls-crypt doesn't
need that parameter; it's symmetric.)

If you want per-client revocability of this shared key too (so a compromised
client can't be used to spoof control-channel traffic even before its
regular cert is revoked), OpenVPN 2.5+ supports tls-crypt-v2, which
issues a unique wrapped key per client instead of one shared key for
everyone. Worth it for larger deployments; probably overkill for a handful
of personal devices.

Cipher suite

data-ciphers AES-256-GCM
tls-version-min 1.2

AES-256-GCM is an AEAD cipher — encryption and authentication in one pass,
replacing the older cipher AES-256-CBC + auth SHA256 pairing (CBC mode
encryption plus a separate HMAC bolted on afterward). AEAD is both faster
and removes an entire bug class (padding-oracle attacks against
CBC-then-MAC constructions) by construction. data-ciphers (plural) also
lets you list a negotiable set, e.g. data-ciphers AES-256-GCM:AES-128-GCM,
which is useful during a migration but not necessary for a single
consistent deployment.

tls-version-min 1.2 is a floor, not a pin — leave tls-version-max unset
so a client/server pair that both support TLS 1.3 negotiate up to it
automatically.

Compression: leave it off

The old notes used comp-lzo. Don't re-add it (or its successor,
compress) unless you have a specific, understood reason to. Compressing
before encrypting on a channel where an attacker can influence part of the
plaintext (e.g. anything carrying HTTP traffic) leaks information through
the compressed size — the general class is a compression oracle attack, and
the OpenVPN-specific instance is called VORACLE. Link-layer compression
almost never pays for itself over any halfway-modern connection anyway.

Certificate Revocation List (CRL)

# server.conf
crl-verify /etc/openvpn/easy-rsa/easyrsa3/pki/crl.pem

Regenerate and redeploy the CRL any time you revoke a cert:

cd /etc/openvpn/easy-rsa/easyrsa3
./easyrsa revoke <client-cn>
./easyrsa gen-crl
cp pki/crl.pem /etc/openvpn/easy-rsa/easyrsa3/pki/crl.pem

The CRL file is refreshed by OpenVPN automatically on modification — no
service restart needed, as long as crl-verify points at the file you just
overwrote.

What a revoked connection attempt looks like server-side:

TLS: Initial packet from [AF_INET]64.7.137.182:55321, sid=595e13a9 e3de4147
CRL CHECK OK: C=CA, ST=Ontario, L=Kitchener, O=Andrew Mercer, CN=ca.example.net, emailAddress=andrew@example.net
VERIFY OK: depth=1, C=CA, ST=Ontario, L=Kitchener, O=Andrew Mercer, CN=ca.example.net, emailAddress=andrew@example.net
CRL CHECK FAILED: C=CA, ST=Ontario, L=Kitchener, O=Andrew Mercer, CN=amercer-pc2, emailAddress=andrew@example.net is REVOKED
TLS_ERROR: BIO read tls_read_plaintext error: error:140890B2:SSL routines:SSL3_GET_CLIENT_CERTIFICATE:no certificate returned
TLS Error: TLS object -> incoming plaintext read error
TLS Error: TLS handshake failed
SIGUSR1[soft,tls-error] received, client-instance restarting

A valid, non-revoked client simply completes the handshake and shows up in
status openvpn-status.log — there's no separate "CRL passed" log line to
look for beyond the absence of the failure above.

Server identity verification (client-side)

# client config
remote-cert-tls server
verify-x509-name "vpn.example.net" name

verify-x509-name pins the client to a specific server CN, so a
differently-issued-but-CA-signed cert (say, from a compromised
intermediate step in your own PKI process) can't silently be swapped in.
remote-cert-tls server additionally checks that the presented cert is
flagged as a server cert specifically (EasyRSA's sign-req server sets
this), so a valid client cert from the same CA can't be used to
impersonate the server.

Key and cert lifecycle

  • Track expiry. EasyRSA certs default to a finite validity window;
    check remaining lifetime with:
    bash openssl x509 -enddate -noout -in pki/issued/<cn>.crt
    Put a reminder somewhere you'll actually see it before the server cert
    expires — an expired server cert takes down every client at once.
  • Rotate the tls-crypt key periodically, independent of individual
    client cert expiry, since it's shared across all clients and a leak of it
    alone doesn't show up as a "this client is misbehaving" signal the way a
    cert compromise might.
  • Keep the CA key offline where practical (see
    openvpn-server-configuration.md) —
    everything above is moot if the CA private key itself leaks, since that
    lets an attacker mint trusted certs directly.