From 77a332e324c502ce1d8bf4c3e9fa28b6cdc072dd Mon Sep 17 00:00:00 2001 From: Patrick Van der Veken Date: Wed, 30 Dec 2020 17:06:02 +0100 Subject: [PATCH] * added support for standard home directory location for public keys (`$HOME/.ssh`). Set `$key_location=use_sshd` in `update_ssh.conf[.local]` with `AuthorizedKeysFile` set to the default value in `sshd_config` (or use the value `.ssh/authorized_keys`). * added support for SELinux (CentOS/RHEL 8.x) * various fixes (incl. shellcheck + quoting) --- README.md | 11 +- manage_ssh.sh | 269 +++++++++++++++++++++++++++++------------------- update_ssh.conf | 8 ++ update_ssh.pl | 190 +++++++++++++++++++++++++--------- 4 files changed, 321 insertions(+), 157 deletions(-) diff --git a/README.md b/README.md index e778bb4..0243c31 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,14 @@

SSH Controls Logo

+## What's new + +:loudspeaker: **30/12/2020**: +* added support for standard home directory location for public keys (`$HOME/.ssh`). Set `$key_location=use_sshd` in `update_ssh.conf[.local]` with `AuthorizedKeysFile` set to the default value in `sshd_config` (or use the value `.ssh/authorized_keys`). *Caveat*: SSH Controls will not create parent nor intermediate directories in the public key file path if they are missing. +* added support for SELinux (CentOS/RHEL 8.x) +* various fixes + +## About + SSH Controls is a light-weight SSH **public key** distribution & management framework * uses a **desired state** model: SSH Controls *pushes* public keys from a key master (or slave) server onto client host(s) and applies them according to the central configuration. @@ -8,7 +17,7 @@ SSH Controls is a light-weight SSH **public key** distribution & management fram * supports a **Master→Slave→Client** model so that information can be propagated within more complex LAN set-ups. -* **shields** public keys from owners/users on client systems: SSH Controls requires the standard `sshd_config` to be reconfigured with an alternate path for the `AuthorizedKeysFile` setting so that public keys are stored in common location which cannot be manipulated by the owners of the public keys. This allows for more administrative control and better security. +* can **shield** public keys from owners/users on client systems: SSH Controls may require the standard `sshd_config` to be reconfigured with an alternate path for the `AuthorizedKeysFile` setting so that public keys are stored in common location which cannot be manipulated by the owners of the public keys. This allows for more administrative control and better security. * performs operations with **least privileges**: copy/distribute operations are performed with a low-privileged account. Only the actual key updates requires super-user privileges which need to be configured via SUDO. diff --git a/manage_ssh.sh b/manage_ssh.sh index 53a9ad8..f9dc9c6 100644 --- a/manage_ssh.sh +++ b/manage_ssh.sh @@ -43,7 +43,7 @@ # or LOCAL_CONFIG_FILE instead # define the version (YYYY-MM-DD) -typeset -r SCRIPT_VERSION="2020-05-28" +typeset -r SCRIPT_VERSION="2020-12-30" # name of the global configuration file (script) typeset -r GLOBAL_CONFIG_FILE="manage_ssh.conf" # name of the local configuration file (script) @@ -289,11 +289,11 @@ fi # --targets if [[ -n "${ARG_TARGETS}" ]] then - : > ${TMP_FILE} + : > "${TMP_FILE}" # write comma-separated target list to the temporary file - print "${ARG_TARGETS}" | tr -s ',' '\n' | while read TARGET_HOST + print "${ARG_TARGETS}" | tr -s ',' '\n' | while read -r TARGET_HOST do - print ${TARGET_HOST} >>${TMP_FILE} + print "${TARGET_HOST}" >>"${TMP_FILE}" done fi # --update + --fix-local + --resolve-alias @@ -333,7 +333,7 @@ function check_root_user typeset UID="" # shellcheck disable=SC2046 -(IFS='()'; set -- $(id); print $2) | read UID +(IFS='()'; set -- $(id); print "$2") | read -r UID if [[ "${UID}" = "root" ]] then return 0 @@ -427,6 +427,7 @@ if (( DO_SSH_AGENT )) then # ssh-agent which ssh-agent >/dev/null 2>/dev/null + # shellcheck disable=SC2181 if (( $? > 0 )) then print -u2 "WARN: ssh-agent not available on ${HOST_NAME}" @@ -457,24 +458,25 @@ typeset KEY_FILE="" typeset KEY_FIELDS="" # access should have 3 fields -grep -v -E -e '^#|^$' "${LOCAL_DIR}/access" 2>/dev/null | while read ACCESS_LINE +grep -v -E -e '^#|^$' "${LOCAL_DIR}/access" 2>/dev/null | while read -r ACCESS_LINE do ACCESS_FIELDS=$(count_fields "${ACCESS_LINE}" ":") (( ACCESS_FIELDS != 3 )) && die "line '${ACCESS_LINE}' in access file has missing or too many field(s) (should be 3)" done # alias should have 2 fields -grep -v -E -e '^#|^$' "${LOCAL_DIR}/alias" 2>/dev/null | while read ALIASES_LINE +grep -v -E -e '^#|^$' "${LOCAL_DIR}/alias" 2>/dev/null | while read -r ALIASES_LINE do ALIAS_FIELDS=$(count_fields "${ALIASES_LINE}" ":") (( ALIAS_FIELDS != 2 )) && die "line '${ALIASES_LINE}' in alias file has missing or too many field(s) (should be 2)" done # key files should have 3 fields -ls -1 ${LOCAL_DIR}/keys.d/* ${LOCAL_DIR}/keys 2>/dev/null | while read KEY_FILE +# shellcheck disable=SC2012 +ls -1 "${LOCAL_DIR}"/keys.d/* "${LOCAL_DIR}"/keys 2>/dev/null | while read -r KEY_FILE do - grep -v -E -e '^#|^$' ${KEY_FILE} 2>/dev/null |\ - while read KEY_LINE + grep -v -E -e '^#|^$' "${KEY_FILE}" 2>/dev/null |\ + while read -r KEY_LINE do KEY_FIELDS=$(count_fields "${KEY_LINE}" ",") (( KEY_FIELDS != 3 )) && die "line '${KEY_LINE}' in a keys file has missing or too many field(s) (should be 3)" @@ -530,7 +532,7 @@ typeset NUM_FIELDS=0 NUM_FIELDS=$(print "${CHECK_LINE}" | awk -F "${CHECK_DELIM}" '{ print NF }' 2>/dev/null) -print ${NUM_FIELDS} +print "${NUM_FIELDS}" return 0 } @@ -547,7 +549,7 @@ if [[ -n "$1" ]] then if (( ARG_LOG > 0 )) then - print - "$*" | while read LOG_LINE + print - "$*" | while read -r LOG_LINE do # check for leading log sigils and retain them case "${LOG_LINE}" in @@ -567,10 +569,10 @@ then LOG_SIGIL="ERROR" ;; esac - print "${NOW}: ${LOG_SIGIL}: [$$]:" "${LOG_LINE}" >>${LOG_FILE} + print "${NOW}: ${LOG_SIGIL}: [$$]:" "${LOG_LINE}" >>"${LOG_FILE}" done fi - print - "$*" | while read LOG_LINE + print - "$*" | while read -r LOG_LINE do # check for leading log sigils and retain them case "${LOG_LINE}" in @@ -622,6 +624,7 @@ Parameters: --fix-local : fix permissions on the local SSH controls repository (local SSH controls repository given by --fix-dir) --fix-remote : fix permissions on the remote SSH controls repository + (only valud when using a centralized public key location) --fix-user : UNIX account to own SSH controls files [default: current user] --help|-h : this help text --local-dir : location of the SSH control files on the local filesystem. @@ -668,8 +671,9 @@ typeset TMP_WORK_DIR="" typeset BLACKLIST_FILE="" # convert line to hostname -SERVER=${SERVER%%;*} -resolve_host ${SERVER} +SERVER="${SERVER%%;*}" +resolve_host "${SERVER}" +# shellcheck disable=SC2181 if (( $? > 0 )) then warn "could not lookup host ${SERVER}, skipping" @@ -686,10 +690,11 @@ for FILE in "${LOCAL_DIR}/access!660" \ "${SCRIPT_DIR}/${GLOBAL_CONFIG_FILE}!660" do # sftp transfer - sftp_file ${FILE} ${SERVER} + sftp_file "${FILE}" "${SERVER}" COPY_RC=$? if (( COPY_RC == 0 )) then + # shellcheck disable=SC2086 log "transferred ${FILE%!*} to ${SERVER}:${REMOTE_DIR}" else warn "failed to transfer ${FILE%!*} to ${SERVER}:${REMOTE_DIR} [RC=${COPY_RC}]" @@ -702,16 +707,17 @@ if [[ -n "${KEYS_DIR}" ]] then # merge keys file(s) before copy (in a temporary location) TMP_WORK_DIR="${TMP_DIR}/$0.${RANDOM}" - mkdir -p ${TMP_WORK_DIR} + mkdir -p "${TMP_WORK_DIR}" + # shellcheck disable=SC2181 if (( $? > 0 )) then die "unable to create temporary directory ${TMP_WORK_DIR} for mangling of 'keys' file" fi TMP_MERGE_FILE="${TMP_WORK_DIR}/keys" log "keys are stored in a DIRECTORY, first merging all keys into ${TMP_MERGE_FILE}" - cat ${KEYS_DIR}/* >${TMP_MERGE_FILE} + cat "${KEYS_DIR}"/* >"${TMP_MERGE_FILE}" # sftp transfer - sftp_file "${TMP_MERGE_FILE}!640" ${SERVER} + sftp_file "${TMP_MERGE_FILE}!640" "${SERVER}" COPY_RC=$? if (( COPY_RC == 0 )) then @@ -720,9 +726,9 @@ then warn "failed to transfer ${TMP_MERGE_FILE%!*} to ${SERVER}:${REMOTE_DIR} [RC=${COPY_RC}]" ERROR_COUNT=$(( ERROR_COUNT + 1 )) fi - [[ -d ${TMP_WORK_DIR} ]] && rm -rf ${TMP_WORK_DIR} 2>/dev/null + [[ -d ${TMP_WORK_DIR} ]] && rm -rf "${TMP_WORK_DIR}" 2>/dev/null else - sftp_file "${KEYS_FILE}!640" ${SERVER} + sftp_file "${KEYS_FILE}!640" "${SERVER}" COPY_RC=$? if (( COPY_RC == 0 )) then @@ -735,14 +741,14 @@ fi # discover a keys blacklist file, also copy it across if we find one # never use a keys blacklist file from the local config though [[ -r ${LOCAL_DIR}/update_ssh.conf ]] && \ - BLACKLIST_FILE="$(grep -E -e '^blacklist_file' ${LOCAL_DIR}/update_ssh.conf 2>/dev/null | cut -f2 -d'=')" + BLACKLIST_FILE=$(grep -E -e '^blacklist_file' "${LOCAL_DIR}/update_ssh.conf" 2>/dev/null | cut -f2 -d'=') if [[ -n "${BLACKLIST_FILE}" ]] then if [[ -r "${BLACKLIST_FILE}" ]] then log "keys blacklist file found at ${BLACKLIST_FILE}" # sftp transfer - sftp_file "${BLACKLIST_FILE}!660" ${SERVER} + sftp_file "${BLACKLIST_FILE}!660" "${SERVER}" COPY_RC=$? if (( COPY_RC == 0 )) then @@ -768,8 +774,9 @@ typeset DISTRIBUTE_OPTS="" typeset RC=0 # convert line to hostname -SERVER=${SERVER%%;*} -resolve_host ${SERVER} +SERVER="${SERVER%%;*}" +resolve_host "${SERVER}" +# shellcheck disable=SC2181 if (( $? > 0 )) then warn "could not lookup host ${SERVER}, skipping" @@ -782,13 +789,15 @@ then fi log "copying SSH controls on ${SERVER} in slave mode, this may take a while ..." -( RC=0; ssh -A ${SSH_ARGS} ${SERVER} ${REMOTE_DIR}/${SCRIPT_NAME} --copy ${DISTRIBUTE_OPTS}; - print "$?" > ${TMP_RC_FILE}; exit -) 2>&1 | logc +# shellcheck disable=SC2029 +( RC=0; ssh -A ${SSH_ARGS} "${SERVER}" "${REMOTE_DIR}/${SCRIPT_NAME} --copy ${DISTRIBUTE_OPTS}"; + print "$?" > "${TMP_RC_FILE}"; exit +) 2>&1 | logc "" # fetch return code from subshell -RC="$(< ${TMP_RC_FILE})" +RC=$(< "${TMP_RC_FILE}") +# shellcheck disable=SC2086 return ${RC} } @@ -799,9 +808,9 @@ function do_cleanup log "performing cleanup ..." # remove temporary file(s) -[[ -f ${TMP_FILE} ]] && rm -f ${TMP_FILE} >/dev/null 2>&1 -[[ -f ${TMP_MERGE_FILE} ]] && rm -f ${TMP_MERGE_FILE} >/dev/null 2>&1 -[[ -f ${TMP_RC_FILE} ]] && rm -f ${TMP_RC_FILE} >/dev/null 2>&1 +[[ -f ${TMP_FILE} ]] && rm -f "${TMP_FILE}" >/dev/null 2>&1 +[[ -f ${TMP_MERGE_FILE} ]] && rm -f "${TMP_MERGE_FILE}" >/dev/null 2>&1 +[[ -f ${TMP_RC_FILE} ]] && rm -f "${TMP_RC_FILE}" >/dev/null 2>&1 log "*** finish of ${SCRIPT_NAME} [${CMD_LINE}] /$$@${HOST_NAME}/ ***" return 0 @@ -820,8 +829,9 @@ typeset FIX_OPTS="" typeset RC=0 # convert line to hostname -SERVER=${SERVER%%;*} -resolve_host ${SERVER} +SERVER="${SERVER%%;*}" +resolve_host "${SERVER}" +# shellcheck disable=SC2181 if (( $? > 0 )) then warn "could not lookup host ${SERVER}, skipping" @@ -842,25 +852,29 @@ log "fixing SSH controls on ${SERVER} ..." if [[ -z "${SSH_UPDATE_USER}" ]] then # own user w/ sudo - ( RC=0; ssh ${SSH_ARGS} ${SERVER} sudo -n ${REMOTE_DIR}/${SCRIPT_NAME} --fix-local --fix-dir=${SERVER_DIR} --fix-user=${SERVER_USER} ${FIX_OPTS}; - print "$?" > ${TMP_RC_FILE}; exit - ) 2>&1 | logc + # shellcheck disable=SC2029 + ( RC=0; ssh ${SSH_ARGS} "${SERVER}" "sudo -n ${REMOTE_DIR}/${SCRIPT_NAME} --fix-local --fix-dir=${SERVER_DIR} --fix-user=${SERVER_USER} ${FIX_OPTS}"; + print "$?" > "${TMP_RC_FILE}"; exit + ) 2>&1 | logc "" elif [[ "${SSH_UPDATE_USER}" != "root" ]] then # other user w/ sudo - ( RC=0; ssh ${SSH_ARGS} ${SSH_UPDATE_USER}@${SERVER} sudo -n ${REMOTE_DIR}/${SCRIPT_NAME} --fix-local --fix-dir=${SERVER_DIR} --fix-user=${SERVER_USER} ${FIX_OPTS}; - print "$?" > ${TMP_RC_FILE}; exit - ) 2>&1 | logc + # shellcheck disable=SC2029 + ( RC=0; ssh ${SSH_ARGS} "${SSH_UPDATE_USER}@${SERVER}" "sudo -n ${REMOTE_DIR}/${SCRIPT_NAME} --fix-local --fix-dir=${SERVER_DIR} --fix-user=${SERVER_USER} ${FIX_OPTS}"; + print "$?" > "${TMP_RC_FILE}"; exit + ) 2>&1 | logc "" else # root user w/o sudo - ( RC=0; ssh ${SSH_ARGS} root@${SERVER} ${REMOTE_DIR}/${SCRIPT_NAME} --fix-local --fix-dir=${SERVER_DIR} --fix-user="root" ${FIX_OPTS}; - print "$?" > ${TMP_RC_FILE}; exit - ) 2>&1 | logc + # shellcheck disable=SC2029 + ( RC=0; ssh ${SSH_ARGS} "root@${SERVER}" "${REMOTE_DIR}/${SCRIPT_NAME} --fix-local --fix-dir=${SERVER_DIR} --fix-user=root ${FIX_OPTS}"; + print "$?" > "${TMP_RC_FILE}"; exit + ) 2>&1 | logc "" fi # fetch return code from subshell -RC="$(< ${TMP_RC_FILE})" +RC=$(< "${TMP_RC_FILE}") +# shellcheck disable=SC2086 return ${RC} } @@ -877,8 +891,9 @@ typeset FIX_OPTS="" typeset RC=0 # convert line to hostname -SERVER=${SERVER%%;*} -resolve_host ${SERVER} +SERVER="${SERVER%%;*}" +resolve_host "${SERVER}" +# shellcheck disable=SC2181 if (( $? > 0 )) then warn "could not lookup host ${SERVER}, skipping" @@ -896,13 +911,15 @@ then fi log "fixing SSH controls on ${SERVER} in slave mode, this may take a while ..." -( RC=0; ssh -A ${SSH_ARGS} ${SERVER} ${REMOTE_DIR}/${SCRIPT_NAME} --fix-remote --fix-dir=${SERVER_DIR} --fix-user=${SERVER_USER} ${FIX_OPTS}; - print "$?" > ${TMP_RC_FILE}; exit -) 2>&1 | logc +# shellcheck disable=SC2029 +( RC=0; ssh -A ${SSH_ARGS} "${SERVER}" "${REMOTE_DIR}/${SCRIPT_NAME} --fix-remote --fix-dir=${SERVER_DIR} --fix-user=${SERVER_USER} ${FIX_OPTS}"; + print "$?" > "${TMP_RC_FILE}"; exit +) 2>&1 | logc "" # fetch return code from subshell -RC="$(< ${TMP_RC_FILE})" +RC=$(< "${TMP_RC_FILE}") +# shellcheck disable=SC2086 return ${RC} } @@ -941,6 +958,9 @@ then *release\ 7*) RHEL_VERSION=7 ;; + *release\ 7*) + RHEL_VERSION=8 + ;; *) RHEL_VERSION="" ;; @@ -967,7 +987,7 @@ if [[ -n "$1" ]] then if (( ARG_LOG > 0 )) then - print - "$*" | while read LOG_LINE + print - "$*" | while read -r LOG_LINE do # check for leading log sigils and retain them case "${LOG_LINE}" in @@ -987,12 +1007,12 @@ then LOG_SIGIL="INFO" ;; esac - print "${NOW}: ${LOG_SIGIL}: [$$]:" "${LOG_LINE}" >>${LOG_FILE} + print "${NOW}: ${LOG_SIGIL}: [$$]:" "${LOG_LINE}" >>"${LOG_FILE}" done fi if (( ARG_VERBOSE > 0 )) then - print - "$*" | while read LOG_LINE + print - "$*" | while read -r LOG_LINE do # check for leading log sigils and retain them case "${LOG_LINE}" in @@ -1027,7 +1047,7 @@ if [[ -n "${LOG_STDIN}" ]] then if (( ARG_LOG > 0 )) then - print - "${LOG_STDIN}" | while read LOG_LINE + print - "${LOG_STDIN}" | while read -r LOG_LINE do # check for leading log sigils and retain them case "${LOG_LINE}" in @@ -1047,12 +1067,12 @@ then LOG_SIGIL="INFO" ;; esac - print "${NOW}: ${LOG_SIGIL}: [$$]:" "${LOG_LINE}" >>${LOG_FILE} + print "${NOW}: ${LOG_SIGIL}: [$$]:" "${LOG_LINE}" >>"${LOG_FILE}" done fi if (( ARG_VERBOSE > 0 )) then - print - "${LOG_STDIN}" | while read LOG_LINE + print - "${LOG_STDIN}" | while read -r LOG_LINE do # check for leading log sigils and retain them case "${LOG_LINE}" in @@ -1072,7 +1092,7 @@ if [[ -n "$1" ]] then if (( ARG_LOG > 0 )) then - print - "$*" | while read LOG_LINE + print - "$*" | while read -r LOG_LINE do # check for leading log sigils and retain them case "${LOG_LINE}" in @@ -1092,12 +1112,12 @@ then LOG_SIGIL="INFO" ;; esac - print "${NOW}: ${LOG_SIGIL}: [$$]:" "${LOG_LINE}" >>${LOG_FILE} + print "${NOW}: ${LOG_SIGIL}: [$$]:" "${LOG_LINE}" >>"${LOG_FILE}" done fi if (( ARG_VERBOSE > 0 )) then - print - "$*" | while read LOG_LINE + print - "$*" | while read -r LOG_LINE do case "${LOG_LINE}" in INFO:*|WARN:*|ERROR*) @@ -1138,7 +1158,7 @@ then fi # get aliases from alias line -ALIASES_LINE=$(grep -E -e "^${NEEDLE}.*:" ${LOCAL_DIR}/alias 2>/dev/null | cut -f2 -d':' 2>/dev/null) +ALIASES_LINE=$(grep -E -e "^${NEEDLE}.*:" "${LOCAL_DIR}/alias" 2>/dev/null | cut -f2 -d':' 2>/dev/null) if [[ -z "${ALIASES_LINE}" ]] then @@ -1159,6 +1179,7 @@ do RECURSION_COUNT=$(( RECURSION_COUNT + 1 )) EXPANDED_ALIASES=$(resolve_alias "${ALIAS}" ${RECURSION_COUNT}) RECURSION_COUNT=$(( RECURSION_COUNT - 1 )) + # shellcheck disable=SC2181 if (( $? == 0 )) then if [[ -z "${ALIAS_LIST}" ]] @@ -1217,6 +1238,7 @@ do if (( IS_TARGET > 0 )) then EXPANDED_TARGETS=$(resolve_alias "${TARGET}" 0) + # shellcheck disable=SC2181 if (( $? == 0 )) then if [[ -z "${TARGETS_LIST}" ]] @@ -1240,6 +1262,7 @@ done # sort final output and hand it back to the caller print "${TARGETS_LIST}" | grep -v '^$' 2>/dev/null | sort -u 2>/dev/null +# shellcheck disable=SC2086 return $0 } @@ -1264,26 +1287,26 @@ TRANSFER_FILE="${TRANSFER_FILE%!*}" SOURCE_FILE="${TRANSFER_FILE##*/}" # shellcheck disable=SC2164 OLD_PWD=$(pwd) -cd ${TRANSFER_DIR} || return 1 +cd "${TRANSFER_DIR}" || return 1 # transfer, (possibly) chmod the file to/on the target server (keep STDERR) if (( DO_SFTP_CHMOD > 1 )) then - sftp ${SFTP_ARGS} ${SSH_TRANSFER_USER}@${TRANSFER_HOST} >/dev/null </dev/null </dev/null </dev/null < 0 )) then warn "could not lookup host ${SERVER}, skipping" @@ -1392,25 +1421,29 @@ log "setting SSH controls on ${SERVER} ..." if [[ -z "${SSH_UPDATE_USER}" ]] then # own user w/ sudo - ( RC=0; ssh ${SSH_ARGS} ${SERVER} sudo -n ${REMOTE_DIR}/${SCRIPT_NAME} --update ${UPDATE_OPTS}; - print "$?" > ${TMP_RC_FILE}; exit - ) 2>&1 | logc + # shellcheck disable=SC2029 + ( RC=0; ssh ${SSH_ARGS} "${SERVER}" "sudo -n ${REMOTE_DIR}/${SCRIPT_NAME} --update ${UPDATE_OPTS}"; + print "$?" > "${TMP_RC_FILE}"; exit + ) 2>&1 | logc "" elif [[ "${SSH_UPDATE_USER}" != "root" ]] then # other user w/ sudo - ( RC=0; ssh ${SSH_ARGS} ${SSH_UPDATE_USER}@${SERVER} sudo -n ${REMOTE_DIR}/${SCRIPT_NAME} --update ${UPDATE_OPTS}; - print "$?" > ${TMP_RC_FILE}; exit - ) 2>&1 | logc + # shellcheck disable=SC2029 + ( RC=0; ssh ${SSH_ARGS} "${SSH_UPDATE_USER}@${SERVER}" "sudo -n ${REMOTE_DIR}/${SCRIPT_NAME} --update ${UPDATE_OPTS}"; + print "$?" > "${TMP_RC_FILE}"; exit + ) 2>&1 | logc "" else # root user w/o sudo - ( RC=0; ssh ${SSH_ARGS} root@${SERVER} ${REMOTE_DIR}/${SCRIPT_NAME} --update ${UPDATE_OPTS}; - print "$?" > ${TMP_RC_FILE}; exit - ) 2>&1 | logc + # shellcheck disable=SC2029 + ( RC=0; ssh ${SSH_ARGS} "root@${SERVER}" "${REMOTE_DIR}/${SCRIPT_NAME} --update ${UPDATE_OPTS}"; + print "$?" > "${TMP_RC_FILE}"; exit + ) 2>&1 | logc "" fi # fetch return code from subshell -RC="$(< ${TMP_RC_FILE})" +RC=$(< "${TMP_RC_FILE}") +# shellcheck disable=SC2086 return ${RC} } @@ -1424,8 +1457,9 @@ typeset UPDATE_OPTS="" typeset RC=0 # convert line to hostname -SERVER=${SERVER%%;*} -resolve_host ${SERVER} +SERVER="${SERVER%%;*}" +resolve_host "${SERVER}" +# shellcheck disable=SC2181 if (( $? > 0 )) then warn "could not lookup host ${SERVER}, skipping" @@ -1438,13 +1472,15 @@ then fi log "applying SSH controls on ${SERVER} in slave mode, this may take a while ..." -( RC=0; ssh -A ${SSH_ARGS} ${SERVER} ${REMOTE_DIR}/${SCRIPT_NAME} --apply ${UPDATE_OPTS}; - print "$?" > ${TMP_RC_FILE}; exit -) 2>&1 | logc +# shellcheck disable=SC2029 +( RC=0; ssh -A ${SSH_ARGS} "${SERVER}" "${REMOTE_DIR}/${SCRIPT_NAME} --apply ${UPDATE_OPTS}"; + print "$?" > "${TMP_RC_FILE}"; exit +) 2>&1 | logc "" # fetch return code from subshell -RC="$(< ${TMP_RC_FILE})" +RC=$(< "${TMP_RC_FILE}") +# shellcheck disable=SC2086 return ${RC} } @@ -1467,18 +1503,20 @@ FINGER_FIELDS=$(count_fields "${FINGER_LINE}" ",") (( FINGER_FIELDS != 3 )) && die "line '${FINGER_LINE}' has missing or too many field(s) (should be 3))" # create fingerprint -FINGER_USER="$(print ${FINGER_LINE} | awk -F, '{print $1}')" -print "${FINGER_LINE}" | awk -F, '{print $2 " " $3}' > ${TMP_FILE} +FINGER_USER=$(print "${FINGER_LINE}" | awk -F, '{print $1}') +print "${FINGER_LINE}" | awk -F, '{print $2 " " $3}' > "${TMP_FILE}" # check if fingerprint is valid -FINGERPRINT="$(ssh-keygen ${SSH_KEYGEN_OPTS} -l -f ${TMP_FILE} 2>&1)" +FINGERPRINT=$(ssh-keygen ${SSH_KEYGEN_OPTS} -l -f "${TMP_FILE}" 2>&1) FINGER_RC=$? if (( FINGER_RC == 0 )) then case "${OS_NAME}" in HP-UX) + # shellcheck disable=SC2086 FINGER_ENTRY="$(print ${FINGERPRINT} | awk '{print $1,$2,$4}')" ;; *) + # shellcheck disable=SC2086 FINGER_ENTRY="$(print ${FINGERPRINT} | awk '{print $1,$2,$5}')" ;; esac @@ -1523,6 +1561,7 @@ do do shift # child is still alive? + # shellcheck disable=SC2086 if kill -0 ${PID} 2>/dev/null then (( ARG_DEBUG > 0 )) && print -u2 "DEBUG: ${PID} is still alive" @@ -1530,6 +1569,7 @@ do # wait for sigchild, catching child exit codes is unreliable because # the child might have already ended before we get here (caveat emptor) else + # shellcheck disable=SC2086 wait ${PID} RC=$? if (( RC > 0 )) @@ -1561,7 +1601,7 @@ if [[ -n "$1" ]] then if (( ARG_LOG > 0 )) then - print - "$*" | while read LOG_LINE + print - "$*" | while read -r LOG_LINE do # check for leading log sigils and retain them case "${LOG_LINE}" in @@ -1581,12 +1621,12 @@ then LOG_SIGIL="WARN" ;; esac - print "${NOW}: ${LOG_SIGIL}: [$$]:" "${LOG_LINE}" >>${LOG_FILE} + print "${NOW}: ${LOG_SIGIL}: [$$]:" "${LOG_LINE}" >>"${LOG_FILE}" done fi if (( ARG_VERBOSE > 0 )) then - print - "$*" | while read LOG_LINE + print - "$*" | while read -r LOG_LINE do # check for leading log sigils and retain them case "${LOG_LINE}" in @@ -1613,7 +1653,7 @@ return 0 CMD_LINE="$*" for PARAMETER in ${CMD_LINE} do - case ${PARAMETER} in + case "${PARAMETER}" in -a|-apply|--apply) (( ARG_ACTION > 0 )) && { print -u2 "ERROR: multiple actions specified" @@ -1821,6 +1861,7 @@ case ${ARG_ACTION} in then die "no targets to process" else + # shellcheck disable=SC2086 log "processing targets: $(print ${CLIENTS} | tr -s '\n' ' ' 2>/dev/null)" fi @@ -1828,6 +1869,7 @@ case ${ARG_ACTION} in if (( DO_SSH_AGENT > 0 && CAN_START_AGENT > 0 )) then start_ssh_agent + # shellcheck disable=SC2181 if (( $? > 0 )) then die "problem with launching an SSH agent, bailing out" @@ -1840,9 +1882,9 @@ case ${ARG_ACTION} in do if (( DO_SLAVE > 0 )) then - update2slave ${CLIENT} & + update2slave "${CLIENT}" & else - update2host ${CLIENT} & + update2host "${CLIENT}" & fi PID=$! log "updating ${CLIENT} in background [PID=${PID}] ..." @@ -1852,6 +1894,7 @@ case ${ARG_ACTION} in if (( COUNT <= 0 )) then # wait until all background processes are completed + # shellcheck disable=SC2086 wait_for_children ${PIDS} || \ warn "$? background jobs (possibly) failed to complete correctly" PIDS='' @@ -1881,6 +1924,7 @@ case ${ARG_ACTION} in then die "no targets to process" else + # shellcheck disable=SC2086 log "processing targets: $(print ${CLIENTS} | tr -s '\n' ' ' 2>/dev/null)" fi @@ -1888,6 +1932,7 @@ case ${ARG_ACTION} in if (( DO_SSH_AGENT > 0 && CAN_START_AGENT > 0 )) then start_ssh_agent + # shellcheck disable=SC2181 if (( $? > 0 )) then die "problem with launching an SSH agent, bailing out" @@ -1900,9 +1945,9 @@ case ${ARG_ACTION} in do if (( DO_SLAVE )) then - distribute2slave ${CLIENT} & + distribute2slave "${CLIENT}" & else - distribute2host ${CLIENT} & + distribute2host "${CLIENT}" & fi PID=$! log "copying/distributing to ${CLIENT} in background [PID=${PID}] ..." @@ -1912,6 +1957,7 @@ case ${ARG_ACTION} in if (( COUNT <= 0 )) then # wait until all background processes are completed + # shellcheck disable=SC2086 wait_for_children ${PIDS} || \ warn "$? background jobs (possibly) failed to complete correctly" PIDS='' @@ -1964,16 +2010,16 @@ case ${ARG_ACTION} in # are keys stored in a file or a directory? if [[ -n "${KEYS_DIR}" ]] then - cat ${KEYS_DIR}/* | sort | while read LINE + cat "${KEYS_DIR}"/* | sort | while read -r LINE do update_fingerprints "${LINE}" KEY_COUNT=$(( KEY_COUNT + 1 )) done else - while read LINE + while read -r LINE do update_fingerprints "${LINE}" - done < ${KEYS_FILE} + done < "${KEYS_FILE}" fi log "${KEY_COUNT} public keys discovered with following bits distribution:" log " 1024 bits: ${KEY_1024_COUNT}" @@ -1984,11 +2030,11 @@ case ${ARG_ACTION} in ;; 4) # apply SSH controls locally (root user) log "ACTION: apply SSH controls locally" - ( RC=0; ${LOCAL_DIR}/update_ssh.pl ${SSH_UPDATE_OPTS}; - print "$?" > ${TMP_RC_FILE}; exit - ) 2>&1 | logc + ( RC=0; "${LOCAL_DIR}/update_ssh.pl" ${SSH_UPDATE_OPTS}; + print "$?" > "${TMP_RC_FILE}"; exit + ) 2>&1 | logc "" # fetch return code from subshell - RC="$(< ${TMP_RC_FILE})" + RC=$(< "${TMP_RC_FILE}") if (( RC > 0 )) then die "failed to apply SSH controls locally [RC=${RC}]" @@ -2000,7 +2046,7 @@ case ${ARG_ACTION} in log "ACTION: fix local SSH controls repository" check_root_user || die "must be run as user 'root'" log "resetting ownerships to UNIX user ${SUDO_FIX_USER}" - if [[ ${SSH_FIX_USER} = "root" ]] + if [[ "${SSH_FIX_USER}" = "root" ]] then warn "!!! resetting ownerships to user root !!!" fi @@ -2072,7 +2118,7 @@ case ${ARG_ACTION} in 5) chcon -R -t sshd_key_t "${FIX_DIR}/keys.d" ;; - 6|7) + 6|7|8) chcon -R -t ssh_home_t "${FIX_DIR}/keys.d" ;; *) @@ -2116,6 +2162,7 @@ case ${ARG_ACTION} in then die "no targets to process" else + # shellcheck disable=SC2086 log "processing targets: $(print ${CLIENTS} | tr -s '\n' ' ' 2>/dev/null)" fi @@ -2123,6 +2170,7 @@ case ${ARG_ACTION} in if (( DO_SSH_AGENT > 0 && CAN_START_AGENT > 0 )) then start_ssh_agent + # shellcheck disable=SC2181 if (( $? > 0 )) then die "problem with launching an SSH agent, bailing out" @@ -2135,9 +2183,9 @@ case ${ARG_ACTION} in do if (( DO_SLAVE > 0 )) then - fix2slave ${CLIENT} "${FIX_DIR}" "${SSH_UPDATE_USER}" & + fix2slave "${CLIENT}" "${FIX_DIR}" "${SSH_UPDATE_USER}" & else - fix2host ${CLIENT} "${FIX_DIR}" "${SSH_UPDATE_USER}" & + fix2host "${CLIENT}" "${FIX_DIR}" "${SSH_UPDATE_USER}" & fi PID=$! log "fixing SSH controls on ${CLIENT} in background [PID=${PID}] ..." @@ -2147,6 +2195,7 @@ case ${ARG_ACTION} in if (( COUNT <= 0 )) then # wait until all background processes are completed + # shellcheck disable=SC2086 wait_for_children ${PIDS} || \ warn "$? background jobs (possibly) failed to complete correctly" PIDS='' @@ -2164,7 +2213,7 @@ case ${ARG_ACTION} in ;; 7) # dump the configuration namespace log "ACTION: dumping the global access namespace with resolved aliases ..." - ${LOCAL_DIR}/update_ssh.pl --preview --global + "${LOCAL_DIR}/update_ssh.pl" --preview --global log "finished dumping the global namespace" ;; 8) # check syntax of the access/alias/keys files @@ -2185,16 +2234,21 @@ case ${ARG_ACTION} in # keys files if [[ -n "${KEYS_DIR}" ]] then + # shellcheck disable=SC2086 log "$(tar -cvf ${BACKUP_TAR_FILE} ${KEYS_DIR} 2>/dev/null)" else + # shellcheck disable=SC2086 log "$(tar -cvf ${BACKUP_TAR_FILE} ${KEYS_FILE} 2>/dev/null)" fi # configuration files for FILE in "${LOCAL_DIR}/access" "${LOCAL_DIR}/alias ${LOCAL_DIR}/targets" do + # shellcheck disable=SC2086 log "$(tar -rvf ${BACKUP_TAR_FILE} ${FILE} 2>/dev/null)" done + # shellcheck disable=SC2086 log "$(gzip ${BACKUP_TAR_FILE} 2>/dev/null)" + # shellcheck disable=SC2086 log "resulting backup file is: $(ls -1 ${BACKUP_TAR_FILE}* 2>/dev/null)" else die "could not find backup directory ${BACKUP_DIR}. Host is not an SSH master?" @@ -2210,6 +2264,7 @@ case ${ARG_ACTION} in then die "no targets to process" else + # shellcheck disable=SC2086 log "processing targets: $(print ${CLIENTS} | tr -s '\n' ' ' 2>/dev/null)" fi print "${CLIENTS}" | ${SSH_KEYSCAN_BIN} ${SSH_KEYSCAN_ARGS} -f - 2>/dev/null @@ -2219,10 +2274,12 @@ case ${ARG_ACTION} in 11) # resolve an alias log "ACTION: resolving alias ${ARG_ALIAS} ..." RESOLVE_ALIAS=$(resolve_alias "${ARG_ALIAS}" 0) + # shellcheck disable=SC2181 if (( $? > 0 )) && [[ -z "${RESOLVE_ALIAS}" ]] then die "alias ${ARG_ALIAS} did not resolve correctly" else + # shellcheck disable=SC2086 log "alias ${ARG_ALIAS} resolves to: $(print ${RESOLVE_ALIAS} | tr -s '\n' ' ' 2>/dev/null)" fi log "finished resolving alias" diff --git a/update_ssh.conf b/update_ssh.conf index c403fea..80194c8 100644 --- a/update_ssh.conf +++ b/update_ssh.conf @@ -15,6 +15,14 @@ use_fqdn=1 # target directory for allowed SSH key files access_dir=/etc/ssh_controls/keys.d +# toggle to specify the final location of public keys by allowing to override +# the value of $access_dir with the 'AuthorizedKeysFile' in sshd (=enables the +# use of $HOME/.ssh for public keys for example): +# 'use_controls': take the value from the configured 'access_dir' option +# 'use_sshd' : use the value from 'AuthorizedKeysFile' setting in sshd +# [default: use_controls] +key_location=use_controls + # location of the keys blacklist file blacklist_file=/etc/ssh_controls/keys.blacklisted diff --git a/update_ssh.pl b/update_ssh.pl index a7bca31..4fafe77 100644 --- a/update_ssh.pl +++ b/update_ssh.pl @@ -42,7 +42,7 @@ use Pod::Usage; # ------------------------- CONFIGURATION starts here ------------------------- # define the version (YYYY-MM-DD) -my $script_version = "2018-11-03"; +my $script_version = "2020-12-30"; # name of global configuration file (no path, must be located in the script directory) my $global_config_file = "update_ssh.conf"; # name of localized configuration file (no path, must be located in the script directory) @@ -52,13 +52,18 @@ my $max_recursion = 5; # selinux context labels of key files for different RHEL version my %selinux_contexts = ( '5' => 'sshd_key_t', '6' => 'ssh_home_t', - '7' => 'ssh_home_t'); + '7' => 'ssh_home_t', + '8' => 'ssh_home_t'); +# disallowed paths for home directories for accounts +my @disallowed_homes = ('/', '/etc', '/bin', '/sbin', '/usr/bin', '/usr/sbin'); +# disallowed login shells for @accounts +my @disallowed_shells = ('/bin/nologin','/bin/false','/sbin/nologin','/sbin/false'); # ------------------------- CONFIGURATION ends here --------------------------- # initialize variables my ($debug, $verbose, $preview, $remove, $global, $use_fqdn) = (0,0,0,0,0,0); -my (@config_files, @zombie_files, $access_dir, $blacklist_file); +my (@config_files, @zombie_files, $access_dir, $key_location, $blacklist_file); my (%options, @uname, @pwgetent, @accounts, %aliases, %keys, %access, @blacklist); -my ($os, $hostname, $run_dir); +my ($os, $hostname, $run_dir, $authorizedkeys_option); my ($selinux_status, $selinux_context, $linux_version, $has_selinux, $recursion_count) = ("","","",0,1); $|++; @@ -89,7 +94,7 @@ sub parse_config_file { my $config_file = shift; unless (open (CONF_FD, "<", $config_file)) { - do_log ("ERROR: failed to open the configuration file ${config_file} [$! $hostname]") + do_log ("ERROR: failed to open the configuration file ${config_file} [$!/$hostname]") and exit (1); } while () { @@ -98,7 +103,7 @@ sub parse_config_file { if (/^\s*$/ || /^#/) { next; } else { - if (/^\s*use_fqdn\s*=\s*([0-9]+)\s*$/) { + if (/^\s*use_fqdn\s*=\s*(0|1)\s*$/) { $use_fqdn = $1; do_log ("DEBUG: picking up setting: use_fqdn=${use_fqdn}"); } @@ -106,6 +111,15 @@ sub parse_config_file { $access_dir = $1; do_log ("DEBUG: picking up setting: access_dir=${access_dir}"); } + if (/^\s*key_location\s*=\s*(use_controls|use_sshd)\s*/) { + $key_location = $1; + do_log ("DEBUG: picking up setting: key_location=${key_location}"); + if ($key_location eq 'use_sshd') { + do_log ("DEBUG: applied setting: key_location=${key_location}"); + } else { + do_log ("DEBUG: applied default setting: key_location=${key_location}"); + } + } if (/^\s*blacklist_file\s*=\s*([0-9A-Za-z_\-\.\/~]+)\s*$/) { $blacklist_file = $1; # support tilde (~) expansion for ~root @@ -151,10 +165,10 @@ sub set_file { my ($file, $perm, $uid, $gid) = @_; chmod ($perm, "$file") - or do_log ("ERROR: cannot set permissions on $file [$! $hostname]") + or do_log ("ERROR: cannot set permissions on $file [$!/$hostname]") and exit (1); chown ($uid, $gid, "$file") - or do_log ("ERROR: cannot set ownerships on $file [$! $hostname]") + or do_log ("ERROR: cannot set ownerships on $file [$!/$hostname]") and exit (1); return (1); @@ -228,7 +242,7 @@ $verbose = 1 if ($options{'verbose'}); # where am I? (1/2) $0 =~ /^(.+[\\\/])[^\\\/]+[\\\/]*$/; -my $run_dir = $1 || "."; +$run_dir = $1 || "."; $run_dir =~ s#/$##; # remove trailing slash # don't do anything without configuration file(s) @@ -245,14 +259,19 @@ foreach my $config_file (@config_files) { parse_config_file ($config_file); } -# is the target directory for keys present? (not for global preview) -unless ($preview and $global) { - do_log ("INFO: checking for SSH control mode ..."); +# is the target directory for keys present? (not for global preview and +# not when $key_location is use_sshd) +unless (($preview and $global) or $key_location eq 'use_sshd') { + do_log ("INFO: checking for SSH controls mode ..."); if (-d $access_dir) { - do_log ("INFO: host is under SSH control via $access_dir"); + do_log ("INFO: host is under SSH controls via $access_dir"); } else { - do_log ("ERROR: host is not under SSH keys only control [$hostname]") - and exit (1); + if ($key_location eq 'use_sshd') { + do_log ("INFO: skipped check since public key location is determined by sshd [$hostname]") + } else { + do_log ("ERROR: host is not under SSH keys only control [$hostname]") + and exit (1); + } } } @@ -261,7 +280,7 @@ unless ($preview and $global) { do_log ("INFO: checking for keys blacklist file ..."); if (-f $blacklist_file) { open (BLACKLIST, "<", $blacklist_file) or \ - do_log ("ERROR: cannot read keys blacklist file [$! $hostname]") + do_log ("ERROR: cannot read keys blacklist file [$!/$hostname]") and exit (1); @blacklist = ; close (BLACKLIST); @@ -291,6 +310,28 @@ if ($use_fqdn) { do_log ("INFO: runtime info: ".getpwuid ($<)."; ${hostname}\@${run_dir}; Perl v$]"); +# ----------------------------------------------------------------------------- +# resolve and check key location +# ----------------------------------------------------------------------------- +if ($key_location eq 'use_sshd') { + + # get sshd setting but only take 1st path into account + $authorizedkeys_option = qx#sshd -T | grep "authorizedkeysfile" 2>/dev/null | cut -f2 -d' '#; + chomp ($authorizedkeys_option); + if (defined ($authorizedkeys_option)) { + do_log ("INFO: AuthorizedkeysFile resolves to $authorizedkeys_option [$hostname]"); + } else { + do_log ("ERROR: unable to get AuthorizedkeysFile value from sshd [$hostname]") + and exit (1); + } +} else { + # for SSH controls native logic we require an absolute path + if ($authorizedkeys_option =~ /^\//) { + do_log ("ERROR: option \$access_dir requires and absolute path [$hostname]") + and exit (1); + } +} + # ----------------------------------------------------------------------------- # collect user accounts via getpwent() # result: @accounts @@ -319,7 +360,7 @@ print Dumper (\@accounts) if $debug; do_log ("INFO: reading 'alias' file ..."); open (ALIASES, "<", "${run_dir}/alias") - or do_log ("ERROR: cannot read 'alias' file [$! $hostname]") and exit (1); + or do_log ("ERROR: cannot read 'alias' file [$!/$hostname]") and exit (1); while () { my ($key, $value, @values); @@ -398,7 +439,7 @@ if (-d "${run_dir}/keys.d" && -f "${run_dir}/keys") { if (-d "${run_dir}/keys.d") { do_log ("INFO: local 'keys' are stored in a DIRECTORY on $hostname"); opendir (KEYS_DIR, "${run_dir}/keys.d") - or do_log ("ERROR: cannot open 'keys.d' directory [$! $hostname]") + or do_log ("ERROR: cannot open 'keys.d' directory [$!/$hostname]") and exit (1); while (my $key_file = readdir (KEYS_DIR)) { next if ($key_file =~ /^\./); @@ -416,7 +457,7 @@ if (-d "${run_dir}/keys.d") { # process 'keys' files foreach my $key_file (@key_files) { open (KEYS, "<", $key_file) - or do_log ("ERROR: cannot read 'keys' file [$! $hostname]") and exit (1); + or do_log ("ERROR: cannot read 'keys' file [$!/$hostname]") and exit (1); do_log ("INFO: reading public keys from file: $key_file"); while () { @@ -454,7 +495,7 @@ print Dumper(\%keys) if $debug; do_log ("INFO: reading 'access' file ..."); open (ACCESS, "<", "${run_dir}/access") - or do_log ("ERROR: cannot read 'access' file [$! $hostname]") and exit (1); + or do_log ("ERROR: cannot read 'access' file [$!/$hostname]") and exit (1); while () { my ($who, $where, $what, @who, @where, @what); @@ -507,7 +548,7 @@ if ($preview && $global) { do_log ("INFO: display GLOBAL configuration ...."); open (ACCESS, "<", "${run_dir}/access") - or do_log ("ERROR: cannot read 'access' file [$! $hostname]") and exit (1); + or do_log ("ERROR: cannot read 'access' file [$!/$hostname]") and exit (1); while () { my ($who, $where, $what, @who, @where, @what); @@ -542,7 +583,8 @@ if ($preview && $global) { } # ----------------------------------------------------------------------------- -# distribute keys into authorized_keys files in $access_dir +# distribute keys into authorized_keys files +# (defined by $key_location and/or $access_dir) # ----------------------------------------------------------------------------- do_log ("INFO: applying SSH access rules ...."); @@ -578,6 +620,10 @@ unless ($preview) { $linux_version = 7; last SWITCH_RELEASE; }; + $release_string =~ m/release 8/i && do { + $linux_version = 8; + last SWITCH_RELEASE; + }; } } # use fall back in case we cannot determine the version @@ -599,17 +645,50 @@ unless ($preview) { # only add authorized_keys for existing accounts, # otherwise revoke access if needed -foreach my $account (sort (@accounts)) { +SET_KEY: foreach my $account (sort (@accounts)) { - my $access_file = "$access_dir/$account"; + my ($access_file, $authorizedkeys_file, $uid, $gid, $home_dir, $login_shell) = (undef, undef, undef, undef, undef, undef); + + # set $access_file when using SSH controls logic + if ($key_location eq 'use_sshd' and defined ($authorizedkeys_option)) { + # use sshd logic (replacing %u,%h, %%) + $authorizedkeys_file = $authorizedkeys_option; + $authorizedkeys_file =~ s/%u/$account/g; + $authorizedkeys_file =~ s/%h/$hostname/g; + $authorizedkeys_file =~ s/%%/%/g; + # check relative path (assume $HOME needs to be added) + if ($authorizedkeys_file !~ /^\//) { + ($uid, $gid, $home_dir, $login_shell) = (getpwnam($account))[2,3,7,8]; + # do not accept invalid $HOME or shells + if (defined ($home_dir)) { + if (grep( /^$home_dir$/, @disallowed_homes) or grep( /^$login_shell/, @disallowed_shells)) { + do_log ("DEBUG: invalid HOME or SHELL for $account [$hostname]"); + next SET_KEY; + } else { + $authorizedkeys_file = $home_dir."/".$authorizedkeys_file; + do_log ("DEBUG: adding $home_dir to public key path for $account [$hostname]"); + } + } else { + do_log ("ERROR: unable to get HOME for $account [$hostname]"); + next SET_KEY; + } + } + $access_file = $authorizedkeys_file; + } else { + # use native SSH controls logic + $access_file = "$access_dir/$account"; + } + do_log ("DEBUG: public key location for $account resolves to $authorizedkeys_file [$hostname]"); # only add authorised_keys if there are access definitions if ($access{$account}) { unless ($preview) { + # do not create root or intermediate paths in $access_file; + # e.g. if $HOME/.ssh/authorized_keys is the public key path, then $HOME/.ssh must already exist open (KEYFILE, "+>", $access_file) - or do_log ("ERROR: cannot open file for writing in $access_dir [$! $hostname]") - and exit (1); + or do_log ("ERROR: cannot open file for writing at $access_file [$!/$hostname]") + and next SET_KEY; } foreach my $person (sort (@{$access{$account}})) { my $real_name = $person; @@ -626,9 +705,13 @@ foreach my $account (sort (@accounts)) { } close (KEYFILE) unless $preview; - # set permissions to world readable and check for SELinux context + # set ownerships/permissions on public key file and check for SELinux context unless ($preview) { - set_file ($access_file, 0644, 0, 0); + if ($key_location eq 'use_controls') { + set_file ($access_file, 0644, 0, 0); + } else { + set_file ($access_file, 0600, $uid, $gid); + } # selinux labels SWITCH: { $os eq "Linux" && do { @@ -645,7 +728,7 @@ foreach my $account (sort (@accounts)) { if (-f $access_file) { unless ($preview) { unlink ($access_file) - or do_log ("ERROR: cannot remove obsolete access file(s) [$! $hostname]") + or do_log ("ERROR: cannot remove obsolete access file $access_file [$!/$hostname]") and exit (1); } else { do_log ("INFO: removing obsolete access $access_file on $hostname"); @@ -655,32 +738,35 @@ foreach my $account (sort (@accounts)) { } # ----------------------------------------------------------------------------- -# alert on/remove extraneous authorized_keys files +# alert on/remove extraneous authorized_keys files (SSH controls logic only) # (access files for which no longer a valid UNIX account exists) # ----------------------------------------------------------------------------- -do_log ("INFO: checking for extraneous access files ...."); +if ($key_location eq 'use_controls') { -opendir (ACCESS_DIR, $access_dir) - or do_log ("ERROR: cannot open directory $access_dir [$! $hostname]") + do_log ("INFO: checking for extraneous access files ...."); + + opendir (ACCESS_DIR, $access_dir) + or do_log ("ERROR: cannot open directory $access_dir [$!/$hostname]") and exit (1); -while (my $access_file = readdir (ACCESS_DIR)) { - next if ($access_file =~ /^\./); - unless (grep (/$access_file/, @accounts)) { - do_log ("WARN: found extraneous access file in $access_dir/$access_file [$hostname]"); - push (@zombie_files, "$access_dir/$access_file"); + while (my $access_file = readdir (ACCESS_DIR)) { + next if ($access_file =~ /^\./); + unless (grep (/$access_file/, @accounts)) { + do_log ("WARN: found extraneous access file in $access_dir/$access_file [$hostname]"); + push (@zombie_files, "$access_dir/$access_file"); + } } -} -closedir (ACCESS_DIR); -do_log ("INFO: ".scalar (@zombie_files)." extraneous access file(s) found on $hostname"); -print Dumper (\@zombie_files) if $debug; + closedir (ACCESS_DIR); + do_log ("INFO: ".scalar (@zombie_files)." extraneous access file(s) found on $hostname"); + print Dumper (\@zombie_files) if $debug; -# remove if requested and needed -if ($remove && @zombie_files) { - my $count = unlink (@zombie_files) - or do_log ("ERROR: cannot remove extraneous access file(s) [$! $hostname]") - and exit (1); - do_log ("INFO: $count extraneous access files removed $hostname"); + # remove if requested and needed + if ($remove && @zombie_files) { + my $count = unlink (@zombie_files) + or do_log ("ERROR: cannot remove extraneous access file(s) [$!/$hostname]") + and exit (1); + do_log ("INFO: $count extraneous access files removed $hostname"); + } } exit (0); @@ -711,9 +797,11 @@ update_ssh.pl - distributes SSH public keys in a desired state model. =head1 DESCRIPTION B distributes SSH keys to the appropriate files (.e. 'authorized_keys') into the C<$access_dir> repository based on the F, F and F files. +Alternatively B can distribute public keys to the location specified in the AuthorizedkeysFile setting of F (allowing public keys to be distributed +to the traditional location in a user's HOME directory). See C setting in Ffor more information. This script should be run on each host where SSH key authentication is the exclusive method of (remote) authentication. -For update SSH public keys must be stored in a generic F file within the same directory as B script. +Orginally SSH public keys must be stored in a generic F file within the same directory as B script. Alternatively key files may be stored as set of individual key files within a called sub-directory called F. Both methods are mutually exclusive and the latter always take precedence. @@ -739,6 +827,8 @@ Following settings must be configured: =item * B : target directory for allowed SSH public key files +=item * B : whether or not to use AuthorizedkeysFile setting in sshd_config for overriding $access_dir + =item * B : location of the file with blacklisted SSH public keys =back @@ -746,7 +836,7 @@ Following settings must be configured: =head1 BLACKLISTING Key blacklisting can be performed by adding a public key definition in its entirety to the blacklist keys file. When a blacklisted key is -found in the available F file(s) during SSH control updates, an alert will be shown on STDOUT and the key will be ignored for the rest. +found in the available F file(s) during SSH controls updates, an alert will be shown on STDOUT and the key will be ignored for the rest. Examples: