DB-유

OpenVPN Server on Ubuntu

· linux, openvpn, vpn, ubuntu

I'm uneasy whenever database ports, admin panels and SSH are open to the whole Internet behind nothing but passwords. One authenticated, encrypted door in front of all of it is much easier to sleep with, so I run OpenVPN. It's mature and runs everywhere, and with EasyRSA 3 the certificate handling, once its most annoying part, is a handful of commands.

This is an OpenVPN server on Ubuntu 18.04 with its own CA, tls-auth, and a script that makes one .ovpn file per client. It's a gateway to an internal subnet, not a full-tunnel proxy, which is the usual office case. You need a public IP with UDP 1194 reachable (or another port), and to know which internal network to expose, say 192.168.10.0/24, and whether this server can route to it.

Install OpenVPN and EasyRSA

sudo apt-get update
sudo apt-get install -y openvpn easy-rsa
openvpn --version | head -1

Ubuntu 18.04 ships OpenVPN 2.4.4 and EasyRSA 3.0.4. Set up a working directory for the CA:

make-cadir ~/openvpn-ca
cd ~/openvpn-ca

Edit vars and set the organisational defaults so you don't get prompted every time:

set_var EASYRSA_REQ_COUNTRY    "KR"
set_var EASYRSA_REQ_PROVINCE   "Seoul"
set_var EASYRSA_REQ_CITY       "Seoul"
set_var EASYRSA_REQ_ORG        "Example"
set_var EASYRSA_REQ_EMAIL      "admin@example.com"
set_var EASYRSA_REQ_OU         "IT"
set_var EASYRSA_KEY_SIZE       2048
set_var EASYRSA_CA_EXPIRE      3650
set_var EASYRSA_CERT_EXPIRE    1080

CA and server certificate

./easyrsa init-pki
./easyrsa build-ca nopass
./easyrsa gen-req server nopass
./easyrsa sign-req server server
./easyrsa gen-dh
openvpn --genkey --secret ta.key

Ideally the CA key doesn't live on the VPN server: build it on an offline machine and copy over only the signed certificates. For a small setup I at least give the CA key a passphrase by dropping nopass from build-ca.

Copy the server files into place:

sudo cp pki/ca.crt pki/issued/server.crt pki/private/server.key pki/dh.pem ta.key /etc/openvpn/server/

server.conf

Create /etc/openvpn/server/server.conf:

port 1194
proto udp
dev tun

ca ca.crt
cert server.crt
key server.key
dh dh.pem
tls-auth ta.key 0

server 10.8.0.0 255.255.255.0
topology subnet
ifconfig-pool-persist ipp.txt

push "route 192.168.10.0 255.255.255.0"
push "dhcp-option DNS 192.168.10.5"

keepalive 10 120
cipher AES-256-GCM
auth SHA256
tls-version-min 1.2
user nobody
group nogroup
persist-key
persist-tun

status /var/log/openvpn/status.log
log-append /var/log/openvpn/openvpn.log
verb 3
explicit-exit-notify 1

The push "route" line is what makes the internal subnet reachable through the tunnel. To send all client traffic through the VPN instead, replace it with push "redirect-gateway def1 bypass-dhcp".

Forwarding and NAT

The server has to forward packets between tun0 and its LAN interface and, unless the internal routers know about 10.8.0.0/24, masquerade them. Forgetting this is the classic "connected but can't reach anything".

echo 'net.ipv4.ip_forward = 1' | sudo tee /etc/sysctl.d/99-openvpn.conf
sudo sysctl -p /etc/sysctl.d/99-openvpn.conf

With ufw, set the forward policy in /etc/default/ufw:

DEFAULT_FORWARD_POLICY="ACCEPT"

Then add NAT rules to the top of /etc/ufw/before.rules, before the *filter line (replace ens3 with your LAN interface from ip route | grep default):

*nat
:POSTROUTING ACCEPT [0:0]
-A POSTROUTING -s 10.8.0.0/24 -o ens3 -j MASQUERADE
COMMIT
sudo ufw allow 1194/udp
sudo ufw disable && sudo ufw enable

Start it

sudo mkdir -p /var/log/openvpn
sudo systemctl enable --now openvpn-server@server
sudo systemctl status openvpn-server@server
ip addr show tun0

The openvpn-server@ template reads /etc/openvpn/server/<name>.conf, which is why the files went there and not straight into /etc/openvpn.

Clients

Sign a client certificate:

cd ~/openvpn-ca
./easyrsa gen-req alice nopass
./easyrsa sign-req client alice

I don't hand out four files per user; support requests aren't worth it. I embed everything in one profile. Save this as ~/openvpn-ca/make-client.sh:

#!/bin/bash
CLIENT=$1
OUT=~/clients/${CLIENT}.ovpn
mkdir -p ~/clients
cat > "$OUT" <<EOF
client
dev tun
proto udp
remote vpn.example.com 1194
resolv-retry infinite
nobind
persist-key
persist-tun
remote-cert-tls server
cipher AES-256-GCM
auth SHA256
key-direction 1
verb 3
<ca>
$(cat pki/ca.crt)
</ca>
<cert>
$(openssl x509 -in pki/issued/${CLIENT}.crt)
</cert>
<key>
$(cat pki/private/${CLIENT}.key)
</key>
<tls-auth>
$(cat ta.key)
</tls-auth>
EOF
echo "Wrote $OUT"
chmod +x make-client.sh && ./make-client.sh alice

Send alice.ovpn to the user over a secure channel. To revoke a client later: ./easyrsa revoke alice && ./easyrsa gen-crl, copy pki/crl.pem to /etc/openvpn/server/, add crl-verify crl.pem to the server config and restart.

Connect and look around

Import the profile on a client (OpenVPN Connect, Tunnelblick, or sudo openvpn --config alice.ovpn on Linux), connect, and then:

ip route | grep 192.168.10        # route pushed
ping 192.168.10.5                 # internal host reachable

On the server, cat /var/log/openvpn/status.log lists connected clients with their virtual and real addresses.

When it fails

  • Client log says TLS Error: TLS key negotiation failed to occur within 60 seconds: UDP 1194 isn't reaching the server. Check the cloud provider's security group as well as ufw; sudo tcpdump -ni ens3 udp port 1194 on the server shows whether packets arrive.
  • AUTH_FAILED or an immediate TLS Error: TLS handshake failed: key-direction mismatch (server 0, client 1) or different ta.key files.
  • Connected but can't reach the internal subnet: forwarding is off (sysctl net.ipv4.ip_forward) or the MASQUERADE rule is missing. sudo iptables -t nat -L POSTROUTING -n -v should show it with a non-zero packet count.
  • Options error: --dh fails with 'dh.pem': No such file or directory: paths are relative to /etc/openvpn/server for the template unit. Put the files there, not in /etc/openvpn.
  • Connects, but internal DNS names fail on Linux: the pushed dhcp-option DNS is ignored without a script. Install openvpn-systemd-resolved or use update-resolv-conf with script-security 2.

After that it needs almost no attention. The last step is closing every other inbound port on the internal servers so the VPN really is the only way in.