How to setup Site to (2) Site VPN with OpenVPN

Oren Oichman
13 min readOct 15, 2023

More then One

In Today’s world the need for VPN is sometimes as important as having internet connection by it self. I have already written a story about how to setup a simple home VPN server with all the available free tools (if this is what you looking for you can find it here) so in this tutorial we will show how we can take a few advanced configuration andenable Site to Site communication.

Site 2 Site

Site to Site (Site 2 Site or S2S) communication means that we are connecting two (2) Local Area Network (LAN) and enabling route between the two (2) LAN’s sites over another private network (E.G VPN).
The following simple diagram describes how the VPN communication works.

To make things Simple the Diagram above contain 2 Servers (Server and Client) with IP address in their respective LAN. Those 2 Server create a VPN tunnel between the sites (with OpenVPN) and allow communication between server-a to Server-b and vise versa.

Routing Table

To make things more simple at the end of the setup the following routing table will be configured on the hosts:

We will configure all the relevant routes on the hosts once we will setup the VPN.

VPN Alternatives

There are a few VPN alternatives in order to create the VPN tunnel such as GRE , PPTP , L2TP , IPSEC , Wireguard , Stunnel and SSH to name a few But I decided to go with OpenVPN for this tutorial because there are a lot of example around this topic but in order to create a stable tunnel OpenVPN does not require any modules present on the system and is relative easy to configure.

Server Configuration

First we will start with the Server Packages and configuration. it is important to note that the server needs to listen to port (1194 UDP in our example) so it will require to configure NAPT (Network Address Port translation) on the Route which is on Site 1 (with the 192.168.100.0/24 subnet).

Installing the Server is very easy to do , it’s a one single dnf command:

# dnf install -y openvpn openssl

Once the packages are installed let’s start by creating the TLS certificates. Openvpn is based on TLS certificates so we need to run the same steps as for a regular TLS Server.

For the first step we will create the necessary directories we need to work with :

# cd /etc/openvpn && mkdir {certs,ccd,Keys,CA}

And Make sure all the directories are accessible by the openvpn user :

# chown root:openvpn /etc/openvpn/*

Now in order to create the certificates we will use our openvpn. we will start by creating the CA server with certificate and key

# openssl genrsa -out /etc/openvpn/CA/ca.key 4096

And now Let’s run the command :

# openssl req -new -x509 -key CA/ca.key -days 730 -out CA/ca.crt  \
-subj "/CN=ca.example.com/O=Example/C=US/ST=Center/L=NY" \
-addext "basicConstraints=CA:TRUE" -addext "subjectKeyIdentifier=hash" \
-addext "authorityKeyIdentifier=keyid,issuer"

NOTE
if you are already using your own CA internally for other services that you can skip the CA creating part.
Now we have 2 new files under the CA directory: “ca.crt” and “ca.key”
We will use the openssl command to explore the certificate and make sure it is marked as “CA:TRUE” with the following command :

# openssl x509 -in CA/ca.crt -text -noout | grep CA
CA:TRUE

Now that we have our CA we can move on and start creating our certificates starting with the Server certificate.

Certificate file

