#!/bin/bash # # https://github.com/hwdsl2/docker-openvpn # # Copyright (C) 2026 Lin Song # Copyright (C) 2013 Nyr # # Based on the work of Nyr and contributors at: # https://github.com/Nyr/openvpn-install # # This work is licensed under the MIT License # See: https://opensource.org/licenses/MIT export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" export EASYRSA_PKI="/etc/openvpn/server/easy-rsa/pki" exiterr() { echo "Error: $1" >&2; exit 1; } easyrsa_run() { echo "+ easyrsa $*" >&2; easyrsa "$@" >/dev/null 2>&1; } show_usage() { if [ -n "$1" ]; then echo "Error: $1" >&2 fi cat 1>&2 <<'EOF' OpenVPN Docker - Client Management https://github.com/hwdsl2/docker-openvpn Usage: docker exec ovpn_manage [options] Options: --addclient [client name] add a new client --exportclient [client name] export configuration for an existing client --listclients list the names of existing clients --revokeclient [client name] revoke an existing client -y, --yes assume "yes" when revoking a client -h, --help show this help message and exit EOF exit 1 } check_container() { if [ ! -f "/.dockerenv" ] && [ ! -f "/run/.containerenv" ] \ && [ -z "$KUBERNETES_SERVICE_HOST" ] \ && ! head -n 1 /proc/1/sched 2>/dev/null | grep -q '^run\.sh '; then exiterr "This script must be run inside a container (e.g. Docker, Podman)." fi } check_setup() { if [ ! -f /etc/openvpn/server/server.conf ]; then exiterr "OpenVPN has not been set up yet. Please start the container first." fi } set_client_name() { client=$(printf '%s' "$unsanitized_client" | \ sed 's/[^0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-]/_/g') } parse_args() { add_client=0 export_client=0 list_clients=0 revoke_client=0 assume_yes=0 unsanitized_client="" while [ "$#" -gt 0 ]; do case "$1" in --addclient) add_client=1 unsanitized_client="$2" shift; shift ;; --exportclient) export_client=1 unsanitized_client="$2" shift; shift ;; --listclients) list_clients=1 shift ;; --revokeclient) revoke_client=1 unsanitized_client="$2" shift; shift ;; -y|--yes) assume_yes=1 shift ;; -h|--help) show_usage ;; *) show_usage "Unknown parameter: $1" ;; esac done } check_args() { if [ "$((add_client + export_client + list_clients + revoke_client))" -eq 0 ]; then show_usage fi if [ "$((add_client + export_client + list_clients + revoke_client))" -gt 1 ]; then show_usage "Specify only one of '--addclient', '--exportclient', '--listclients' or '--revokeclient'." fi if [ "$((add_client + export_client + revoke_client))" -eq 1 ]; then set_client_name if [ -z "$client" ]; then exiterr "Invalid client name. Use one word only, no special characters except '-' and '_'." fi fi if [ "$add_client" = 1 ]; then if [ -f "$EASYRSA_PKI/issued/${client}.crt" ]; then exiterr "'$client': invalid name. Client already exists." fi fi if [ "$export_client" = 1 ] || [ "$revoke_client" = 1 ]; then if [ ! -f "$EASYRSA_PKI/issued/${client}.crt" ]; then exiterr "Invalid client name, or client does not exist." fi fi } gen_client_ovpn() { local c="$1" local ovpn_file="/etc/openvpn/clients/${c}.ovpn" mkdir -p /etc/openvpn/clients { cat /etc/openvpn/server/client-common.txt echo "" cat /etc/openvpn/server/easy-rsa/pki/ca.crt echo "" echo "" sed -ne '/BEGIN CERTIFICATE/,$ p' \ /etc/openvpn/server/easy-rsa/pki/issued/"${c}".crt echo "" echo "" cat /etc/openvpn/server/easy-rsa/pki/private/"${c}".key echo "" echo "" sed -ne '/BEGIN OpenVPN Static key/,$ p' /etc/openvpn/server/tc.key echo "" } > "$ovpn_file" chmod 600 "$ovpn_file" } do_add_client() { echo echo "Adding client '$client'..." easyrsa_run --batch --days=3650 build-client-full "$client" nopass gen_client_ovpn "$client" echo echo "Client '$client' added. Config: /etc/openvpn/clients/$client.ovpn" echo "Use 'docker cp :/etc/openvpn/clients/$client.ovpn .' to download it." echo } do_export_client() { gen_client_ovpn "$client" cat /etc/openvpn/clients/"$client".ovpn } do_list_clients() { echo echo "Checking for existing clients..." num=$(grep -c "^V" "$EASYRSA_PKI/index.txt" 2>/dev/null || echo 0) if [ "$num" -eq 0 ]; then echo echo "No clients found." echo exit 0 fi echo grep "^V" "$EASYRSA_PKI/index.txt" | cut -d '=' -f 2 | nl -s ') ' echo if [ "$num" -eq 1 ]; then printf '%s\n\n' "Total: 1 client" else printf '%s\n\n' "Total: $num clients" fi } do_revoke_client() { if [ "$assume_yes" != 1 ]; then echo printf 'Revoke client '"'"'%s'"'"'? This cannot be undone. [y/N]: ' "$client" read -r revoke case "$revoke" in [yY][eE][sS]|[yY]) ;; *) echo; echo "Revocation aborted."; echo; exit 1 ;; esac fi echo echo "Revoking client '$client'..." easyrsa_run --batch revoke "$client" easyrsa_run --batch --days=3650 gen-crl # Update the CRL used by the running OpenVPN server rm -f /etc/openvpn/server/crl.pem cp /etc/openvpn/server/easy-rsa/pki/crl.pem /etc/openvpn/server/crl.pem chown nobody:nobody /etc/openvpn/server/crl.pem # Remove the client .ovpn file if it exists rm -f /etc/openvpn/clients/"$client".ovpn echo echo "Client '$client' revoked." echo } check_container check_setup parse_args "$@" check_args if [ "$add_client" = 1 ]; then do_add_client exit 0 fi if [ "$export_client" = 1 ]; then do_export_client exit 0 fi if [ "$list_clients" = 1 ]; then do_list_clients exit 0 fi if [ "$revoke_client" = 1 ]; then do_revoke_client exit 0 fi