462 lines
15 KiB
Bash
Executable File
462 lines
15 KiB
Bash
Executable File
#!/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\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
|
||
## }}}
|
||
## 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
|