Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@ jobs:
fetch-depth: 0
persist-credentials: false
- name: Lint Code Base
uses: super-linter/super-linter@502f4fe48a81a392756e173e39a861f8c8efe056 # v8.3.0
uses: super-linter/super-linter@47984f49b4e87383eed97890fe2dca6063bbd9c3 # v8.3.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
51 changes: 49 additions & 2 deletions FAQ.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,38 @@ Sysctl options are at `/etc/sysctl.d/99-openvpn.conf`

---

**Q:** How can I access computers the OpenVPN server's remote LAN?
**Q:** How can I access computers on the OpenVPN server's LAN?

**A:** Add a route with the subnet of the remote network to `/etc/openvpn/server/server.conf` and restart OpenVPN. Example: `push "route 192.168.1.0 255.255.255.0"` if the server's LAN is `192.168.1.0/24`
**A:** Two steps are required:

1. **Push a route to clients** - Add the LAN subnet to `/etc/openvpn/server/server.conf`:

```
push "route 192.168.1.0 255.255.255.0"
```

Replace `192.168.1.0/24` with your actual LAN subnet.

2. **Enable routing back to VPN clients** - Choose one of these options:
- **Option A: Add a static route on your router** (recommended when you can configure your router)

On your LAN router, add a route for the VPN subnet (default `10.8.0.0/24`) pointing to the OpenVPN server's LAN IP. This allows LAN devices to reply to VPN clients without NAT.

- **Option B: Masquerade VPN traffic to LAN**

If you can't modify your router, add a masquerade rule so VPN traffic appears to come from the server:

```bash
# iptables
iptables -t nat -A POSTROUTING -s 10.8.0.0/24 -d 192.168.1.0/24 -j MASQUERADE

# or nftables
nft add rule ip nat postrouting ip saddr 10.8.0.0/24 ip daddr 192.168.1.0/24 masquerade
```

Make this persistent by adding it to your firewall scripts.

Restart OpenVPN after making changes: `systemctl restart openvpn-server@server`

---

Expand Down Expand Up @@ -183,3 +212,21 @@ label fc00::/7 1
```

This will not work properly unless you add you your VPN server `server.conf` one or two lines to push at least 1 (one) IPv6 DNS server. Most providers have IPv6 servers as well, add two more lines of `push "dhcp-option DNS <IPv6>"`

---

**Q:** How can I run OpenVPN on port 443 alongside a web server?

**A:** Use OpenVPN's `port-share` feature to multiplex both services on the same port. When OpenVPN receives non-VPN traffic, it forwards it to your web server.

1. During installation, select **TCP** and port **443**
2. Configure your web server to listen on a different port (e.g., 8443)
3. Add to `/etc/openvpn/server/server.conf`:

```
port-share 127.0.0.1 8443
```

4. Restart OpenVPN: `systemctl restart openvpn-server@server`

This is useful when your network only allows outbound connections on port 443. Note that TCP has worse performance than UDP for VPN traffic due to head-of-line blocking, so only use this when necessary.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
- Certificate renewal for both client and server certificates
- 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
- 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`)
Expand Down Expand Up @@ -135,7 +136,7 @@ For automation and scripting, use the CLI interface:
# List clients
./openvpn-install.sh client list

# Revoke a client
# Revoke a client (immediately disconnects if connected)
./openvpn-install.sh client revoke alice
```

Expand Down
40 changes: 33 additions & 7 deletions openvpn-install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2574,20 +2574,21 @@ function installOpenVPN() {
installOpenVPNRepo

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

# Verify ChaCha20-Poly1305 compatibility if selected
Expand Down Expand Up @@ -2946,8 +2947,12 @@ 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

# Create management socket directory
run_cmd_fatal "Creating management socket directory" mkdir -p /var/run/openvpn

# Create client-config-dir dir
run_cmd_fatal "Creating client config directory" mkdir -p /etc/openvpn/server/ccd
# Create log dir
Expand Down Expand Up @@ -3727,9 +3732,30 @@ function revokeClient() {
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"

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() {
local client_cert_duration_days

Expand Down
1 change: 1 addition & 0 deletions test/Dockerfile.server
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ ENV ENABLE_NFTABLES=${ENABLE_NFTABLES}

# Install basic dependencies based on the OS
# 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 \
apt-get update && apt-get install -y --no-install-recommends \
iproute2 iptables curl procps systemd systemd-sysv dnsutils jq \
Expand Down
43 changes: 21 additions & 22 deletions test/client-entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -192,36 +192,35 @@ echo "PASS: Can ping VPN gateway with revoke test client"
# Signal server that we're connected with revoke test client
touch /shared/revoke-client-connected

# Wait for server to signal us to disconnect
echo "Waiting for server to signal disconnect..."
while [ ! -f /shared/revoke-client-disconnect ]; do
sleep 2
done

# Disconnect
echo "Disconnecting revoke test client..."
pkill openvpn || true

# 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
# Wait for server to revoke and auto-disconnect us via management interface
# We detect disconnect by checking if ping to VPN gateway fails
echo "Waiting for server to revoke certificate and disconnect us..."
DISCONNECT_DETECTED=false
for i in $(seq 1 60); do
if ! ping -c 1 -W 2 "$VPN_GATEWAY" >/dev/null 2>&1; then
echo "Disconnect detected: cannot ping VPN gateway"
DISCONNECT_DETECTED=true
break
fi
sleep 1
WAITED=$((WAITED + 1))
echo "Still connected, waiting for revoke/disconnect ($i/60)..."
done

# Verify disconnected
if ip addr show tun0 2>/dev/null | grep -q "inet "; then
echo "FAIL: tun0 still has IP after disconnect"
if [ "$DISCONNECT_DETECTED" = true ]; then
echo "PASS: Client was auto-disconnected by revoke"
# 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
fi
echo "PASS: Disconnected successfully"

# Signal server that we're disconnected
# Signal server that we detected the disconnect
touch /shared/revoke-client-disconnected

# Wait for server to revoke the certificate and signal us to reconnect
echo "Waiting for server to revoke certificate and signal reconnect..."
# Wait for server to signal us to try reconnecting
echo "Waiting for server to signal reconnect attempt..."
while [ ! -f /shared/revoke-try-reconnect ]; do
sleep 2
done
Expand Down
98 changes: 85 additions & 13 deletions test/server-entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,39 @@ fi

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
# =====================================================
Expand Down Expand Up @@ -769,18 +802,8 @@ fi

echo "=== Server Status Tests PASSED ==="

# Signal client to disconnect before revocation
touch /shared/revoke-client-disconnect

# 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'..."
# Now revoke the certificate (this should auto-disconnect the client via management interface)
echo "Revoking certificate for '$REVOKE_CLIENT' (should auto-disconnect client)..."
REVOKE_OUTPUT="/tmp/revoke-output.log"
(bash /opt/openvpn-install.sh client revoke "$REVOKE_CLIENT" --force) 2>&1 | tee "$REVOKE_OUTPUT" || true

Expand All @@ -801,6 +824,22 @@ else
exit 1
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)
touch /shared/revoke-try-reconnect

Expand Down Expand Up @@ -1025,8 +1064,41 @@ done
echo "PASS: Client connected with passphrase-protected certificate"

echo "=== PASSPHRASE Support Tests PASSED ==="

# =====================================================
# Test management interface is running
# =====================================================
echo ""
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 Revocation Tests PASSED ==="
echo "=== All Tests PASSED ==="

# Server tests complete - systemd keeps the container running via /sbin/init
# OpenVPN service (openvpn-server@server) continues independently
Expand Down
Loading