For this part we will create a certificate request with all the relevant V3 extensions to make sure the Alter DNS name correlates with the public name which is being exposed to the internet (It can be dynamic DNS or even IP address.

As a good practice I like to create 2 environment variables that will help use the certificate signing. the first is the domain name and the second is the name of the server (AKA short name).
To define the 2 environment variables :

# export DOMAIN=$(hostname -d)
# export SHORT_NAME=$(hostname -s)

The main reason I am doing this is to make your job easier so you can set up the variable to suite your request and then just copy/paste the rest of the commands.

Let’s start by generating the Key :

# openssl genrsa -out Keys/${SHORT_NAME}.key 4096

For the next part we need to create a certificate request for our Server and then use the CA certificate (and key) and sign it.
To generate the request :

# openssl req -new -key Keys/${SHORT_NAME}.key -out certs/${SHORT_NAME}.csr \
-subj "/CN=${SHORT_NAME}/O=${DOMAIN}/C=US/ST=Center/L=NY" \
-addext "subjectAltName = DNS:${SHORT_NAME},DNS:${SHORT_NAME}.${DOMAIN}" \
-addext "keyUsage=digitalSignature" -addext "basicConstraints=CA:FALSE" \
-addext "nsCertType=server" -addext "extendedKeyUsage=serverAuth"

Now that we have our Certificate request in place we can go ahead and sign the certificate with our CA :

# openssl x509 -req -in certs/${SHORT_NAME}.csr -CA CA/ca.crt -CAkey CA/ca.key \
-CAcreateserial -out certs/${SHORT_NAME}.crt -days 730 \
-copy_extensions copy

We can now verify that our server certificate was signed by the CA and not self signed with the following command :

# openssl verify -CAfile CA/ca.crt certs/${SHORT_NAME}.crt

View our newly generated certificate and make sure it has the “TLS web server authentication” certificate.

#openssl x509 -in certs/${SHORT_NAME}.crt -text -noout | grep TLS
TLS Web Server Authentication

Build Diffie-Hellman Key

This action will take a lot of time, depending on the key length that we chose and the available entropy on the server.

Generate the Diffie-Hellman key using command below.

# openssl dhparam -out CA/dh.pem 2048

Generate the CRL Key

The CRL (Certificate Revoking List) key will be used for revoking the client key. If you have multiple client certificates on your vpn server, and you want to revoke some key, you just need to revoke using the easy-rsa command.

# openssl ca -gencrl -keyfile CA/ca.key -cert CA/ca.crt -out CA/crl.pem

NOTE

if you are using the same configuration that is built-in with the Server then run the following commands before generating the CRL:

# mkdir /etc/pki/CA
# touch /etc/pki/CA/index.txt
# echo "232434" > /etc/pki/CA/crlnumber
# openssl ca -gencrl -keyfile CA/ca.key -cert CA/ca.crt -out CA/crl.pem

now for the configuration section we need to create a “server.conf” file under the /etc/openvpn/server/ directory :

# cat > /etc/openvpn/server/server.conf << EOF
# OpenVPN Port, Protocol and the Tun
port 1194
proto udp
dev tun

# OpenVPN Server Certificate - CA, server key and certificate
ca /etc/openvpn/CA/ca.crt
cert /etc/openvpn/certs/${SHORT_NAME}.crt
key /etc/openvpn/Keys/${SHORT_NAME}.key

#DH and CRL key
dh /etc/openvpn/CA/dh.pem
#crl-verify /etc/openvpn/CA/crl.pem

# Network Configuration - Internal network
# Redirect all Connection through OpenVPN Server
server 172.16.16.0 255.255.255.0
#push "redirect-gateway def1"

# Using the DNS from https://dns.watch
push "dhcp-option DNS 192.168.100.2"
push "dhcp-option DNS 192.168.100.3"
push "route 192.168.100.0 255.255.255.0"
#Enable multiple client to connect with same Certificate key
duplicate-cn

# TLS Security
cipher AES-256-CBC
tls-version-min 1.2
tls-cipher TLS-DHE-RSA-WITH-AES-256-GCM-SHA384:TLS-DHE-RSA-WITH-AES-256-CBC-SHA256:TLS-DHE-RSA-WITH-AES-128-GCM-SHA256:TLS-DHE-RSA-WITH-AES-128-CBC-SHA256
auth SHA512
auth-nocache

# Other Configuration
keepalive 20 60
persist-key
persist-tun
comp-lzo yes
daemon
user openvpn
group openvpn
client-config-dir /etc/openvpn/ccd

# OpenVPN Log
log-append /var/log/openvpn.log
verb 7
EOF

Save and exit.

Once all the configuration files are in place let’s make sure the openvpn user has access to everything it needs:

# chown -R root:openvpn /etc/openvpn/*

The configuration for OpenVPN has been created.

Firewall And NAT Configuration

In this step, we will enable Port-forwarding kernel module and configure routing ‘Firewalld’ for OpenVPN.

Enable the port-forwarding kernel module by running following commands:

# echo 'net.ipv4.ip_forward = 1' >> /etc/sysctl.d/98-openvpn.conf
# echo 'net.ipv4.conf.all.accept_redirects = 0' >> /etc/sysctl.d/98-openvpn.conf
# echo 'net.ipv4.conf.all.send_redirects = 0' >> /etc/sysctl.d/98-openvpn.conf
# sysctl -w net.ipv4.ip_forward=1
# sysctl -w net.ipv4.conf.all.accept_redirects=0
# sysctl -w net.ipv4.conf.all.send_redirects=0
# sysctl -p

Next, configure routing using the Firewalld for OpenVPN.

To see your zone you can run the following command :

# firewall-cmd --get-default-zone
# export FWZONE=$(firewall-cmd --get-default-zone)

This will give us the zone we need for our services configuration.

Add the ‘openvpn’ service to the firewalld list service and add the ‘tun0’ interface to the firewalld trusted zone.

# firewall-cmd --permanent --add-service=openvpn
# firewall-cmd --permanent --zone=${FWZONE} --add-interface=tun0

Enable ‘MASQUERADE’ and ‘FORWARD’ on the ‘trusted’ zone firewalld.

# firewall-cmd --permanent --zone=${FWZONE} --add-masquerade
# firewall-cmd --permanent --zone=${FWZONE} --add-forward

Enable NAT for OpenVPN internal IP address ‘172.16.16.0/24’ to the external IP address ‘SERVERIP’.

# SERVERIP=$(ip route get 1.1.1.1 | awk 'NR==1 {print $(NF-2)}')
# echo $SERVERIP

# firewall-cmd --permanent --direct --passthrough ipv4 -t nat \
-A POSTROUTING -s 172.16.16.0/24 -o $SERVERIP -j MASQUERADE

If you need to make changes the configuration are been kept at the file “direct.xml”

# cat /etc/firewalld/direct.xml << EOF
<?xml version=”1.0" encoding=”utf-8"?>
<direct>
<passthrough ipv=”ipv4">-t nat -A POSTROUTING -s 172.16.16.0/24 -o $SERVERIP -j MASQUERADE
</passthrough>
</direct>
EOF

And reload firewall.

# firewall-cmd --reload

Client Configuration Directory

If you notice in the configuration file there is a line that set the client configuration directory that will hold all the client according to the CN within the client certificate (we will get to that in later section of this tutorial). When the client performs authentication with the Server the CN is it’s key identifier.
The line in the configuration file is : “client-config-dir /etc/openvpn/ccd” and we have created the directory earlier and now we can create a file named according to the certificate DN. In our example we will call the client “client” so for now we will create a file accordingly and add the IP and the routing of our Server LAN to it :

We will create the file :

# touch /etc/openvpn/ccd/client

Now let’s add the IP and the route to it :

# echo 'ifconfig-push 172.16.16.4 172.16.16.3' >> /etc/openvpn/ccd/client
# echo 'iroute 192.168.200.0 255.255.255.0' >> /etc/openvpn/ccd/client

The first 2 IP addresses in the VPN network are reserved for the Server so we will use the following par witch is our example 172.16.16.3 and 172.16.16.4. Note that the first IP address is for the gateway and the second if for the actual client.
By setting the “iroute” we are telling the VPN server to add the route of the remote network once the connection is establish.

Services

Start the OpenVPN Server :

# systemctl enable --now openvpn-server@server.service

Test the setting with the following command:

# systemctl status openvpn-server@server.service && \
netstat -plntu | grep 1194

The output should look like :

● openvpn-server@server.service - OpenVPN service for server
Loaded: loaded (/usr/lib/systemd/system/openvpn-server@.service; enabled; vendor preset: disabled)
Active: active (running) since Thu 2023-10-12 08:29:24 EDT; 33s ago
Docs: man:openvpn(8)
https://community.openvpn.net/openvpn/wiki/Openvpn24ManPage
https://community.openvpn.net/openvpn/wiki/HOWTO
Main PID: 1618 (openvpn)
Status: "Initialization Sequence Completed"
Tasks: 1 (limit: 10800)
Memory: 2.8M
CPU: 20ms
CGroup: /system.slice/system-openvpn\x2dserver.slice/openvpn-server@server.service
└─1618 /usr/sbin/openvpn --status
/run/openvpn-server/status-server.log --status-version 2
--suppress-timestamps --cipher AES-256-GCM
--data-ciphers AES-256-G>

Oct 12 08:29:24 serverc.lab.example.com systemd[1]: Starting OpenVPN service
for server...
Oct 12 08:29:24 serverc.lab.example.com openvpn[1618]: WARNING: Compression
for receiving enabled. Compression has been used in the past to break
encryption. Sent packets >
Oct 12 08:29:24 serverc.lab.example.com systemd[1]: Started OpenVPN service
for server.
udp 0 0 0.0.0.0:1194 0.0.0.0:* 1618/openvpn

Client Certificate

For our last part on the Server we will create a client certificate with the DN that matches the file we’ve created for the client and add a few relevant v3 extension so it will act as a client certificate. Sense the VPN Server is our CA in this example we will create the client certificate on it.

Same as before we need to redefine the SHORT_NAME environment variable and generate a private key for the client.

# export SHORT_NAME="client"
# export DOMAIN="example.com"

Let’s generate the key

# openssl genrsa -out Keys/${SHORT_NAME}.key 4096

Now that we have the key in place we need generate the client certificate which will be almost identical to the Server certificate with a few changes.
instead of “server” we will put “client” in the extensions part :

# openssl req -new -key Keys/${SHORT_NAME}.key -out certs/${SHORT_NAME}.csr \
-subj "/CN=${SHORT_NAME}/O=${DOMAIN}/OU=Users" \
-addext "subjectAltName = DNS:${SHORT_NAME},DNS:${SHORT_NAME}.${DOMAIN}" \
-addext "keyUsage=digitalSignature" -addext "basicConstraints=CA:FALSE" \
-addext "nsCertType=client" -addext "extendedKeyUsage=clientAuth"

NOTE!

We defined the CN (comma name ) as the short name without the Domain. ,the Organization is the DOMAIN and the Organization unit is “Users”.
Now , the same as before we will use our CA to sign the new client certificate :

# openssl x509 -req -in certs/${SHORT_NAME}.csr -CA CA/ca.crt -CAkey CA/ca.key \
-CAcreateserial -out certs/${SHORT_NAME}.crt -days 730 \
-copy_extensions copy

Now let’s see our new client certificate and we are going to see something a bit different then before :

# openssl x509 -in certs/client.crt -text -noout | grep TLS
TLS Web Client Authentication

As we can see the client certificate is a defined in the extension part as a “TLS Web Client Authentication” which is what we wanted.

Server-a Configuration

Routing Table

For Server-a in our design we need to add a route to let it know that if it needs to reach the 192.168.200.0/24 network it needs to go through the VPN server.

In order to add the route we will us the nmcli command.

First we need to set the connection name. we can see the active connection by running :

nmcli con show --active | grep -v tun
NAME UUID TYPE DEVICE
Wired connection 1 a7922770-a11a-1111-1111-b18851520900 ethernet eth0

As you can see from the example above the interface is “eth0” and the connection name is “Wired connection 1”.

To add a route to the connection run the following command :

# nmcli connection modify "Wired connection 1" +ipv4.routes \
"192.168.200.0/24 192.168.100.100"

Client Configuration

The Client configuration are consistent with a simple configuration file for OpenVPN and the TLS certificate , TLS Key and the CA certificate with in the configuration file.

create the “/etc/openvpn/client/client.conf” file

# touch /etc/openvpn/client/client.conf

With your favorite editor update the file under “/etc/openvpn/client/” named client.conf and add the following content :

client
remote 'server.example.com'
cipher AES-256-CBC
comp-lzo adaptive
dev tun
proto udp
port 1194
route '192.168.100.0' '255.255.255.0' '172.16.16.1'
nobind
auth-nocache
script-security 2
persist-key
persist-tun
user openvpn
group openvpn
ca [inline]
cert [inline]
key [inline]

In the configuration file we noted to the OpenVPN that it is the client , gate the server address by FQDN and once it is connected add the route to network on the Server site (which is 192.168.100.0/24).

Now that the file is in place we need to add the TLS certificate , key and CA to it.

We will start by copying all the files from the VPN Server to the VPN client. in case the network connectivity does not allow it we can copy past the content of the file into the client.conf file.

we will start with the CA configuration. we can Copy/Paste the content of the file and add with the following command :

# cat >> /etc/openvpn/client/client.conf << EOF
<ca>
-----BEGIN CERTIFICATE-----

-----END CERTIFICATE-----
</ca>
EOF

Now we will do the same for the certificate file :

# cat >> /etc/openvpn/client/client.conf << EOF
<cert>
-----BEGIN CERTIFICATE-----

-----END CERTIFICATE-----
</cert>
EOF

And the Key to close the file :

# cat >> /etc/openvpn/client/client.conf << EOF
<key>
-----BEGIN PRIVATE KEY-----

-----END PRIVATE KEY-----
</key>
EOF

Now that everything is in place let’s take care of the networking and firewall so the VPN client will act as a router.

Firewall And NAT Configuration

Just like in the Server, we will enable Port-forwarding kernel module and configure routing ‘Firewalld’ for OpenVPN.

Enable the port-forwarding kernel module by running following commands:

# echo 'net.ipv4.ip_forward = 1' >> /etc/sysctl.d/98-openvpn.conf
# echo 'net.ipv4.conf.all.accept_redirects = 0' >> /etc/sysctl.d/98-openvpn.conf
# echo 'net.ipv4.conf.all.send_redirects = 0' >> /etc/sysctl.d/98-openvpn.conf
# sysctl -w net.ipv4.ip_forward=1
# sysctl -w net.ipv4.conf.all.accept_redirects=0
# sysctl -w net.ipv4.conf.all.send_redirects=0
# sysctl -p

Next, configure routing using the Firewalld for OpenVPN.
To see your zone you can run the following command :

# firewall-cmd --get-default-zone
# export FWZONE=$(firewall-cmd --get-default-zone)

This will give us the zone we need for our services configuration.
Add the ‘tun0’ interface to the firewalld trusted zone.

# firewall-cmd --permanent --zone=${FWZONE} --add-interface=tun0

Enable ‘MASQUERADE’ and ‘FORWARD’ on the ‘trusted’ zone firewalld.

# firewall-cmd --permanent --zone=${FWZONE} --add-masquerade
# firewall-cmd --permanent --zone=${FWZONE} --add-forward

Enable NAT for OpenVPN internal IP address ‘172.16.16.0/24’ to the external IP address ‘SERVERIP’.

# SERVERIP=$(ip route get 1.1.1.1 | awk 'NR==1 {print $(NF-2)}')
# echo $SERVERIP
# firewall-cmd --permanent --direct --passthrough ipv4 -t nat \
-A POSTROUTING -s 172.16.16.0/24 -o $SERVERIP -j MASQUERADE

If you need to make changes the configuration are been kept at the file “direct.xml”

# cat /etc/firewalld/direct.xml << EOF
<?xml version=”1.0" encoding=”utf-8"?>
<direct>
<passthrough ipv=”ipv4">-t nat -A POSTROUTING -s 172.16.16.0/24 -o $SERVERIP -j MASQUERADE
</passthrough>
</direct>
EOF

And reload firewall.

# firewall-cmd --reload

Services

Start the OpenVPN Server :

# systemctl enable --now openvpn-client@client.service

Test the setting with the following command:

# systemctl status openvpn-client@client.service && \
netstat -plntu | grep 1194

Server-b Configuration

Routing Table

For Server-b in our design we need to add a route to let it know that if it needs to reach the 192.168.100.0/24 network it needs to go through the VPN server.
In order to add the route we will us the nmcli command.
First we need to set the connection name. we can see the active connection by running :

nmcli con show --active | grep -v tun
NAME UUID TYPE DEVICE
Wired connection 1 a7922770-a11a-1111-1111-b18851520900 ethernet eth0

As you can see from the example above the interface is “eth0” and the connection name is “Wired connection 1”.
To add a route to the connection run the following command :

# nmcli connection modify "Wired connection 1" +ipv4.routes \
"192.168.100.0/24 192.168.200.100"

Now Server-a and Server-b can communicate with each other directly Over the VPN connection.

DHCP Environment

In case site 1 or site 2 are using DHCP and we want to advertise the route from the DHCP server there is a simple why to do that.

In case you are using dhcpd then we need to the following to our DHCP configuration file.

Open the dhcpd.conf file with your favorite editor and add the following lines :

option rfc3442-classless-static-routes code 121 = array of integer 8;
option ms-classless-static-routes code 249 = array of integer 8;

Now with in the subnet section add the routes that you want to distribute for example if we want site 1 to route all the traffic to 192.168.200.0/24 through 192.168.100.100 then add the following lines :

subnet .... {
option rfc3442-classless-static-routes 24, 192,168,200, 192,168,100,100;
option ms-classless-static-routes 24, 192,168,200, 192,168,100,100;
}

If you need to advertise more route then just add them with a comma separator to those lines.For exampling adding the 172.16.16.0/24 as well will look as such :

subnet .... {
option rfc3442-classless-static-routes 24, 192,168,200, 192,168,100,100, 24, 172,16,16, 192,168,100,100;
option ms-classless-static-routes 24, 192,168,200, 192,168,100,100, 24, 172,16,16, 192,168,100,100;
}

That is it , restart the dhcpd service and your routes will be published.

# systemctl restart dhcpd

If you have any question feel free to respond/ leave a comment.
You can find me on linkedin at : https://www.linkedin.com/in/orenoichman
Or twitter at : https://twitter.com/ooichman

--

--