diff --git a/.github/workflows/docker-test.yml b/.github/workflows/docker-test.yml index f1e83ab..94c90eb 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 @@ -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/.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 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/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 4c6ed27..7a9825b 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 @@ -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 diff --git a/openvpn-install.sh b/openvpn-install.sh index 5332ab6..54b0e62 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 @@ -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 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/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..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)) @@ -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 ==="