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-cryptkey 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.