mirror of
https://github.com/angristan/openvpn-install.git
synced 2025-12-20 02:27:01 +01:00
feat: disconnect clients immediately on certificate revocation (#1432)
## Summary Adds immediate client disconnect when a certificate is revoked, via OpenVPN management interface. Previously, revoked clients stayed connected until they voluntarily disconnected or the server restarted. Fixes #1199 ## Changes - Enable management interface (Unix socket at `/var/run/openvpn/server.sock`) - Add `disconnectClient()` function to send `kill` command on revoke - Add `socat` dependency for socket communication
This commit is contained in:
@@ -41,6 +41,7 @@ That said, OpenVPN still makes sense when you need:
|
|||||||
- CLI interface for automation and scripting (non-interactive mode with JSON output)
|
- CLI interface for automation and scripting (non-interactive mode with JSON output)
|
||||||
- Certificate renewal for both client and server certificates
|
- Certificate renewal for both client and server certificates
|
||||||
- List and monitor connected clients
|
- List and monitor connected clients
|
||||||
|
- Immediate client disconnect on certificate revocation (via management interface)
|
||||||
- Uses [official OpenVPN repositories](https://community.openvpn.net/openvpn/wiki/OpenvpnSoftwareRepos) when possible for the latest stable releases
|
- Uses [official OpenVPN repositories](https://community.openvpn.net/openvpn/wiki/OpenvpnSoftwareRepos) when possible for the latest stable releases
|
||||||
- Firewall rules and forwarding managed seamlessly (native firewalld and nftables support, iptables fallback)
|
- Firewall rules and forwarding managed seamlessly (native firewalld and nftables support, iptables fallback)
|
||||||
- Configurable VPN subnets (IPv4: default `10.8.0.0/24`, IPv6: default `fd42:42:42:42::/112`)
|
- Configurable VPN subnets (IPv4: default `10.8.0.0/24`, IPv6: default `fd42:42:42:42::/112`)
|
||||||
@@ -135,7 +136,7 @@ For automation and scripting, use the CLI interface:
|
|||||||
# List clients
|
# List clients
|
||||||
./openvpn-install.sh client list
|
./openvpn-install.sh client list
|
||||||
|
|
||||||
# Revoke a client
|
# Revoke a client (immediately disconnects if connected)
|
||||||
./openvpn-install.sh client revoke alice
|
./openvpn-install.sh client revoke alice
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -2574,20 +2574,21 @@ function installOpenVPN() {
|
|||||||
installOpenVPNRepo
|
installOpenVPNRepo
|
||||||
|
|
||||||
log_info "Installing OpenVPN and dependencies..."
|
log_info "Installing OpenVPN and dependencies..."
|
||||||
|
# socat is used for communicating with the OpenVPN management interface (client disconnect on revoke)
|
||||||
if [[ $OS =~ (debian|ubuntu) ]]; then
|
if [[ $OS =~ (debian|ubuntu) ]]; then
|
||||||
run_cmd_fatal "Installing OpenVPN" apt-get install -y openvpn iptables openssl curl ca-certificates tar dnsutils
|
run_cmd_fatal "Installing OpenVPN" apt-get install -y openvpn iptables openssl curl ca-certificates tar dnsutils socat
|
||||||
elif [[ $OS == 'centos' ]]; then
|
elif [[ $OS == 'centos' ]]; then
|
||||||
run_cmd_fatal "Installing OpenVPN" yum install -y openvpn iptables openssl ca-certificates curl tar bind-utils 'policycoreutils-python*'
|
run_cmd_fatal "Installing OpenVPN" yum install -y openvpn iptables openssl ca-certificates curl tar bind-utils socat 'policycoreutils-python*'
|
||||||
elif [[ $OS == 'oracle' ]]; then
|
elif [[ $OS == 'oracle' ]]; then
|
||||||
run_cmd_fatal "Installing OpenVPN" yum install -y openvpn iptables openssl ca-certificates curl tar bind-utils policycoreutils-python-utils
|
run_cmd_fatal "Installing OpenVPN" yum install -y openvpn iptables openssl ca-certificates curl tar bind-utils socat policycoreutils-python-utils
|
||||||
elif [[ $OS == 'amzn2023' ]]; then
|
elif [[ $OS == 'amzn2023' ]]; then
|
||||||
run_cmd_fatal "Installing OpenVPN" dnf install -y openvpn iptables openssl ca-certificates curl tar bind-utils
|
run_cmd_fatal "Installing OpenVPN" dnf install -y openvpn iptables openssl ca-certificates curl tar bind-utils socat
|
||||||
elif [[ $OS == 'fedora' ]]; then
|
elif [[ $OS == 'fedora' ]]; then
|
||||||
run_cmd_fatal "Installing OpenVPN" dnf install -y openvpn iptables openssl ca-certificates curl tar bind-utils policycoreutils-python-utils
|
run_cmd_fatal "Installing OpenVPN" dnf install -y openvpn iptables openssl ca-certificates curl tar bind-utils socat policycoreutils-python-utils
|
||||||
elif [[ $OS == 'opensuse' ]]; then
|
elif [[ $OS == 'opensuse' ]]; then
|
||||||
run_cmd_fatal "Installing OpenVPN" zypper install -y openvpn iptables openssl ca-certificates curl tar bind-utils
|
run_cmd_fatal "Installing OpenVPN" zypper install -y openvpn iptables openssl ca-certificates curl tar bind-utils socat
|
||||||
elif [[ $OS == 'arch' ]]; then
|
elif [[ $OS == 'arch' ]]; then
|
||||||
run_cmd_fatal "Installing OpenVPN" pacman --needed --noconfirm -Syu openvpn iptables openssl ca-certificates curl tar bind
|
run_cmd_fatal "Installing OpenVPN" pacman --needed --noconfirm -Syu openvpn iptables openssl ca-certificates curl tar bind socat
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Verify ChaCha20-Poly1305 compatibility if selected
|
# Verify ChaCha20-Poly1305 compatibility if selected
|
||||||
@@ -2946,8 +2947,12 @@ tls-cipher $CC_CIPHER
|
|||||||
tls-ciphersuites $TLS13_CIPHERSUITES
|
tls-ciphersuites $TLS13_CIPHERSUITES
|
||||||
client-config-dir ccd
|
client-config-dir ccd
|
||||||
status /var/log/openvpn/status.log
|
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
|
||||||
|
|
||||||
# Create client-config-dir dir
|
# Create client-config-dir dir
|
||||||
run_cmd_fatal "Creating client config directory" mkdir -p /etc/openvpn/server/ccd
|
run_cmd_fatal "Creating client config directory" mkdir -p /etc/openvpn/server/ccd
|
||||||
# Create log dir
|
# Create log dir
|
||||||
@@ -3727,9 +3732,30 @@ function revokeClient() {
|
|||||||
run_cmd "Removing IP assignment" sed -i "/^$CLIENT,.*/d" /etc/openvpn/server/ipp.txt
|
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}
|
run_cmd "Backing up index" cp /etc/openvpn/server/easy-rsa/pki/index.txt{,.bk}
|
||||||
|
|
||||||
|
# Disconnect the client if currently connected
|
||||||
|
disconnectClient "$CLIENT"
|
||||||
|
|
||||||
log_success "Certificate for client $CLIENT revoked."
|
log_success "Certificate for client $CLIENT revoked."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Disconnect a client via the management interface
|
||||||
|
function disconnectClient() {
|
||||||
|
local client_name="$1"
|
||||||
|
local mgmt_socket="/var/run/openvpn/server.sock"
|
||||||
|
|
||||||
|
if [[ ! -S "$mgmt_socket" ]]; then
|
||||||
|
log_warning "Management socket not found. Client may still be connected until they reconnect."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Disconnecting client $client_name..."
|
||||||
|
if echo "kill $client_name" | socat - UNIX-CONNECT:"$mgmt_socket" >/dev/null 2>&1; then
|
||||||
|
log_success "Client $client_name disconnected."
|
||||||
|
else
|
||||||
|
log_warning "Could not disconnect client (they may not be connected)."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
function renewClient() {
|
function renewClient() {
|
||||||
local client_cert_duration_days
|
local client_cert_duration_days
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ ENV ENABLE_NFTABLES=${ENABLE_NFTABLES}
|
|||||||
|
|
||||||
# Install basic dependencies based on the OS
|
# Install basic dependencies based on the OS
|
||||||
# dnsutils/bind-utils provides dig for DNS testing with Unbound
|
# dnsutils/bind-utils provides dig for DNS testing with Unbound
|
||||||
|
# Note: socat is installed by openvpn-install.sh during OpenVPN installation
|
||||||
RUN if command -v apt-get >/dev/null; then \
|
RUN if command -v apt-get >/dev/null; then \
|
||||||
apt-get update && apt-get install -y --no-install-recommends \
|
apt-get update && apt-get install -y --no-install-recommends \
|
||||||
iproute2 iptables curl procps systemd systemd-sysv dnsutils jq \
|
iproute2 iptables curl procps systemd systemd-sysv dnsutils jq \
|
||||||
|
|||||||
@@ -192,36 +192,35 @@ echo "PASS: Can ping VPN gateway with revoke test client"
|
|||||||
# Signal server that we're connected with revoke test client
|
# Signal server that we're connected with revoke test client
|
||||||
touch /shared/revoke-client-connected
|
touch /shared/revoke-client-connected
|
||||||
|
|
||||||
# Wait for server to signal us to disconnect
|
# Wait for server to revoke and auto-disconnect us via management interface
|
||||||
echo "Waiting for server to signal disconnect..."
|
# We detect disconnect by checking if ping to VPN gateway fails
|
||||||
while [ ! -f /shared/revoke-client-disconnect ]; do
|
echo "Waiting for server to revoke certificate and disconnect us..."
|
||||||
sleep 2
|
DISCONNECT_DETECTED=false
|
||||||
done
|
for i in $(seq 1 60); do
|
||||||
|
if ! ping -c 1 -W 2 "$VPN_GATEWAY" >/dev/null 2>&1; then
|
||||||
# Disconnect
|
echo "Disconnect detected: cannot ping VPN gateway"
|
||||||
echo "Disconnecting revoke test client..."
|
DISCONNECT_DETECTED=true
|
||||||
pkill openvpn || true
|
break
|
||||||
|
fi
|
||||||
# Wait for openvpn to fully exit and tun0 to be released
|
|
||||||
WAITED=0
|
|
||||||
MAX_WAIT_DISCONNECT=10
|
|
||||||
while (pgrep openvpn >/dev/null || ip addr show tun0 2>/dev/null | grep -q "inet ") && [ $WAITED -lt $MAX_WAIT_DISCONNECT ]; do
|
|
||||||
sleep 1
|
sleep 1
|
||||||
WAITED=$((WAITED + 1))
|
echo "Still connected, waiting for revoke/disconnect ($i/60)..."
|
||||||
done
|
done
|
||||||
|
|
||||||
# Verify disconnected
|
if [ "$DISCONNECT_DETECTED" = true ]; then
|
||||||
if ip addr show tun0 2>/dev/null | grep -q "inet "; then
|
echo "PASS: Client was auto-disconnected by revoke"
|
||||||
echo "FAIL: tun0 still has IP after disconnect"
|
# Kill openvpn process to clean up
|
||||||
|
pkill openvpn 2>/dev/null || true
|
||||||
|
sleep 1
|
||||||
|
else
|
||||||
|
echo "FAIL: Client was not disconnected within 60 seconds"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo "PASS: Disconnected successfully"
|
|
||||||
|
|
||||||
# Signal server that we're disconnected
|
# Signal server that we detected the disconnect
|
||||||
touch /shared/revoke-client-disconnected
|
touch /shared/revoke-client-disconnected
|
||||||
|
|
||||||
# Wait for server to revoke the certificate and signal us to reconnect
|
# Wait for server to signal us to try reconnecting
|
||||||
echo "Waiting for server to revoke certificate and signal reconnect..."
|
echo "Waiting for server to signal reconnect attempt..."
|
||||||
while [ ! -f /shared/revoke-try-reconnect ]; do
|
while [ ! -f /shared/revoke-try-reconnect ]; do
|
||||||
sleep 2
|
sleep 2
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -135,6 +135,39 @@ fi
|
|||||||
|
|
||||||
echo "All required files present"
|
echo "All required files present"
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# Verify management interface configuration
|
||||||
|
# =====================================================
|
||||||
|
echo ""
|
||||||
|
echo "=== Verifying Management Interface Configuration ==="
|
||||||
|
|
||||||
|
# Verify management socket is configured in server.conf
|
||||||
|
if grep -q "management /var/run/openvpn/server.sock unix" /etc/openvpn/server/server.conf; then
|
||||||
|
echo "PASS: Management interface configured in server.conf"
|
||||||
|
else
|
||||||
|
echo "FAIL: Management interface not found in server.conf"
|
||||||
|
grep "management" /etc/openvpn/server/server.conf || echo "No management directive found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verify management socket directory exists
|
||||||
|
if [ -d /var/run/openvpn ]; then
|
||||||
|
echo "PASS: Management socket directory exists"
|
||||||
|
else
|
||||||
|
echo "FAIL: Management socket directory /var/run/openvpn not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verify socat is available (needed for management interface communication)
|
||||||
|
if command -v socat >/dev/null 2>&1; then
|
||||||
|
echo "PASS: socat is available"
|
||||||
|
else
|
||||||
|
echo "FAIL: socat is not installed (required for management interface)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "=== Management Interface Configuration Verified ==="
|
||||||
|
|
||||||
# =====================================================
|
# =====================================================
|
||||||
# Test duplicate client name handling
|
# Test duplicate client name handling
|
||||||
# =====================================================
|
# =====================================================
|
||||||
@@ -769,18 +802,8 @@ fi
|
|||||||
|
|
||||||
echo "=== Server Status Tests PASSED ==="
|
echo "=== Server Status Tests PASSED ==="
|
||||||
|
|
||||||
# Signal client to disconnect before revocation
|
# Now revoke the certificate (this should auto-disconnect the client via management interface)
|
||||||
touch /shared/revoke-client-disconnect
|
echo "Revoking certificate for '$REVOKE_CLIENT' (should auto-disconnect client)..."
|
||||||
|
|
||||||
# Wait for client to disconnect
|
|
||||||
echo "Waiting for client to disconnect..."
|
|
||||||
while [ ! -f /shared/revoke-client-disconnected ]; do
|
|
||||||
sleep 2
|
|
||||||
done
|
|
||||||
echo "Client disconnected"
|
|
||||||
|
|
||||||
# Now revoke the certificate
|
|
||||||
echo "Revoking certificate for '$REVOKE_CLIENT'..."
|
|
||||||
REVOKE_OUTPUT="/tmp/revoke-output.log"
|
REVOKE_OUTPUT="/tmp/revoke-output.log"
|
||||||
(bash /opt/openvpn-install.sh client revoke "$REVOKE_CLIENT" --force) 2>&1 | tee "$REVOKE_OUTPUT" || true
|
(bash /opt/openvpn-install.sh client revoke "$REVOKE_CLIENT" --force) 2>&1 | tee "$REVOKE_OUTPUT" || true
|
||||||
|
|
||||||
@@ -801,6 +824,22 @@ else
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Wait for client to confirm it was disconnected by the revoke
|
||||||
|
echo "Waiting for client to confirm auto-disconnect..."
|
||||||
|
DISCONNECT_WAIT=0
|
||||||
|
while [ ! -f /shared/revoke-client-disconnected ] && [ $DISCONNECT_WAIT -lt 60 ]; do
|
||||||
|
sleep 2
|
||||||
|
DISCONNECT_WAIT=$((DISCONNECT_WAIT + 2))
|
||||||
|
echo "Waiting for disconnect confirmation... ($DISCONNECT_WAIT/60s)"
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -f /shared/revoke-client-disconnected ]; then
|
||||||
|
echo "PASS: Client was auto-disconnected by revoke command"
|
||||||
|
else
|
||||||
|
echo "FAIL: Client was not disconnected within 60 seconds"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
# Signal client to try reconnecting (should fail)
|
# Signal client to try reconnecting (should fail)
|
||||||
touch /shared/revoke-try-reconnect
|
touch /shared/revoke-try-reconnect
|
||||||
|
|
||||||
@@ -1025,8 +1064,41 @@ done
|
|||||||
echo "PASS: Client connected with passphrase-protected certificate"
|
echo "PASS: Client connected with passphrase-protected certificate"
|
||||||
|
|
||||||
echo "=== PASSPHRASE Support Tests PASSED ==="
|
echo "=== PASSPHRASE Support Tests PASSED ==="
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# Test management interface is running
|
||||||
|
# =====================================================
|
||||||
echo ""
|
echo ""
|
||||||
echo "=== All Revocation Tests PASSED ==="
|
echo "=== Testing Management Interface ==="
|
||||||
|
|
||||||
|
MGMT_SOCKET="/var/run/openvpn/server.sock"
|
||||||
|
|
||||||
|
# Verify management socket exists and is accessible
|
||||||
|
if [ -S "$MGMT_SOCKET" ]; then
|
||||||
|
echo "PASS: Management socket exists at $MGMT_SOCKET"
|
||||||
|
else
|
||||||
|
echo "FAIL: Management socket not found at $MGMT_SOCKET"
|
||||||
|
ls -la /var/run/openvpn/ || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test that we can communicate with the management interface
|
||||||
|
echo "Testing management interface communication..."
|
||||||
|
MGMT_STATUS=$(echo "status" | socat - UNIX-CONNECT:"$MGMT_SOCKET" 2>&1 | head -20)
|
||||||
|
if echo "$MGMT_STATUS" | grep -q "CLIENT LIST"; then
|
||||||
|
echo "PASS: Management interface is responsive"
|
||||||
|
echo "Status output:"
|
||||||
|
echo "$MGMT_STATUS"
|
||||||
|
else
|
||||||
|
echo "FAIL: Management interface not responding correctly"
|
||||||
|
echo "Response: $MGMT_STATUS"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "=== Management Interface Tests PASSED ==="
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== All Tests PASSED ==="
|
||||||
|
|
||||||
# Server tests complete - systemd keeps the container running via /sbin/init
|
# Server tests complete - systemd keeps the container running via /sbin/init
|
||||||
# OpenVPN service (openvpn-server@server) continues independently
|
# OpenVPN service (openvpn-server@server) continues independently
|
||||||
|
|||||||
Reference in New Issue
Block a user