Merge origin/master into pr-962

This commit is contained in:
Stanislas Lange
2025-12-13 20:51:11 +01:00
9 changed files with 515 additions and 103 deletions

View File

@@ -9,7 +9,7 @@ name: Docker Test
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
permissions:
contents: read
@@ -89,6 +89,15 @@ jobs:
name: tls-auth
sig: "3"
key_file: tls-auth.key
# Test firewalld support on Fedora
- os:
name: fedora-42-firewalld
image: fedora:42
enable_firewalld: true
tls:
name: tls-crypt-v2
sig: "1"
key_file: tls-crypt-v2.key
name: ${{ matrix.os.name }}
steps:
@@ -103,6 +112,7 @@ jobs:
run: |
docker build \
--build-arg BASE_IMAGE=${{ matrix.os.image }} \
--build-arg ENABLE_FIREWALLD=${{ matrix.os.enable_firewalld && 'y' || 'n' }} \
-t openvpn-server \
-f test/Dockerfile.server .

View File

@@ -8,7 +8,7 @@ name: Lint
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
permissions:
contents: read

View File

@@ -61,10 +61,13 @@ jobs:
- name: Commit changes
if: env.HASH_CHANGED == 'true'
env:
PAT: ${{ secrets.PAT }}
run: |
if ! git diff --quiet openvpn-install.sh; then
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git remote set-url origin "https://x-access-token:${PAT}@github.com/${{ github.repository }}"
git add openvpn-install.sh
git commit -m "chore: update Easy-RSA SHA256 hash"
git push

4
FAQ.md
View File

@@ -87,9 +87,9 @@ If your client is <2.3.3, remove `tls-version-min 1.2` from your `/etc/openvpn/s
---
**Q:** What syctl and iptables changes are made by the script?
**Q:** What sysctl and firewall changes are made by the script?
**A:** Iptables rules are saved at `/etc/iptables/add-openvpn-rules.sh` and `/etc/iptables/rm-openvpn-rules.sh`. They are managed by the service `/etc/systemd/system/iptables-openvpn.service`
**A:** If firewalld is active, the script uses `firewall-cmd --permanent` to configure port, masquerade, and rich rules. Otherwise, iptables rules are saved at `/etc/iptables/add-openvpn-rules.sh` and `/etc/iptables/rm-openvpn-rules.sh`, managed by `/etc/systemd/system/iptables-openvpn.service`.
Sysctl options are at `/etc/sysctl.d/99-openvpn.conf`

View File

@@ -59,7 +59,8 @@ The first time you run it, you'll have to follow the assistant and answer a few
When OpenVPN is installed, you can run the script again, and you will get the choice to:
- Add a client
- Remove a client
- List client certificates
- Revoke a client
- Renew certificates (client or server)
- Uninstall OpenVPN
@@ -95,10 +96,11 @@ If you want to customise your installation, you can export them or specify them
- `COMPRESSION_ENABLED=n`
- `CUSTOMIZE_ENC=n`
- `CLIENT=clientname`
- `PASS=1`
- `PASS=1` (set to `2` for password-protected clients, requires `PASSPHRASE`)
- `MULTI_CLIENT=n`
- `CLIENT_CERT_DURATION_DAYS=3650`
- `SERVER_CERT_DURATION_DAYS=3650`
- `NEW_CLIENT=y` (set to `n` to skip client creation after installation)
- `CLIENT_FILEPATH=/custom/path/client.ovpn` (optional, overrides default output path)
The `.ovpn` file is saved to `CLIENT_FILEPATH` if defined, otherwise: the client's home directory if it exists (`/home/$CLIENT`), otherwise `SUDO_USER`'s home, otherwise `/root`.
@@ -107,8 +109,6 @@ If the server is behind NAT, you can specify its endpoint with the `ENDPOINT` va
Other variables can be set depending on your choice (encryption, compression). You can search for them in the `installQuestions()` function of the script.
Password-protected clients are not supported by the headless installation method since user input is expected by Easy-RSA.
The headless install is more-or-less idempotent, in that it has been made safe to run multiple times with the same parameters, e.g. by a state provisioner like Ansible/Terraform/Salt/Chef/Puppet. It will only install and regenerate the Easy-RSA PKI if it doesn't already exist, and it will only install OpenVPN and other upstream dependencies if OpenVPN isn't already installed. It will recreate all local config and re-generate the client file on each headless run.
### Headless User Addition
@@ -121,20 +121,42 @@ The following Bash script adds a new user `foo` to an existing OpenVPN configura
#!/bin/bash
export MENU_OPTION="1"
export CLIENT="foo"
export PASS="1"
export PASS="1" # set to "2" for a password-protected client, and set PASSPHRASE
# export CLIENT_FILEPATH="/etc/openvpn/clients/foo.ovpn"
./openvpn-install.sh
```
**Note:** When a client name matches a system user (e.g., `foo` and `/home/foo` exists), the script automatically sets proper ownership and permissions on the `.ovpn` file.
### Headless User Revocation
It's also possible to automate the revocation of an existing user. The key is to provide the `MENU_OPTION` variable set to `3` along with either `CLIENT` (client name) or `CLIENTNUMBER` (1-based index from the client list).
The following Bash script revokes the existing user `foo`:
```bash
#!/bin/bash
export MENU_OPTION="3"
export CLIENT="foo"
./openvpn-install.sh
```
Alternatively, you can use the client number:
```bash
#!/bin/bash
export MENU_OPTION="3"
export CLIENTNUMBER="1" # Revokes the first client in the list
./openvpn-install.sh
```
## Features
- Installs and configures a ready-to-use OpenVPN server
- Certificate renewal for both client and server certificates
- Uses [official OpenVPN repositories](https://community.openvpn.net/openvpn/wiki/OpenvpnSoftwareRepos) when possible for the latest stable releases
- Iptables rules and forwarding managed in a seamless way
- If needed, the script can cleanly remove OpenVPN, including configuration and iptables rules
- Firewall rules and forwarding managed seamlessly (native firewalld support, iptables fallback)
- If needed, the script can cleanly remove OpenVPN, including configuration and firewall rules
- Customisable encryption settings, enhanced default settings (see [Security and Encryption](#security-and-encryption) below)
- OpenVPN 2.4 features, mainly encryption improvements (see [Security and Encryption](#security-and-encryption) below)
- Variety of DNS resolvers to be pushed to the clients

View File

@@ -9,8 +9,8 @@
# Configuration constants
readonly DEFAULT_CERT_VALIDITY_DURATION_DAYS=3650 # 10 years
readonly DEFAULT_CRL_VALIDITY_DURATION_DAYS=5475 # 15 years
readonly EASYRSA_VERSION="3.2.4"
readonly EASYRSA_SHA256="ed65e88cea892268efa71eb1161ce13af3beded6754301e1e713e36ff3613cac"
readonly EASYRSA_VERSION="3.2.5"
readonly EASYRSA_SHA256="662ee3b453155aeb1dff7096ec052cd83176c460cfa82ac130ef8568ec4df490"
# =============================================================================
# Logging Configuration
@@ -671,7 +671,7 @@ function installQuestions() {
log_menu " 12) NextDNS (Anycast: worldwide)"
log_menu " 13) Custom"
until [[ $DNS =~ ^[0-9]+$ ]] && [ "$DNS" -ge 1 ] && [ "$DNS" -le 13 ]; do
read -rp "DNS [1-13]: " -e -i 11 DNS
read -rp "DNS [1-13]: " -e -i 3 DNS
if [[ $DNS == 2 ]] && [[ -e /etc/unbound/unbound.conf ]]; then
log_menu ""
log_prompt "Unbound is already installed."
@@ -981,7 +981,7 @@ function installOpenVPN() {
IPV6_SUPPORT=${IPV6_SUPPORT:-n}
PORT_CHOICE=${PORT_CHOICE:-1}
PROTOCOL_CHOICE=${PROTOCOL_CHOICE:-1}
DNS=${DNS:-1}
DNS=${DNS:-3}
COMPRESSION_ENABLED=${COMPRESSION_ENABLED:-n}
MULTI_CLIENT=${MULTI_CLIENT:-n}
CUSTOMIZE_ENC=${CUSTOMIZE_ENC:-n}
@@ -990,6 +990,7 @@ function installOpenVPN() {
CLIENT_CERT_DURATION_DAYS=${CLIENT_CERT_DURATION_DAYS:-$DEFAULT_CERT_VALIDITY_DURATION_DAYS}
SERVER_CERT_DURATION_DAYS=${SERVER_CERT_DURATION_DAYS:-$DEFAULT_CERT_VALIDITY_DURATION_DAYS}
CONTINUE=${CONTINUE:-y}
NEW_CLIENT=${NEW_CLIENT:-y}
if [[ -z $ENDPOINT ]]; then
ENDPOINT=$(resolvePublicIP)
@@ -1083,11 +1084,6 @@ function installOpenVPN() {
# Create the server directory (OpenVPN 2.4+ directory structure)
run_cmd_fatal "Creating server directory" mkdir -p /etc/openvpn/server
# An old version of easy-rsa was available by default in some openvpn packages
if [[ -d /etc/openvpn/server/easy-rsa/ ]]; then
run_cmd "Removing old Easy-RSA" rm -rf /etc/openvpn/server/easy-rsa/
fi
fi
# Determine which user/group OpenVPN should run as
@@ -1175,11 +1171,11 @@ function installOpenVPN() {
;;
2)
# Generate tls-crypt key
run_cmd_fatal "Generating tls-crypt key" openvpn --genkey --secret /etc/openvpn/server/tls-crypt.key
run_cmd_fatal "Generating tls-crypt key" openvpn --genkey secret /etc/openvpn/server/tls-crypt.key
;;
3)
# Generate tls-auth key
run_cmd_fatal "Generating tls-auth key" openvpn --genkey --secret /etc/openvpn/server/tls-auth.key
run_cmd_fatal "Generating tls-auth key" openvpn --genkey secret /etc/openvpn/server/tls-auth.key
;;
esac
else
@@ -1423,48 +1419,66 @@ verb 3" >>/etc/openvpn/server/server.conf
installUnbound
fi
# Add iptables rules in two scripts
# Configure firewall rules
log_info "Configuring firewall rules..."
run_cmd_fatal "Creating iptables directory" mkdir -p /etc/iptables
# Script to add rules
echo "#!/bin/sh
if systemctl is-active --quiet firewalld; then
# Use firewalld native commands for systems with firewalld active
log_info "firewalld detected, using firewall-cmd..."
run_cmd "Adding OpenVPN port to firewalld" firewall-cmd --permanent --add-port="$PORT/$PROTOCOL"
run_cmd "Adding masquerade to firewalld" firewall-cmd --permanent --add-masquerade
# Add rich rules for VPN traffic (source-based rules work reliably with dynamic tun0 interface)
run_cmd "Adding VPN subnet rule" firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="10.8.0.0/24" accept'
if [[ $IPV6_SUPPORT == 'y' ]]; then
run_cmd "Adding IPv6 source rule" firewall-cmd --permanent --add-rich-rule='rule family="ipv6" source address="fd42:42:42:42::/112" accept'
fi
run_cmd "Reloading firewalld" firewall-cmd --reload
else
# Use iptables for systems without firewalld
run_cmd_fatal "Creating iptables directory" mkdir -p /etc/iptables
# Script to add rules
echo "#!/bin/sh
iptables -t nat -I POSTROUTING 1 -s 10.8.0.0/24 -o $NIC -j MASQUERADE
iptables -I INPUT 1 -i tun0 -j ACCEPT
iptables -I FORWARD 1 -i $NIC -o tun0 -j ACCEPT
iptables -I FORWARD 1 -i tun0 -o $NIC -j ACCEPT
iptables -I INPUT 1 -i $NIC -p $PROTOCOL --dport $PORT -j ACCEPT" >/etc/iptables/add-openvpn-rules.sh
if [[ $IPV6_SUPPORT == 'y' ]]; then
echo "ip6tables -t nat -I POSTROUTING 1 -s fd42:42:42:42::/112 -o $NIC -j MASQUERADE
if [[ $IPV6_SUPPORT == 'y' ]]; then
echo "ip6tables -t nat -I POSTROUTING 1 -s fd42:42:42:42::/112 -o $NIC -j MASQUERADE
ip6tables -I INPUT 1 -i tun0 -j ACCEPT
ip6tables -I FORWARD 1 -i $NIC -o tun0 -j ACCEPT
ip6tables -I FORWARD 1 -i tun0 -o $NIC -j ACCEPT
ip6tables -I INPUT 1 -i $NIC -p $PROTOCOL --dport $PORT -j ACCEPT" >>/etc/iptables/add-openvpn-rules.sh
fi
fi
# Script to remove rules
echo "#!/bin/sh
# Script to remove rules
echo "#!/bin/sh
iptables -t nat -D POSTROUTING -s 10.8.0.0/24 -o $NIC -j MASQUERADE
iptables -D INPUT -i tun0 -j ACCEPT
iptables -D FORWARD -i $NIC -o tun0 -j ACCEPT
iptables -D FORWARD -i tun0 -o $NIC -j ACCEPT
iptables -D INPUT -i $NIC -p $PROTOCOL --dport $PORT -j ACCEPT" >/etc/iptables/rm-openvpn-rules.sh
if [[ $IPV6_SUPPORT == 'y' ]]; then
echo "ip6tables -t nat -D POSTROUTING -s fd42:42:42:42::/112 -o $NIC -j MASQUERADE
if [[ $IPV6_SUPPORT == 'y' ]]; then
echo "ip6tables -t nat -D POSTROUTING -s fd42:42:42:42::/112 -o $NIC -j MASQUERADE
ip6tables -D INPUT -i tun0 -j ACCEPT
ip6tables -D FORWARD -i $NIC -o tun0 -j ACCEPT
ip6tables -D FORWARD -i tun0 -o $NIC -j ACCEPT
ip6tables -D INPUT -i $NIC -p $PROTOCOL --dport $PORT -j ACCEPT" >>/etc/iptables/rm-openvpn-rules.sh
fi
fi
run_cmd "Making add-openvpn-rules.sh executable" chmod +x /etc/iptables/add-openvpn-rules.sh
run_cmd "Making rm-openvpn-rules.sh executable" chmod +x /etc/iptables/rm-openvpn-rules.sh
run_cmd "Making add-openvpn-rules.sh executable" chmod +x /etc/iptables/add-openvpn-rules.sh
run_cmd "Making rm-openvpn-rules.sh executable" chmod +x /etc/iptables/rm-openvpn-rules.sh
# Handle the rules via a systemd script
echo "[Unit]
# Handle the rules via a systemd script
echo "[Unit]
Description=iptables rules for OpenVPN
After=firewalld.service
Before=network-online.target
Wants=network-online.target
@@ -1477,10 +1491,11 @@ RemainAfterExit=yes
[Install]
WantedBy=multi-user.target" >/etc/systemd/system/iptables-openvpn.service
# Enable service and apply rules
run_cmd "Reloading systemd" systemctl daemon-reload
run_cmd "Enabling iptables service" systemctl enable iptables-openvpn
run_cmd "Starting iptables service" systemctl start iptables-openvpn
# Enable service and apply rules
run_cmd "Reloading systemd" systemctl daemon-reload
run_cmd "Enabling iptables service" systemctl enable iptables-openvpn
run_cmd "Starting iptables service" systemctl start iptables-openvpn
fi
# If the server is behind a NAT, use the correct IP address for the clients to connect to
if [[ $ENDPOINT != "" ]]; then
@@ -1522,9 +1537,13 @@ verb 3" >>/etc/openvpn/server/client-template.txt
fi
# Generate the custom client.ovpn
log_info "Generating first client certificate..."
newClient
log_success "If you want to add more clients, you simply need to run this script another time!"
if [[ $NEW_CLIENT == "n" ]]; then
log_info "No clients added. To add clients, simply run the script again."
else
log_info "Generating first client certificate..."
newClient
log_success "If you want to add more clients, you simply need to run this script another time!"
fi
}
# Helper function to get the home directory for storing client configs
@@ -1651,6 +1670,15 @@ function selectClient() {
log_fatal "You have no existing clients!"
fi
# If CLIENT is set, validate it exists as a valid client
if [[ -n $CLIENT ]]; then
if tail -n +2 /etc/openvpn/server/easy-rsa/pki/index.txt | grep "^V" | cut -d '=' -f 2 | grep -qx "$CLIENT"; then
return
else
log_fatal "Client '$CLIENT' not found or not valid"
fi
fi
if [[ $show_expiry == "true" ]]; then
local i=1
while read -r client; do
@@ -1677,6 +1705,88 @@ function selectClient() {
CLIENT=$(tail -n +2 /etc/openvpn/server/easy-rsa/pki/index.txt | grep "^V" | cut -d '=' -f 2 | sed -n "$CLIENTNUMBER"p)
}
function listClients() {
log_header "Client Certificates"
local index_file="/etc/openvpn/server/easy-rsa/pki/index.txt"
local number_of_clients
# Exclude server certificates (CN starting with server_)
number_of_clients=$(tail -n +2 "$index_file" | grep "^[VR]" | grep -cv "/CN=server_")
if [[ $number_of_clients == '0' ]]; then
log_warn "You have no existing client certificates!"
return
fi
log_info "Found $number_of_clients client certificate(s)"
log_menu ""
printf " %-25s %-10s %-12s %s\n" "Name" "Status" "Expiry" "Remaining"
printf " %-25s %-10s %-12s %s\n" "----" "------" "------" "---------"
local cert_dir="/etc/openvpn/server/easy-rsa/pki/issued"
# Parse index.txt and sort by expiry date (oldest first)
# Exclude server certificates (CN starting with server_)
{
while read -r line; do
local status="${line:0:1}"
local client_name
client_name=$(echo "$line" | sed 's/.*\/CN=//')
# Format status
local status_text
if [[ "$status" == "V" ]]; then
status_text="Valid"
elif [[ "$status" == "R" ]]; then
status_text="Revoked"
else
status_text="Unknown"
fi
# Get expiry date from certificate file
local cert_file="$cert_dir/$client_name.crt"
local expiry_date="unknown"
local relative="unknown"
if [[ -f "$cert_file" ]]; then
# Get expiry from certificate (format: notAfter=Mon DD HH:MM:SS YYYY GMT)
local enddate
enddate=$(openssl x509 -enddate -noout -in "$cert_file" 2>/dev/null | cut -d= -f2)
if [[ -n "$enddate" ]]; then
# Parse date and convert to epoch
local expiry_epoch
expiry_epoch=$(date -d "$enddate" +%s 2>/dev/null || date -j -f "%b %d %H:%M:%S %Y %Z" "$enddate" +%s 2>/dev/null)
if [[ -n "$expiry_epoch" ]]; then
# Format as YYYY-MM-DD
expiry_date=$(date -d "@$expiry_epoch" +%Y-%m-%d 2>/dev/null || date -r "$expiry_epoch" +%Y-%m-%d 2>/dev/null)
# Calculate days remaining
local now_epoch days_remaining
now_epoch=$(date +%s)
days_remaining=$(((expiry_epoch - now_epoch) / 86400))
if [[ $days_remaining -lt 0 ]]; then
relative="$((-days_remaining)) days ago"
elif [[ $days_remaining -eq 0 ]]; then
relative="today"
elif [[ $days_remaining -eq 1 ]]; then
relative="1 day"
else
relative="$days_remaining days"
fi
fi
fi
fi
printf " %-25s %-10s %-12s %s\n" "$client_name" "$status_text" "$expiry_date" "$relative"
done < <(tail -n +2 "$index_file" | grep "^[VR]" | grep -v "/CN=server_" | sort -t$'\t' -k2)
}
log_menu ""
}
function newClient() {
log_header "New Client Setup"
log_prompt "Tell me a name for the client."
@@ -1717,10 +1827,18 @@ function newClient() {
run_cmd_fatal "Building client certificate" ./easyrsa --batch build-client-full "$CLIENT" nopass
;;
2)
log_warn "You will be asked for the client password below"
# Run directly (not via run_cmd) so password prompt is visible to user
if ! ./easyrsa --batch build-client-full "$CLIENT"; then
log_fatal "Building client certificate failed"
if [[ -z "$PASSPHRASE" ]]; then
log_warn "You will be asked for the client password below"
# Run directly (not via run_cmd) so password prompt is visible to user
if ! ./easyrsa --batch build-client-full "$CLIENT"; then
log_fatal "Building client certificate failed"
fi
else
log_info "Using provided passphrase for client certificate"
# Use env var to avoid exposing passphrase in install log
export EASYRSA_PASSPHRASE="$PASSPHRASE"
run_cmd_fatal "Building client certificate" ./easyrsa --batch --passin=env:EASYRSA_PASSPHRASE --passout=env:EASYRSA_PASSPHRASE build-client-full "$CLIENT"
unset EASYRSA_PASSPHRASE
fi
;;
esac
@@ -2010,15 +2128,24 @@ function removeOpenVPN() {
# Remove customised service
run_cmd "Removing service file" rm -f /etc/systemd/system/openvpn-server@.service
# Remove the iptables rules related to the script
log_info "Removing iptables rules..."
run_cmd "Stopping iptables service" systemctl stop iptables-openvpn
# Cleanup
run_cmd "Disabling iptables service" systemctl disable iptables-openvpn
run_cmd "Removing iptables service file" rm /etc/systemd/system/iptables-openvpn.service
run_cmd "Reloading systemd" systemctl daemon-reload
run_cmd "Removing iptables add script" rm /etc/iptables/add-openvpn-rules.sh
run_cmd "Removing iptables rm script" rm /etc/iptables/rm-openvpn-rules.sh
# Remove firewall rules
log_info "Removing firewall rules..."
if systemctl is-active --quiet firewalld && firewall-cmd --list-ports | grep -q "$PORT/$PROTOCOL"; then
# firewalld was used
run_cmd "Removing OpenVPN port from firewalld" firewall-cmd --permanent --remove-port="$PORT/$PROTOCOL"
run_cmd "Removing masquerade from firewalld" firewall-cmd --permanent --remove-masquerade
run_cmd "Removing VPN subnet rule" firewall-cmd --permanent --remove-rich-rule='rule family="ipv4" source address="10.8.0.0/24" accept' 2>/dev/null || true
run_cmd "Removing IPv6 source rule" firewall-cmd --permanent --remove-rich-rule='rule family="ipv6" source address="fd42:42:42:42::/112" accept' 2>/dev/null || true
run_cmd "Reloading firewalld" firewall-cmd --reload
elif [[ -f /etc/systemd/system/iptables-openvpn.service ]]; then
# iptables was used
run_cmd "Stopping iptables service" systemctl stop iptables-openvpn
run_cmd "Disabling iptables service" systemctl disable iptables-openvpn
run_cmd "Removing iptables service file" rm /etc/systemd/system/iptables-openvpn.service
run_cmd "Reloading systemd" systemctl daemon-reload
run_cmd "Removing iptables add script" rm -f /etc/iptables/add-openvpn-rules.sh
run_cmd "Removing iptables rm script" rm -f /etc/iptables/rm-openvpn-rules.sh
fi
# SELinux
if hash sestatus 2>/dev/null; then
@@ -2085,12 +2212,13 @@ function manageMenu() {
log_menu ""
log_prompt "What do you want to do?"
log_menu " 1) Add a new user"
log_menu " 2) Revoke existing user"
log_menu " 3) Renew certificate"
log_menu " 4) Remove OpenVPN"
log_menu " 5) Exit"
until [[ ${MENU_OPTION:-$menu_option} =~ ^[1-5]$ ]]; do
read -rp "Select an option [1-5]: " menu_option
log_menu " 2) List client certificates"
log_menu " 3) Revoke existing user"
log_menu " 4) Renew certificate"
log_menu " 5) Remove OpenVPN"
log_menu " 6) Exit"
until [[ ${MENU_OPTION:-$menu_option} =~ ^[1-6]$ ]]; do
read -rp "Select an option [1-6]: " menu_option
done
menu_option="${MENU_OPTION:-$menu_option}"
@@ -2099,15 +2227,18 @@ function manageMenu() {
newClient
;;
2)
revokeClient
listClients
;;
3)
renewMenu
revokeClient
;;
4)
removeOpenVPN
renewMenu
;;
5)
removeOpenVPN
;;
6)
exit 0
;;
esac

View File

@@ -5,7 +5,10 @@ ARG BASE_IMAGE=ubuntu:24.04
FROM ${BASE_IMAGE}
ARG BASE_IMAGE
# Set to "y" to install and enable firewalld for testing
ARG ENABLE_FIREWALLD=n
ENV DEBIAN_FRONTEND=noninteractive
ENV ENABLE_FIREWALLD=${ENABLE_FIREWALLD}
# Install basic dependencies based on the OS
# dnsutils/bind-utils provides dig for DNS testing with Unbound
@@ -16,10 +19,12 @@ RUN if command -v apt-get >/dev/null; then \
elif command -v dnf >/dev/null; then \
dnf install -y --allowerasing \
iproute iptables curl procps-ng systemd tar gzip bind-utils \
&& if [ "$ENABLE_FIREWALLD" = "y" ]; then dnf install -y firewalld; fi \
&& dnf clean all; \
elif command -v yum >/dev/null; then \
yum install -y \
iproute iptables curl procps-ng systemd tar gzip bind-utils \
&& if [ "$ENABLE_FIREWALLD" = "y" ]; then yum install -y firewalld; fi \
&& yum clean all; \
elif command -v pacman >/dev/null; then \
pacman -Syu --noconfirm \
@@ -31,6 +36,11 @@ RUN if command -v apt-get >/dev/null; then \
&& zypper clean -a; \
fi
# Enable firewalld if requested (must be done after systemd is available)
RUN if [ "$ENABLE_FIREWALLD" = "y" ] && command -v firewall-cmd >/dev/null; then \
systemctl enable firewalld; \
fi
# Create TUN device (will be mounted at runtime)
RUN mkdir -p /dev/net

View File

@@ -359,6 +359,86 @@ touch /shared/new-client-connected
echo ""
echo "=== Certificate Revocation E2E Tests PASSED ==="
# =====================================================
# Test PASSPHRASE-protected client connection
# =====================================================
echo ""
echo "=== Testing PASSPHRASE-protected Client Connection ==="
PASSPHRASE_CLIENT="passphrasetest"
# Wait for passphrase test client config
echo "Waiting for passphrase test client config..."
MAX_WAIT=120
WAITED=0
while [ ! -f /shared/passphrase-client-config-ready ] && [ $WAITED -lt $MAX_WAIT ]; do
sleep 2
WAITED=$((WAITED + 2))
echo "Waiting for passphrase test config... ($WAITED/$MAX_WAIT seconds)"
done
if [ ! -f /shared/passphrase-client-config-ready ]; then
echo "FAIL: Passphrase test client config not ready in time"
exit 1
fi
if [ ! -f "/shared/$PASSPHRASE_CLIENT.ovpn" ]; then
echo "FAIL: Passphrase test client config file not found"
exit 1
fi
if [ ! -f "/shared/$PASSPHRASE_CLIENT.pass" ]; then
echo "FAIL: Passphrase file not found"
exit 1
fi
echo "Passphrase test client config found!"
# Disconnect current VPN before connecting with passphrase client
echo "Disconnecting current VPN connection..."
pkill openvpn || true
sleep 2
# Connect with passphrase-protected client using --askpass
echo "Connecting with '$PASSPHRASE_CLIENT' certificate (passphrase-protected)..."
openvpn --config "/shared/$PASSPHRASE_CLIENT.ovpn" --askpass "/shared/$PASSPHRASE_CLIENT.pass" --daemon --log /var/log/openvpn-passphrase.log
# Wait for connection
echo "Waiting for VPN connection with passphrase-protected client..."
MAX_WAIT=60
WAITED=0
while ! ip addr show tun0 2>/dev/null | grep -q "inet " && [ $WAITED -lt $MAX_WAIT ]; do
sleep 2
WAITED=$((WAITED + 2))
echo "Waiting for tun0... ($WAITED/$MAX_WAIT seconds)"
if [ -f /var/log/openvpn-passphrase.log ]; then
tail -3 /var/log/openvpn-passphrase.log
fi
done
if ! ip addr show tun0 2>/dev/null | grep -q "inet "; then
echo "FAIL: VPN connection with passphrase-protected client failed"
cat /var/log/openvpn-passphrase.log || true
exit 1
fi
echo "PASS: Connected with passphrase-protected '$PASSPHRASE_CLIENT' certificate"
ip addr show tun0
# Verify connectivity
if ping -c 2 10.8.0.1 >/dev/null 2>&1; then
echo "PASS: Can ping VPN gateway with passphrase-protected client"
else
echo "FAIL: Cannot ping VPN gateway with passphrase-protected client"
exit 1
fi
# Signal server that we connected with passphrase client
touch /shared/passphrase-client-connected
echo ""
echo "=== PASSPHRASE-protected Client Tests PASSED ==="
echo ""
echo "=========================================="
echo " ALL TESTS PASSED!"

View File

@@ -77,15 +77,22 @@ fi
# Verify all expected files were created
echo "Verifying installation..."
MISSING_FILES=0
for f in \
/etc/openvpn/server/server.conf \
/etc/openvpn/server/ca.crt \
/etc/openvpn/server/ca.key \
"/etc/openvpn/server/$TLS_KEY_FILE" \
/etc/openvpn/server/crl.pem \
/etc/openvpn/server/easy-rsa/pki/ca.crt \
/etc/iptables/add-openvpn-rules.sh \
/root/testclient.ovpn; do
# Build list of required files
REQUIRED_FILES=(
/etc/openvpn/server/server.conf
/etc/openvpn/server/ca.crt
/etc/openvpn/server/ca.key
"/etc/openvpn/server/$TLS_KEY_FILE"
/etc/openvpn/server/crl.pem
/etc/openvpn/server/easy-rsa/pki/ca.crt
/root/testclient.ovpn
)
# Only check for iptables script if firewalld is not active
if ! systemctl is-active --quiet firewalld; then
REQUIRED_FILES+=(/etc/iptables/add-openvpn-rules.sh)
fi
for f in "${REQUIRED_FILES[@]}"; do
if [ ! -f "$f" ]; then
echo "ERROR: Missing file: $f"
MISSING_FILES=$((MISSING_FILES + 1))
@@ -177,7 +184,7 @@ echo "Original client certificate serial: $ORIG_CERT_SERIAL"
# Test client certificate renewal using the script
echo "Testing client certificate renewal..."
RENEW_OUTPUT="/tmp/renew-client-output.log"
(MENU_OPTION=3 RENEW_OPTION=1 CLIENTNUMBER=1 CLIENT_CERT_DURATION_DAYS=3650 bash /opt/openvpn-install.sh) 2>&1 | tee "$RENEW_OUTPUT" || true
(MENU_OPTION=4 RENEW_OPTION=1 CLIENTNUMBER=1 CLIENT_CERT_DURATION_DAYS=3650 bash /opt/openvpn-install.sh) 2>&1 | tee "$RENEW_OUTPUT" || true
# Verify renewal succeeded
if grep -q "Certificate for client testclient renewed" "$RENEW_OUTPUT"; then
@@ -257,7 +264,7 @@ echo "Original server certificate serial: $ORIG_SERVER_SERIAL"
# Test server certificate renewal
echo "Testing server certificate renewal..."
RENEW_SERVER_OUTPUT="/tmp/renew-server-output.log"
(MENU_OPTION=3 RENEW_OPTION=2 CONTINUE=y SERVER_CERT_DURATION_DAYS=3650 bash /opt/openvpn-install.sh) 2>&1 | tee "$RENEW_SERVER_OUTPUT" || true
(MENU_OPTION=4 RENEW_OPTION=2 CONTINUE=y SERVER_CERT_DURATION_DAYS=3650 bash /opt/openvpn-install.sh) 2>&1 | tee "$RENEW_SERVER_OUTPUT" || true
# Verify renewal succeeded
if grep -q "Server certificate renewed successfully" "$RENEW_SERVER_OUTPUT"; then
@@ -380,21 +387,58 @@ echo ""
# Verify OpenVPN server (started by systemd via install script)
echo "Verifying OpenVPN server..."
# Verify iptables NAT rules exist (applied by iptables-openvpn service)
echo "Verifying iptables NAT rules..."
for _ in $(seq 1 10); do
if iptables -t nat -L POSTROUTING -n | grep -q "10.8.0.0"; then
echo "PASS: NAT POSTROUTING rule for 10.8.0.0/24 exists"
break
# Verify firewall rules exist
echo "Verifying firewall rules..."
if systemctl is-active --quiet firewalld; then
# firewalld is active - verify masquerade is enabled
echo "firewalld detected, checking masquerade..."
for _ in $(seq 1 10); do
if firewall-cmd --query-masquerade 2>/dev/null; then
echo "PASS: firewalld masquerade is enabled"
break
fi
sleep 1
done
if ! firewall-cmd --query-masquerade 2>/dev/null; then
echo "FAIL: firewalld masquerade is not enabled"
echo "Current firewalld config:"
firewall-cmd --list-all 2>&1 || true
exit 1
fi
# Verify port is open
if firewall-cmd --list-ports | grep -q "1194/udp"; then
echo "PASS: OpenVPN port is open in firewalld"
else
echo "FAIL: OpenVPN port not found in firewalld"
firewall-cmd --list-ports
exit 1
fi
# Verify VPN subnet rich rule exists
if firewall-cmd --list-rich-rules | grep -q 'source address="10.8.0.0/24"'; then
echo "PASS: VPN subnet rich rule is configured"
else
echo "FAIL: VPN subnet rich rule not found in firewalld"
echo "Current rich rules:"
firewall-cmd --list-rich-rules
exit 1
fi
else
# iptables mode - verify NAT rules
echo "iptables mode, checking NAT rules..."
for _ in $(seq 1 10); do
if iptables -t nat -L POSTROUTING -n | grep -q "10.8.0.0"; then
echo "PASS: NAT POSTROUTING rule for 10.8.0.0/24 exists"
break
fi
sleep 1
done
if ! iptables -t nat -L POSTROUTING -n | grep -q "10.8.0.0"; then
echo "FAIL: NAT POSTROUTING rule for 10.8.0.0/24 not found"
echo "Current NAT rules:"
iptables -t nat -L POSTROUTING -n -v
systemctl status iptables-openvpn 2>&1 || true
exit 1
fi
sleep 1
done
if ! iptables -t nat -L POSTROUTING -n | grep -q "10.8.0.0"; then
echo "FAIL: NAT POSTROUTING rule for 10.8.0.0/24 not found"
echo "Current NAT rules:"
iptables -t nat -L POSTROUTING -n -v
systemctl status iptables-openvpn 2>&1 || true
exit 1
fi
# Verify IP forwarding is enabled
@@ -501,19 +545,11 @@ if [ ! -f /shared/revoke-client-disconnected ]; then
fi
echo "Client disconnected"
# Now revoke the certificate
# Now revoke the certificate using the new CLIENT name feature
echo "Revoking certificate for '$REVOKE_CLIENT'..."
REVOKE_OUTPUT="/tmp/revoke-output.log"
# MENU_OPTION=2 is revoke, CLIENTNUMBER is dynamically determined from index.txt
# We need to find the client number for revoketest
REVOKE_CLIENT_NUM=$(tail -n +2 /etc/openvpn/server/easy-rsa/pki/index.txt | grep "^V" | grep -n "CN=$REVOKE_CLIENT\$" | cut -d: -f1)
if [ -z "$REVOKE_CLIENT_NUM" ]; then
echo "ERROR: Could not find client number for '$REVOKE_CLIENT'"
cat /etc/openvpn/server/easy-rsa/pki/index.txt
exit 1
fi
echo "Revoke client number: $REVOKE_CLIENT_NUM"
(MENU_OPTION=2 CLIENTNUMBER=$REVOKE_CLIENT_NUM bash /opt/openvpn-install.sh) 2>&1 | tee "$REVOKE_OUTPUT" || true
# MENU_OPTION=3 is revoke, CLIENT specifies the client name directly
(MENU_OPTION=3 CLIENT=$REVOKE_CLIENT bash /opt/openvpn-install.sh) 2>&1 | tee "$REVOKE_OUTPUT" || true
if grep -q "Certificate for client $REVOKE_CLIENT revoked" "$REVOKE_OUTPUT"; then
echo "PASS: Certificate for '$REVOKE_CLIENT' revoked successfully"
@@ -553,6 +589,47 @@ echo "PASS: Connection with revoked certificate correctly rejected"
echo "=== Certificate Revocation Tests PASSED ==="
# =====================================================
# Test listing client certificates
# =====================================================
echo ""
echo "=== Testing List Client Certificates ==="
# At this point we have 3 client certificates:
# - testclient (Valid) - the renewed certificate
# - testclient (Revoked) - the old certificate revoked during renewal
# - revoketest (Revoked) - the revoked certificate
LIST_OUTPUT="/tmp/list-clients-output.log"
(MENU_OPTION=2 bash /opt/openvpn-install.sh) 2>&1 | tee "$LIST_OUTPUT" || true
# Verify list output contains expected clients
if grep -q "testclient" "$LIST_OUTPUT" && grep -q "Valid" "$LIST_OUTPUT"; then
echo "PASS: List shows testclient as Valid"
else
echo "FAIL: List does not show testclient correctly"
cat "$LIST_OUTPUT"
exit 1
fi
if grep -q "$REVOKE_CLIENT" "$LIST_OUTPUT" && grep -q "Revoked" "$LIST_OUTPUT"; then
echo "PASS: List shows $REVOKE_CLIENT as Revoked"
else
echo "FAIL: List does not show $REVOKE_CLIENT correctly"
cat "$LIST_OUTPUT"
exit 1
fi
# Verify certificate count (3 certs: testclient valid, testclient revoked from renewal, revoketest revoked)
if grep -q "Found 3 client certificate(s)" "$LIST_OUTPUT"; then
echo "PASS: List shows correct certificate count"
else
echo "FAIL: List does not show correct certificate count"
cat "$LIST_OUTPUT"
exit 1
fi
echo "=== List Client Certificates Tests PASSED ==="
# =====================================================
# Test reusing revoked client name
# =====================================================
@@ -618,6 +695,85 @@ fi
echo "PASS: Client connected with new '$REVOKE_CLIENT' certificate"
echo "=== Reuse of Revoked Client Name Tests PASSED ==="
# =====================================================
# Test PASSPHRASE support for headless client creation
# =====================================================
echo ""
echo "=== Testing PASSPHRASE Support ==="
PASSPHRASE_CLIENT="passphrasetest"
TEST_PASSPHRASE="TestP@ssw0rd#123"
echo "Creating client '$PASSPHRASE_CLIENT' with passphrase in headless mode..."
PASSPHRASE_OUTPUT="/tmp/passphrase-output.log"
(MENU_OPTION=1 CLIENT=$PASSPHRASE_CLIENT PASS=2 PASSPHRASE="$TEST_PASSPHRASE" CLIENT_CERT_DURATION_DAYS=3650 bash /opt/openvpn-install.sh) 2>&1 | tee "$PASSPHRASE_OUTPUT" || true
# Verify client was created
if [ -f "/root/$PASSPHRASE_CLIENT.ovpn" ]; then
echo "PASS: Client '$PASSPHRASE_CLIENT' with passphrase created successfully"
else
echo "FAIL: Failed to create client '$PASSPHRASE_CLIENT' with passphrase"
cat "$PASSPHRASE_OUTPUT"
exit 1
fi
# Verify the passphrase is NOT leaked in the output
if grep -q "$TEST_PASSPHRASE" "$PASSPHRASE_OUTPUT"; then
echo "FAIL: Passphrase was leaked in command output!"
exit 1
else
echo "PASS: Passphrase not leaked in command output"
fi
# Verify the log file doesn't contain the passphrase
if [ -f /opt/openvpn-install.log ] && grep -q "$TEST_PASSPHRASE" /opt/openvpn-install.log; then
echo "FAIL: Passphrase was leaked in log file!"
exit 1
else
echo "PASS: Passphrase not leaked in log file"
fi
# Verify certificate was created with encryption (key should be encrypted)
CLIENT_KEY="/etc/openvpn/server/easy-rsa/pki/private/$PASSPHRASE_CLIENT.key"
if [ -f "$CLIENT_KEY" ]; then
if grep -q "ENCRYPTED" "$CLIENT_KEY"; then
echo "PASS: Client key is encrypted"
else
echo "FAIL: Client key is not encrypted"
exit 1
fi
else
echo "FAIL: Client key not found at $CLIENT_KEY"
exit 1
fi
# Copy config for passphrase client connectivity test
cp "/root/$PASSPHRASE_CLIENT.ovpn" "/shared/$PASSPHRASE_CLIENT.ovpn"
sed -i 's/^remote .*/remote openvpn-server 1194/' "/shared/$PASSPHRASE_CLIENT.ovpn"
# Write passphrase to a file for client to use with --askpass
echo "$TEST_PASSPHRASE" >"/shared/$PASSPHRASE_CLIENT.pass"
echo "Copied $PASSPHRASE_CLIENT config and passphrase to /shared/"
# Signal client that passphrase test config is ready
touch /shared/passphrase-client-config-ready
# Wait for client to confirm connection with passphrase client
echo "Waiting for client to connect with '$PASSPHRASE_CLIENT' certificate..."
MAX_WAIT=60
WAITED=0
while [ ! -f /shared/passphrase-client-connected ] && [ $WAITED -lt $MAX_WAIT ]; do
sleep 2
WAITED=$((WAITED + 2))
echo "Waiting for passphrase client connection... ($WAITED/$MAX_WAIT seconds)"
done
if [ ! -f /shared/passphrase-client-connected ]; then
echo "FAIL: Client did not connect with passphrase-protected certificate"
exit 1
fi
echo "PASS: Client connected with passphrase-protected certificate"
echo "=== PASSPHRASE Support Tests PASSED ==="
echo ""
echo "=== All Revocation Tests PASSED ==="