scripts/duplicati/add.ssh.pubkey.sh

464 lines
15 KiB
Bash
Raw Normal View History

#!/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
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
2022-12-19 16:16:27 +01:00
printf "%s\n" "${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
## }}}
2023-01-25 11:23:36 +01:00
## If the key is present {{{
## --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
2023-01-25 11:23:36 +01:00
## Rename the pubkey file to keep a trace
## AND exit with success
printf "%b\n" "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})." \
&& mv --force -- "${SSH_PUBKEY_FILE}" "${SSH_PUBKEY_FILE}.done.${USER_NAME}" \
&& 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