From 2a34399e614637791bba6026de588e9937238f96 Mon Sep 17 00:00:00 2001 From: Clemens Schwaighofer Date: Fri, 30 Aug 2024 10:42:33 +0900 Subject: [PATCH] Add user public keys to admin user on the server This only adds the admin user authorized_keys file, not the master one --- .shellcheckrc | 3 + bin/user-add-ssh-key.sh | 186 +++++++++++++++++++++++ bin/user-remove-ssh-key.sh | 157 +++++++++++++++++++ ssh-public-keys/user-current/.gitignore | 2 + ssh-public-keys/user-previous/.gitignore | 2 + 5 files changed, 350 insertions(+) create mode 100644 .shellcheckrc create mode 100644 bin/user-add-ssh-key.sh create mode 100644 bin/user-remove-ssh-key.sh create mode 100644 ssh-public-keys/user-current/.gitignore create mode 100644 ssh-public-keys/user-previous/.gitignore diff --git a/.shellcheckrc b/.shellcheckrc new file mode 100644 index 0000000..e60d9fe --- /dev/null +++ b/.shellcheckrc @@ -0,0 +1,3 @@ +shell=bash +# source-path=config +external-sources=true diff --git a/bin/user-add-ssh-key.sh b/bin/user-add-ssh-key.sh new file mode 100644 index 0000000..c79f2e6 --- /dev/null +++ b/bin/user-add-ssh-key.sh @@ -0,0 +1,186 @@ +#!/usr/bin/env bash + +# Add user key to admin SSH KEY LIST + +# base folder for all data +BASE_FOLDER=$(dirname "$(readlink -f "$0")")"/"; +# config folder +CONFIG_BASE="${BASE_FOLDER}../config/"; +# current public key folder for user +SSH_PUBLIC_KEYS_CURRENT="${BASE_FOLDER}../ssh-public-keys/user-current/"; + +DRY_RUN=0; +GO=0; +HOST_ONLY=""; +USER_ONLY=""; +USER_PUBLIC_KEY=""; +# +while getopts ":h:u:k:ng" opt; do + case "${opt}" in + h) # hostname + HOST_ONLY="${OPTARG}"; + ;; + u) # username + USER_ONLY="${OPTARG}"; + ;; + k) # user public key + USER_PUBLIC_KEY="${OPTARG}"; + ;; + n) # dry-run + DRY_RUN=1; + ;; + g) # go + GO=1; + ;; + \?) + echo -e "\n Option does not exist: ${OPTARG}\n"; + echo "-h override single host name"; + echo "-u override user name for a host"; + echo "-k public key file to add"; + echo "-n dry run"; + echo "-g flag for actual change call"; + echo "" + exit 1; + ;; + esac +done + +# the public key file for the user +SSH_KEY_PUB_FILE="${SSH_PUBLIC_KEYS_CURRENT}${USER_PUBLIC_KEY}" ; + +# no current public key file with given name +if [ ! -f "${SSH_KEY_PUB_FILE}" ]; then + echo "Missing user ssh public key file to add: ${SSH_KEY_PUB_FILE}"; + exit; +else + # validate key + SSH_PUBLIC_KEY_VALIDATE="ssh-keygen -l -f"; + # message=$(${SSH_PUBLIC_KEY_VALIDATE} "${SSH_KEY_PUB_FILE}"); + if ! message=$(${SSH_PUBLIC_KEY_VALIDATE} "${SSH_KEY_PUB_FILE}" 2>&1); then + echo "Failed to parse ssh public key: ${message}"; + exit; + fi; +fi + +# load config +if [ ! -f "${CONFIG_BASE}settings.ini" ]; then + echo "Missing 'settings.ini' file in ${CONFIG_BASE}"; + exit; +fi +# shellcheck source=../config/settings.ini +# shellcheck disable=SC1094 +source <(grep "=" "${CONFIG_BASE}settings.ini" | sed 's/ *= */=/g') +if [ -z "${server_list}" ]; then + echo "No server list is defined in the settings"; + exit; +fi; +# we must have "server_list" set and file must be in config folder +if [ ! -f "${CONFIG_BASE}${server_list}" ]; then + echo "Cannot find ${server_list} file in the config folder"; + exit +fi +# abort if go not set +if [ ${GO} -eq 0 ] && [ ${DRY_RUN} -eq 1 ]; then + GO=1; +elif [ ${GO} -eq 0 ]; then + echo "No -g (go) parameter set. aborting. For testing set -n for dry run" + exit; +fi + +# default ssh command +# -t is needed for systens when "Defaults requiretty" is set +SSH="ssh -a -x -n"; + +# Add the SSH Key to an auth file if it does not exist yet and the auth file does exist +# Build bash command to run this +# @Params +# AUTH_KEY_FILE {1}: the auth key file where to add the key +# PUB_KEY_FILE {2}: Public key file name +add_ssh_key() { + AUTH_KEY_FILE="${1}"; + PUB_KEY_FILE="${2}"; + AUTH_KEY_SETTINGS="${3}"; + RMV_CHATTR_I="chattr -i" + ADD_CHATTR_I="chattr +i" + RMV_CHMOD_UW="chmod u-w" + ADD_CHMOD_UW="chmod u+w" + # check if the auth file exists and the key is not yet in the auth file + # the -z `tail ...` checks for a trailing newline. The echo adds one if was missing (from ssh-copy-id) + # PROBLEM: + # for grep from pipe, the left data is removed. we also can't cat from pipe + # into a var as that would go through a pipe and not be visible + # so we get the pub key file name and read it here + pub_key=$(cat "${PUB_KEY_FILE}"); + # if we have auth key settings, prefix them to the pub key + # Note that the check key "pub_key" ignores any prefixes, but we add with settings prefix + if [ -n "${AUTH_KEY_SETTINGS}" ]; then + pub_key_write="${AUTH_KEY_SETTINGS} ${pub_key}"; + else + pub_key_write="${pub_key}"; + fi + INSTALLKEYS_SH=$(tr '\t\n' ' ' <<-EOF + if [ -f "${AUTH_KEY_FILE}" ] && ! grep "${pub_key}" "${AUTH_KEY_FILE}" >> /dev/null; then + ${RMV_CHATTR_I} "${AUTH_KEY_FILE}"; + ${ADD_CHMOD_UW} "${AUTH_KEY_FILE}"; + { [ -z \`tail -1c ${AUTH_KEY_FILE} 2>/dev/null\` ] || + echo >> "${AUTH_KEY_FILE}" || exit 1; } && + echo "${pub_key_write}" >> "${AUTH_KEY_FILE}" || exit 1; + ${RMV_CHMOD_UW} "${AUTH_KEY_FILE}"; + ${ADD_CHATTR_I} "${AUTH_KEY_FILE}"; + fi; + EOF + ); + # to defend against quirky remote shells: use 'exec sh -c' to get POSIX; + printf "exec sudo sh -c '%s'" "${INSTALLKEYS_SH}" +} + +# install call +# @Params +# HOSTNAME {1} hostname to access +# USERNAME {2} username to use +# PUB_KEY_FILE {3} public key file to add +# AUTH_KEY_FILE {4} auth key file where to add the public key +install_ssh_key() { + HOSTNAME="${1}"; + USERNAME="${2}"; + PUB_KEY_FILE="${3}"; + AUTH_KEY_FILE="${4}"; + AUTH_KEY_SETTINGS="${5}"; + echo "[.] Add to auth file: ${AUTH_KEY_FILE}"; + if [ ${DRY_RUN} -eq 0 ]; then + ${SSH} "${USERNAME}"@"${HOSTNAME}" "$(add_ssh_key "${AUTH_KEY_FILE}" "${PUB_KEY_FILE}" "${AUTH_KEY_SETTINGS}")" + else + echo "${SSH} \"${USERNAME}\"@\"${HOSTNAME}\" \"\$(add_ssh_key \"${AUTH_KEY_FILE}\" \"${PUB_KEY_FILE}\" \"${AUTH_KEY_SETTINGS}\")\""; + fi +} + +while read -r line; do + if [[ "${line}" =~ ^\# ]]; then + continue; + fi + # hostname is on pos 1 + hostname=$(echo "${line}" | cut -d "," -f 1); + # if hostname opt set and not matching skip + if [ -n "${HOST_ONLY}" ] && [ "${HOST_ONLY}" != "${hostname}" ]; then + continue; + fi + # login user name + username=$(echo "${line}" | cut -d "," -f 2); + # if username opt set and not matching skip + if [ -n "${USER_ONLY}" ] && [ "${USER_ONLY}" != "${username}" ]; then + continue; + fi + + echo "[+] Add new public key '${SSH_KEY_PUB_FILE}' to: ${username}@${hostname}"; + + # flags: (not used at the moment) + # Possible: U (add to .ssh/authorized_keys) + # flags=$(echo "${line}" | cut -d "," -f 3); + # auth key settings (in front of auth key) + auth_key_settings=$(echo "${line}" | cut -d "," -f 4); + install_ssh_key "${hostname}" "${username}" "${SSH_KEY_PUB_FILE}" "/etc/ssh/authorized_keys/${username}" "${auth_key_settings}"; + + echo "[=] ............... DONE"; +done <<<"$(sed 1d "${CONFIG_BASE}${server_list}")"; + +# __END__ diff --git a/bin/user-remove-ssh-key.sh b/bin/user-remove-ssh-key.sh new file mode 100644 index 0000000..c1f77f6 --- /dev/null +++ b/bin/user-remove-ssh-key.sh @@ -0,0 +1,157 @@ +#!/usr/bin/env bash + +# Remove user key to admin SSH KEY LIST + +# base folder for all data +BASE_FOLDER=$(dirname "$(readlink -f "$0")")"/"; +# config folder +CONFIG_BASE="${BASE_FOLDER}../config/"; +# current public key folder for user +SSH_PUBLIC_KEYS_PREVIOUS="${BASE_FOLDER}../ssh-public-keys/user-previous/"; + +DRY_RUN=0; +GO=0; +HOST_ONLY=""; +USER_ONLY=""; +USER_PUBLIC_KEY=""; +# +while getopts ":h:u:k:ng" opt; do + case "${opt}" in + h) # hostname + HOST_ONLY="${OPTARG}"; + ;; + u) # username + USER_ONLY="${OPTARG}"; + ;; + k) # user public key + USER_PUBLIC_KEY="${OPTARG}"; + ;; + n) # dry-run + DRY_RUN=1; + ;; + g) # go + GO=1; + ;; + \?) + echo -e "\n Option does not exist: ${OPTARG}\n"; + echo "-h override single host name"; + echo "-u override user name for a host"; + echo "-k public key file to remove"; + echo "-n dry run"; + echo "-g flag for actual change call"; + echo "" + exit 1; + ;; + esac +done + +# the public key file for the user +SSH_KEY_PUB_FILE="${SSH_PUBLIC_KEYS_PREVIOUS}${USER_PUBLIC_KEY}" ; + +# no current public key file with given name +if [ ! -f "${SSH_KEY_PUB_FILE}" ]; then + echo "Missing user ssh public key file to remove: ${SSH_KEY_PUB_FILE}"; + exit; +else + # validate key + SSH_PUBLIC_KEY_VALIDATE="ssh-keygen -l -f"; + # message=$(${SSH_PUBLIC_KEY_VALIDATE} "${SSH_KEY_PUB_FILE}"); + if ! message=$(${SSH_PUBLIC_KEY_VALIDATE} "${SSH_KEY_PUB_FILE}" 2>&1); then + echo "Failed to parse ssh public key: ${message}"; + exit; + fi; +fi + +# load config +if [ ! -f "${CONFIG_BASE}settings.ini" ]; then + echo "Missing 'settings.ini' file in ${CONFIG_BASE}"; + exit; +fi +# shellcheck source=../config/settings.ini +# shellcheck disable=SC1094 +source <(grep "=" "${CONFIG_BASE}settings.ini" | sed 's/ *= */=/g') +if [ -z "${server_list}" ]; then + echo "No server list is defined in the settings"; + exit; +fi; +# we must have "server_list" set and file must be in config folder +if [ ! -f "${CONFIG_BASE}${server_list}" ]; then + echo "Cannot find ${server_list} file in the config folder"; + exit +fi +# abort if go not set +if [ ${GO} -eq 0 ] && [ ${DRY_RUN} -eq 1 ]; then + GO=1; +elif [ ${GO} -eq 0 ]; then + echo "No -g (go) parameter set. aborting. For testing set -n for dry run" + exit; +fi + +# default ssh command +# -t is needed for systens when "Defaults requiretty" is set +SSH="ssh -a -x -n"; + +remove_ssh_key() { + AUTH_KEY_FILE="${1}"; + PUB_KEY_FILE="${2}"; + RMV_CHATTR_I="chattr -i" + ADD_CHATTR_I="chattr +i" + RMV_CHMOD_UW="chmod u-w" + ADD_CHMOD_UW="chmod u+w" + pub_key=$(cat "${PUB_KEY_FILE}"); + # we need to escape for sed + pub_key_escaped=$(printf '%s\n' "$pub_key" | sed -e 's/[]\/$*.^[]/\\&/g'); + # the -z `tail ...` checks for a trailing newline. The echo adds one if was missing (from ssh-copy-id) + UNINSTALLKEYS_SH=$(tr '\t\n' ' ' <<-EOF + if [ -f "${AUTH_KEY_FILE}" ] && grep "${pub_key}" "${AUTH_KEY_FILE}" >> /dev/null; then + ${RMV_CHATTR_I} "${AUTH_KEY_FILE}"; + ${ADD_CHMOD_UW} "${AUTH_KEY_FILE}"; + sed -i "/${pub_key_escaped}/d" "${AUTH_KEY_FILE}"; + ${RMV_CHMOD_UW} "${AUTH_KEY_FILE}"; + ${ADD_CHATTR_I} "${AUTH_KEY_FILE}"; + fi; + EOF + ); + # to defend against quirky remote shells: use 'exec sh -c' to get POSIX; + printf "exec sudo sh -c '%s'" "${UNINSTALLKEYS_SH}" +} + +uninstall_ssh_key() { + HOSTNAME="${1}"; + USERNAME="${2}"; + PUB_KEY_FILE="${3}"; + AUTH_KEY_FILE="${4}"; + echo "[.] Remove from auth file: ${AUTH_KEY_FILE}"; + if [ ${DRY_RUN} -eq 0 ]; then + # find the pub key in the file and remove this line only + ${SSH} "${USERNAME}"@"${HOSTNAME}" "$(remove_ssh_key "${AUTH_KEY_FILE}" "${PUB_KEY_FILE}")" + else + echo "${SSH} \"${USERNAME}\"@\"${HOSTNAME}\" \"\$(remove_ssh_key \"${AUTH_KEY_FILE}\" \"${PUB_KEY_FILE}\")\""; + fi +} + +while read -r line; do + if [[ "${line}" =~ ^\# ]]; then + continue; + fi + # hostname is on pos 1 + hostname=$(echo "${line}" | cut -d "," -f 1); + # if hostname opt set and not matching skip + if [ -n "${HOST_ONLY}" ] && [ "${HOST_ONLY}" != "${hostname}" ]; then + continue; + fi + # login user name + username=$(echo "${line}" | cut -d "," -f 2); + # if username opt set and not matching skip + if [ -n "${USER_ONLY}" ] && [ "${USER_ONLY}" != "${username}" ]; then + continue; + fi + + echo "[-] Remove public key '${SSH_KEY_PUB_FILE}' from: ${username}@${hostname}"; + + uninstall_ssh_key "${hostname}" "${username}" "${SSH_KEY_PUB_FILE}" "/etc/ssh/authorized_keys/${username}" + + echo "[=] ............... DONE"; +done <<<"$(sed 1d "${CONFIG_BASE}${server_list}")"; + +# __END__ diff --git a/ssh-public-keys/user-current/.gitignore b/ssh-public-keys/user-current/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/ssh-public-keys/user-current/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/ssh-public-keys/user-previous/.gitignore b/ssh-public-keys/user-previous/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/ssh-public-keys/user-previous/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore