From 99c74e5af4f335e64a7f4aac1fe8699f2c2190c5 Mon Sep 17 00:00:00 2001 From: Cezar Lungu Date: Sat, 13 Dec 2025 14:11:17 +0000 Subject: [PATCH 01/10] Delete old easy-rsa remove (#655) It isn't packaged anymore with openvpn in the supported distros Co-authored-by: Stanislas Lange --- openvpn-install.sh | 5 ----- 1 file changed, 5 deletions(-) diff --git a/openvpn-install.sh b/openvpn-install.sh index bda5b48..5fa0270 100755 --- a/openvpn-install.sh +++ b/openvpn-install.sh @@ -1083,11 +1083,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 From 75ea8ef1c1c5062d02ee0c26680dd1c22064b38a Mon Sep 17 00:00:00 2001 From: Stanislas Date: Sat, 13 Dec 2025 15:14:15 +0100 Subject: [PATCH 02/10] ci: only cancel in-progress jobs for pull requests (#1378) - Only cancel in-progress CI jobs for pull requests, not for master branch pushes - Ensures all master branch jobs run to completion while still saving CI resources on PRs --- .github/workflows/docker-test.yml | 2 +- .github/workflows/lint.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker-test.yml b/.github/workflows/docker-test.yml index f1e83ab..8908876 100644 --- a/.github/workflows/docker-test.yml +++ b/.github/workflows/docker-test.yml @@ -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 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 6a04a2f..88f318a 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -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 From cb2d67be7470bafae7ff41e28f16c643f24558db Mon Sep 17 00:00:00 2001 From: Siebren Kraak Date: Sat, 13 Dec 2025 15:42:43 +0100 Subject: [PATCH 03/10] Add PASSPHRASE support in headless mode (#1015) Add support for a password protected user in headless mode Fixes #389 --------- Co-authored-by: Siebren Kraak Co-authored-by: Stanislas Lange --- README.md | 6 +-- openvpn-install.sh | 16 ++++++-- test/client-entrypoint.sh | 80 +++++++++++++++++++++++++++++++++++++++ test/server-entrypoint.sh | 79 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 173 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index fd280c1..bc40145 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ 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` @@ -104,8 +104,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 @@ -118,7 +116,7 @@ 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 ./openvpn-install.sh ``` diff --git a/openvpn-install.sh b/openvpn-install.sh index 5fa0270..cc0b43b 100755 --- a/openvpn-install.sh +++ b/openvpn-install.sh @@ -1688,10 +1688,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 diff --git a/test/client-entrypoint.sh b/test/client-entrypoint.sh index 648c285..9b46fe4 100755 --- a/test/client-entrypoint.sh +++ b/test/client-entrypoint.sh @@ -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!" diff --git a/test/server-entrypoint.sh b/test/server-entrypoint.sh index 83e5328..3b2daa0 100755 --- a/test/server-entrypoint.sh +++ b/test/server-entrypoint.sh @@ -618,6 +618,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 ===" From 90f2313ff3145936164ca2239858a6d531564bed Mon Sep 17 00:00:00 2001 From: Stanislas Date: Sat, 13 Dec 2025 18:59:40 +0100 Subject: [PATCH 04/10] fix: use non-deprecated --genkey syntax for tls-crypt and tls-auth (#1383) ## Summary - Replace deprecated `--genkey --secret` syntax with `--genkey secret` for tls-crypt and tls-auth key generation The OpenVPN source explicitly warns about this: ``` WARNING: Using --genkey --secret filename is DEPRECATED. Use --genkey secret filename instead. ``` Closes #1256 Close https://github.com/angristan/openvpn-install/issues/1280 --- openvpn-install.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openvpn-install.sh b/openvpn-install.sh index cc0b43b..7ac7823 100755 --- a/openvpn-install.sh +++ b/openvpn-install.sh @@ -1170,11 +1170,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 From 190e49ec3379d8dbd1db92691cda6c526b246eb1 Mon Sep 17 00:00:00 2001 From: Stanislas Date: Sat, 13 Dec 2025 19:17:30 +0100 Subject: [PATCH 05/10] feat: add list clients menu option (#1382) ## Summary - Add new "List existing users" option to management menu (option 2) - Displays all client certificates with status (Valid/Revoked), expiration date, and days remaining - Reads expiry directly from certificate files using openssl for accurate 4-digit year dates - Output sorted by expiration date (oldest first) - Updates test MENU_OPTION values to match new menu numbering Example output: ``` === Existing Clients === Found 2 certificate(s) Name Status Expiry Remaining ---- ------ ------ --------- user1 Valid 2035-12-11 3649 days user2 Revoked unknown unknown ``` Closes #567 Closes #563 Closes #587 --- README.md | 3 +- openvpn-install.sh | 104 ++++++++++++++++++++++++++++++++++---- test/server-entrypoint.sh | 49 ++++++++++++++++-- 3 files changed, 142 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index bc40145..4ff0a04 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/openvpn-install.sh b/openvpn-install.sh index 7ac7823..9859cf7 100755 --- a/openvpn-install.sh +++ b/openvpn-install.sh @@ -1648,6 +1648,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." @@ -2036,12 +2118,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}" @@ -2050,15 +2133,18 @@ function manageMenu() { newClient ;; 2) - revokeClient + listClients ;; 3) - renewMenu + revokeClient ;; 4) - removeOpenVPN + renewMenu ;; 5) + removeOpenVPN + ;; + 6) exit 0 ;; esac diff --git a/test/server-entrypoint.sh b/test/server-entrypoint.sh index 3b2daa0..b8152e5 100755 --- a/test/server-entrypoint.sh +++ b/test/server-entrypoint.sh @@ -177,7 +177,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 +257,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 @@ -504,7 +504,7 @@ echo "Client disconnected" # Now revoke the certificate echo "Revoking certificate for '$REVOKE_CLIENT'..." REVOKE_OUTPUT="/tmp/revoke-output.log" -# MENU_OPTION=2 is revoke, CLIENTNUMBER is dynamically determined from index.txt +# MENU_OPTION=3 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 @@ -513,7 +513,7 @@ if [ -z "$REVOKE_CLIENT_NUM" ]; then 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 CLIENTNUMBER=$REVOKE_CLIENT_NUM 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 +553,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 # ===================================================== From 0f2bd044479ce1b2dddfe3f81a5dcec73d57d8a8 Mon Sep 17 00:00:00 2001 From: Stanislas Date: Sat, 13 Dec 2025 19:32:07 +0100 Subject: [PATCH 06/10] feat: change default DNS resolver to Cloudflare (#1385) - Change default DNS resolver from AdGuard DNS to Cloudflare (1.1.1.1) - Applies to both interactive mode and AUTO_INSTALL mode --- openvpn-install.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openvpn-install.sh b/openvpn-install.sh index 9859cf7..eb1fa80 100755 --- a/openvpn-install.sh +++ b/openvpn-install.sh @@ -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} From be2a195bb5eb3eda29b6b79f37ef8b8901f77698 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 13 Dec 2025 19:48:07 +0100 Subject: [PATCH 07/10] chore(deps): update dependency openvpn/easy-rsa to v3.2.5 (#1381) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Update | Change | |---|---|---| | [OpenVPN/easy-rsa](https://redirect.github.com/OpenVPN/easy-rsa) | patch | `3.2.4` -> `3.2.5` | --- ### Release Notes
OpenVPN/easy-rsa (OpenVPN/easy-rsa) ### [`v3.2.5`](https://redirect.github.com/OpenVPN/easy-rsa/releases/tag/v3.2.5): 3.2.5 [Compare Source](https://redirect.github.com/OpenVPN/easy-rsa/compare/v3.2.4...v3.2.5) #### What's Changed - Replace `local` / `global` `openssl-easyrsa.cnf` by [@​TinCanTech](https://redirect.github.com/TinCanTech) in [#​1394](https://redirect.github.com/OpenVPN/easy-rsa/pull/1394) - init-pki: Introduce configurable cryptography by [@​TinCanTech](https://redirect.github.com/TinCanTech) in [#​1397](https://redirect.github.com/OpenVPN/easy-rsa/pull/1397) - Drop x509 type kdc built-in by [@​TinCanTech](https://redirect.github.com/TinCanTech) in [#​1399](https://redirect.github.com/OpenVPN/easy-rsa/pull/1399) - Always generate an `openssl-easyrsa.cnf` or `x509-types` tmp-file by [@​TinCanTech](https://redirect.github.com/TinCanTech) in [#​1401](https://redirect.github.com/OpenVPN/easy-rsa/pull/1401) - Libressl use `$EASYRSA_FORCE_SAFE_SSL` by [@​TinCanTech](https://redirect.github.com/TinCanTech) in [#​1402](https://redirect.github.com/OpenVPN/easy-rsa/pull/1402) - Update EasyRSA-Advanced.md by [@​TinCanTech](https://redirect.github.com/TinCanTech) in [#​1403](https://redirect.github.com/OpenVPN/easy-rsa/pull/1403) - `source_vars()`: Add `grep` regex for assign by equal `=` by [@​TinCanTech](https://redirect.github.com/TinCanTech) in [#​1405](https://redirect.github.com/OpenVPN/easy-rsa/pull/1405) - export\_pkcs(), PKCS12 inline: Respect $EASYRSA\_NO\_INLINE by [@​TinCanTech](https://redirect.github.com/TinCanTech) in [#​1407](https://redirect.github.com/OpenVPN/easy-rsa/pull/1407) - Introduce peer-fingerprint inline lists by [@​TinCanTech](https://redirect.github.com/TinCanTech) in [#​1410](https://redirect.github.com/OpenVPN/easy-rsa/pull/1410) - help: Add '-b' alias for --batch and correct default 'vars' file by [@​TinCanTech](https://redirect.github.com/TinCanTech) in [#​1411](https://redirect.github.com/OpenVPN/easy-rsa/pull/1411) - New function ssl\_cert\_sig\_digest(); Extract certificae digest name by [@​TinCanTech](https://redirect.github.com/TinCanTech) in [#​1414](https://redirect.github.com/OpenVPN/easy-rsa/pull/1414) - Upgrading OpenSSL for Windows to 3.6.0 by [@​ecrist](https://redirect.github.com/ecrist) in [#​1416](https://redirect.github.com/OpenVPN/easy-rsa/pull/1416) **Full Changelog**:
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/angristan/openvpn-install). --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Stanislas Lange Co-authored-by: github-actions[bot] --- .github/workflows/update-easyrsa-hash.yml | 3 +++ openvpn-install.sh | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/update-easyrsa-hash.yml b/.github/workflows/update-easyrsa-hash.yml index d3cff5c..8345c14 100644 --- a/.github/workflows/update-easyrsa-hash.yml +++ b/.github/workflows/update-easyrsa-hash.yml @@ -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 diff --git a/openvpn-install.sh b/openvpn-install.sh index eb1fa80..02a57e8 100755 --- a/openvpn-install.sh +++ b/openvpn-install.sh @@ -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 From 9fd183caedf3e589f6df92511e1ce4ae4b08d0f0 Mon Sep 17 00:00:00 2001 From: Podesta <11512008+Podesta@users.noreply.github.com> Date: Sat, 13 Dec 2025 15:57:02 -0300 Subject: [PATCH 08/10] feat: add flag for creation or not of new client after install (#1010) Add a flag `NEW_CLIENT` so that the user can choose whether or not he wishes to create a new user after installation. It is specially useful on headless installations, when upgrading to a different server, but keeping old credentials. It does not change any defaults, so if no flag is passed, it still creates the new user. --------- Co-authored-by: Stanislas Lange --- README.md | 1 + openvpn-install.sh | 11 ++++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 4ff0a04..f096531 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,7 @@ If you want to customise your installation, you can export them or specify them - `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) If the server is behind NAT, you can specify its endpoint with the `ENDPOINT` variable. If the endpoint is the public IP address which it is behind, you can use `ENDPOINT=$(curl -4 ifconfig.co)` (the script will default to this). The endpoint can be an IPv4 or a domain. diff --git a/openvpn-install.sh b/openvpn-install.sh index 02a57e8..7578f61 100755 --- a/openvpn-install.sh +++ b/openvpn-install.sh @@ -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) @@ -1517,9 +1518,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 From 9175c2c221532b4444131caf3f95f07b900bedb2 Mon Sep 17 00:00:00 2001 From: Stanislas Date: Sat, 13 Dec 2025 20:18:07 +0100 Subject: [PATCH 09/10] feat: support headless client revocation by name (#1387) Add support for revoking clients by setting the CLIENT environment variable directly with the client name, in addition to the existing CLIENTNUMBER support (from https://github.com/angristan/openvpn-install/pull/1328) This makes headless revocation more user-friendly as users no longer need to know the client's index number. --- README.md | 22 ++++++++++++++++++++++ openvpn-install.sh | 9 +++++++++ test/server-entrypoint.sh | 14 +++----------- 3 files changed, 34 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index f096531..b3b34e3 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,28 @@ export PASS="1" # set to "2" for a password-protected client, and set PASSPHRASE ./openvpn-install.sh ``` +### 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 `2` 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="2" +export CLIENT="foo" +./openvpn-install.sh +``` + +Alternatively, you can use the client number: + +```bash +#!/bin/bash +export MENU_OPTION="2" +export CLIENTNUMBER="1" # Revokes the first client in the list +./openvpn-install.sh +``` + ## Features - Installs and configures a ready-to-use OpenVPN server diff --git a/openvpn-install.sh b/openvpn-install.sh index 7578f61..b3c9aa0 100755 --- a/openvpn-install.sh +++ b/openvpn-install.sh @@ -1627,6 +1627,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 diff --git a/test/server-entrypoint.sh b/test/server-entrypoint.sh index b8152e5..a909836 100755 --- a/test/server-entrypoint.sh +++ b/test/server-entrypoint.sh @@ -501,19 +501,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=3 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=3 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" From d8aa6256398d19474ed4b6aab4ee421060ccd7ab Mon Sep 17 00:00:00 2001 From: Stanislas Date: Sat, 13 Dec 2025 20:49:40 +0100 Subject: [PATCH 10/10] feat: add native firewalld support (#1388) ## Summary - Add native firewalld support for RHEL/Fedora/CentOS systems - When firewalld is active, use `firewall-cmd --permanent` instead of raw iptables - Rules persist across `firewall-cmd --reload` - Fall back to iptables when firewalld is not active - Add `After=firewalld.service` to iptables systemd unit for safety ## Changes **Install:** Detect firewalld, use `firewall-cmd` to add port, masquerade, and rich rules. Fall back to iptables if inactive. **Uninstall:** Detect which method was used and clean up accordingly. **Tests:** Add `fedora-42-firewalld` CI test with firewalld enabled. --- Closes https://github.com/angristan/openvpn-install/issues/356 Closes https://github.com/angristan/openvpn-install/pull/1200 --- .github/workflows/docker-test.yml | 10 ++++ FAQ.md | 4 +- README.md | 4 +- openvpn-install.sh | 86 +++++++++++++++++++---------- test/Dockerfile.server | 10 ++++ test/server-entrypoint.sh | 90 +++++++++++++++++++++++-------- 6 files changed, 148 insertions(+), 56 deletions(-) diff --git a/.github/workflows/docker-test.yml b/.github/workflows/docker-test.yml index 8908876..94c90eb 100644 --- a/.github/workflows/docker-test.yml +++ b/.github/workflows/docker-test.yml @@ -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 . diff --git a/FAQ.md b/FAQ.md index f05bfdb..ef94482 100644 --- a/FAQ.md +++ b/FAQ.md @@ -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` diff --git a/README.md b/README.md index b3b34e3..f3f9675 100644 --- a/README.md +++ b/README.md @@ -149,8 +149,8 @@ export CLIENTNUMBER="1" # Revokes the first client in the list - 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 diff --git a/openvpn-install.sh b/openvpn-install.sh index b3c9aa0..97fe45b 100755 --- a/openvpn-install.sh +++ b/openvpn-install.sh @@ -1419,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 @@ -1473,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 @@ -2057,15 +2076,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 diff --git a/test/Dockerfile.server b/test/Dockerfile.server index 6f9438c..e27a3c8 100644 --- a/test/Dockerfile.server +++ b/test/Dockerfile.server @@ -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 diff --git a/test/server-entrypoint.sh b/test/server-entrypoint.sh index a909836..4ea0bfc 100755 --- a/test/server-entrypoint.sh +++ b/test/server-entrypoint.sh @@ -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)) @@ -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