From df242ee069e06b312c6af0b06487d108decd24fc Mon Sep 17 00:00:00 2001 From: Stanislas Date: Thu, 18 Dec 2025 17:20:28 +0100 Subject: [PATCH] feat: add peer-fingerprint authentication mode (OpenVPN 2.6+) (#1437) ## Summary Implements support for OpenVPN's `--peer-fingerprint` option, enabling PKI-less authentication using SHA256 certificate fingerprints instead of a CA chain. Closes #1361 ## Changes - Add `--auth-mode` option (`pki` or `fingerprint`) for install command - Use Easy-RSA's `self-sign-server` and `self-sign-client` commands for fingerprint mode - Server stores client fingerprints in `` block in `server.conf` - Clients verify server using `peer-fingerprint` directive instead of CA - Revocation removes fingerprint from config and reloads OpenVPN (instant effect) - Version check ensures OpenVPN 2.6+ when fingerprint mode is selected ## Usage ```bash # Interactive mode prompts for auth mode choice # CLI mode ./openvpn-install.sh install --auth-mode fingerprint ``` ## Comparison | Aspect | PKI Mode | Fingerprint Mode | |--------|----------|------------------| | Server cert | CA-signed | Self-signed | | Client cert | CA-signed | Self-signed | | Revocation | CRL-based | Remove fingerprint | | OpenVPN | Any version | 2.6.0+ required | | Best for | Large deployments | Small/home setups | --- .github/workflows/docker-test.yml | 10 + README.md | 41 ++++ openvpn-install.sh | 349 ++++++++++++++++++++++++------ test/server-entrypoint.sh | 179 ++++++++++----- 4 files changed, 465 insertions(+), 114 deletions(-) diff --git a/.github/workflows/docker-test.yml b/.github/workflows/docker-test.yml index 4cd15cd..60759f6 100644 --- a/.github/workflows/docker-test.yml +++ b/.github/workflows/docker-test.yml @@ -118,6 +118,15 @@ jobs: name: tls-crypt-v2 sig: crypt-v2 key_file: tls-crypt-v2.key + # Test peer-fingerprint authentication mode (OpenVPN 2.6+) + - os: + name: ubuntu-24.04-fingerprint + image: ubuntu:24.04 + auth_mode: fingerprint + tls: + name: tls-crypt-v2 + sig: crypt-v2 + key_file: tls-crypt-v2.key name: ${{ matrix.os.name }} steps: @@ -166,6 +175,7 @@ jobs: -e TLS_SIG=${{ matrix.tls.sig }} \ -e TLS_KEY_FILE=${{ matrix.tls.key_file }} \ -e CLIENT_IPV6=${{ matrix.os.client_ipv6 && 'y' || 'n' }} \ + -e AUTH_MODE=${{ matrix.os.auth_mode || 'pki' }} \ openvpn-server - name: Wait for server installation and startup diff --git a/README.md b/README.md index 77a09df..274bed3 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,7 @@ That said, OpenVPN still makes sense when you need: - Randomised server certificate name - Choice to protect clients with a password (private key encryption) - Option to allow multiple devices to use the same client profile simultaneously (disables persistent IP addresses) +- **Peer fingerprint authentication** (OpenVPN 2.6+): Simplified WireGuard-like authentication without a CA - Many other little things! ## Compatibility @@ -317,6 +318,7 @@ The `install` command supports many options for customization: - `--rsa-bits <2048|3072|4096>` - RSA key size (default: `2048`) - `--hmac ` - HMAC algorithm (default: `SHA256`). Options: `SHA256`, `SHA384`, `SHA512` - `--tls-sig ` - TLS mode (default: `crypt-v2`). Options: `crypt-v2`, `crypt`, `auth` +- `--auth-mode ` - Authentication mode (default: `pki`). Options: `pki` (CA-based), `fingerprint` (peer-fingerprint, requires OpenVPN 2.6+) - `--tls-version-min <1.2|1.3>` - Minimum TLS version (default: `1.2`) - `--tls-ciphersuites ` - TLS 1.3 cipher suites, colon-separated (default: `TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256:TLS_CHACHA20_POLY1305_SHA256`) - `--tls-groups ` - Key exchange groups, colon-separated (default: `X25519:prime256v1:secp384r1:secp521r1`) @@ -470,6 +472,45 @@ It defaults to ECDSA with `prime256v1`. OpenVPN uses `SHA-256` as the signature hash by default, and so does the script. It provides no other choice as of now. +### Authentication Mode + +The script supports two authentication modes: + +#### PKI Mode (default) + +Traditional Certificate Authority (CA) based authentication. The server and all clients have certificates signed by the same CA. Client revocation is handled via Certificate Revocation Lists (CRL). + +This is the recommended mode for larger deployments where you need: + +- Centralized certificate management +- Standard CRL-based revocation +- Compatibility with all OpenVPN versions + +#### Peer Fingerprint Mode (OpenVPN 2.6+) + +A simplified WireGuard-like authentication model using SHA256 certificate fingerprints instead of a CA chain. Each peer (server and clients) has a self-signed certificate, and peers authenticate each other by verifying fingerprints. + +```bash +# Install with fingerprint mode +./openvpn-install.sh install --auth-mode fingerprint +``` + +Benefits: + +- Simpler setup: No CA infrastructure needed +- Easier to understand: Similar to SSH's `known_hosts` model +- Ideal for small setups: Home networks, labs, small teams + +How it works: + +1. Server generates a self-signed certificate and stores its fingerprint +2. Each client generates a self-signed certificate +3. Client fingerprints are added to the server's `` block +4. Clients verify the server using the server's fingerprint +5. Revocation removes the fingerprint from the server config (no CRL needed) + +Trade-off: Revoking a client requires reloading OpenVPN (fingerprints are in server.conf). In PKI mode, the CRL file is re-read automatically on new connections. + ### Data channel > [!NOTE] diff --git a/openvpn-install.sh b/openvpn-install.sh index a5aaeb7..119bf38 100755 --- a/openvpn-install.sh +++ b/openvpn-install.sh @@ -239,6 +239,8 @@ show_install_help() { (default: X25519:prime256v1:secp384r1:secp521r1) --hmac HMAC algorithm: SHA256, SHA384, SHA512 (default: SHA256) --tls-sig TLS mode: crypt-v2, crypt, auth (default: crypt-v2) + --auth-mode Auth mode: pki, fingerprint (default: pki) + fingerprint requires OpenVPN 2.6+ --server-cert-days Server cert validity in days (default: 3650) Other Options: @@ -477,6 +479,9 @@ readonly TLS_VERSIONS=("1.2" "1.3") # TLS signature modes (use strings) readonly TLS_SIG_MODES=("crypt-v2" "crypt" "auth") +# Authentication modes: pki (CA-based) or fingerprint (peer-fingerprint, OpenVPN 2.6+) +readonly AUTH_MODES=("pki" "fingerprint") + # HMAC algorithms readonly HMAC_ALGS=("SHA256" "SHA384" "SHA512") @@ -516,6 +521,7 @@ set_installation_defaults() { TLS_GROUPS="${TLS_GROUPS:-X25519:prime256v1:secp384r1:secp521r1}" HMAC_ALG="${HMAC_ALG:-SHA256}" TLS_SIG="${TLS_SIG:-crypt-v2}" + AUTH_MODE="${AUTH_MODE:-pki}" # Derive CC_CIPHER from CERT_TYPE if not set if [[ -z $CC_CIPHER ]]; then @@ -536,6 +542,18 @@ set_installation_defaults() { # are computed in prepare_network_config() which is called after validation } +# Version comparison: returns 0 if version1 >= version2 +version_ge() { + local ver1="$1" ver2="$2" + # Use sort -V for version comparison + [[ "$(printf '%s\n%s' "$ver1" "$ver2" | sort -V | head -n1)" == "$ver2" ]] +} + +# Get installed OpenVPN version (e.g., "2.6.12") +get_openvpn_version() { + openvpn --version 2>/dev/null | head -1 | awk '{print $2}' +} + # Validation functions validate_port() { local port="$1" @@ -642,6 +660,21 @@ validate_configuration() { *) log_fatal "Invalid TLS signature mode: $TLS_SIG. Must be 'crypt-v2', 'crypt', or 'auth'." ;; esac + # Validate AUTH_MODE + case "$AUTH_MODE" in + pki | fingerprint) ;; + *) log_fatal "Invalid auth mode: $AUTH_MODE. Must be 'pki' or 'fingerprint'." ;; + esac + + # Fingerprint mode requires OpenVPN 2.6+ + if [[ $AUTH_MODE == "fingerprint" ]]; then + local openvpn_ver + openvpn_ver=$(get_openvpn_version) + if [[ -n "$openvpn_ver" ]] && ! version_ge "$openvpn_ver" "2.6.0"; then + log_fatal "Fingerprint mode requires OpenVPN 2.6.0 or later. Installed version: $openvpn_ver" + fi + fi + # Validate PORT if ! [[ "$PORT" =~ ^[0-9]+$ ]] || [[ "$PORT" -lt 1 ]] || [[ "$PORT" -gt 65535 ]]; then log_fatal "Invalid port: $PORT. Must be a number between 1 and 65535." @@ -1034,6 +1067,14 @@ cmd_install() { esac shift 2 ;; + --auth-mode) + [[ -z "${2:-}" ]] && log_fatal "--auth-mode requires an argument" + case "$2" in + pki | fingerprint) AUTH_MODE="$2" ;; + *) log_fatal "Invalid auth mode: $2. Use 'pki' or 'fingerprint'." ;; + esac + shift 2 + ;; --server-cert-days) [[ -z "${2:-}" ]] && log_fatal "--server-cert-days requires an argument" validate_positive_int "$2" "server-cert-days" @@ -1251,6 +1292,7 @@ cmd_client_add() { fi newClient + exit 0 } # Handle client list command @@ -2336,6 +2378,30 @@ function installQuestions() { done fi log_menu "" + log_prompt "Choose the authentication mode:" + log_menu " 1) PKI (Certificate Authority) - Traditional CA-based authentication (recommended for larger setups)" + log_menu " 2) Peer Fingerprint - Simplified WireGuard-like authentication using certificate fingerprints" + log_menu " Note: Fingerprint mode requires OpenVPN 2.6+ and is ideal for small/home setups" + local auth_mode_choice + until [[ $auth_mode_choice =~ ^[1-2]$ ]]; do + read -rp "Authentication mode [1-2]: " -e -i 1 auth_mode_choice + done + case $auth_mode_choice in + 1) + AUTH_MODE="pki" + ;; + 2) + AUTH_MODE="fingerprint" + # Verify OpenVPN 2.6+ is available for fingerprint mode + local openvpn_ver + openvpn_ver=$(get_openvpn_version) + if [[ -n "$openvpn_ver" ]] && ! version_ge "$openvpn_ver" "2.6.0"; then + log_warn "OpenVPN $openvpn_ver detected. Fingerprint mode requires 2.6.0+." + log_warn "OpenVPN 2.6+ will be installed during setup." + fi + ;; + esac + log_menu "" log_prompt "Do you want to customize encryption settings?" log_prompt "Unless you know what you're doing, you should stick with the default parameters provided by the script." log_prompt "Note that whatever you choose, all the choices presented in the script are safe (unlike OpenVPN's defaults)." @@ -2541,6 +2607,7 @@ function installOpenVPN() { log_info " DNS=$DNS" [[ -n $MTU ]] && log_info " MTU=$MTU" log_info " MULTI_CLIENT=$MULTI_CLIENT" + log_info " AUTH_MODE=$AUTH_MODE" log_info " CLIENT=$CLIENT" log_info " CLIENT_CERT_DURATION_DAYS=$CLIENT_CERT_DURATION_DAYS" log_info " SERVER_CERT_DURATION_DAYS=$SERVER_CERT_DURATION_DAYS" @@ -2680,15 +2747,34 @@ function installOpenVPN() { # Create the PKI, set up the CA, the DH params and the server certificate log_info "Initializing PKI..." run_cmd_fatal "Initializing PKI" ./easyrsa init-pki - export EASYRSA_CA_EXPIRE=$DEFAULT_CERT_VALIDITY_DURATION_DAYS - log_info "Building CA..." - run_cmd_fatal "Building CA" ./easyrsa --batch --req-cn="$SERVER_CN" build-ca nopass - export EASYRSA_CERT_EXPIRE=${SERVER_CERT_DURATION_DAYS:-$DEFAULT_CERT_VALIDITY_DURATION_DAYS} - log_info "Building server certificate..." - run_cmd_fatal "Building server certificate" ./easyrsa --batch build-server-full "$SERVER_NAME" nopass - export EASYRSA_CRL_DAYS=$DEFAULT_CRL_VALIDITY_DURATION_DAYS - run_cmd_fatal "Generating CRL" ./easyrsa gen-crl + if [[ $AUTH_MODE == "pki" ]]; then + # Traditional PKI mode with CA + export EASYRSA_CA_EXPIRE=$DEFAULT_CERT_VALIDITY_DURATION_DAYS + log_info "Building CA..." + run_cmd_fatal "Building CA" ./easyrsa --batch --req-cn="$SERVER_CN" build-ca nopass + + export EASYRSA_CERT_EXPIRE=${SERVER_CERT_DURATION_DAYS:-$DEFAULT_CERT_VALIDITY_DURATION_DAYS} + log_info "Building server certificate..." + run_cmd_fatal "Building server certificate" ./easyrsa --batch build-server-full "$SERVER_NAME" nopass + export EASYRSA_CRL_DAYS=$DEFAULT_CRL_VALIDITY_DURATION_DAYS + run_cmd_fatal "Generating CRL" ./easyrsa gen-crl + else + # Fingerprint mode with self-signed certificates (OpenVPN 2.6+) + log_info "Building self-signed server certificate for fingerprint mode..." + export EASYRSA_CERT_EXPIRE=${SERVER_CERT_DURATION_DAYS:-$DEFAULT_CERT_VALIDITY_DURATION_DAYS} + run_cmd_fatal "Building self-signed server certificate" ./easyrsa --batch self-sign-server "$SERVER_NAME" nopass + + # Extract and store server fingerprint + SERVER_FINGERPRINT=$(openssl x509 -in "pki/issued/$SERVER_NAME.crt" -fingerprint -sha256 -noout | cut -d'=' -f2) + if [[ -z $SERVER_FINGERPRINT ]]; then + log_error "Failed to extract server certificate fingerprint" + exit 1 + fi + mkdir -p /etc/openvpn/server + echo "$SERVER_FINGERPRINT" >/etc/openvpn/server/server-fingerprint + log_info "Server fingerprint: $SERVER_FINGERPRINT" + fi log_info "Generating TLS key..." case $TLS_SIG in @@ -2705,19 +2791,32 @@ function installOpenVPN() { run_cmd_fatal "Generating tls-auth key" openvpn --genkey secret /etc/openvpn/server/tls-auth.key ;; esac + # Store auth mode for later use + echo "$AUTH_MODE" >AUTH_MODE_GENERATED else # If easy-rsa is already installed, grab the generated SERVER_NAME # for client configs cd /etc/openvpn/server/easy-rsa/ || return SERVER_NAME=$(cat SERVER_NAME_GENERATED) + # Read stored auth mode + if [[ -f AUTH_MODE_GENERATED ]]; then + AUTH_MODE=$(cat AUTH_MODE_GENERATED) + else + # Default to pki for existing installations + AUTH_MODE="pki" + fi fi # Move all the generated files log_info "Copying certificates..." - run_cmd_fatal "Copying certificates to /etc/openvpn/server" cp pki/ca.crt pki/private/ca.key "pki/issued/$SERVER_NAME.crt" "pki/private/$SERVER_NAME.key" /etc/openvpn/server/easy-rsa/pki/crl.pem /etc/openvpn/server - - # Make cert revocation list readable for non-root - run_cmd "Setting CRL permissions" chmod 644 /etc/openvpn/server/crl.pem + if [[ $AUTH_MODE == "pki" ]]; then + run_cmd_fatal "Copying certificates to /etc/openvpn/server" cp pki/ca.crt pki/private/ca.key "pki/issued/$SERVER_NAME.crt" "pki/private/$SERVER_NAME.key" /etc/openvpn/server/easy-rsa/pki/crl.pem /etc/openvpn/server + # Make cert revocation list readable for non-root + run_cmd "Setting CRL permissions" chmod 644 /etc/openvpn/server/crl.pem + else + # Fingerprint mode: only copy server cert and key (no CA or CRL) + run_cmd_fatal "Copying certificates to /etc/openvpn/server" cp "pki/issued/$SERVER_NAME.crt" "pki/private/$SERVER_NAME.key" /etc/openvpn/server + fi # Generate server.conf log_info "Generating server configuration..." @@ -2931,9 +3030,13 @@ topology subnet" >>/etc/openvpn/server/server.conf ;; esac - echo "crl-verify crl.pem -ca ca.crt -cert $SERVER_NAME.crt + # Common server config options + # PKI mode adds crl-verify, ca, and remote-cert-tls + # Fingerprint mode: block is added when first client is created + { + [[ $AUTH_MODE == "pki" ]] && echo "crl-verify crl.pem +ca ca.crt" + echo "cert $SERVER_NAME.crt key $SERVER_NAME.key auth $HMAC_ALG cipher $CIPHER @@ -2941,14 +3044,15 @@ ignore-unknown-option data-ciphers data-ciphers $CIPHER ncp-ciphers $CIPHER tls-server -tls-version-min $TLS_VERSION_MIN -remote-cert-tls client -tls-cipher $CC_CIPHER +tls-version-min $TLS_VERSION_MIN" + [[ $AUTH_MODE == "pki" ]] && echo "remote-cert-tls client" + echo "tls-cipher $CC_CIPHER tls-ciphersuites $TLS13_CIPHERSUITES client-config-dir ccd status /var/log/openvpn/status.log management /var/run/openvpn/server.sock unix -verb 3" >>/etc/openvpn/server/server.conf +verb 3" + } >>/etc/openvpn/server/server.conf # Create management socket directory run_cmd_fatal "Creating management socket directory" mkdir -p /var/run/openvpn @@ -3028,7 +3132,11 @@ verb 3" >>/etc/openvpn/server/server.conf run_cmd "Reloading systemd" systemctl daemon-reload run_cmd "Enabling OpenVPN service" systemctl enable openvpn-server@server - run_cmd "Starting OpenVPN service" systemctl restart openvpn-server@server + # In fingerprint mode, delay service start until first client is created + # (OpenVPN requires at least one fingerprint or a CA to start) + if [[ $AUTH_MODE == "pki" ]]; then + run_cmd "Starting OpenVPN service" systemctl restart openvpn-server@server + fi if [[ $DNS == "unbound" ]]; then installUnbound @@ -3207,15 +3315,19 @@ WantedBy=multi-user.target" >/etc/systemd/system/iptables-openvpn.service elif [[ $PROTOCOL == 'tcp6' ]]; then echo "proto tcp6-client" >>/etc/openvpn/server/client-template.txt fi - echo "remote $IP $PORT + # Common client template options + # PKI mode adds remote-cert-tls and verify-x509-name + # Fingerprint mode adds peer-fingerprint when generating client config + { + echo "remote $IP $PORT dev tun resolv-retry infinite nobind persist-key -persist-tun -remote-cert-tls server -verify-x509-name $SERVER_NAME name -auth $HMAC_ALG +persist-tun" + [[ $AUTH_MODE == "pki" ]] && echo "remote-cert-tls server +verify-x509-name $SERVER_NAME name" + echo "auth $HMAC_ALG auth-nocache cipher $CIPHER ignore-unknown-option data-ciphers @@ -3227,7 +3339,8 @@ tls-cipher $CC_CIPHER tls-ciphersuites $TLS13_CIPHERSUITES ignore-unknown-option block-outside-dns setenv opt block-outside-dns # Prevent Windows 10 DNS leak -verb 3" >>/etc/openvpn/server/client-template.txt +verb 3" + } >>/etc/openvpn/server/client-template.txt if [[ -n $MTU ]]; then echo "tun-mtu $MTU" >>/etc/openvpn/server/client-template.txt @@ -3235,10 +3348,18 @@ verb 3" >>/etc/openvpn/server/client-template.txt # Generate the custom client.ovpn if [[ $NEW_CLIENT == "n" ]]; then - log_info "No clients added. To add clients, simply run the script again." + if [[ $AUTH_MODE == "fingerprint" ]]; then + log_info "No clients added. OpenVPN will not start until you add at least one client." + else + log_info "No clients added. To add clients, simply run the script again." + fi else log_info "Generating first client certificate..." newClient + # In fingerprint mode, start service now that we have at least one fingerprint + if [[ $AUTH_MODE == "fingerprint" ]]; then + run_cmd "Starting OpenVPN service" systemctl restart openvpn-server@server + fi log_success "If you want to add more clients, you simply need to run this script another time!" fi } @@ -3333,6 +3454,12 @@ function generateClientConfig() { local client="$1" local filepath="$2" + # Read auth mode + local auth_mode="pki" + if [[ -f /etc/openvpn/server/easy-rsa/AUTH_MODE_GENERATED ]]; then + auth_mode=$(cat /etc/openvpn/server/easy-rsa/AUTH_MODE_GENERATED) + fi + # Determine if we use tls-crypt-v2, tls-crypt, or tls-auth local tls_sig="" if grep -qs "^tls-crypt-v2" /etc/openvpn/server/server.conf; then @@ -3346,9 +3473,25 @@ function generateClientConfig() { # Generate the custom client.ovpn run_cmd "Creating client config" cp /etc/openvpn/server/client-template.txt "$filepath" { - echo "" - cat "/etc/openvpn/server/easy-rsa/pki/ca.crt" - echo "" + if [[ $auth_mode == "pki" ]]; then + # PKI mode: include CA certificate + echo "" + cat "/etc/openvpn/server/easy-rsa/pki/ca.crt" + echo "" + else + # Fingerprint mode: use server fingerprint instead of CA + local server_fingerprint + if [[ ! -f /etc/openvpn/server/server-fingerprint ]]; then + log_error "Server fingerprint file not found" + exit 1 + fi + server_fingerprint=$(cat /etc/openvpn/server/server-fingerprint) + if [[ -z $server_fingerprint ]]; then + log_error "Server fingerprint is empty" + exit 1 + fi + echo "peer-fingerprint $server_fingerprint" + fi echo "" awk '/BEGIN/,/END CERTIFICATE/' "/etc/openvpn/server/easy-rsa/pki/issued/$client.crt" @@ -3677,45 +3820,96 @@ function newClient() { done fi - CLIENTEXISTS=$(tail -n +2 /etc/openvpn/server/easy-rsa/pki/index.txt | grep -E "^V" | grep -c -E "/CN=$CLIENT\$") + cd /etc/openvpn/server/easy-rsa/ || return + + # Read auth mode + if [[ -f AUTH_MODE_GENERATED ]]; then + AUTH_MODE=$(cat AUTH_MODE_GENERATED) + else + AUTH_MODE="pki" + fi + + # Check if client already exists + if [[ -f pki/index.txt ]]; then + CLIENTEXISTS=$(tail -n +2 /etc/openvpn/server/easy-rsa/pki/index.txt | grep -E "^V" | grep -c -E "/CN=$CLIENT\$") + else + CLIENTEXISTS=0 + fi + if [[ $CLIENTEXISTS != '0' ]]; then log_error "The specified client CN was already found in easy-rsa, please choose another name." exit 1 - else - cd /etc/openvpn/server/easy-rsa/ || return - log_info "Generating client certificate..." - export EASYRSA_CERT_EXPIRE=$CLIENT_CERT_DURATION_DAYS - case $PASS in - 1) - run_cmd_fatal "Building client certificate" ./easyrsa --batch build-client-full "$CLIENT" nopass - ;; - 2) - 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 - log_success "Client $CLIENT added and is valid for $CLIENT_CERT_DURATION_DAYS days." fi + log_info "Generating client certificate..." + export EASYRSA_CERT_EXPIRE=$CLIENT_CERT_DURATION_DAYS + + # Determine easyrsa command based on auth mode + local easyrsa_cmd cert_desc + if [[ $AUTH_MODE == "pki" ]]; then + easyrsa_cmd="build-client-full" + cert_desc="client certificate" + else + easyrsa_cmd="self-sign-client" + cert_desc="self-signed client certificate" + fi + + case $PASS in + 1) + run_cmd_fatal "Building $cert_desc" ./easyrsa --batch "$easyrsa_cmd" "$CLIENT" nopass + ;; + 2) + if [[ -z "$PASSPHRASE" ]]; then + log_warn "You will be asked for the client password below" + if ! ./easyrsa --batch "$easyrsa_cmd" "$CLIENT"; then + log_fatal "Building $cert_desc failed" + fi + else + log_info "Using provided passphrase for client certificate" + export EASYRSA_PASSPHRASE="$PASSPHRASE" + run_cmd_fatal "Building $cert_desc" ./easyrsa --batch --passin=env:EASYRSA_PASSPHRASE --passout=env:EASYRSA_PASSPHRASE "$easyrsa_cmd" "$CLIENT" + unset EASYRSA_PASSPHRASE + fi + ;; + esac + + # Fingerprint mode: register client fingerprint with server + if [[ $AUTH_MODE == "fingerprint" ]]; then + CLIENT_FINGERPRINT=$(openssl x509 -in "pki/issued/$CLIENT.crt" -fingerprint -sha256 -noout | cut -d'=' -f2) + if [[ -z $CLIENT_FINGERPRINT ]]; then + log_error "Failed to extract client certificate fingerprint" + exit 1 + fi + log_info "Client fingerprint: $CLIENT_FINGERPRINT" + + # Add fingerprint to server.conf's block + # Create the block if this is the first client + if ! grep -q '' /etc/openvpn/server/server.conf; then + echo "# Client fingerprints are listed below + +# $CLIENT +$CLIENT_FINGERPRINT +" >>/etc/openvpn/server/server.conf + else + # Insert comment and fingerprint before closing tag + sed -i "/<\/peer-fingerprint>/i # $CLIENT\n$CLIENT_FINGERPRINT" /etc/openvpn/server/server.conf + fi + + # Reload OpenVPN to pick up new fingerprint + log_info "Reloading OpenVPN to apply new fingerprint..." + if systemctl is-active --quiet openvpn-server@server; then + systemctl reload openvpn-server@server 2>/dev/null || systemctl restart openvpn-server@server + fi + fi + + log_success "Client $CLIENT added and is valid for $CLIENT_CERT_DURATION_DAYS days." + # Write the .ovpn config file with proper path and permissions writeClientConfig "$CLIENT" log_menu "" log_success "The configuration file has been written to $GENERATED_CONFIG_PATH." log_info "Download the .ovpn file and import it in your OpenVPN client." - - exit 0 } function revokeClient() { @@ -3724,13 +3918,45 @@ function revokeClient() { selectClient cd /etc/openvpn/server/easy-rsa/ || return + + # Read auth mode + local auth_mode="pki" + if [[ -f AUTH_MODE_GENERATED ]]; then + auth_mode=$(cat AUTH_MODE_GENERATED) + fi + log_info "Revoking certificate for $CLIENT..." - run_cmd_fatal "Revoking certificate" ./easyrsa --batch revoke-issued "$CLIENT" - regenerateCRL + + if [[ $auth_mode == "pki" ]]; then + # PKI mode: use Easy-RSA revocation and CRL + run_cmd_fatal "Revoking certificate" ./easyrsa --batch revoke-issued "$CLIENT" + regenerateCRL + run_cmd "Backing up index" cp /etc/openvpn/server/easy-rsa/pki/index.txt{,.bk} + else + # Fingerprint mode: remove fingerprint from server.conf and delete cert files + log_info "Removing client fingerprint from server configuration..." + + # Remove comment line and fingerprint line below it from server.conf + sed -i "/^# $CLIENT\$/{N;d;}" /etc/openvpn/server/server.conf + + # Remove client certificate and key + rm -f "pki/issued/$CLIENT.crt" "pki/private/$CLIENT.key" + + # Mark as revoked in index.txt if it exists (for client listing) + if [[ -f pki/index.txt ]]; then + sed -i "s|^V\(.*\)/CN=$CLIENT\$|R\1/CN=$CLIENT|" pki/index.txt + fi + + # Reload OpenVPN to apply fingerprint removal + log_info "Reloading OpenVPN to apply fingerprint removal..." + if systemctl is-active --quiet openvpn-server@server; then + systemctl reload openvpn-server@server 2>/dev/null || systemctl restart openvpn-server@server + fi + fi + run_cmd "Removing client config from /home" find /home/ -maxdepth 2 -name "$CLIENT.ovpn" -delete run_cmd "Removing client config from /root" rm -f "/root/$CLIENT.ovpn" run_cmd "Removing IP assignment" sed -i "/^$CLIENT,.*/d" /etc/openvpn/server/ipp.txt - run_cmd "Backing up index" cp /etc/openvpn/server/easy-rsa/pki/index.txt{,.bk} # Disconnect the client if currently connected disconnectClient "$CLIENT" @@ -4102,6 +4328,7 @@ function manageMenu() { case $menu_option in 1) newClient + exit 0 ;; 2) listClients diff --git a/test/server-entrypoint.sh b/test/server-entrypoint.sh index 524dffd..3f80b55 100755 --- a/test/server-entrypoint.sh +++ b/test/server-entrypoint.sh @@ -42,6 +42,10 @@ TLS_KEY_FILE="${TLS_KEY_FILE:-tls-crypt-v2.key}" TLS_VERSION_MIN="${TLS_VERSION_MIN:-1.2}" TLS13_CIPHERSUITES="${TLS13_CIPHERSUITES:-TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256:TLS_CHACHA20_POLY1305_SHA256}" +# Authentication mode configuration +# AUTH_MODE: pki (default, CA-based) or fingerprint (peer-fingerprint, OpenVPN 2.6+) +AUTH_MODE="${AUTH_MODE:-pki}" + # Build install command with CLI flags (using array for proper quoting) INSTALL_CMD=(/opt/openvpn-install.sh install) INSTALL_CMD+=(--endpoint openvpn-server) @@ -75,6 +79,12 @@ if [ "$TLS13_CIPHERSUITES" != "TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256:TLS echo "Testing TLS 1.3 ciphersuites: $TLS13_CIPHERSUITES" fi +# Add auth mode if non-default +if [ "$AUTH_MODE" != "pki" ]; then + INSTALL_CMD+=(--auth-mode "$AUTH_MODE") + echo "Testing authentication mode: $AUTH_MODE" +fi + echo "Running OpenVPN install script..." echo "Command: ${INSTALL_CMD[*]}" # Run in subshell because the script calls 'exit 0' after generating client config @@ -104,16 +114,26 @@ fi # Verify all expected files were created echo "Verifying installation..." MISSING_FILES=0 -# Build list of required files +# Build list of required files based on auth mode 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 ) +if [ "$AUTH_MODE" = "pki" ]; then + # PKI mode requires CA and CRL files + REQUIRED_FILES+=( + /etc/openvpn/server/ca.crt + /etc/openvpn/server/ca.key + /etc/openvpn/server/crl.pem + /etc/openvpn/server/easy-rsa/pki/ca.crt + ) +else + # Fingerprint mode requires server fingerprint file + REQUIRED_FILES+=( + /etc/openvpn/server/server-fingerprint + ) +fi # Only check for iptables script if firewalld and nftables are not active if ! systemctl is-active --quiet firewalld && ! systemctl is-active --quiet nftables; then REQUIRED_FILES+=(/etc/iptables/add-openvpn-rules.sh) @@ -197,13 +217,16 @@ sed -i 's/^remote .*/remote openvpn-server 1194/' /shared/client.ovpn echo "Client config copied to /shared/client.ovpn" # Write VPN network info to shared volume for client tests -echo "VPN_SUBNET_IPV4=$VPN_SUBNET_IPV4" >/shared/vpn-config.env -echo "VPN_GATEWAY=$VPN_GATEWAY" >>/shared/vpn-config.env -echo "CLIENT_IPV6=$CLIENT_IPV6" >>/shared/vpn-config.env -if [ "$CLIENT_IPV6" = "y" ]; then - echo "VPN_SUBNET_IPV6=$VPN_SUBNET_IPV6" >>/shared/vpn-config.env - echo "VPN_GATEWAY_IPV6=$VPN_GATEWAY_IPV6" >>/shared/vpn-config.env -fi +{ + echo "VPN_SUBNET_IPV4=$VPN_SUBNET_IPV4" + echo "VPN_GATEWAY=$VPN_GATEWAY" + echo "CLIENT_IPV6=$CLIENT_IPV6" + echo "AUTH_MODE=$AUTH_MODE" + if [ "$CLIENT_IPV6" = "y" ]; then + echo "VPN_SUBNET_IPV6=$VPN_SUBNET_IPV6" + echo "VPN_GATEWAY_IPV6=$VPN_GATEWAY_IPV6" + fi +} >/shared/vpn-config.env echo "VPN config written to /shared/vpn-config.env" # ===================================================== @@ -396,12 +419,14 @@ else exit 1 fi -# Verify CRL was updated -if [ -f /etc/openvpn/server/crl.pem ]; then - echo "PASS: CRL file exists" -else - echo "FAIL: CRL file missing after renewal" - exit 1 +# Verify CRL was updated (PKI mode only) +if [ "$AUTH_MODE" = "pki" ]; then + if [ -f /etc/openvpn/server/crl.pem ]; then + echo "PASS: CRL file exists" + else + echo "FAIL: CRL file missing after renewal" + exit 1 + fi fi # Update shared client config with renewed certificate @@ -815,13 +840,25 @@ else exit 1 fi -# Verify certificate is marked as revoked in index.txt -if tail -n +2 /etc/openvpn/server/easy-rsa/pki/index.txt | grep -q "^R.*CN=$REVOKE_CLIENT\$"; then - echo "PASS: Certificate marked as revoked in index.txt" +# Verify revocation was applied correctly +if [ "$AUTH_MODE" = "pki" ]; then + # PKI mode: verify certificate is marked as revoked in index.txt + if tail -n +2 /etc/openvpn/server/easy-rsa/pki/index.txt | grep -q "^R.*CN=$REVOKE_CLIENT\$"; then + echo "PASS: Certificate marked as revoked in index.txt" + else + echo "FAIL: Certificate not marked as revoked" + cat /etc/openvpn/server/easy-rsa/pki/index.txt + exit 1 + fi else - echo "FAIL: Certificate not marked as revoked" - cat /etc/openvpn/server/easy-rsa/pki/index.txt - exit 1 + # Fingerprint mode: verify fingerprint was removed from server.conf + if ! grep -q "# $REVOKE_CLIENT\$" /etc/openvpn/server/server.conf; then + echo "PASS: Client fingerprint removed from server.conf" + else + echo "FAIL: Client fingerprint still present in server.conf" + grep "$REVOKE_CLIENT" /etc/openvpn/server/server.conf || true + exit 1 + fi fi # Wait for client to confirm it was disconnected by the revoke @@ -883,13 +920,26 @@ else 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" +# Verify certificate count (varies by auth mode) +if [ "$AUTH_MODE" = "pki" ]; then + # PKI mode: 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 else - echo "FAIL: List does not show correct certificate count" - cat "$LIST_OUTPUT" - exit 1 + # Fingerprint mode: 2 certs (testclient valid, revoketest revoked) + # In fingerprint mode, renewal doesn't create a separate revoked entry + if grep -q "Found [23] client certificate(s)" "$LIST_OUTPUT"; then + echo "PASS: List shows correct certificate count for fingerprint mode" + else + echo "FAIL: List does not show correct certificate count" + cat "$LIST_OUTPUT" + exit 1 + fi fi # Test JSON output @@ -906,14 +956,25 @@ else exit 1 fi -# Verify client count in JSON +# Verify client count in JSON (varies by auth mode) JSON_CLIENT_COUNT=$(jq '.clients | length' "$LIST_JSON_OUTPUT") -if [ "$JSON_CLIENT_COUNT" -eq 3 ]; then - echo "PASS: Client list JSON has correct count ($JSON_CLIENT_COUNT)" +if [ "$AUTH_MODE" = "pki" ]; then + if [ "$JSON_CLIENT_COUNT" -eq 3 ]; then + echo "PASS: Client list JSON has correct count ($JSON_CLIENT_COUNT)" + else + echo "FAIL: Client list JSON has wrong count: $JSON_CLIENT_COUNT (expected 3)" + cat "$LIST_JSON_OUTPUT" + exit 1 + fi else - echo "FAIL: Client list JSON has wrong count: $JSON_CLIENT_COUNT (expected 3)" - cat "$LIST_JSON_OUTPUT" - exit 1 + # Fingerprint mode may have fewer entries + if [ "$JSON_CLIENT_COUNT" -ge 2 ] && [ "$JSON_CLIENT_COUNT" -le 3 ]; then + echo "PASS: Client list JSON has correct count for fingerprint mode ($JSON_CLIENT_COUNT)" + else + echo "FAIL: Client list JSON has wrong count: $JSON_CLIENT_COUNT (expected 2-3)" + cat "$LIST_JSON_OUTPUT" + exit 1 + fi fi # Verify valid client in JSON @@ -955,25 +1016,37 @@ else exit 1 fi -# Verify the new certificate is valid (V) in index.txt -if tail -n +2 /etc/openvpn/server/easy-rsa/pki/index.txt | grep -q "^V.*CN=$REVOKE_CLIENT\$"; then - echo "PASS: New certificate is valid in index.txt" -else - echo "FAIL: New certificate not marked as valid" - cat /etc/openvpn/server/easy-rsa/pki/index.txt - exit 1 -fi +# Verify the new certificate is valid +if [ "$AUTH_MODE" = "pki" ]; then + # PKI mode: verify in index.txt + if tail -n +2 /etc/openvpn/server/easy-rsa/pki/index.txt | grep -q "^V.*CN=$REVOKE_CLIENT\$"; then + echo "PASS: New certificate is valid in index.txt" + else + echo "FAIL: New certificate not marked as valid" + cat /etc/openvpn/server/easy-rsa/pki/index.txt + exit 1 + fi -# Verify there's also a revoked entry (both should exist) -REVOKED_COUNT=$(tail -n +2 /etc/openvpn/server/easy-rsa/pki/index.txt | grep -c "^R.*CN=$REVOKE_CLIENT\$") -VALID_COUNT=$(tail -n +2 /etc/openvpn/server/easy-rsa/pki/index.txt | grep -c "^V.*CN=$REVOKE_CLIENT\$") -echo "Certificates for '$REVOKE_CLIENT': $REVOKED_COUNT revoked, $VALID_COUNT valid" -if [ "$REVOKED_COUNT" -ge 1 ] && [ "$VALID_COUNT" -eq 1 ]; then - echo "PASS: Both revoked and new valid certificate entries exist" + # Verify there's also a revoked entry (both should exist) + REVOKED_COUNT=$(tail -n +2 /etc/openvpn/server/easy-rsa/pki/index.txt | grep -c "^R.*CN=$REVOKE_CLIENT\$") + VALID_COUNT=$(tail -n +2 /etc/openvpn/server/easy-rsa/pki/index.txt | grep -c "^V.*CN=$REVOKE_CLIENT\$") + echo "Certificates for '$REVOKE_CLIENT': $REVOKED_COUNT revoked, $VALID_COUNT valid" + if [ "$REVOKED_COUNT" -ge 1 ] && [ "$VALID_COUNT" -eq 1 ]; then + echo "PASS: Both revoked and new valid certificate entries exist" + else + echo "FAIL: Unexpected certificate state" + cat /etc/openvpn/server/easy-rsa/pki/index.txt + exit 1 + fi else - echo "FAIL: Unexpected certificate state" - cat /etc/openvpn/server/easy-rsa/pki/index.txt - exit 1 + # Fingerprint mode: verify fingerprint was added back to server.conf + if grep -q "# $REVOKE_CLIENT\$" /etc/openvpn/server/server.conf; then + echo "PASS: New client fingerprint added to server.conf" + else + echo "FAIL: New client fingerprint not found in server.conf" + cat /etc/openvpn/server/server.conf | grep -A5 "" || true + exit 1 + fi fi # Copy the new config