Skip to content

Instantly share code, notes, and snippets.

@alkavan
Last active April 5, 2025 10:54
Show Gist options
  • Save alkavan/180e206b7d023a95bab64cbd0dd4fce7 to your computer and use it in GitHub Desktop.
Save alkavan/180e206b7d023a95bab64cbd0dd4fce7 to your computer and use it in GitHub Desktop.
Rock Linux 9 | OpenVPN Server

Rocky Linux 9 | OpenVPN Server Installation


Step 1: Enable EPEL and Install Packages

Update Fedora and install OpenVPN and Easy-RSA:

sudo dnf install -y epel-release
sudo dnf update -y
sudo dnf install -y openvpn easy-rsa

Install some useful system amdin tools:

sudo dnf install -y nano tmux htop

Restart the instance:

sudo reboot

Step 2: Set Up Certificate Authority

Set up Easy-RSA to manage certificates:

sudo mkdir -p /etc/openvpn/easy-rsa
sudo cp -a /usr/share/easy-rsa/3/* /etc/openvpn/easy-rsa/
cd /etc/openvpn/easy-rsa
sudo ./easyrsa init-pki

Edit vars before creating the CA:

sudo cp /etc/openvpn/easy-rsa/pki/vars.example /etc/openvpn/easy-rsa/pki/vars
sudo nano /etc/openvpn/easy-rsa/pki/vars

For simple configurations, add to vars file:

set_var EASYRSA_REQ_CN          "vpn1.example.com"

Generate CA:

sudo ./easyrsa build-ca

Step 3: Generate Certificates

Generate certificates for the server and clients:

Server Certificate:

sudo ./easyrsa build-server-full server nopass

Client Certificate (repeat for each client):

sudo ./easyrsa build-client-full client1 nopass

Diffie-Hellman Parameters:

sudo ./easyrsa gen-dh

Step 4: Configure OpenVPN Server

Copy and edit the sample configuration:

sudo cp /usr/share/doc/openvpn/sample/sample-config-files/server.conf /etc/openvpn/server/server.conf
sudo nano /etc/openvpn/server/server.conf

Create TA key:

openvpn --genkey tls-auth /etc/openvpn/ta.key

HMAC Firewall: It creates a cryptographic signature (Hash-based Message Authentication Code) for each packet.

Update these lines with the correct paths:

ca /etc/openvpn/easy-rsa/pki/ca.crt
cert /etc/openvpn/easy-rsa/pki/issued/server.crt
key /etc/openvpn/easy-rsa/pki/private/server.key
dh /etc/openvpn/easy-rsa/pki/dh.pem
tls-auth /etc/openvpn/ta.key 0

notice these lines are not defined together in the server.conf file.

Route all client traffic through the VPN:

push "redirect-gateway def1 bypass-dhcp"

To avoid DNS leaks, push a DNS server (e.g., Google’s):

push "dhcp-option DNS 8.8.8.8"
push "dhcp-option DNS 8.8.4.4"

this step is optional.

Config ciphers and fallback:

#cipher AES-256-CBC data-ciphers AES-256-GCM:AES-256-CBC data-ciphers-fallback AES-256-CBC

comment cipher AES-256-CBC line.

Configure log location:

The default location will be blocked by selinux because it's in the /etc/ directory.

For high traffic and production this log storage location probably needs to be more robust.

status /var/log/openvpn-status.log

Set VPN subnet (usually already set):

server 10.8.0.0 255.255.255.0

Step 5: Start OpenVPN Service

sudo systemctl enable --now openvpn-server@server
sudo systemctl status openvpn-server@server

Step 6: Configure Firewall Rules

Install firewalld service and check it's running:

sudo dnf install -y firewalld
sudo systemctl start firewalld
sudo firewall-cmd --state

Allow OpenVPN and Enable NAT:

sudo firewall-cmd --add-service=openvpn --permanent
sudo firewall-cmd --add-masquerade --permanent

Block Access to Local Networks (optional):

Block traffic from the VPN subnet 10.8.0.0/24 to the server’s private subnets: This is a common example on a typical VPS with two network interfaces.

View interfaces with ip addr and block them,
10.18.0.8/16 and 10.133.0.7/16 interfaces in this case:

  • Block 10.18.0.0/16 (eth0 private network):

    sudo firewall-cmd --permanent --direct --add-rule ipv4 filter FORWARD 0 -s 10.8.0.0/24 -d 10.18.0.0/16 -j DROP
  • Block 10.133.0.0/16 (eth1 private network):

    sudo firewall-cmd --permanent --direct --add-rule ipv4 filter FORWARD 0 -s 10.8.0.0/24 -d 10.133.0.0/16 -j DROP

Block Server Access (except VPN port):

⚠️ This command can be destructive on VPS as it will prevent any network access to the server except via the VPN, including via SSH or the cloud provider terminal. Use only for securing production-grade servers, and fully understand what you're doing. I recommand making a snapshot of the instance before applying this firewall rule for easy rollback. ⚠️

sudo firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT 0 -s 10.8.0.0/24 -j DROP
sudo firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT 1 -s 10.8.0.0/24 -p udp --dport 1194 -j ACCEPT

Block Access to the Server Itself

Prevent VPN clients from accessing the server’s IPs (except for VPN traffic):

Drop all incoming traffic from VPN clients:

sudo firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT 0 -s 10.8.0.0/24 -j DROP

Allow only VPN traffic (UDP 1194):

sudo firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT 1 -s 10.8.0.0/24 -p udp --dport 1194 -j ACCEPT

Reload Firewall and Enable IP Forwarding:

sudo firewall-cmd --reload
echo 'net.ipv4.ip_forward = 1' | sudo tee -a /etc/sysctl.conf
sudo sysctl -p

Notes and Troubleshoot

Public Subnets

In our VPS example we had the following interface:

2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether a2:e6:a3:72:42:2f brd ff:ff:ff:ff:ff:ff
    altname enp0s3
    altname ens3
    inet 134.209.194.59/20 brd 134.209.207.255 scope global noprefixroute eth0
       valid_lft forever preferred_lft forever
    inet 10.18.0.8/16 brd 10.18.255.255 scope global noprefixroute eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::a0e6:a3ff:fe72:422f/64 scope link
       valid_lft forever preferred_lft forever

We didn’t block 134.209.192.0/20, and it’s not necessary since it’s the internet-facing range clients use for NAT.

If you want to block it as well, add to firewall rules:

sudo firewall-cmd --permanent --direct --add-rule ipv4 filter FORWARD 0 -s 10.8.0.0/24 -d 134.209.192.0/20 -j DROP
sudo firewall-cmd --reload

SELinux

It might be good idea to disable selinux for some, but if it makes trouble:

sudo restorecon -Rv /etc/openvpn

Trace service logs

sudo journalctl -u openvpn-server@server -f

Trace VPN status log:

tail -f /var/log/openvpn-status.log

this files shows who is currently connected, aad resets every X seconds, it does aggregate over time or contain traffic information or data.

Inspect (direct) firewall rules:

sudo firewall-cmd --permanent --direct --get-all-rules

Client-Side Setup

This section guides you through setting up an OpenVPN client config file and using on Windows with OpenVPN GUI.

Step 1: Prepare .ovpn client configuration file

Create OpenVPN config file client1.ovpn for the first client:

sudo nano /etc/openvpn/client/client1.ovpn

Put this contents inside and replace [ ... ] with the file contents.

client
dev tun
proto udp
remote vpn1.example.com 1194
resolv-retry infinite
nobind
persist-key
persist-tun
remote-cert-tls server
data-ciphers AES-256-GCM:AES-256-CBC
data-ciphers-fallback AES-256-CBC
verb 3
key-direction 1
<ca>
[contents of /etc/openvpn/easy-rsa/pki/ca.crt]
</ca>
<cert>
[contents of /etc/openvpn/easy-rsa/pki/issued/client1.crt]
</cert>
<key>
[contents of /etc/openvpn/easy-rsa/pki/private/client1.key]
</key>
<tls-auth>
[contents of /etc/openvpn/ta.key]
</tls-auth>

Download the config file (use Git Bash on Windows):

scp [email protected]:/etc/openvpn/client/client1.ovpn .

Step 2: Install OpenVPN (Linux)

On Debian/Ubuntu-based systems:

sudo apt install openvpn

On RHEL/Rocky Linux/CentOS systems:

sudo dnf install openvpn

Copy VPN configuration:

sudo cp client1.ovpn /etc/openvpn/client/

Run inline client:

sudo openvpn --config /etc/openvpn/client/client1.ovpn

So setup as service instead copy the file as client.conf:

sudo cp client1.ovpn /etc/openvpn/client/client.conf

Enable and start OpenVPN client service:

sudo systemctl enable openvpn-client@client
sudo systemctl start openvpn-client@client

Check OpenVPN client service status:

sudo systemctl status openvpn-client@client

Or, you can import the file into Network Manager GUI:

nmcli connection import type openvpn file client1.ovpn

Step 2: Install OpenVPN GUI (Windows)

Install OpenVPN Community Client (Windows)

  1. Download: Get the OpenVPN Community Client from OpenVPN Downloads.
  2. Run the installer with default settings. This includes the OpenVPN GUI and TAP adapter.
  3. Place client1.ovpn we downloaded inside C:\Program Files\OpenVPN\config.
  4. Start OpenVPN GUI program, it should auto-connect to the server.

OpenVPN Client Configuration Helper

The following template and script will help you creating unique configuration for each VPN client.

Create the client config template

cd /etc/openvpn/client && touch client.ovpn.tpl && touch create.py

Put this config contents inside client.ovpn.tpl, don't change the file paths:

client
dev tun
proto udp
remote vpn1.example.com 1194
resolv-retry infinite
nobind
persist-key
persist-tun
remote-cert-tls server
data-ciphers AES-256-GCM:AES-256-CBC
data-ciphers-fallback AES-256-CBC
verb 3
key-direction 1
<ca>
[/etc/openvpn/easy-rsa/pki/ca.crt]
</ca>
<cert>
[/etc/openvpn/easy-rsa/pki/issued/client.crt]
</cert>
<key>
[/etc/openvpn/easy-rsa/pki/private/client.key]
</key>
<tls-auth>
[/etc/openvpn/ta.key]
</tls-auth>

Create the client creation script

Put this script contents inside create.py and give it execution permission chmod +x create.py:

#!/usr/bin/python

import re
import os
import sys
import argparse

# Set up argument parser
parser = argparse.ArgumentParser(description="Generate an OpenVPN client configuration file from a template.")
parser.add_argument('output', help='Output .ovpn file name (required)')
parser.add_argument('--template', default='client.ovpn.tpl', help='Path to the template file (default: client.ovpn.tpl)')
parser.add_argument('--ca', help='Path to the CA certificate file (overrides template path)')
parser.add_argument('--cert', help='Path to the client certificate file (overrides template path)')
parser.add_argument('--key', help='Path to the client key file (overrides template path)')
parser.add_argument('--tls-auth', help='Path to the TLS auth key file (overrides template path)')
args = parser.parse_args()

# Read the template file
try:
    with open(args.template, 'r') as f:
        template_str = f.read()
except FileNotFoundError:
    print(f"Error: Template file '{args.template}' not found.")
    sys.exit(1)

# Define tags and their corresponding argument values
tags = {
    'ca': args.ca,
    'cert': args.cert,
    'key': args.key,
    'tls-auth': args.tls_auth
}

# Function to replace a placeholder with file contents
def replace_placeholder(match, tag, alt_path):
    default_path = match.group(1)  # Path inside [ ]
    path = alt_path if alt_path else default_path
    if not os.path.exists(path):
        print(f"Error: File '{path}' does not exist.")
        sys.exit(1)
    with open(path, 'r') as f:
        contents = f.read()
    return f'<{tag}>\n{contents}\n</{tag}>'

# Process each tag in the template
for tag in tags:
    pattern = r'<{tag}>\s*\[(.*?)\]\s*</{tag}>'.format(tag=tag)
    template_str = re.sub(
        pattern,
        lambda m: replace_placeholder(m, tag, tags[tag]),
        template_str,
        flags=re.DOTALL
    )

# Check if the output file exists and handle overwrite
if os.path.exists(args.output):
    confirm = input(f"File '{args.output}' already exists. Overwrite? (y/n): ")
    if confirm.lower() != 'y':
        print("Aborting.")
        sys.exit(0)

# Write the processed template to the output file
with open(args.output, 'w') as f:
    f.write(template_str)
print(f"Generated '{args.output}' successfully.")

Usage example of create.py to create client2.ovpn configurtion

The script will ask you if you overwrite if client file already exists.

sudo ./easyrsa build-client-full client2 nopass
sudo ./create.py client2.ovpn \
--cert /etc/openvpn/easy-rsa/pki/issued/client2.crt \
--key /etc/openvpn/easy-rsa/pki/private/client2.key
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment