OpenVPN server in routing mode while not on default gateway

My previous article, related to OpenVPN, concerned the installation of the OpenVPN server in bridge mode. This solution seemed to me the only possible solution when running the OpenVPN server not on the gateway. The disadvantage of this solution is that you need to put the network interface into promiscuous mode, which is usually not allowed in any virtual environment.

This article will describe another solution in which OpenVPN will work in routing mode, but outgoing internal traffic will be SNAT modified, ie, traffic will loks like come from the OpenVPN server itself. This solution is suitable even for AWS EC2 instances with one network interface.

This POC we will do on Fedora 26, just because it is already installed on my test VM. This choice affected additional commands related to selinux.

I generally disable the firewall and will not refer to it during the article.

# systemctl stop firewalld
# systemctl disable firewalld

Configure OpenVPN Server

Obviously, install OpenVPN:

# dnf install -y openvpn

At the very beginning, it is very important to determine the authentication of users. In the previous example, there were no real users in the OS, only certificates used as authentication. In this example, I come with the opposite approach. Certificates will be almost not used, but real local users will be created with strong passwords and forbidden login. This will simplify the management of certificates, but will require user management.

As an additional limitation, only users belonging to the ovpn group will be allowed to use VPN.

# groupadd ovpn
# useradd -g ovpn -s /sbin/nologin user1
# passwd user1
# useradd -s /sbin/nologin user2
# passwd user2

As already mentioned, a minimal, self-signed certificate will be used instead of a real CA control. This command will generate the certificate /etc/ssl/ovpn-cert.pem, which will also be used as a CA certificate, and an unencrypted private key /etc/ssl/ovpn-nopasskey.pem. Location of files is important to the Fedora/selinux environment. Of course, you can adapt the certificate subject to your own more realistic parameters.

# openssl req -x509 -days 3650 -subj "/O=INFO/OU=VOLEG/CN=OVPN" -newkey rsa:2048 \
  -nodes -keyout /etc/ssl/ovpn-nopasskey.pem -out /etc/ssl/ovpn-cert.pem
# openssl dhparam -out /etc/ssl/dh2048.pem 2048

You can copy the sample server configuration file and then edit it. Reading the manual is also helpful.

# cp /usr/share/doc/openvpn/sample/sample-config-files/server.conf /etc/openvpn/server/
# vi /etc/openvpn/server/server.conf
# man openvpn

Here is the configuration file of my server with comments in it:

# cat /etc/openvpn/server/server.conf 
# Use another than standart port:
port 1234
proto udp

# See https://whois.arin.net/rest/net/NET-100-64-0-0-1.html about this subnet.
server 100.99.1.0 255.255.255.0

# I've added only my destination server here,
# you should add all destination subnets, multiple "push" allowed.
push "route 192.168.122.100 255.255.255.255"

# Crypto staff. We had generated these files already.
tls-server
ca /etc/ssl/ovpn-cert.pem
cert /etc/ssl/ovpn-cert.pem
key /etc/ssl/ovpn-nopasskey.pem
dh /etc/ssl/dh2048.pem

# Authentication tuning. Read manual.
duplicate-cn
verify-client-cert optional
plugin /usr/lib64/openvpn/plugins/openvpn-plugin-auth-pam.so openvpn

# Pretty common staff, read manual.
dev tun
keepalive 10 120
cipher AES-256-CBC
compress lz4
verb 3
explicit-exit-notify 1
max-clients 10
user nobody
group nobody
persist-key
persist-tun
# File names and location are hard-coded in selinux rules.
ifconfig-pool-persist /etc/openvpn/ipp.txt
status /var/log/openvpn-status.log

As you can see from the configuration file, the PAM plugin is used for authentication. Let's create its configuration file /etc/pam.d/openvpn. As you can see, only users from the "ovpn" group are allowed to authenticate by this service.

#%PAM-1.0
auth    required        pam_env.so
auth    required        pam_succeed_if.so quiet user ingroup ovpn
auth    sufficient      pam_unix.so nullok try_first_pass
auth    required        pam_deny.so

