pivpn/scripts/openvpn/makeOVPN.sh

556 lines
15 KiB
Bash
Raw Normal View History

2019-10-14 10:27:28 +00:00
#!/bin/bash
2022-07-27 12:53:36 +00:00
2019-10-14 10:27:28 +00:00
# Create OVPN Client
# Default Variable Declarations
2020-04-28 22:44:56 +00:00
setupVars="/etc/pivpn/openvpn/setupVars.conf"
2019-10-14 10:27:28 +00:00
DEFAULT="Default.txt"
FILEEXT=".ovpn"
CRT=".crt"
KEY=".key"
CA="ca.crt"
TA="ta.key"
INDEX="/etc/openvpn/easy-rsa/pki/index.txt"
2022-07-27 12:53:36 +00:00
if [[ ! -f "${setupVars}" ]]; then
err "::: Missing setup vars file!"
exit 1
2019-10-14 10:27:28 +00:00
fi
# shellcheck disable=SC1090
2019-10-14 10:27:28 +00:00
source "${setupVars}"
2022-07-27 12:53:36 +00:00
err() {
echo "[$(date +'%Y-%m-%dT%H:%M:%S%z')]: $*" >&2
}
2019-10-14 10:27:28 +00:00
helpFunc() {
2022-07-27 12:53:36 +00:00
echo "::: Create a client ovpn profile, optional nopass"
echo ":::"
echo -n "::: Usage: pivpn <-a|add> [-n|--name <arg>] "
echo -n "[-p|--password <arg>]|[nopass] [-d|--days <number>] "
echo "[-b|--bitwarden] [-i|--iOS] [-o|--ovpn] [-h|--help]"
echo ":::"
echo "::: Commands:"
echo "::: [none] Interactive mode"
echo "::: nopass Create a client without a password"
echo -n "::: -n,--name Name for the Client "
echo "(default: \"$(hostname)\")"
echo "::: -p,--password Password for the Client (no default)"
echo -n "::: -d,--days Expire the certificate after specified "
echo "number of days (default: 1080)"
echo "::: -b,--bitwarden Create and save a client through Bitwarden"
echo -n "::: -i,--iOS Generate a certificate that leverages iOS "
echo "keychain"
echo -n "::: -o,--ovpn Regenerate a .ovpn config file for an "
echo "existing client"
echo "::: -h,--help Show this help dialog"
2019-10-14 10:27:28 +00:00
}
2022-07-27 12:53:36 +00:00
if [[ -z "${HELP_SHOWN}" ]]; then
helpFunc
echo
echo "HELP_SHOWN=1" >> "${setupVars}"
2019-10-14 10:27:28 +00:00
fi
# Parse input arguments
2022-07-27 12:53:36 +00:00
while [[ "$#" -gt 0 ]]; do
_key="${1}"
case "${_key}" in
-n | --name | --name=*)
_val="${_key##--name=}"
if [[ "${_val}" == "${_key}" ]]; then
[[ "$#" -lt 2 ]] &&
err "Missing value for the optional argument '${_key}'." &&
exit 1
_val="${2}"
shift
fi
NAME="${_val}"
;;
-p | --password | --password=*)
_val="${_key##--password=}"
if [[ "${_val}" == "${_key}" ]]; then
[[ "$#" -lt 2 ]] &&
err "Missing value for the optional argument '${_key}'." &&
exit 1
_val="${2}"
shift
fi
PASSWD="${_val}"
;;
-d | --days | --days=*)
_val="${_key##--days=}"
if [[ "${_val}" == "${_key}" ]]; then
[[ "$#" -lt 2 ]] &&
err "Missing value for the optional argument '${_key}'." &&
exit 1
_val="${2}"
shift
fi
DAYS="${_val}"
;;
-i | --iOS)
if [[ "${TWO_POINT_FOUR}" -ne 1 ]]; then
iOS=1
else
err "Sorry, can't generate iOS-specific configs for ECDSA certificates"
err "Generate traditional certificates using 'pivpn -a' or reinstall PiVPN without opting in for OpenVPN 2.4 features"
exit 1
fi
;;
-h | --help)
helpFunc
exit 0
;;
nopass)
NO_PASS="1"
;;
-b | --bitwarden)
if command -v bw > /dev/null; then
BITWARDEN="2"
else
echo 'Bitwarden not found, please install bitwarden'
if [[ "${PLAT}" == 'Alpine' ]]; then
echo 'You can download it through the following commands:'
echo -n $'\t''curl -fLo bitwarden.zip --no-cache https://github.com/'
echo -n 'bitwarden/clients/releases/download/cli-v2022.6.2/'
echo 'bw-linux-2022.6.2.zip'
echo $'\t''apk --no-cache unzip'
echo $'\t''unzip bitwarden.zip'
echo $'\t''mv bw /opt/bw'
echo $'\t''chmod 755 /opt/bw'
echo $'\t''rm bitwarden.zip'
echo $'\t''apk --no-cache --purge del -r unzip'
fi
exit 1
fi
;;
-o | --ovpn)
GENOVPNONLY=1
;;
*)
err "Error: Got an unexpected argument '${1}'"
helpFunc
exit 1
;;
esac
shift
2019-10-14 10:27:28 +00:00
done
# Functions def
2022-07-27 12:53:36 +00:00
keynoPASS() {
# Build the client key
expect << EOF
2019-10-14 10:27:28 +00:00
set timeout -1
set env(EASYRSA_CERT_EXPIRE) "${DAYS}"
spawn ./easyrsa build-client-full "${NAME}" nopass
expect eof
EOF
2022-07-27 12:53:36 +00:00
cd pki || exit
2019-10-14 10:27:28 +00:00
}
2022-07-27 12:53:36 +00:00
useBitwarden() {
# login and unlock vault
printf "****Bitwarden Login****"
printf "\n"
SESSION_KEY="$(bw login --raw)"
export BW_SESSION="${SESSION_KEY}"
printf "Successfully Logged in!"
printf "\n"
# ask user for username
printf "Enter the username: "
read -r NAME
# check name
until [[ "${NAME}" =~ ^[a-zA-Z0-9.@_-]+$ ]] &&
[[ "${NAME::1}" != "." ]] &&
[[ "${NAME::1}" != "-" ]]; do
echo -n "Name can only contain alphanumeric characters and these "
echo -n "characters (.-@_). The name also cannot start with a dot (.)"
echo " or a dash (-). Please try again."
# ask user for username again
printf "Enter the username: "
2019-10-14 10:27:28 +00:00
read -r NAME
2022-07-27 12:53:36 +00:00
done
2019-10-14 10:27:28 +00:00
2022-07-27 12:53:36 +00:00
# ask user for length of password
printf "Please enter the length of characters you want your password to be "
printf "(minimum 12): "
read -r LENGTH
2019-10-14 10:27:28 +00:00
2022-07-27 12:53:36 +00:00
# check length
until [[ "${LENGTH}" -gt 11 ]] && [[ "${LENGTH}" -lt 129 ]]; do
echo "Password must be between from 12 to 128 characters, please try again."
2019-10-14 10:27:28 +00:00
# ask user for length of password
2022-07-27 12:53:36 +00:00
printf "Please enter the length of characters you want your password to be "
printf "(minimum 12): "
2019-10-14 10:27:28 +00:00
read -r LENGTH
2022-07-27 12:53:36 +00:00
done
printf "Creating a PiVPN item for your vault..."
printf "\n"
# create a new item for your PiVPN Password
PASSWD="$(bw generate -usln --length "${LENGTH}")"
bw get template item |
jq '.login.type = "1"' |
jq '.name = "PiVPN"' |
jq -r --arg NAME "${NAME}" '.login.username = $NAME' |
jq -r --arg PASSWD "${PASSWD}" '.login.password = $PASSWD' |
bw encode |
bw create item
bw logout
}
2019-10-14 10:27:28 +00:00
2022-07-27 12:53:36 +00:00
keyPASS() {
if [[ -z "${PASSWD}" ]]; then
stty -echo
2019-10-14 10:27:28 +00:00
2022-07-27 12:53:36 +00:00
while true; do
printf "Enter the password for the client: "
read -r PASSWD
printf "\n"
printf "Enter the password again to verify: "
read -r PASSWD2
printf "\n"
2019-10-14 10:27:28 +00:00
2022-07-27 12:53:36 +00:00
[[ "${PASSWD}" == "${PASSWD2}" ]] && break
printf "Passwords do not match! Please try again.\n"
done
2019-10-14 10:27:28 +00:00
2022-07-27 12:53:36 +00:00
stty echo
2019-10-14 10:27:28 +00:00
if [[ -z "${PASSWD}" ]]; then
2022-07-27 12:53:36 +00:00
err "You left the password blank"
err "If you don't want a password, please run:"
err "pivpn add nopass"
exit 1
2019-10-14 10:27:28 +00:00
fi
2022-07-27 12:53:36 +00:00
fi
2019-10-14 10:27:28 +00:00
2022-07-27 12:53:36 +00:00
if [[ "${#PASSWD}" -lt 4 ]] || [[ "${#PASSWD}" -gt 1024 ]]; then
err "Password must be between from 4 to 1024 characters"
exit 1
fi
# Escape chars in PASSWD
PASSWD_UNESCAPED="${PASSWD}"
PASSWD="${PASSWD//\\/\\\\}"
PASSWD="${PASSWD//\//\\\/}"
PASSWD="${PASSWD//\$/\\\$}"
PASSWD="${PASSWD//!/\\!}"
PASSWD="${PASSWD//\./\\\.}"
PASSWD="${PASSWD//\'/\\\'}"
PASSWD="${PASSWD//\"/\\\"}"
PASSWD="${PASSWD//*/\\*}"
PASSWD="${PASSWD//@/\\@}"
PASSWD="${PASSWD//#/\\#}"
PASSWD="${PASSWD///\\}"
PASSWD="${PASSWD//%/\\%}"
PASSWD="${PASSWD//\^/\\\^}"
PASSWD="${PASSWD//\&/\\\&}"
PASSWD="${PASSWD//\(/\\\(}"
PASSWD="${PASSWD//\)/\\\)}"
PASSWD="${PASSWD//\-/\\\-}"
PASSWD="${PASSWD//\_/\\\_}"
PASSWD="${PASSWD//\+/\\\+}"
PASSWD="${PASSWD//\=/\\\=}"
PASSWD="${PASSWD//\[/\\\[}"
PASSWD="${PASSWD//\]/\\\]}"
PASSWD="${PASSWD//:/\\:}"
PASSWD="${PASSWD//\;/\\\;}"
PASSWD="${PASSWD//\|/\\\|}"
PASSWD="${PASSWD//\</\\\<}"
PASSWD="${PASSWD//\>/\\\>}"
PASSWD="${PASSWD//\,/\\\,}"
PASSWD="${PASSWD//\~/\\\~}"
PASSWD="${PASSWD//\?/\\\?}"
PASSWD="${PASSWD//\{/\\\{}"
PASSWD="${PASSWD//\}/\\\}}"
# Build the client key and then encrypt the key
expect << EOF
set timeout -1
set env(EASYRSA_CERT_EXPIRE) "${DAYS}"
spawn ./easyrsa build-client-full "${NAME}"
expect "Enter PEM pass phrase" {sleep 0.1; send -- "${PASSWD}\r"}
expect "Verifying - Enter PEM pass phrase" {sleep 0.1; send -- "${PASSWD}\r"}
expect eof
2019-10-14 10:27:28 +00:00
EOF
2022-07-27 12:53:36 +00:00
cd pki || exit
2019-10-14 10:27:28 +00:00
}
#make sure ovpns dir exists
# Disabling warning for SC2154, var sourced externaly
# shellcheck disable=SC2154
2022-07-27 12:53:36 +00:00
if [[ ! -d "${install_home}/ovpns" ]]; then
mkdir "${install_home}/ovpns"
chown "${install_user}:${install_user}" "${install_home}/ovpns"
chmod 0750 "${install_home}/ovpns"
2019-10-14 10:27:28 +00:00
fi
#bitWarden
if [[ "${BITWARDEN}" =~ "2" ]]; then
2022-07-27 12:53:36 +00:00
useBitwarden
fi
2019-10-14 10:27:28 +00:00
if [[ -z "${NAME}" ]]; then
2022-07-27 12:53:36 +00:00
printf "Enter a Name for the Client: "
read -r NAME
elif [[ "${NAME::1}" == "." ]] || [[ "${NAME::1}" == "-" ]]; then
err "Names cannot start with a dot (.) or a dash (-)."
exit 1
elif [[ "${NAME}" =~ [^a-zA-Z0-9.@_-] ]]; then
err "Name can only contain alphanumeric characters and these symbols (.-@_)."
exit 1
elif [[ "${NAME}" =~ ^[0-9]+$ ]]; then
err "Names cannot be integers."
exit 1
elif [[ -z "${NAME}" ]]; then
err "You cannot leave the name blank."
exit 1
2019-10-14 10:27:28 +00:00
fi
2022-07-27 12:53:36 +00:00
if [[ "${GENOVPNONLY}" == 1 ]]; then
# Generate .ovpn configuration file
cd /etc/openvpn/easy-rsa/pki || exit
2020-05-31 22:39:18 +00:00
else
2022-07-27 12:53:36 +00:00
# Check if name is already in use
while read -r line || [[ -n "${line}" ]]; do
STATUS=$(echo "${line}" | awk '{print $1}')
if [[ "${STATUS}" == "V" ]]; then
# Disabling SC2001 as ${variable//search/replace}
# doesn't go well with regexp
# shellcheck disable=SC2001
CERT="$(echo "${line}" | sed -e 's:.*/CN=::')"
if [[ "${CERT}" == "${NAME}" ]]; then
INUSE="1"
break
fi
2020-05-31 22:39:18 +00:00
fi
2022-07-27 12:53:36 +00:00
done < "${INDEX}"
2019-10-14 10:27:28 +00:00
2022-07-27 12:53:36 +00:00
if [[ "${INUSE}" == 1 ]]; then
err "!! This name is already in use by a Valid Certificate."
err "Please choose another name or revoke this certificate first."
exit 1
# Check if name is reserved
elif [[ "${NAME}" == "ta" ]] ||
[[ "${NAME}" == "server" ]] ||
[[ "${NAME}" == "ca" ]]; then
err "Sorry, this is in use by the server and cannot be used by clients."
exit 1
fi
# As of EasyRSA 3.0.6, by default certificates last 1080 days,
# see https://github.com/OpenVPN/easy-rsa/blob/6b7b6bf1f0d3c9362b5618ad18c66677351cacd1/easyrsa3/vars.example
if [[ -z "${DAYS}" ]]; then
read -r -e -p "How many days should the certificate last? " -i 1080 DAYS
fi
if [[ ! "${DAYS}" =~ ^[0-9]+$ ]] ||
[[ "${DAYS}" -lt 1 ]] ||
[[ "${DAYS}" -gt 3650 ]]; then
# The CRL lasts 3650 days so it doesn't make much sense
# that certificates would last longer
err "Please input a valid number of days, between 1 and 3650 inclusive."
exit 1
fi
2019-10-14 10:27:28 +00:00
2022-07-27 12:53:36 +00:00
cd /etc/openvpn/easy-rsa || exit
2019-10-14 10:27:28 +00:00
2022-07-27 12:53:36 +00:00
if [[ "${NO_PASS}" =~ "1" ]]; then
if [[ -n "${PASSWD}" ]]; then
err "Both nopass and password arguments passed to the script. Please use either one."
exit 1
2019-10-14 10:27:28 +00:00
else
2022-07-27 12:53:36 +00:00
keynoPASS
2019-10-14 10:27:28 +00:00
fi
2022-07-27 12:53:36 +00:00
else
keyPASS
fi
2019-10-14 10:27:28 +00:00
fi
#1st Verify that clients Public Key Exists
2022-07-27 12:53:36 +00:00
if [[ ! -f "issued/${NAME}${CRT}" ]]; then
err "[ERROR]: Client Public Key Certificate not found: ${NAME}${CRT}"
exit
2019-10-14 10:27:28 +00:00
fi
2022-07-27 12:53:36 +00:00
echo "Client's cert found: ${NAME}${CRT}"
2019-10-14 10:27:28 +00:00
#Then, verify that there is a private key for that client
2022-07-27 12:53:36 +00:00
if [[ ! -f "private/${NAME}${KEY}" ]]; then
err "[ERROR]: Client Private Key not found: ${NAME}${KEY}"
exit
2019-10-14 10:27:28 +00:00
fi
2022-07-27 12:53:36 +00:00
echo "Client's Private Key found: ${NAME}${KEY}"
2019-10-14 10:27:28 +00:00
#Confirm the CA public key exists
2022-07-27 12:53:36 +00:00
if [[ ! -f "${CA}" ]]; then
err "[ERROR]: CA Public Key not found: ${CA}"
exit
2019-10-14 10:27:28 +00:00
fi
2022-07-27 12:53:36 +00:00
echo "CA public Key found: ${CA}"
2019-10-14 10:27:28 +00:00
#Confirm the tls key file exists
2022-07-27 12:53:36 +00:00
if [[ ! -f "${TA}" ]]; then
err "[ERROR]: tls Private Key not found: ${TA}"
exit
2019-10-14 10:27:28 +00:00
fi
2022-07-27 12:53:36 +00:00
echo "tls Private Key found: ${TA}"
2019-10-14 10:27:28 +00:00
## Added new step to create an .ovpn12 file that can be stored on iOS keychain
2022-07-27 12:53:36 +00:00
## This step is more secure method and does not require the end-user to keep
## entering passwords, or storing the client private cert where it can be easily
## tampered
2019-10-14 10:27:28 +00:00
## https://openvpn.net/faq/how-do-i-use-a-client-certificate-and-private-key-from-the-ios-keychain/
2022-07-27 12:53:36 +00:00
# Generates the .ovpn file WITHOUT the client private key
{
# Start by populating with the default file
cat "${DEFAULT}"
# Now, append the CA Public Cert
echo "<ca>"
cat "${CA}"
echo "</ca>"
# Next append the client Public Cert
echo "<cert>"
sed \
-n \
-e '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' \
< "issued/${NAME}${CRT}"
echo "</cert>"
if [[ "${iOS}" != 1 ]]; then
# Then, append the client Private Key
2019-10-14 10:27:28 +00:00
echo "<key>"
cat "private/${NAME}${KEY}"
echo "</key>"
2022-07-27 12:53:36 +00:00
fi
2019-10-14 10:27:28 +00:00
2022-07-27 12:53:36 +00:00
# Finally, append the tls Private Key
if [[ "${iOS}" != 1 ]] && [[ "${TWO_POINT_FOUR}" -eq 1 ]]; then
echo "<tls-crypt>"
cat "${TA}"
echo "</tls-crypt>"
else
echo "<tls-auth>"
cat "${TA}"
echo "</tls-auth>"
fi
} > "${NAME}${FILEEXT}"
if [[ "${iOS}" == 1 ]]; then
# Copy the .ovpn profile to the home directory for convenient remote access
printf "========================================================\n"
printf "Generating an .ovpn12 file for use with iOS devices\n"
printf "Please remember the export password\n"
printf "as you will need this import the certificate on your iOS device\n"
printf "========================================================\n"
openssl pkcs12 \
-passin pass:"${PASSWD_UNESCAPED}" \
-export \
-in "issued/${NAME}${CRT}" \
-inkey "private/${NAME}${KEY}" \
-certfile "${CA}" \
-name "${NAME}" \
-out "${install_home}/ovpns/${NAME}.ovpn12"
chown "${install_user}:${install_user}" "${install_home}/ovpns/${NAME}.ovpn12"
chmod 640 "${install_home}/ovpns/${NAME}.ovpn12"
printf "========================================================\n"
printf "\e[1mDone! %s successfully created!\e[0m \n" "${NAME}.ovpn12"
printf "You will need to transfer both the .ovpn and .ovpn12 files\n"
printf "to your iOS device.\n"
printf "========================================================\n\n"
2019-10-14 10:27:28 +00:00
fi
2022-07-27 12:53:36 +00:00
cidrToMask() {
# Source: https://stackoverflow.com/a/20767392
set -- $((5 - (${1} / 8))) \
255 255 255 255 \
$(((255 << (8 - (${1} % 8))) & 255)) \
0 0 0
shift "${1}"
echo "${1-0}.${2-0}.${3-0}.${4-0}"
}
#disabling SC2514, variable sourced externaly
# shellcheck disable=SC2154
NET_REDUCED="${pivpnNET::-2}"
# Find an unused number for the last octet of the client IP
for i in {2..254}; do
2022-07-27 12:53:36 +00:00
# find returns 0 if the folder is empty, so we create the 'ls -A [...]'
# exception to stop at the first static IP (10.8.0.2). Otherwise it would
# cycle to the end without finding and available octet.
# disabling SC2514, variable sourced externaly
# shellcheck disable=SC2154
if [[ -z "$(ls -A /etc/openvpn/ccd)" ]] ||
! find /etc/openvpn/ccd \
-type f \
-exec grep -q "${NET_REDUCED}.${i}" {} +; then
COUNT="${i}"
echo -n "ifconfig-push ${NET_REDUCED}.${i}" >> /etc/openvpn/ccd/"${NAME}"
cidrToMask "${subnetClass}" >> /etc/openvpn/ccd/"${NAME}"
break
fi
done
2019-10-14 10:27:28 +00:00
2022-07-27 12:53:36 +00:00
if [[ -f /etc/pivpn/hosts.openvpn ]]; then
echo "${NET_REDUCED}.${COUNT} ${NAME}.pivpn" >> /etc/pivpn/hosts.openvpn
if killall -SIGHUP pihole-FTL; then
echo "::: Updated hosts file for Pi-hole"
else
err "::: Failed to reload pihole-FTL configuration"
fi
fi
2019-10-14 10:27:28 +00:00
# Copy the .ovpn profile to the home directory for convenient remote access
2022-07-27 12:53:36 +00:00
dest_path="${install_home}/ovpns/${NAME}${FILEEXT}"
cp "/etc/openvpn/easy-rsa/pki/${NAME}${FILEEXT}" "${dest_path}"
chown "${install_user}:${install_user}" "${dest_path}"
chmod 640 "/etc/openvpn/easy-rsa/pki/${NAME}${FILEEXT}"
chmod 640 "${dest_path}"
unset dest_path
2019-10-14 10:27:28 +00:00
printf "\n\n"
printf "========================================================\n"
2022-07-27 12:53:36 +00:00
printf "\e[1mDone! %s successfully created!\e[0m \n" "${NAME}${FILEEXT}"
printf "%s was copied to:\n" "${NAME}${FILEEXT}"
printf " %s/ovpns\n" "${install_home}"
2019-10-14 10:27:28 +00:00
printf "for easy transfer. Please use this profile only on one\n"
printf "device and create additional profiles for other devices.\n"
printf "========================================================\n\n"