#!/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 {{{ ## --fixed-strings to be able to manage backslash ### AND Ensure it's set with the right options ### AND Exit the script if grep --fixed-strings --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 {{{ ## --fixed-strings to be able to manage backslash if grep --fixed-strings --word-regexp --quiet -- "${SSH_PUBKEY_OPTION} ${SSH_PUBKEY}" "${USER_AUTHORIZED_KEYS_FILE}"; then 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 else error_message "Error with verification of user authorized_keys content (${USER_AUTHORIZED_KEYS_FILE}). Can't detect the new SSH_PUBKEY." 33 fi ## }}} } # }}} # 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