openvpn-docker/clientmgmt

411 lines
9.2 KiB
Plaintext
Raw Permalink Normal View History

2024-08-24 21:05:07 +02:00
#!/usr/bin/env bash
set -euo pipefail
## CONSTANTS ##
readonly REGEX_IP_ADDRESS="^((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])){3})|(([0-9a-f]{1,4}:){7,7}[0-9a-f]{1,4}|([0-9a-f]{1,4}:){1,7}:|([0-9a-f]{1,4}:){1,6}:[0-9a-f]{1,4}|([0-9a-f]{1,4}:){1,5}(:[0-9a-f]{1,4}){1,2}|([0-9a-f]{1,4}:){1,4}(:[0-9a-f]{1,4}){1,3}|([0-9a-f]{1,4}:){1,3}(:[0-9a-f]{1,4}){1,4}|([0-9a-f]{1,4}:){1,2}(:[0-9a-f]{1,4}){1,5}|[0-9a-f]{1,4}:((:[0-9a-f]{1,4}){1,6})|:((:[0-9a-f]{1,4}){1,7}|:)|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-f]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])))$"
readonly DATA_DIR="/etc/openvpn-data"
readonly EASYRSA_DIR="${DATA_DIR}/easyrsa"
readonly DATA_SERVER_DIR="${DATA_DIR}/server"
readonly DATA_CLIENT_DIR="${DATA_DIR}/client"
readonly ARG_LIST=("$@")
## LIB: COLOR ##
color_bold=""
color_underline=""
color_standout=""
color_normal=""
color_black=""
color_red=""
color_green=""
color_yellow=""
color_blue=""
color_magenta=""
color_cyan=""
color_white=""
if test -t 1; then
# check count of supported colors
color_count=$(tput colors)
if [[ ! -z "$color_count" && "$color_count" -ge 8 ]]; then
color_bold="$(tput bold)"
color_underline="$(tput smul)"
color_standout="$(tput smso)"
color_normal="$(tput sgr0)"
color_black="$(tput setaf 0)"
color_red="$(tput setaf 1)"
color_green="$(tput setaf 2)"
color_yellow="$(tput setaf 3)"
color_blue="$(tput setaf 4)"
color_magenta="$(tput setaf 5)"
color_cyan="$(tput setaf 6)"
color_white="$(tput setaf 7)"
fi
fi
## LIB: LOG ##
log_debug(){
echo -ne "${color_green}[DEBUG]${color_normal} " 1>&2
echo "$1" 1>&2
}
log_info(){
echo -ne "${color_blue}[INFO ]${color_normal} " 1>&2
echo "$1" 1>&2
}
log_warn(){
echo -ne "${color_yellow}[WARN ]${color_normal} " 1>&2
echo "$1" 1>&2
}
log_error(){
echo -ne "${color_red}[ERROR]${color_normal} " 1>&2
echo "$1" 1>&2
exit 1
}
#
# Explain usage of script.
#
clientmgmt_help(){
echo "Usage: $0 <command> [...args]"
echo
echo "Commands:"
echo " add [name] - Generate new client keys and output its config"
echo " list - List registered client keys"
echo " revoke [name] - Revoke a client's keys"
echo
echo " help - Display this help message and exit"
}
#
# Run command `add`.
#
clientmgmt_add(){
## GET NAME ##
local exit=0
local name
name=$(clientmgmt_askname noexist) || exit=$?
[[ "$exit" -gt 0 ]] && exit 1
## GET PUBLIC IP ##
# ask whether auto-detection should be used
local answer="x"
local first=0
2024-08-27 22:39:12 +02:00
until [[ -z "$answer" || "$answer" =~ ^(Y|y|N|n)$ ]]; do
2024-08-24 21:05:07 +02:00
[[ "$first" -le 0 ]] && first=1 || echo "Invalid answer '$answer'"
read -ep "Auto-detect public IP-Address using icanhazip.com? [Y/n]: " answer
done
# maybe do auto-detection
local public_ip=""
2024-08-27 22:39:12 +02:00
if [[ ! "$answer" =~ ^(N|n)$ ]]; then
2024-08-24 21:05:07 +02:00
local exit=0
local icanhazip
icanhazip=$(wget -O - -q icanhazip.com) || exit=$?
if [[ "$exit" -le 0 && "$icanhazip" =~ $REGEX_IP_ADDRESS ]]; then
public_ip="$icanhazip"
log_info "Found public IP-Address: '$public_ip'"
else
log_warn "Failed to auto-detect public IP-Address"
fi
fi
# use ask as fallback
local first=0
until [[ "$public_ip" =~ $REGEX_IP_ADDRESS ]]; do
[[ "$first" -le 0 ]] && first=1 || echo "Invalid IP-Address '$public_ip'"
read -ep "Public IP-Address: " public_ip
done
## AUTO-DETECT PROTO AND PORT ##
local proto
local port
# stream server config
while IFS= read -r line; do
# trim whitespaces
line=$(echo "$line" | sed -E "s/^\s*//g" | sed -E "s/\s*$//g")
# ignore blank lines and comments
[[ "$line" == "" || "$line" =~ ^# ]] && continue
# match proto
if [[ "$line" =~ ^proto[[:blank:]] ]]; then
if [[ ! "$line" =~ ^proto[[:blank:]]+(tcp|udp)$ ]]; then
log_warn "Unexpected config line format for key 'proto': '$line'"
continue
fi
proto=$(echo "$line" | sed -E "s/^proto[[:blank:]]+(tcp|udp)$/\1/g")
fi
# match port
if [[ "$line" =~ ^port[[:blank:]] ]]; then
if [[ ! "$line" =~ ^port[[:blank:]]+([0-9]+)$ ]]; then
log_warn "Unexpected config line format for key 'port': '$line'"
continue
fi
local parsed_port=$(echo "$line" | sed -E "s/^port[[:blank:]]+([0-9]+)$/\1/g")
if [[ "$parsed_port" -le 0 || "$parsed_port" -gt 65535 ]]; then
log_warn "Configured port number out of range: '$parsed_port'"
continue
fi
port="$parsed_port"
fi
done < "${DATA_SERVER_DIR}/server.conf"
# validate
if [[ -z "${proto:+x}" ]]; then
proto="udp"
log_warn "Unable to auto-detect proto from server config file; using fallback: '$proto'"
fi
if [[ -z "${port:+x}" ]]; then
port="1194"
log_warn "Unable to auto-detect port from server config file; using fallback: '$port'"
fi
## GENERATE CLIENT KEYS ##
local prev_pwd="$PWD" && cd "$EASYRSA_DIR"
log_info "Generating client keys"
/usr/bin/easyrsa --batch --days=7300 build-client-full "$name" nopass
cd "$prev_pwd"
## BUILD CLIENT CONFIG ##
# header
echo
echo -e "${color_magenta}Client Config (${name}.ovpn):${color_normal}"
echo -e "${color_magenta}----------------------------------------------------------------${color_normal}"
# static content
echo "client
dev tun
proto $proto
remote $public_ip $port
resolv-retry infinite
nobind
persist-key
persist-tun
remote-cert-tls server
auth SHA512
ignore-unknown-option block-outside-dns
verb 3"
echo
# ca cert
echo "<ca>"
cat "${EASYRSA_DIR}/pki/ca.crt"
echo "</ca>"
# client cert
echo "<cert>"
sed -ne '/BEGIN CERTIFICATE/,$ p' "${EASYRSA_DIR}/pki/issued/${name}.crt"
echo "</cert>"
# client key
echo "<key>"
cat "${EASYRSA_DIR}/pki/private/${name}.key"
echo "</key>"
# tls-crypt key
echo "<tls-crypt>"
sed -ne '/BEGIN OpenVPN Static key/,$ p' "${DATA_SERVER_DIR}/tls-crypt.key"
echo "</tls-crypt>"
# footer
echo -e "${color_magenta}----------------------------------------------------------------${color_normal}"
}
#
# Run command `revoke`.
#
clientmgmt_revoke(){
## GET NAME ##
local exit=0
local name
name=$(clientmgmt_askname exist) || exit=$?
[[ "$exit" -gt 0 ]] && exit 1
## REVOKE CLIENT KEYS ##
local prev_pwd="$PWD" && cd "$EASYRSA_DIR"
log_info "Revoking client keys"
# revoke keys
/usr/bin/easyrsa --batch revoke "$name"
# update revoke list
/usr/bin/easyrsa --batch --days=7300 gen-crl
chown nobody:nobody pki/crl.pem
# restore pwd
cd "$prev_pwd"
# print info
log_info "Client revoked: '$name'"
}
#
# Run command `list`.
#
clientmgmt_list(){
## GET LIST ##
list=$(clientmgmt_clientlist)
## PRINT ##
# warn if there are no clients
if [[ "$list" == "" ]]; then
log_warn "No clients registered"
exit 0
fi
# list
echo "Clients:"
for one_client_name in $list; do
echo " $one_client_name"
done
}
#
# HELPER: Parse name parameter or ask if not provided.
#
# @param $1 noexist: Expect this name to be unused
# exist: Expect this name to exist already.
#
# @return Name entered.
# @exit 0: Returned name is valid
# 1: Returned name is invalid.
#
clientmgmt_askname(){
## ASK ##
local answer=""
local first=0
if [[ "${ARG_LIST[1]:+x}" ]]; then
answer="${ARG_LIST[1]}"
first=1
fi
until [[ "$answer" =~ ^[abcdefghijklmnopqrstuvwxyz0123456789-]{2,64}$ ]]; do
[[ "$first" -le 0 ]] && first=1 || echo "Invalid client name '$answer'" 1>&2
read -ep "Client name: " answer
done
## VALIDATE ##
# check for reserved names
2024-08-27 22:39:12 +02:00
if [[ "$answer" =~ ^(ca|server)$ ]]; then
2024-08-24 21:05:07 +02:00
log_error "Name is reserved for internal use: '$answer'"
fi
# make sure name is exists or does not exist
case "$1" in
noexist)
if [[ -f "${EASYRSA_DIR}/pki/issued/${answer}.crt" ]]; then
log_error "Name already in use: '$answer'"
fi
;;
exist)
if [[ ! -f "${EASYRSA_DIR}/pki/issued/${answer}.crt" ]]; then
log_error "Client does not exist: '$answer'"
fi
;;
*) log_error "Unknown exist goal: '$1'";;
esac
## RETURN ##
echo -n "$answer"
return 0
}
#
# GETTER: Get list of registered client names.
#
# @return Space separated list of registered client names.
#
clientmgmt_clientlist(){
## GET LIST ##
# get from directory
local rawlist=$(echo -n ${EASYRSA_DIR}/pki/issued/*.crt)
# build list
local list=""
for one_cert in $rawlist; do
filename=$(basename $one_cert)
[[ "$filename" == "server.crt" ]] && continue
list="$list $(echo ${filename} | sed -E 's/\.crt$//g')"
done
list="$(echo $list | xargs)"
## RETURN ##
echo -n "$list"
return 0
}
#
# HELPER: Make sure config and key files have been fully initialized.
#
clientmgmt_check_init_done(){
[[ -f "${DATA_SERVER_DIR}/server.conf" ]] && return 0
log_error "Server is still initializing config and key files; try again in a few seconds"
}
2024-08-24 21:05:07 +02:00
## MAIN ##
case ${1:-""} in
add)
clientmgmt_check_init_done
2024-08-24 21:05:07 +02:00
clientmgmt_add
exit 0
;;
list)
clientmgmt_check_init_done
2024-08-24 21:05:07 +02:00
clientmgmt_list
exit 0
;;
revoke)
clientmgmt_check_init_done
2024-08-24 21:05:07 +02:00
clientmgmt_revoke
exit 0
;;
help)
clientmgmt_help
exit 0
;;
"")
log_error "No command given; See '$0 help' for usage information"
exit 1
;;
*)
log_error "Invalid command '$1'; See '$0 help' for usage information"
exit 1
;;
esac