From 190e49ec3379d8dbd1db92691cda6c526b246eb1 Mon Sep 17 00:00:00 2001 From: Stanislas Date: Sat, 13 Dec 2025 19:17:30 +0100 Subject: [PATCH] feat: add list clients menu option (#1382) ## Summary - Add new "List existing users" option to management menu (option 2) - Displays all client certificates with status (Valid/Revoked), expiration date, and days remaining - Reads expiry directly from certificate files using openssl for accurate 4-digit year dates - Output sorted by expiration date (oldest first) - Updates test MENU_OPTION values to match new menu numbering Example output: ``` === Existing Clients === Found 2 certificate(s) Name Status Expiry Remaining ---- ------ ------ --------- user1 Valid 2035-12-11 3649 days user2 Revoked unknown unknown ``` Closes #567 Closes #563 Closes #587 --- README.md | 3 +- openvpn-install.sh | 104 ++++++++++++++++++++++++++++++++++---- test/server-entrypoint.sh | 49 ++++++++++++++++-- 3 files changed, 142 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index bc40145..4ff0a04 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,8 @@ The first time you run it, you'll have to follow the assistant and answer a few When OpenVPN is installed, you can run the script again, and you will get the choice to: - Add a client -- Remove a client +- List client certificates +- Revoke a client - Renew certificates (client or server) - Uninstall OpenVPN diff --git a/openvpn-install.sh b/openvpn-install.sh index 7ac7823..9859cf7 100755 --- a/openvpn-install.sh +++ b/openvpn-install.sh @@ -1648,6 +1648,88 @@ function selectClient() { CLIENT=$(tail -n +2 /etc/openvpn/server/easy-rsa/pki/index.txt | grep "^V" | cut -d '=' -f 2 | sed -n "$CLIENTNUMBER"p) } +function listClients() { + log_header "Client Certificates" + + local index_file="/etc/openvpn/server/easy-rsa/pki/index.txt" + local number_of_clients + # Exclude server certificates (CN starting with server_) + number_of_clients=$(tail -n +2 "$index_file" | grep "^[VR]" | grep -cv "/CN=server_") + + if [[ $number_of_clients == '0' ]]; then + log_warn "You have no existing client certificates!" + return + fi + + log_info "Found $number_of_clients client certificate(s)" + log_menu "" + printf " %-25s %-10s %-12s %s\n" "Name" "Status" "Expiry" "Remaining" + printf " %-25s %-10s %-12s %s\n" "----" "------" "------" "---------" + + local cert_dir="/etc/openvpn/server/easy-rsa/pki/issued" + + # Parse index.txt and sort by expiry date (oldest first) + # Exclude server certificates (CN starting with server_) + { + while read -r line; do + local status="${line:0:1}" + local client_name + client_name=$(echo "$line" | sed 's/.*\/CN=//') + + # Format status + local status_text + if [[ "$status" == "V" ]]; then + status_text="Valid" + elif [[ "$status" == "R" ]]; then + status_text="Revoked" + else + status_text="Unknown" + fi + + # Get expiry date from certificate file + local cert_file="$cert_dir/$client_name.crt" + local expiry_date="unknown" + local relative="unknown" + + if [[ -f "$cert_file" ]]; then + # Get expiry from certificate (format: notAfter=Mon DD HH:MM:SS YYYY GMT) + local enddate + enddate=$(openssl x509 -enddate -noout -in "$cert_file" 2>/dev/null | cut -d= -f2) + + if [[ -n "$enddate" ]]; then + # Parse date and convert to epoch + local expiry_epoch + expiry_epoch=$(date -d "$enddate" +%s 2>/dev/null || date -j -f "%b %d %H:%M:%S %Y %Z" "$enddate" +%s 2>/dev/null) + + if [[ -n "$expiry_epoch" ]]; then + # Format as YYYY-MM-DD + expiry_date=$(date -d "@$expiry_epoch" +%Y-%m-%d 2>/dev/null || date -r "$expiry_epoch" +%Y-%m-%d 2>/dev/null) + + # Calculate days remaining + local now_epoch days_remaining + now_epoch=$(date +%s) + days_remaining=$(((expiry_epoch - now_epoch) / 86400)) + + if [[ $days_remaining -lt 0 ]]; then + relative="$((-days_remaining)) days ago" + elif [[ $days_remaining -eq 0 ]]; then + relative="today" + elif [[ $days_remaining -eq 1 ]]; then + relative="1 day" + else + relative="$days_remaining days" + fi + fi + fi + fi + + printf " %-25s %-10s %-12s %s\n" "$client_name" "$status_text" "$expiry_date" "$relative" + done < <(tail -n +2 "$index_file" | grep "^[VR]" | grep -v "/CN=server_" | sort -t$'\t' -k2) + } + + log_menu "" +} + function newClient() { log_header "New Client Setup" log_prompt "Tell me a name for the client." @@ -2036,12 +2118,13 @@ function manageMenu() { log_menu "" log_prompt "What do you want to do?" log_menu " 1) Add a new user" - log_menu " 2) Revoke existing user" - log_menu " 3) Renew certificate" - log_menu " 4) Remove OpenVPN" - log_menu " 5) Exit" - until [[ ${MENU_OPTION:-$menu_option} =~ ^[1-5]$ ]]; do - read -rp "Select an option [1-5]: " menu_option + log_menu " 2) List client certificates" + log_menu " 3) Revoke existing user" + log_menu " 4) Renew certificate" + log_menu " 5) Remove OpenVPN" + log_menu " 6) Exit" + until [[ ${MENU_OPTION:-$menu_option} =~ ^[1-6]$ ]]; do + read -rp "Select an option [1-6]: " menu_option done menu_option="${MENU_OPTION:-$menu_option}" @@ -2050,15 +2133,18 @@ function manageMenu() { newClient ;; 2) - revokeClient + listClients ;; 3) - renewMenu + revokeClient ;; 4) - removeOpenVPN + renewMenu ;; 5) + removeOpenVPN + ;; + 6) exit 0 ;; esac diff --git a/test/server-entrypoint.sh b/test/server-entrypoint.sh index 3b2daa0..b8152e5 100755 --- a/test/server-entrypoint.sh +++ b/test/server-entrypoint.sh @@ -177,7 +177,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=3 RENEW_OPTION=1 CLIENTNUMBER=1 CLIENT_CERT_DURATION_DAYS=3650 bash /opt/openvpn-install.sh) 2>&1 | tee "$RENEW_OUTPUT" || true +(MENU_OPTION=4 RENEW_OPTION=1 CLIENTNUMBER=1 CLIENT_CERT_DURATION_DAYS=3650 bash /opt/openvpn-install.sh) 2>&1 | tee "$RENEW_OUTPUT" || true # Verify renewal succeeded if grep -q "Certificate for client testclient renewed" "$RENEW_OUTPUT"; then @@ -257,7 +257,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=3 RENEW_OPTION=2 CONTINUE=y SERVER_CERT_DURATION_DAYS=3650 bash /opt/openvpn-install.sh) 2>&1 | tee "$RENEW_SERVER_OUTPUT" || true +(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 # Verify renewal succeeded if grep -q "Server certificate renewed successfully" "$RENEW_SERVER_OUTPUT"; then @@ -504,7 +504,7 @@ echo "Client disconnected" # Now revoke the certificate echo "Revoking certificate for '$REVOKE_CLIENT'..." REVOKE_OUTPUT="/tmp/revoke-output.log" -# MENU_OPTION=2 is revoke, CLIENTNUMBER is dynamically determined from index.txt +# MENU_OPTION=3 is revoke, CLIENTNUMBER is dynamically determined from index.txt # We need to find the client number for revoketest REVOKE_CLIENT_NUM=$(tail -n +2 /etc/openvpn/server/easy-rsa/pki/index.txt | grep "^V" | grep -n "CN=$REVOKE_CLIENT\$" | cut -d: -f1) if [ -z "$REVOKE_CLIENT_NUM" ]; then @@ -513,7 +513,7 @@ if [ -z "$REVOKE_CLIENT_NUM" ]; then exit 1 fi echo "Revoke client number: $REVOKE_CLIENT_NUM" -(MENU_OPTION=2 CLIENTNUMBER=$REVOKE_CLIENT_NUM bash /opt/openvpn-install.sh) 2>&1 | tee "$REVOKE_OUTPUT" || true +(MENU_OPTION=3 CLIENTNUMBER=$REVOKE_CLIENT_NUM bash /opt/openvpn-install.sh) 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" @@ -553,6 +553,47 @@ echo "PASS: Connection with revoked certificate correctly rejected" echo "=== Certificate Revocation Tests PASSED ===" +# ===================================================== +# Test listing client certificates +# ===================================================== +echo "" +echo "=== Testing List Client Certificates ===" + +# At this point we have 3 client certificates: +# - testclient (Valid) - the renewed certificate +# - 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 + +# Verify list output contains expected clients +if grep -q "testclient" "$LIST_OUTPUT" && grep -q "Valid" "$LIST_OUTPUT"; then + echo "PASS: List shows testclient as Valid" +else + echo "FAIL: List does not show testclient correctly" + cat "$LIST_OUTPUT" + exit 1 +fi + +if grep -q "$REVOKE_CLIENT" "$LIST_OUTPUT" && grep -q "Revoked" "$LIST_OUTPUT"; then + echo "PASS: List shows $REVOKE_CLIENT as Revoked" +else + echo "FAIL: List does not show $REVOKE_CLIENT correctly" + cat "$LIST_OUTPUT" + 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" +else + echo "FAIL: List does not show correct certificate count" + cat "$LIST_OUTPUT" + exit 1 +fi + +echo "=== List Client Certificates Tests PASSED ===" + # ===================================================== # Test reusing revoked client name # =====================================================