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 `<peer-fingerprint>` 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 |
This commit is contained in:
Stanislas
2025-12-18 17:20:28 +01:00
committed by GitHub
parent 13008ef45c
commit df242ee069
4 changed files with 465 additions and 114 deletions

View File

@@ -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

View File

@@ -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 <alg>` - HMAC algorithm (default: `SHA256`). Options: `SHA256`, `SHA384`, `SHA512`
- `--tls-sig <mode>` - TLS mode (default: `crypt-v2`). Options: `crypt-v2`, `crypt`, `auth`
- `--auth-mode <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 <list>` - TLS 1.3 cipher suites, colon-separated (default: `TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256:TLS_CHACHA20_POLY1305_SHA256`)
- `--tls-groups <list>` - 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 `<peer-fingerprint>` 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]

View File

@@ -239,6 +239,8 @@ show_install_help() {
(default: X25519:prime256v1:secp384r1:secp521r1)
--hmac <alg> HMAC algorithm: SHA256, SHA384, SHA512 (default: SHA256)
--tls-sig <mode> TLS mode: crypt-v2, crypt, auth (default: crypt-v2)
--auth-mode <mode> Auth mode: pki, fingerprint (default: pki)
fingerprint requires OpenVPN 2.6+
--server-cert-days <n> 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: <peer-fingerprint> 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 "<ca>"
cat "/etc/openvpn/server/easy-rsa/pki/ca.crt"
echo "</ca>"
if [[ $auth_mode == "pki" ]]; then
# PKI mode: include CA certificate
echo "<ca>"
cat "/etc/openvpn/server/easy-rsa/pki/ca.crt"
echo "</ca>"
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 "<cert>"
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 <peer-fingerprint> block
# Create the block if this is the first client
if ! grep -q '<peer-fingerprint>' /etc/openvpn/server/server.conf; then
echo "# Client fingerprints are listed below
<peer-fingerprint>
# $CLIENT
$CLIENT_FINGERPRINT
</peer-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

View File

@@ -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 "<peer-fingerprint>" || true
exit 1
fi
fi
# Copy the new config