diff --git a/duplicati/add.ssh.pubkey.sh b/duplicati/add.ssh.pubkey.sh new file mode 100755 index 0000000..7aedb45 --- /dev/null +++ b/duplicati/add.ssh.pubkey.sh @@ -0,0 +1,455 @@ +#!/bin/sh +# +# Purpose {{{ +# This script will try to allow SSH connection for a user by +# 1. Getting the username. +# * Directly from the argument. +# * Or from an ldap search with a given email address. +# * Verify that the system knows this user. +# 2. Adding the pubkey to a specific authorized_keys file. +# * System wide authorized_keys directory (default to /etc/ssh/authorized_keys). +# * One authorized_keys file per user. +# * Adding some restriction to the pubkey (allow only scp by default). +# … +# +# 2022-07-19 +# }}} +# Vars {{{ +PROGNAME=$(basename "${0}"); readonly PROGNAME +PROGDIR=$(readlink -m $(dirname "${0}")); readonly PROGDIR +ARGS="${*}"; readonly ARGS +readonly NBARGS="${#}" +[ -z "${DEBUG}" ] && DEBUG=1 +## Export DEBUG for sub-script +export DEBUG + +## Default values for some vars +readonly SSH_PUBKEY_FILE_DEFAULT="/tmp/duplicati_id_ed25519.pub" +readonly SSH_PUBKEY_OPTION_DEFAULT='no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,command="/usr/lib/openssh/sftp-server"' +readonly SSHD_AUTH_KEY_DIR_DEFAULT="/home/.duplicati/ssh/authorized_keys" + +## Colors +readonly PURPLE='\033[1;35m' +readonly RED='\033[0;31m' +readonly RESET='\033[0m' +readonly COLOR_DEBUG="${PURPLE}" +# }}} +usage() { # {{{ + + cat <<- HELP +usage: $PROGNAME [-a|-d|-f|-h|-k|-o] -e|-u + +This script will try to add a SSH pubkey to allow user's connection. + +EXAMPLES : + - Add default pubkey file (${SSH_PUBKEY_FILE_DEFAULT} to default authorized_keys location (${SSHD_AUTH_KEY_DIR_DEFAULT}) for USERNAME + ${PROGNAME} --user USERNAME + + - Add pubkey file to a user from the given email address + ${PROGNAME} --keyfile /tmp/id_ed25519.pub --mail last.name@domain.org + + - Add given pubkey for USERNAME + ${PROGNAME} --key "ssh-ed25519 AAAAC3NzaC1…" --user USERNAME + + - Add pubkey to specific authorized_keys location + ${PROGNAME} --user USERNAME --authorized_keys /etc/ssh/authorized_keys + + - Limit "only" port and X11 forwarding on the public key + ${PROGNAME} --user USERNAME --option "no-port-forwarding,no-X11-forwarding" + +OPTIONS : + -a,--authorized,--authorized_keys + Set authorized_keys directory to use. + Default : "${SSHD_AUTH_KEY_DIR_DEFAULT} + + -d,--debug + Enable debug messages. + + -h,--help + Print this help message. + + -e,--email,--mail + Email address to request username with ldapsearch. + + -f,--file,--keyfile + Path to a file containing the pubkey to allow. + Default : ${SSH_PUBKEY_FILE_DEFAULT} + + -k,--key + Complete pubkey with algorithm and the key. + If not set, fall back to default ssh pubkey file (${SSH_PUBKEY_FILE_DEFAULT} . + + -o,--option + Options to add to public key. Default basically allow only scp : + ${SSH_PUBKEY_OPTION_DEFAULT} + + -u,--user,--username + Username that should have access to the system with the given pubkey. + If not set, fall back to an LDAP request with email address to guess the username. + +HELP + +} +# }}} +debug_message() { # {{{ + + local_debug_message="${1}" + + ## Print message if DEBUG is enable (=0) + [ "${DEBUG}" -eq "0" ] && printf "${COLOR_DEBUG}%b${RESET}\n" "DEBUG − ${PROGNAME} : ${local_debug_message}" + + unset local_debug_message + + return 0 +} +# }}} +error_message() { # {{{ + + local_error_message="${1}" + local_error_code="${2}" + + ## Print message + printf '%b\n' "ERROR − ${PROGNAME} : ${RED}${local_error_message}${RESET}" + + exit "${local_error_code:=66}" +} +# }}} +define_vars() { # {{{ + + ## If USER_NAME wasn't defined (argument) {{{ + debug_message "define_vars − \ +Check USER related vars." + if is_var_empty "${USER_NAME}"; then + if is_var_defined "${USER_EMAIL}"; then + ## Search with email address on the ldap + debug_message "define_vars − \ +Use given email address (${RED}${USER_EMAIL}${COLOR_DEBUG}) to request LDAP server for a matching username." + USER_NAME="$(ldapsearch -x -ZZ -s one "(mail=${USER_EMAIL})" \ + | awk '/^uid: / { print $2 }')" + readonly USER_NAME + is_var_defined "${USER_NAME}" \ + || error_message "Error with username (${USER_NAME}), please verify the given email address (${USER_EMAIL})." 01 + else + error_message "Expect USERNAME informations (please use --user or --mail option)." 02 + fi + fi + debug_message "define_vars − \ +->${RED}${USER_NAME}${COLOR_DEBUG}<- username will be used." + ## }}} + ## If SSHD_AUTH_KEY_DIR wasn't defined (argument) {{{ + debug_message "define_vars − \ +Check authorized_keys related var." + if is_var_empty "${SSHD_AUTH_KEY_DIR}"; then + SSHD_AUTH_KEY_DIR="${SSHD_AUTH_KEY_DIR_DEFAULT}" + readonly SSHD_AUTH_KEY_DIR + fi + debug_message "define_vars − \ +->${RED}${SSHD_AUTH_KEY_DIR}${COLOR_DEBUG}<- directory will be used to store public keys." + ## }}} + ## If SSH_PUBKEY wasn't defined (argument) {{{ + debug_message "define_vars − \ +Check SSH_PUBKEY related vars." + if is_var_empty "${SSH_PUBKEY}"; then + ## Try to get SSH_PUBKEY from SSH_PUBKEY_FILE {{{ + ### If SSH_PUBKEY_FILE wasn't defined {{{ + if is_var_empty "${SSH_PUBKEY_FILE}"; then + ### Use default value for SSH_PUBKEY_FILE + SSH_PUBKEY_FILE="${SSH_PUBKEY_FILE_DEFAULT}" + readonly SSH_PUBKEY_FILE + fi + ### }}} + debug_message "define_vars − \ +->${RED}${SSH_PUBKEY_FILE}${COLOR_DEBUG}<- file will be used to provide user's public key." + ### If SSH_PUBKEY_FILE doesn't exists {{{ + #### AND Exit with error + is_file_absent "${SSH_PUBKEY_FILE}" \ + && error_message "Error, public key file (${SSH_PUBKEY_FILE}) doesn't exists (please check --keyfile option)." 03 + ### }}} + SSH_PUBKEY="$(grep --max-count=1 -- "ssh-" "${SSH_PUBKEY_FILE}" \ + || error_message "Error, can't retrieve ssh pubkey content from pubkey file (${SSH_PUBKEY_FILE}" 04)" + ### }}} + fi + # Can't use default debug_message function due to some limitations (size ?) + if [ "${DEBUG}" -eq "0" ]; then + printf "${COLOR_DEBUG}%b${RESET}\n" "DEBUG − ${PROGNAME} : define_vars − ->" + printf "${RED}%b${RESET}\n" "${SSH_PUBKEY}" + printf "${COLOR_DEBUG}%b${RESET}\n" "<- key will be used as user's public key." + fi + ## }}} + ## If SSH_PUBKEY_OPTION wasn't defined (argument) {{{ + debug_message "define_vars − \ +Check ssh_pubkey OPTIONS related var." + if is_var_empty "${SSH_PUBKEY_OPTION}"; then + ### Use default value for SSH_PUBKEY_OPTION + SSH_PUBKEY_OPTION="${SSH_PUBKEY_OPTION_DEFAULT}" + readonly SSH_PUBKEY_OPTION + fi + debug_message "define_vars − \ +->${RED}${SSH_PUBKEY_OPTION}${COLOR_DEBUG}<- options will be used for public key." + ## }}} + +} +# }}} +is_var_defined() { # {{{ + + ## Test if at least one of the given var(s) is defined + + ## Return False by default + return_var_defined="1" + ## Total number of variables to test + local_total_var="${#}" + + loop_count_var_defined="0" + + ## While it remains a variable to test + while [ "${local_total_var}" -gt "${loop_count_var_defined}" ]; do + debug_message "is_var_defined − \ +Test var: ${1}." + ### Test if this is defined and set return value to True + [ -n "${1}" ] && return_var_defined="0" + + ### Increase the number of tested variables + loop_count_var_defined=$((loop_count_var_defined+1)) + + ### Shift to the next variable + shift + done + + unset local_total_var + unset loop_count_var_defined + + return "${return_var_defined}" +} +# }}} +is_var_empty() { # {{{ + + ## Test if at least one of the given var(s) is empty + + ## Return False by default + return_var_empty="1" + ## Total number of variables to test + local_total_var="${#}" + + loop_count_var_empty="0" + + ## While it remains a variable to test + while [ "${local_total_var}" -gt "${loop_count_var_empty}" ]; do + debug_message "is_var_empty − \ +Test var: ${1}." + ### Test if this is empty and set return value to True + [ -z "${1}" ] && return_var_empty="0" + + ### Increase the number of tested variables + loop_count_var_empty=$((loop_count_var_empty+1)) + + ### Shift to the next variable + shift + done + + unset local_total_var + unset loop_count_var_defined + + return "${return_var_empty}" +} +# }}} +is_directory_absent() { # {{{ + + local_directory_absent="${1}" + + ## Directory doesn't exists by default + return_is_directory_absent="0" + + ### Check if the directory exists + # shellcheck disable=SC2086 + if test -d "${local_directory_absent}"; then + return_is_directory_absent="1" + debug_message "is_directory_absent − \ +The directory ${RED}${local_directory_absent}${COLOR_DEBUG} exists." + else + return_is_directory_absent="0" + debug_message "is_directory_absent − \ +The directory ${RED}${local_directory_absent}${COLOR_DEBUG} doesn't exist." + fi + + return "${return_is_directory_absent}" + +} +# }}} +is_file_absent() { # {{{ + + local_file_absent="${1}" + + ## File exists by default + return_is_file_absent="1" + + ### Check if the file exists + # shellcheck disable=SC2086 + if find ${local_file_absent} > /dev/null 2>&1; then + return_is_file_absent="1" + debug_message "is_file_absent − \ +The file ${RED}${local_file_absent}${COLOR_DEBUG} exists." + else + return_is_file_absent="0" + debug_message "is_file_absent − \ +The file ${RED}${local_file_absent}${COLOR_DEBUG} doesn't exist." + fi + + return "${return_is_file_absent}" + +} +# }}} + +main() { # {{{ + + ## Define all vars + define_vars + + ## Check if user exists {{{ + if id -u "${USER_NAME}" >/dev/null 2>&1; then + debug_message "main − \ +The user ${RED}${USER_NAME}${COLOR_DEBUG} was found on the system." + else + ### OR Exit with error message + error_message "According to given informations the user (${USER_NAME}) wasn't found on this system.\nPlease check the value passed to --user or --mail options." 11 + fi + ## }}} + ## Manage authorized_keys file {{{ + USER_AUTHORIZED_KEYS_FILE="${SSHD_AUTH_KEY_DIR}/${USER_NAME}" + ## Ensure authorized_keys directory exists {{{ + if is_directory_absent "${SSHD_AUTH_KEY_DIR}"; then + debug_message "main − \ +Create authorized_keys directory (${RED}${SSHD_AUTH_KEY_DIR}${COLOR_DEBUG})." + mkdir --parents -- "${SSHD_AUTH_KEY_DIR}" \ + || error_message "Error when creating authorized_keys directory (${SSHD_AUTH_KEY_DIR})." 21 + fi + ## }}} + ## Ensure user's authorized_keys file exists {{{ + if is_file_absent "${USER_AUTHORIZED_KEYS_FILE}"; then + debug_message "main − \ +Create authorized_keys file for ${USER_NAME} (${RED}${USER_AUTHORIZED_KEYS_FILE}${COLOR_DEBUG})." + touch -- "${USER_AUTHORIZED_KEYS_FILE}" \ + || error_message "Error when creating ${USER_NAME}'s authorized_keys file (${USER_AUTHORIZED_KEYS_FILE})." 22 + fi + ## }}} + ## }}} + ## If pubkey is already in authorized_keys file {{{ + ### AND Ensure it's set with the right options + ### AND Exit the script + if grep --word-regexp --quiet -- "${SSH_PUBKEY}" "${USER_AUTHORIZED_KEYS_FILE}"; then + sed -i "s|.*${SSH_PUBKEY}.*|${SSH_PUBKEY_OPTION} ${SSH_PUBKEY}|" "${USER_AUTHORIZED_KEYS_FILE}" \ + || error_message "Error during SSH_PUBKEY replacement with expected options in authorized_keys file (${USER_AUTHORIZED_KEYS_FILE})." 31 + debug_message "The given pubkey was already present in authorized_keys file (${RED}${USER_AUTHORIZED_KEYS_FILE}${COLOR_DEBUG}) and now have the expected permissions." + ## }}} + ## If pubkey isn't already in authorized_keys file {{{ + ### Add it with expected options + else + printf "%s" "${SSH_PUBKEY_OPTION} ${SSH_PUBKEY}" >> "${USER_AUTHORIZED_KEYS_FILE}" \ + || error_message "Error while adding SSH_PUBKEY with expected options in authorized_keys file (${USER_AUTHORIZED_KEYS_FILE})." 32 + debug_message "The given pubkey is now present in authorized_keys file (${RED}${USER_AUTHORIZED_KEYS_FILE}${COLOR_DEBUG}) with expected permissions." + fi + ## }}} + ## If the key is present, exit with success {{{ + grep --word-regexp --quiet -- "${SSH_PUBKEY_OPTION} ${SSH_PUBKEY}" "${USER_AUTHORIZED_KEYS_FILE}" \ + && printf "%b" "The given pubkey (${RED}${SSH_PUBKEY_FILE}${RESET}) for ${RED}${USER_NAME}${RESET} user was successfully added to it's authorized_keys file (${RED}${USER_AUTHORIZED_KEYS_FILE}${COLOR_DEBUG})." \ + && exit 0 + ## }}} +} +# }}} + +# Manage arguments # {{{ +# This code can't be in a function due to argument management + +if [ ! "${NBARGS}" -eq "0" ]; then + + manage_arg="0" + + ## If the first argument is not an option + if ! printf -- '%s' "${1}" | grep -q -E -- "^-+"; + then + ## Print help message and exit + printf '%b\n' "${RED}Invalid option: ${1}${RESET}" + printf '%b\n' "---" + usage + + exit 1 + fi + + # Parse all options (start with a "-") one by one + while printf -- '%s' "${1}" | grep -q -E -- "^-+"; do + + case "${1}" in + -a|--authorized_keys|--authorized ) ## Define SSHD_AUTH_KEY_DIR with given arg + ## Move to the next argument + shift + ## Define var + readonly SSHD_AUTH_KEY_DIR="${1}" + ;; + -d|--debug ) ## debug + DEBUG=0 + ;; + -h|--help ) ## help + usage + ## Exit after help informations + exit 0 + ;; + -e|--email|--mail ) ## Define USER_EMAIL with given arg + ## Move to the next argument + shift + ## Define var + readonly USER_EMAIL="${1}" + ;; + -f|--file|--keyfile ) ## Define SSH_PUBKEY_FILE with given arg + ## Move to the next argument + shift + ## Define var + readonly SSH_PUBKEY_FILE="${1}" + ;; + -o|--option|--options ) ## Define SSH_PUBKEY_OPTION with given arg + ## Move to the next argument + shift + ## Define var + readonly SSH_PUBKEY_OPTION="${1}" + ;; + -k|--key ) ## Define SSH_PUBKEY with given arg + ## Move to the next argument + shift + ## Define var + readonly SSH_PUBKEY="${1}" + ;; + -u|--user|--username ) ## Define USER_NAME with given arg + ## Move to the next argument + shift + ## Define var + readonly USER_NAME="${1}" + ;; + * ) ## unknow option + printf '%b\n' "${RED}Invalid option: ${1}${RESET}" + printf '%b\n' "---" + usage + exit 1 + ;; + esac + + debug_message "Arguments management − \ +${RED}${1}${COLOR_DEBUG} option managed." + + ## Move to the next argument + shift + manage_arg=$((manage_arg+1)) + + done + + debug_message "Arguments management − \ +${RED}${manage_arg}${COLOR_DEBUG} argument(s) successfully managed." +else + debug_message "Arguments management − \ +No arguments/options to manage." +fi + +# }}} + +# Call main +main + +# The script should never reach this line +error_message "Unexpected end of the script, please use '--debug' option to have more informations" 255