account required        pam_unix.so
account sufficient      pam_localuser.so
NOTE: Selinux related actions:
# touch /etc/openvpn/ipp.txt /var/log/openvpn-status.log
# restorecon -r /etc/ssl /etc/openvpn /var/log/openvpn-status.log /etc/pam.d/openvpn
# ### Probably you will need install policycoreutils-python-utils to run next command:
# semanage port -a -t openvpn_port_t -p udp 1234 # <- Replace this to port number in your config file.

Enable and start the openvpn server. If your configuration files named as mine: /etc/openvpn/server/server.conf, then do:

# systemctl enable openvpn-server@server.service # <- Replace this to name of your config.file.
# systemctl start openvpn-server@server.service

For testing, copy the CA certificate to the client and verify the connection using a similar command. Later create a working ".ovpn" file.

# openvpn --remote 192.168.122.114 --port 1234 --dev tun --compress lz4 --client --ca /tmp/ovpn-cert.pem --auth-user-pass

Check that you can connect using user1 but not user2.

Configure iptables

First of all, make sure that the forward is enabled on the server. Add the rule to /etc/sysctl.conf and run sysctl -p

# cat /etc/sysctl.conf
net.ipv4.ip_forward = 1

You can use firewalld or iptables-services to manage firewall rules. I will not review all other FW rules (for example, you must allow an incoming UDP port for your OpenVPN and connection-related traffic).

Add the following rule to masquerade all traffic from VPN clients. My fedora instance has a network interface named ens3, so it is shown here, for you it can be eth0. The subnet that appears here as a source subnet must match the same one used in the "server" directive in the OpenVPN server configuration file.

# iptables -t nat -I POSTROUTING -o ens3 -s 100.99.1.0/24 -j MASQUERADE

Test connection and ping remote IP addresses.

Adding MFA (actually 2FA) using Google Autheticator PAM

Install it and run it, answer questions. The result will be at ~/.google_authenticator.

# dnf install -y google-authenticator
# google-authenticator

I will use a common authentication file for all users. It's less secure, but easier to manage. Security is always a trade-off between the complexity of management and the level of security. Then I move the resulting file to a shared location:

# mv ~/.google_authenticator /etc/openvpn/gauth.conf
# chmod 600 /etc/openvpn/gauth.conf
# head -1 /etc/openvpn/gauth.conf
ZBHW3JHX644TEPZ3D3QODXV3OU

The result of the last command is the code that you must enter into the Google Authenticator application on the remote device. Alternatively, you can use the CLI "oathtool" for Linux.

NOTE: Selinux related actions:
# semanage fcontext -a -t openvpn_etc_rw_t /etc/openvpn/gauth.conf
# restorecon /etc/openvpn/gauth.conf

Fix PAM /etc/pam.d/openvpn file to include Google authenticator module:

#%PAM-1.0
auth    required        pam_env.so
auth    required        pam_succeed_if.so quiet user ingroup ovpn
auth    requisite       pam_google_authenticator.so secret=/etc/openvpn/gauth.conf user=root forward_pass
auth    sufficient      pam_unix.so nullok use_first_pass
auth    required        pam_deny.so

account required        pam_unix.so
account sufficient      pam_localuser.so

Google authenticator wants PAM file have more restrictive permissions, then do:

# chmod 600 /etc/openvpn/gauth.conf

Try to connect to openvpn again. You must put your password, combined with the gauth code in the password field:

 ..
Enter Auth Username: user1
Enter Auth Password: ********XXXXXX
 ..

A code itself you can get by command:

$ oathtool --totp -b ZBHW3JHX644TEPZ3D3QODXV3OU # <- The long code is from "head -1 /etc/openvpn/gauth.conf

A code will be replaced every 30 second, hurry.

Update: The default openvpn configuration will attempt to renegotiate the session key every hour and force re-authentication. Because of the MFA, the password will be incorrect, then the session will hang. You will be forced to disconnect and reconnect manually. If you want your sessions be longer than one hour, then add reneg-sec 36000 directive to both client and server configuration.


Updated on Thu May 23 15:31:37 IDT 2019 More documentations here