feat: add CLI interface with subcommands (#1398)

Replace environment variable-based configuration with a proper CLI
interface using subcommands and flags.

### Commands

```
openvpn-install <command> [options]

Commands:
  install       Install and configure OpenVPN server
  uninstall     Remove OpenVPN server
  client        Manage client certificates (add/list/revoke/renew)
  server        Server management (status/renew)
  interactive   Launch interactive menu
```

### Highlights

- **No args → help**: Running without arguments shows help instead of
interactive menu
- **JSON output**: `client list` and `server status` support `--format
json`
- **25+ install flags**: Network, DNS, security, and client options
- **Interactive mode preserved**: `install --interactive` or
`interactive` command

### Breaking Changes

Environment variables (`AUTO_INSTALL`, `MENU_OPTION`, `CLIENT`, etc.)
are no longer supported. Use CLI flags instead.

```bash
# Before
MENU_OPTION=1 CLIENT=foo PASS=1 ./openvpn-install.sh

# After
./openvpn-install.sh client add foo
```


Closes https://github.com/angristan/openvpn-install/issues/1202
This commit is contained in:
Stanislas
2025-12-14 22:08:44 +01:00
committed by GitHub
parent 648fe1ee0b
commit ec3e80ac16
7 changed files with 1654 additions and 302 deletions

View File

@@ -12,54 +12,40 @@ fi
echo "TUN device ready"
# Set up environment for auto-install
export AUTO_INSTALL=y
# Configuration for install
export FORCE_COLOR=1
export APPROVE_INSTALL=y
export APPROVE_IP=y
export IPV6_SUPPORT=n
export VPN_SUBNET=10.9.0.0 # Custom subnet to test configurability
export PORT_CHOICE=1
export PROTOCOL_CHOICE=1
export DNS=2 # Self-hosted Unbound DNS resolver
export COMPRESSION_ENABLED=n
export CLIENT=testclient
export PASS=1
export ENDPOINT=openvpn-server
VPN_SUBNET=10.9.0.0 # Custom subnet to test configurability
# Calculate VPN gateway from subnet (first usable IP)
VPN_GATEWAY="${VPN_SUBNET%.*}.1"
export VPN_GATEWAY
# TLS key type configuration (default: tls-crypt-v2)
# TLS_SIG: 1=tls-crypt-v2, 2=tls-crypt, 3=tls-auth
# TLS_SIG: crypt-v2, crypt, auth
# TLS_KEY_FILE: the expected key file name for verification
TLS_SIG="${TLS_SIG:-1}"
TLS_SIG="${TLS_SIG:-crypt-v2}"
TLS_KEY_FILE="${TLS_KEY_FILE:-tls-crypt-v2.key}"
export TLS_SIG
# If using non-default TLS settings, enable encryption customization
if [ "$TLS_SIG" != "1" ]; then
export CUSTOMIZE_ENC=y
# Set other encryption defaults when customizing
export CIPHER_CHOICE=1 # AES-128-GCM
export CERT_TYPE=1 # ECDSA
export CERT_CURVE_CHOICE=1 # prime256v1
export CC_CIPHER_CHOICE=1 # ECDHE-ECDSA-AES-128-GCM-SHA256
export DH_TYPE=1 # ECDH
export DH_CURVE_CHOICE=1 # prime256v1
export HMAC_ALG_CHOICE=1 # SHA-256
# Build install command with CLI flags (using array for proper quoting)
INSTALL_CMD=(/opt/openvpn-install.sh install)
INSTALL_CMD+=(--endpoint openvpn-server)
INSTALL_CMD+=(--dns unbound)
INSTALL_CMD+=(--subnet "$VPN_SUBNET")
INSTALL_CMD+=(--client testclient)
# Add TLS signature mode if non-default
if [ "$TLS_SIG" != "crypt-v2" ]; then
INSTALL_CMD+=(--tls-sig "$TLS_SIG")
echo "Testing TLS key type: $TLS_SIG (key file: $TLS_KEY_FILE)"
else
export CUSTOMIZE_ENC=n
fi
echo "Running OpenVPN install script..."
echo "Command: ${INSTALL_CMD[*]}"
# Run in subshell because the script calls 'exit 0' after generating client config
# Capture output to validate logging format, while still displaying it
# Use || true to prevent set -e from exiting on failure, then check exit code
INSTALL_OUTPUT="/tmp/install-output.log"
(bash /opt/openvpn-install.sh) 2>&1 | tee "$INSTALL_OUTPUT"
("${INSTALL_CMD[@]}") 2>&1 | tee "$INSTALL_OUTPUT"
INSTALL_EXIT_CODE=${PIPESTATUS[0]}
echo "=== Installation complete (exit code: $INSTALL_EXIT_CODE) ==="
@@ -196,7 +182,7 @@ echo "Original client certificate serial: $ORIG_CERT_SERIAL"
# Test client certificate renewal using the script
echo "Testing client certificate renewal..."
RENEW_OUTPUT="/tmp/renew-client-output.log"
(MENU_OPTION=4 RENEW_OPTION=1 CLIENTNUMBER=1 CLIENT_CERT_DURATION_DAYS=3650 bash /opt/openvpn-install.sh) 2>&1 | tee "$RENEW_OUTPUT" || true
(bash /opt/openvpn-install.sh client renew testclient --cert-days 3650) 2>&1 | tee "$RENEW_OUTPUT" || true
# Verify renewal succeeded
if grep -q "Certificate for client testclient renewed" "$RENEW_OUTPUT"; then
@@ -276,7 +262,7 @@ echo "Original server certificate serial: $ORIG_SERVER_SERIAL"
# Test server certificate renewal
echo "Testing server certificate renewal..."
RENEW_SERVER_OUTPUT="/tmp/renew-server-output.log"
(MENU_OPTION=4 RENEW_OPTION=2 CONTINUE=y SERVER_CERT_DURATION_DAYS=3650 bash /opt/openvpn-install.sh) 2>&1 | tee "$RENEW_SERVER_OUTPUT" || true
(bash /opt/openvpn-install.sh server renew --cert-days 3650 --force) 2>&1 | tee "$RENEW_SERVER_OUTPUT" || true
# Verify renewal succeeded
if grep -q "Server certificate renewed successfully" "$RENEW_SERVER_OUTPUT"; then
@@ -545,7 +531,7 @@ echo "=== Testing Certificate Revocation ==="
REVOKE_CLIENT="revoketest"
echo "Creating client '$REVOKE_CLIENT' for revocation testing..."
REVOKE_CREATE_OUTPUT="/tmp/revoke-create-output.log"
(MENU_OPTION=1 CLIENT=$REVOKE_CLIENT PASS=1 CLIENT_CERT_DURATION_DAYS=3650 bash /opt/openvpn-install.sh) 2>&1 | tee "$REVOKE_CREATE_OUTPUT" || true
(bash /opt/openvpn-install.sh client add "$REVOKE_CLIENT" --cert-days 3650) 2>&1 | tee "$REVOKE_CREATE_OUTPUT" || true
if [ -f "/root/$REVOKE_CLIENT.ovpn" ]; then
echo "PASS: Client '$REVOKE_CLIENT' created successfully"
@@ -579,6 +565,42 @@ if [ ! -f /shared/revoke-client-connected ]; then
fi
echo "PASS: Client connected with '$REVOKE_CLIENT' certificate"
# =====================================================
# Test server status command
# =====================================================
echo ""
echo "=== Testing Server Status ==="
# Note: OpenVPN status file updates periodically (default: 1 min)
# so we just verify the command works, not that a specific client is visible
# Test table output
STATUS_OUTPUT="/tmp/server-status-output.log"
(bash /opt/openvpn-install.sh server status) 2>&1 | tee "$STATUS_OUTPUT" || true
if grep -q "Connected Clients" "$STATUS_OUTPUT"; then
echo "PASS: Server status shows header"
else
echo "FAIL: Server status missing header"
cat "$STATUS_OUTPUT"
exit 1
fi
# Test JSON output
STATUS_JSON_OUTPUT="/tmp/server-status-json-output.log"
(bash /opt/openvpn-install.sh server status --format json) 2>&1 | tee "$STATUS_JSON_OUTPUT" || true
# Validate JSON structure (clients array exists, even if empty)
if jq -e '.clients' "$STATUS_JSON_OUTPUT" >/dev/null 2>&1; then
echo "PASS: Server status JSON is valid"
else
echo "FAIL: Server status JSON is invalid"
cat "$STATUS_JSON_OUTPUT"
exit 1
fi
echo "=== Server Status Tests PASSED ==="
# Signal client to disconnect before revocation
touch /shared/revoke-client-disconnect
@@ -597,11 +619,10 @@ if [ ! -f /shared/revoke-client-disconnected ]; then
fi
echo "Client disconnected"
# Now revoke the certificate using the new CLIENT name feature
# Now revoke the certificate
echo "Revoking certificate for '$REVOKE_CLIENT'..."
REVOKE_OUTPUT="/tmp/revoke-output.log"
# MENU_OPTION=3 is revoke, CLIENT specifies the client name directly
(MENU_OPTION=3 CLIENT=$REVOKE_CLIENT bash /opt/openvpn-install.sh) 2>&1 | tee "$REVOKE_OUTPUT" || true
(bash /opt/openvpn-install.sh client revoke "$REVOKE_CLIENT" --force) 2>&1 | tee "$REVOKE_OUTPUT" || true
if grep -q "Certificate for client $REVOKE_CLIENT revoked" "$REVOKE_OUTPUT"; then
echo "PASS: Certificate for '$REVOKE_CLIENT' revoked successfully"
@@ -652,7 +673,7 @@ echo "=== Testing List Client Certificates ==="
# - testclient (Revoked) - the old certificate revoked during renewal
# - revoketest (Revoked) - the revoked certificate
LIST_OUTPUT="/tmp/list-clients-output.log"
(MENU_OPTION=2 bash /opt/openvpn-install.sh) 2>&1 | tee "$LIST_OUTPUT" || true
(bash /opt/openvpn-install.sh client list) 2>&1 | tee "$LIST_OUTPUT" || true
# Verify list output contains expected clients
if grep -q "testclient" "$LIST_OUTPUT" && grep -q "Valid" "$LIST_OUTPUT"; then
@@ -680,6 +701,48 @@ else
exit 1
fi
# Test JSON output
echo "Testing client list JSON output..."
LIST_JSON_OUTPUT="/tmp/list-clients-json-output.log"
(bash /opt/openvpn-install.sh client list --format json) 2>&1 | tee "$LIST_JSON_OUTPUT" || true
# Validate JSON structure
if jq -e '.clients' "$LIST_JSON_OUTPUT" >/dev/null 2>&1; then
echo "PASS: Client list JSON is valid"
else
echo "FAIL: Client list JSON is invalid"
cat "$LIST_JSON_OUTPUT"
exit 1
fi
# Verify client count in JSON
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)"
else
echo "FAIL: Client list JSON has wrong count: $JSON_CLIENT_COUNT (expected 3)"
cat "$LIST_JSON_OUTPUT"
exit 1
fi
# Verify valid client in JSON
if jq -e '.clients[] | select(.name == "testclient" and .status == "valid")' "$LIST_JSON_OUTPUT" >/dev/null 2>&1; then
echo "PASS: Client list JSON shows testclient as valid"
else
echo "FAIL: Client list JSON does not show testclient correctly"
cat "$LIST_JSON_OUTPUT"
exit 1
fi
# Verify revoked client in JSON
if jq -e ".clients[] | select(.name == \"$REVOKE_CLIENT\" and .status == \"revoked\")" "$LIST_JSON_OUTPUT" >/dev/null 2>&1; then
echo "PASS: Client list JSON shows $REVOKE_CLIENT as revoked"
else
echo "FAIL: Client list JSON does not show $REVOKE_CLIENT correctly"
cat "$LIST_JSON_OUTPUT"
exit 1
fi
echo "=== List Client Certificates Tests PASSED ==="
# =====================================================
@@ -691,7 +754,7 @@ echo "=== Testing Reuse of Revoked Client Name ==="
# Create a new certificate with the same name as the revoked one
echo "Creating new client with same name '$REVOKE_CLIENT'..."
RECREATE_OUTPUT="/tmp/recreate-output.log"
(MENU_OPTION=1 CLIENT=$REVOKE_CLIENT PASS=1 CLIENT_CERT_DURATION_DAYS=3650 bash /opt/openvpn-install.sh) 2>&1 | tee "$RECREATE_OUTPUT" || true
(bash /opt/openvpn-install.sh client add "$REVOKE_CLIENT" --cert-days 3650) 2>&1 | tee "$RECREATE_OUTPUT" || true
if [ -f "/root/$REVOKE_CLIENT.ovpn" ]; then
echo "PASS: New client '$REVOKE_CLIENT' created successfully (reusing revoked name)"
@@ -758,7 +821,7 @@ PASSPHRASE_CLIENT="passphrasetest"
TEST_PASSPHRASE="TestP@ssw0rd#123"
echo "Creating client '$PASSPHRASE_CLIENT' with passphrase in headless mode..."
PASSPHRASE_OUTPUT="/tmp/passphrase-output.log"
(MENU_OPTION=1 CLIENT=$PASSPHRASE_CLIENT PASS=2 PASSPHRASE="$TEST_PASSPHRASE" CLIENT_CERT_DURATION_DAYS=3650 bash /opt/openvpn-install.sh) 2>&1 | tee "$PASSPHRASE_OUTPUT" || true
(bash /opt/openvpn-install.sh client add "$PASSPHRASE_CLIENT" --password "$TEST_PASSPHRASE" --cert-days 3650) 2>&1 | tee "$PASSPHRASE_OUTPUT" || true
# Verify client was created
if [ -f "/root/$PASSPHRASE_CLIENT.ovpn" ]; then