diff --git a/manage_ssh.conf b/manage_ssh.conf index 76a5f63..836b9ca 100644 --- a/manage_ssh.conf +++ b/manage_ssh.conf @@ -13,7 +13,7 @@ # (leave blank for current user) SSH_TRANSFER_USER="" -# name of the OS group that should own the SSH controls files +# name of the UNIX group that should own the SSH controls files (must exist already) SSH_OWNER_GROUP="sshadmin" # whether a 'chmod' needs to be executed after each sftp transfer [0=No; 1=Yes] @@ -42,7 +42,7 @@ SSH_UPDATE_OPTS="--verbose --remove" # path to the ssh-keyscan too SSH_KEYSCAN_BIN="/usr/bin/ssh-keyscan" -# extra arguments/options for the ssh-keyscan command +# extra arguments/options for the ssh-keyscan command # by default -f is used by manage_sudo.sh to supply hostnames, do not add here SSH_KEYSCAN_ARGS="-t rsa" diff --git a/manage_ssh.sh b/manage_ssh.sh index 668f1cd..cb4fa77 100644 --- a/manage_ssh.sh +++ b/manage_ssh.sh @@ -25,9 +25,9 @@ # check_setup(), check_syntax(), count_fields(), die(), display_usage(), # distribute2host(), distribute2slave(), do_cleanup(), fix2host(), # fix2slave(), get_linux_name(), get_linux_version(), log(), logc(), -# resolve_host(), sftp_file(), start_ssh_agent(), stop_ssh_agent(), -# update2host(), update2slave(), update_fingerprints(), -# wait_for_children(), warn() +# resolve_alias(), resolve_host(), resolve_targets(), sftp_file(), +# start_ssh_agent(), stop_ssh_agent(), update2host(), update2slave(), +# update_fingerprints(), wait_for_children(), warn() # For other pre-requisites see the documentation in display_usage() # # ----------------------------------------------------------------------------- @@ -42,46 +42,72 @@ # Below configuration values should not be changed. Use the GLOBAL_CONFIG_FILE # or LOCAL_CONFIG_FILE instead -# define the V.R.F (version/release/fix) -MY_VRF="1.7.0" +# define the version (YYYY-MM-DD) +typeset -r SCRIPT_VERSION="2018-11-03" # name of the global configuration file (script) -GLOBAL_CONFIG_FILE="manage_ssh.conf" +typeset -r GLOBAL_CONFIG_FILE="manage_ssh.conf" # name of the local configuration file (script) -LOCAL_CONFIG_FILE="manage_ssh.conf.local" +typeset -r LOCAL_CONFIG_FILE="manage_ssh.conf.local" +# maxiumum depth of recursion for alias resolution +typeset -r MAX_RECURSION=5 # location of temporary working storage -TMP_DIR="/var/tmp" +typeset -r TMP_DIR="/var/tmp" # ------------------------- CONFIGURATION ends here --------------------------- +# configuration file values +typeset SSH_TRANSFER_USER="" +typeset SSH_OWNER_GROUP="" +typeset SSH_UPDATE_USER="" +typeset SSH_UPDATE_OPTS="" +typeset SSH_KEYSCAN_BIN="" +typeset SSH_KEYSCAN_ARGS="" +typeset BACKUP_DIR="" +typeset LOCAL_DIR="" +typeset LOG_DIR="" +typeset REMOTE_DIR="" +typeset DO_SFTP_CHMOD="" +typeset SFTP_ARGS="" +typeset DO_SSH_AGENT="" +typeset SSH_ARGS="" +typeset SSH_PRIVATE_KEY="" +typeset MAX_BACKGROUND_PROCS="" +typeset FINGERPRINT_TYPE="" # miscelleaneous -PATH=${PATH}:/usr/bin:/usr/local/bin -SCRIPT_NAME="$(basename $0)" -SCRIPT_DIR="$(dirname $0)" -OS_NAME="$(uname)" -HOST_NAME="$(hostname)" -KEYS_FILE="" -KEYS_DIR="" -TARGETS_FILE="" -DO_SLAVE=0 -FIX_CREATE=0 -CAN_DISCOVER_KEYS=0 -CAN_START_AGENT=1 -KEY_COUNT=0 -KEY_1024_COUNT=0 -KEY_2048_COUNT=0 -KEY_4096_COUNT=0 -KEY_OTHER_COUNT=0 -SSH_KEYGEN_OPTS="" -TMP_FILE="${TMP_DIR}/.${SCRIPT_NAME}.$$" -TMP_RC_FILE="${TMP_DIR}/.${SCRIPT_NAME}.rc.$$" +typeset PATH=${PATH}:/usr/bin:/usr/local/bin +typeset SCRIPT_NAME=$(basename "$0") +typeset SCRIPT_DIR=$(dirname "$0") +typeset LOG_FILE="" +typeset OS_NAME="$(uname -s)" +typeset HOST_NAME="$(hostname)" +typeset KEYS_FILE="" +typeset KEYS_DIR="" +typeset TARGETS_FILE="" +typeset DO_SLAVE=0 +typeset FIX_CREATE=0 +typeset CAN_DISCOVER_KEYS=0 +typeset CAN_START_AGENT=1 +typeset KEY_COUNT=0 +typeset KEY_1024_COUNT=0 +typeset KEY_2048_COUNT=0 +typeset KEY_4096_COUNT=0 +typeset KEY_OTHER_COUNT=0 +typeset RESOLVE_ALIAS="" +typeset SSH_KEYGEN_OPTS="" +typeset SSH_FIX_USER="" +typeset TMP_FILE="${TMP_DIR}/.${SCRIPT_NAME}.$$" +typeset TMP_RC_FILE="${TMP_DIR}/.${SCRIPT_NAME}.rc.$$" +typeset TMP_MERGE_FILE="" # command-line parameters -ARG_ACTION=0 # default is nothing -ARG_FIX_DIR="" # location of SSH controls directory -ARG_LOG_DIR="" # location of the log directory (~root etc) -ARG_LOCAL_DIR="" # location of the local SSH control files -ARG_REMOTE_DIR="" # location of the remote SSH control files -ARG_TARGETS="" # list of remote targets -ARG_LOG=1 # logging is on by default -ARG_VERBOSE=1 # STDOUT is on by default -ARG_DEBUG=0 # debug is off by default +typeset ARG_ACTION=0 # default is nothing +typeset ARG_ALIAS="" # alias to resolve +typeset ARG_FIX_DIR="" # location of SSH controls directory +typeset ARG_FIX_USER="" # user to own SSH controls files +typeset ARG_LOG_DIR="" # location of the log directory (~root etc) +typeset ARG_LOCAL_DIR="" # location of the local SSH control files +typeset ARG_REMOTE_DIR="" # location of the remote SSH control files +typeset ARG_TARGETS="" # list of remote targets +typeset ARG_LOG=1 # logging is on by default +typeset ARG_VERBOSE=1 # STDOUT is on by default +typeset ARG_DEBUG=0 # debug is off by default #****************************************************************************** @@ -91,6 +117,8 @@ ARG_DEBUG=0 # debug is off by default # ----------------------------------------------------------------------------- function check_config { +(( ARG_DEBUG > 0 )) && set "${DEBUG_OPTS}" + # SSH_TRANSFER_USER if [[ -z "${SSH_TRANSFER_USER}" ]] then @@ -140,6 +168,12 @@ if [[ -z "${DO_SSH_AGENT}" ]] then print -u2 "ERROR:no value for the DO_SSH_AGENT setting in the configuration file" exit 1 +else + if (( DO_SSH_AGENT > 0 )) && [[ -z "${SSH_PRIVATE_KEY}" ]] + then + print -u2 "ERROR:no value for the SSH_PRIVATE_KEY setting in the configuration file" + exit 1 + fi fi # MAX_BACKGROUND_PROCS if [[ -z "${MAX_BACKGROUND_PROCS}" ]] @@ -153,6 +187,12 @@ then print -u2 "ERROR: no value for the BACKUP_DIR setting in the configuration file" exit 1 fi +# FINGERPRINT_TYPE +if [[ -z "${FINGERPRINT_TYPE}" ]] +then + print -u2 "ERROR: no value for the FINGERPRINT_TYPE setting in the configuration file" + exit 1 +fi return 0 } @@ -160,7 +200,9 @@ return 0 # ----------------------------------------------------------------------------- function check_logging { -if (( ARG_LOG )) +(( ARG_DEBUG > 0 )) && set "${DEBUG_OPTS}" + +if (( ARG_LOG > 0 )) then if [[ ! -d "${LOG_DIR}" ]] then @@ -188,8 +230,10 @@ return 0 # ----------------------------------------------------------------------------- function check_params { +(( ARG_DEBUG > 0 )) && set "${DEBUG_OPTS}" + # -- ALL -if (( ARG_ACTION < 1 || ARG_ACTION > 10 )) +if (( ARG_ACTION < 1 || ARG_ACTION > 11 )) then display_usage exit 0 @@ -204,6 +248,12 @@ then else FIX_DIR="${ARG_FIX_DIR}" fi + if [[ -z "${ARG_FIX_USER}" ]] + then + SSH_FIX_USER="${LOGNAME}" + else + SSH_FIX_USER="${ARG_FIX_USER}" + fi fi # --local-dir if [[ -n "${ARG_LOCAL_DIR}" ]] @@ -246,12 +296,29 @@ then print ${TARGET_HOST} >>${TMP_FILE} done fi -# --update + --fix-local -if (( ARG_ACTION == 4 || ARG_ACTION == 5 )) +# --update + --fix-local + --resolve-alias +if (( ARG_ACTION == 4 || ARG_ACTION == 5 || ARG_ACTION == 11 )) then - if [[ -n "${TARGETS}" ]] + if [[ -n "${ARG_TARGETS}" ]] then - print -u2 "ERROR: you cannot specify '--targets' in this context!" + print -u2 "ERROR: you cannot specify '--targets' in this context" + exit 1 + fi +fi +# --resolve-alias +if (( ARG_ACTION == 11 )) +then + if [[ -z "${ARG_ALIAS}" ]] + then + print -u2 "ERROR: you must specify an alias with the '--resolve-alias' option" + exit 1 + fi +fi +if (( ARG_ACTION < 11 )) +then + if [[ -n "${ARG_ALIAS}" ]] + then + print -u2 "ERROR: you cannot specify '--alias' in this context" exit 1 fi fi @@ -262,7 +329,11 @@ return 0 # ----------------------------------------------------------------------------- function check_root_user { -(IFS='()'; set -- "$(id)"; print $2) | read UID +(( ARG_DEBUG > 0 )) && set "${DEBUG_OPTS}" +typeset UID="" + +# shellcheck disable=SC2046 +(IFS='()'; set -- $(id); print $2) | read UID if [[ "${UID}" = "root" ]] then return 0 @@ -274,6 +345,9 @@ fi # ----------------------------------------------------------------------------- function check_setup { +(( ARG_DEBUG > 0 )) && set "${DEBUG_OPTS}" +typeset FILE="" + # use added fall back for LOCAL_DIR (the default script directory) [[ -d "${LOCAL_DIR}" ]] || LOCAL_DIR="${SCRIPT_DIR}" @@ -353,7 +427,7 @@ if (( DO_SSH_AGENT )) then # ssh-agent which ssh-agent >/dev/null 2>/dev/null - if (( $? )) + if (( $? > 0 )) then print -u2 "WARN: ssh-agent not available on ${HOST_NAME}" CAN_START_AGENT=0 @@ -372,24 +446,34 @@ return 0 # ----------------------------------------------------------------------------- function check_syntax { +(( ARG_DEBUG > 0 )) && set "${DEBUG_OPTS}" +typeset ACCESS_LINE="" +typeset ACCESS_FIELDS="" +typeset ALIASES_LINE="" +typeset ALIAS_FIELDS="" +typeset ALIAS="" +typeset CHECK_ALIAS="" +typeset KEY_FILE="" +typeset KEY_FIELDS="" + # access should have 3 fields -cat "${LOCAL_DIR}/access" | grep -v -E -e '^#|^$' | while read ACCESS_LINE +grep -v -E -e '^#|^$' "${LOCAL_DIR}/access" 2>/dev/null | while read 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)" + (( 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 -cat "${LOCAL_DIR}/alias" | grep -v -E -e '^#|^$' | while read ALIAS_LINE +grep -v -E -e '^#|^$' "${LOCAL_DIR}/alias" 2>/dev/null | while read ALIASES_LINE do - ALIAS_FIELDS=$(count_fields "${ALIAS_LINE}" ":") - (( ALIAS_FIELDS != 2 )) && die "line '${ALIAS_LINE}' in alias file has missing or too many field(s) (should be 2)" + 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 do - cat ${KEY_FILE} 2>/dev/null | grep -v -E -e '^#|^$' |\ + grep -v -E -e '^#|^$' ${KEY_FILE} 2>/dev/null |\ while read KEY_LINE do KEY_FIELDS=$(count_fields "${KEY_LINE}" ",") @@ -397,16 +481,54 @@ do done done +# resolve aliases in access +awk 'BEGIN { needle="@[a-zA-Z0-9_-]+" } + { + if ($0 ~ /^#/) { next; } + while (match($0, needle)) { + print substr ($0, RSTART, RLENGTH); + $0 = substr ($0, RSTART + RLENGTH); + } + }' "${LOCAL_DIR}/access" 2>/dev/null | sort -u 2>/dev/null |\ +while read -r ALIAS +do + CHECK_ALIAS=$(resolve_alias "${ALIAS}" 0) + if [[ -z "${CHECK_ALIAS}" ]] || (( RC > 0 )) + then + die "unable to resolve alias ${ALIAS} in the access file" + fi +done + +# resolve aliases in alias +awk 'BEGIN { needle="@[a-zA-Z0-9_-]+" } + { + if ($0 ~ /^#/) { next; } + while (match($0, needle)) { + print substr ($0, RSTART, RLENGTH); + $0 = substr ($0, RSTART + RLENGTH); + } + }' "${LOCAL_DIR}/alias" 2>/dev/null | sort -u 2>/dev/null |\ +while read -r ALIAS +do + CHECK_ALIAS=$(resolve_alias "${ALIAS}" 0) + if [[ -z "${CHECK_ALIAS}" ]] || (( RC > 0 )) + then + die "unable to resolve alias ${ALIAS} in the alias file" + fi +done + return 0 } # ----------------------------------------------------------------------------- function count_fields { -CHECK_LINE="$1" -CHECK_DELIM="$2" +(( ARG_DEBUG > 0 )) && set "${DEBUG_OPTS}" +typeset CHECK_LINE="$1" +typeset CHECK_DELIM="$2" +typeset NUM_FIELDS=0 -NUM_FIELDS=$(print "${CHECK_LINE}" | awk -F "${CHECK_DELIM}" '{ print NF }') +NUM_FIELDS=$(print "${CHECK_LINE}" | awk -F "${CHECK_DELIM}" '{ print NF }' 2>/dev/null) print ${NUM_FIELDS} @@ -416,11 +538,14 @@ return 0 # ----------------------------------------------------------------------------- function die { -NOW="$(date '+%d-%h-%Y %H:%M:%S')" +(( ARG_DEBUG > 0 )) && set "${DEBUG_OPTS}" +typeset NOW="$(date '+%d-%h-%Y %H:%M:%S')" +typeset LOG_LINE="" +typeset LOG_SIGIL="" if [[ -n "$1" ]] then - if (( ARG_LOG )) + if (( ARG_LOG > 0 )) then print - "$*" | while read LOG_LINE do @@ -459,9 +584,6 @@ then done fi -# finish up work -do_cleanup - exit 1 } @@ -479,12 +601,13 @@ remote, create SSH key fingerprints or copy/distribute the SSH controls files Syntax: ${SCRIPT_DIR}/${SCRIPT_NAME} [--help] | (--backup | --check-syntax | --preview-global | --make-finger | --update ) | (--apply [--slave] [--remote-dir=] [--targets=,,...]) | ((--copy|--distribute) [--slave] [--remote-dir= [--targets=,,...]]) | - (--discover [--targets=,,...]) | - ([--fix-local --fix-dir= [--create-dir]] | [--fix-remote [--slave] [--create-dir] [--targets=,,...]]) + (--discover [--targets=,,...]) | (--resolve-alias --alias=) + ([--fix-local --fix-dir= [--fix-user=] [--create-dir]] | [--fix-remote [--slave] [--create-dir] [--targets=,,...]]) [--local-dir=] [--no-log] [--log-dir=] [--debug] Parameters: +--alias : name of the alias to process --apply|-a : apply SSH controls remotely (~targets) --backup|-b : create a backup of the SSH controls repository (SSH master) --check-syntax|-s : do basic syntax checking on SSH controls configuration @@ -499,6 +622,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 +--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. [default: see LOCAL_DIR setting] @@ -509,9 +633,10 @@ Parameters: --remote-dir : directory where SSH control files are/should be located/copied on/to the target host [default: see REMOTE_DIR setting] +--resolve-alias|-r : resolve an alias into its individual components --slave : perform actions in master->slave mode ---targets : comma-separated list of target hosts to operate on. Override the - hosts contained in the 'targets' configuration file. +--targets : comma-separated list of target hosts or @groups to operate on. + Overrides hosts/@groups contained in the 'targets' file. --update|-u : apply SSH controls locally --version|-V : show the script version/release/fix @@ -534,12 +659,18 @@ return 0 # distribute SSH controls to a single host/client function distribute2host { -SERVER="$1" -ERROR_COUNT=0 +(( ARG_DEBUG > 0 )) && set "${DEBUG_OPTS}" +typeset SERVER="$1" +typeset ERROR_COUNT=0 +typeset FILE="" +typeset COPY_RC=0 +typeset TMP_WORK_DIR="" +typeset BLACKLIST_FILE="" + # convert line to hostname SERVER=${SERVER%%;*} resolve_host ${SERVER} -if (( $? )) +if (( $? > 0 )) then warn "could not lookup host ${SERVER}, skipping" return 1 @@ -557,7 +688,7 @@ do # sftp transfer sftp_file ${FILE} ${SERVER} COPY_RC=$? - if (( ! COPY_RC )) + if (( COPY_RC == 0 )) then log "transferred ${FILE%!*} to ${SERVER}:${REMOTE_DIR}" else @@ -566,13 +697,13 @@ do fi done # 2) keys files -# are keys stored a file or a directory? +# are keys stored in a file or a directory? 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} - if (( $? )) + if (( $? > 0 )) then die "unable to create temporary directory ${TMP_WORK_DIR} for mangling of 'keys' file" fi @@ -582,7 +713,7 @@ then # sftp transfer sftp_file "${TMP_MERGE_FILE}!640" ${SERVER} COPY_RC=$? - if (( ! COPY_RC )) + if (( COPY_RC == 0 )) then log "transferred ${TMP_MERGE_FILE} to ${SERVER}:${REMOTE_DIR}" else @@ -593,7 +724,7 @@ then else sftp_file "${KEYS_FILE}!640" ${SERVER} COPY_RC=$? - if (( ! COPY_RC )) + if (( COPY_RC == 0 )) then log "transferred ${KEYS_FILE} to ${SERVER}:${REMOTE_DIR}" else @@ -613,7 +744,7 @@ then # sftp transfer sftp_file "${BLACKLIST_FILE}!660" ${SERVER} COPY_RC=$? - if (( ! COPY_RC )) + if (( COPY_RC == 0 )) then log "transferred ${BLACKLIST_FILE} to ${SERVER}:${REMOTE_DIR}" else @@ -631,19 +762,27 @@ return ${ERROR_COUNT} # distribute SSH controls to a single host in slave mode function distribute2slave { -SERVER="$1" +(( ARG_DEBUG > 0 )) && set "${DEBUG_OPTS}" +typeset SERVER="$1" +typeset DISTRIBUTE_OPTS="" +typeset RC=0 # convert line to hostname SERVER=${SERVER%%;*} resolve_host ${SERVER} -if (( $? )) +if (( $? > 0 )) then warn "could not lookup host ${SERVER}, skipping" return 1 fi +# propagate the --debug flag +if (( ARG_DEBUG > 0 )) +then + DISTRIBUTE_OPTS="${DISTRIBUTE_OPTS} --debug" +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; +( RC=0; ssh -A ${SSH_ARGS} ${SERVER} ${REMOTE_DIR}/${SCRIPT_NAME} --copy ${DISTRIBUTE_OPTS}; print "$?" > ${TMP_RC_FILE}; exit ) 2>&1 | logc @@ -656,6 +795,7 @@ return ${RC} # ----------------------------------------------------------------------------- function do_cleanup { +(( ARG_DEBUG > 0 )) && set "${DEBUG_OPTS}" log "performing cleanup ..." # remove temporary file(s) @@ -672,33 +812,48 @@ return 0 # !! requires appropriate 'sudo' rules on remote client for privilege elevation function fix2host { -SERVER="$1" -SERVER_DIR="$2" +(( ARG_DEBUG > 0 )) && set "${DEBUG_OPTS}" +typeset SERVER="$1" +typeset SERVER_DIR="$2" +typeset SERVER_USER="$3" +typeset FIX_OPTS="" +typeset RC=0 # convert line to hostname SERVER=${SERVER%%;*} resolve_host ${SERVER} -if (( $? )) +if (( $? > 0 )) then warn "could not lookup host ${SERVER}, skipping" return 1 fi +# propagate the --create-dir flag +if (( FIX_CREATE > 0 )) +then + FIX_OPTS="${FIX_OPTS} --create-dir" +fi +# propagate the --debug flag +if (( ARG_DEBUG > 0 )) +then + FIX_OPTS="${FIX_OPTS} --debug" +fi + 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}; + ( 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}; + ( 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}; + ( 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 @@ -714,20 +869,34 @@ return ${RC} # !! requires appropriate 'sudo' rules on remote client for privilege elevation function fix2slave { -SERVER="$1" -SERVER_DIR="$2" +(( ARG_DEBUG > 0 )) && set "${DEBUG_OPTS}" +typeset SERVER="$1" +typeset SERVER_DIR="$2" +typeset SERVER_USER="$3" +typeset FIX_OPTS="" +typeset RC=0 # convert line to hostname SERVER=${SERVER%%;*} resolve_host ${SERVER} -if (( $? )) +if (( $? > 0 )) then warn "could not lookup host ${SERVER}, skipping" return 1 fi +# propagate the --create-dir flag +if (( FIX_CREATE > 0 )) +then + FIX_OPTS="${FIX_OPTS} --create-dir" +fi +# propagate the --debug flag +if (( ARG_DEBUG > 0 )) +then + FIX_OPTS="${FIX_OPTS} --debug" +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}; +( 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 @@ -740,7 +909,8 @@ return ${RC} # ----------------------------------------------------------------------------- function get_linux_name { -LSB_NAME="$(lsb_release -is 2>/dev/null | cut -f2 -d':')" +(( ARG_DEBUG > 0 )) && set "${DEBUG_OPTS}" +typeset LSB_NAME="$(lsb_release -is 2>/dev/null | cut -f2 -d':' 2>/dev/null)" print "${LSB_NAME}" @@ -750,7 +920,12 @@ return 0 # ----------------------------------------------------------------------------- function get_linux_version { -LSB_VERSION="$(lsb_release -rs 2>/dev/null | cut -f1 -d'.')" +(( ARG_DEBUG > 0 )) && set "${DEBUG_OPTS}" +typeset LSB_VERSION +typeset RELEASE_STRING="" +typeset RHEL_VERSION="" + +LSB_VERSION="$(lsb_release -rs 2>/dev/null | cut -f1 -d'.' 2>/dev/null)" if [[ -z "${LSB_VERSION}" ]] then @@ -782,12 +957,15 @@ return 0 # log an INFO: message (via ARG). function log { -NOW="$(date '+%d-%h-%Y %H:%M:%S')" +(( ARG_DEBUG > 0 )) && set "${DEBUG_OPTS}" +typeset NOW="$(date '+%d-%h-%Y %H:%M:%S')" +typeset LOG_LINE="" +typeset LOG_SIGIL="" # log an INFO: message (via ARG). if [[ -n "$1" ]] then - if (( ARG_LOG )) + if (( ARG_LOG > 0 )) then print - "$*" | while read LOG_LINE do @@ -812,7 +990,7 @@ then print "${NOW}: ${LOG_SIGIL}: [$$]:" "${LOG_LINE}" >>${LOG_FILE} done fi - if (( ARG_VERBOSE )) + if (( ARG_VERBOSE > 0 )) then print - "$*" | while read LOG_LINE do @@ -834,17 +1012,20 @@ return 0 # ----------------------------------------------------------------------------- # log an INFO: message (via STDIN). Do not use when STDIN is still open +(( ARG_DEBUG > 0 )) && set "${DEBUG_OPTS}" # shellcheck disable=SC2120 function logc { -NOW="$(date '+%d-%h-%Y %H:%M:%S')" -LOG_STDIN="" +typeset NOW="$(date '+%d-%h-%Y %H:%M:%S')" +typeset LOG_STDIN="" +typeset LOG_LINE="" +typeset LOG_SIGIL="" # process STDIN (if any) [[ ! -t 0 ]] && LOG_STDIN="$(cat)" if [[ -n "${LOG_STDIN}" ]] then - if (( ARG_LOG )) + if (( ARG_LOG > 0 )) then print - "${LOG_STDIN}" | while read LOG_LINE do @@ -869,7 +1050,7 @@ then print "${NOW}: ${LOG_SIGIL}: [$$]:" "${LOG_LINE}" >>${LOG_FILE} done fi - if (( ARG_VERBOSE )) + if (( ARG_VERBOSE > 0 )) then print - "${LOG_STDIN}" | while read LOG_LINE do @@ -889,7 +1070,7 @@ fi # process ARG (if any) if [[ -n "$1" ]] then - if (( ARG_LOG != 0 )) + if (( ARG_LOG > 0 )) then print - "$*" | while read LOG_LINE do @@ -914,7 +1095,7 @@ then print "${NOW}: ${LOG_SIGIL}: [$$]:" "${LOG_LINE}" >>${LOG_FILE} done fi - if (( ARG_VERBOSE != 0 )) + if (( ARG_VERBOSE > 0 )) then print - "$*" | while read LOG_LINE do @@ -933,21 +1114,143 @@ fi return 0 } +# ----------------------------------------------------------------------------- +# resolve an alias +function resolve_alias +{ +(( ARG_DEBUG > 0 )) && set "${DEBUG_OPTS}" +typeset NEEDLE="$1" +typeset RECURSION_COUNT=$2 +typeset ALIASES_LINE="" +typeset ALIAS_LIST="" +typeset ALIAS="" +typeset EXPANDED_ALIASES="" + +# check MAX_RECURSION to avoid segmentation faults +if (( RECURSION_COUNT > MAX_RECURSION )) +then + # don't scramble function output with warn(), only use for ARG_LOG + # go back up in the recursion tree or exit resolution call + print -u2 "WARN: resolving alias for ${NEEDLE} has exceeded a recursion depth of ${MAX_RECURSION}" + ARG_VERBOSE=0 warn "resolving alias for ${NEEDLE} has exceeded a recursion depth of ${MAX_RECURSION}" + return 1 +fi + +# get aliases from alias line +ALIASES_LINE=$(grep -E -e "^${NEEDLE}.*:" ${LOCAL_DIR}/alias 2>/dev/null | cut -f2 -d':' 2>/dev/null) + +if [[ -z "${ALIASES_LINE}" ]] +then + # don't scramble function output with warn(), only use for ARG_LOG + # go back up in the recursion tree or exit resolution call + print -u2 "WARN: alias ${NEEDLE} does not resolve or is empty" + ARG_VERBOSE=0 warn "alias ${NEEDLE} does not resolve or is empty" + return 1 +fi + +# expand alias line into individual aliases +for ALIAS in ${ALIASES_LINE//,/ } +do + # recurse if the alias is a group + if [[ "${ALIAS}" =~ ^\@ ]] + then + RECURSION_COUNT=$(( RECURSION_COUNT + 1 )) + EXPANDED_ALIASES=$(resolve_alias "${ALIAS}" ${RECURSION_COUNT}) + RECURSION_COUNT=$(( RECURSION_COUNT - 1 )) + if (( $? == 0 )) + then + if [[ -z "${ALIAS_LIST}" ]] + then + ALIAS_LIST="${EXPANDED_ALIASES}" + else + ALIAS_LIST="${ALIAS_LIST}\n${EXPANDED_ALIASES}" + fi + # if the recursion fails, return the current output list + # and go back up into the recursion tree + else + print "${ALIAS_LIST}" + return 1 + fi + # if the alias is not a group, just add it to the output list + else + if [[ -z "${ALIAS_LIST}" ]] + then + ALIAS_LIST="${ALIAS}" + else + ALIAS_LIST="${ALIAS_LIST}\n${ALIAS}" + fi + fi +done + +# sort final output and hand it back to the caller +print "${ALIAS_LIST}" | sort -u 2>/dev/null + +return 0 +} + # ----------------------------------------------------------------------------- # resolve a host (check) function resolve_host { -nslookup "$1" 2>/dev/null | grep -q -E -e 'Address:.*([0-9]{1,3}[\.]){3}[0-9]{1,3}' +(( ARG_DEBUG > 0 )) && set "${DEBUG_OPTS}" +nslookup "$1" 2>/dev/null | grep -q -E -e 'Address:.*([0-9]{1,3}[\.]){3}[0-9]{1,3}' 2>/dev/null return $? } +# ----------------------------------------------------------------------------- +# resolve targets (file) +function resolve_targets +{ +(( ARG_DEBUG > 0 )) && set "${DEBUG_OPTS}" +typeset TARGETS_LIST="" +typeset EXPANDED_TARGETS="" +typeset TARGET="" + +grep -v -E -e '^#' -e '^$' "${TARGETS_FILE}" 2>/dev/null | while read -r TARGET +do + # resolve group target + if [[ "${TARGET}" =~ ^\@ ]] + then + EXPANDED_TARGETS=$(resolve_alias "${TARGET}" 0) + if (( $? == 0 )) + then + if [[ -z "${TARGETS_LIST}" ]] + then + TARGETS_LIST="${EXPANDED_TARGETS}" + else + TARGETS_LIST="${TARGETS_LIST}\n${EXPANDED_TARGETS}" + fi + fi + # add individual target + else + if [[ -z "${TARGETS_LIST}" ]] + then + TARGETS_LIST="${TARGET}" + else + TARGETS_LIST="${TARGETS_LIST}\n${TARGET}" + fi + fi +done + +# sort final output and hand it back to the caller +print "${TARGETS_LIST}" | grep -v '^$' 2>/dev/null | sort -u 2>/dev/null + +return $0 +} + # ----------------------------------------------------------------------------- # transfer a file using sftp function sftp_file { -TRANSFER_FILE="$1" -TRANSFER_HOST="$2" +(( ARG_DEBUG > 0 )) && set "${DEBUG_OPTS}" +typeset TRANSFER_FILE="$1" +typeset TRANSFER_HOST="$2" +typeset TRANSFER_DIR="" +typeset TRANSFER_PERMS="" +typeset SOURCE_FILE="" +typeset OLD_PWD="" +typeset SFTP_RC=0 # find the local directory & permission bits TRANSFER_DIR="${TRANSFER_FILE%/*}" @@ -960,7 +1263,7 @@ OLD_PWD=$(pwd) cd ${TRANSFER_DIR} || return 1 # transfer, (possibly) chmod the file to/on the target server (keep STDERR) -if (( DO_SFTP_CHMOD )) +if (( DO_SFTP_CHMOD > 1 )) then sftp ${SFTP_ARGS} ${SSH_TRANSFER_USER}@${TRANSFER_HOST} >/dev/null < 0 )) && set "${DEBUG_OPTS}" + log "requested to start an SSH agent on ${HOST_NAME} ..." # is there one still running, then we re-use it @@ -1008,7 +1313,7 @@ fi # add the private key log "adding private key ${SSH_PRIVATE_KEY} to SSH agent on ${HOST_NAME} ..." log "$(ssh-add ${SSH_PRIVATE_KEY} 2>&1)" -if (( $? )) +if (( $(ssh-add -l 2>/dev/null | wc -l 2>/dev/null) == 0 )) then warn "unable to add SSH private key to SSH agent on ${HOST_NAME}" return 1 @@ -1020,6 +1325,8 @@ return 0 # ----------------------------------------------------------------------------- function stop_ssh_agent { +(( ARG_DEBUG > 0 )) && set "${DEBUG_OPTS}" + # stop the SSH agent log "stopping SSH agent (if needed) on ${HOST_NAME} ..." @@ -1044,7 +1351,7 @@ else fi # process check -if (( $(pgrep -u "${USER}" ssh-agent | grep -c "${SSH_AGENT_PID}") )) +if (( $(pgrep -u "${USER}" ssh-agent | grep -c "${SSH_AGENT_PID}" 2>/dev/null) )) then warn "unable to stop running SSH agent on ${HOST_NAME} [PID=${SSH_AGENT_PID}]" return 1 @@ -1058,32 +1365,41 @@ return 0 # !! requires appropriate 'sudo' rules on remote client for privilege elevation function update2host { -SERVER="$1" +(( ARG_DEBUG > 0 )) && set "${DEBUG_OPTS}" +typeset SERVER="$1" +typeset UPDATE_OPTS="" +typeset RC=0 # convert line to hostname SERVER=${SERVER%%;*} resolve_host ${SERVER} -if (( $? )) +if (( $? > 0 )) then warn "could not lookup host ${SERVER}, skipping" return 1 fi -log "setting ssh controls on ${SERVER} ..." +# propagate the --debug flag +if (( ARG_DEBUG > 0 )) +then + UPDATE_OPTS="${UPDATE_OPTS} --debug" +fi + +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; + ( 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; + ( 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; + ( RC=0; ssh ${SSH_ARGS} root@${SERVER} ${REMOTE_DIR}/${SCRIPT_NAME} --update ${UPDATE_OPTS}; print "$?" > ${TMP_RC_FILE}; exit ) 2>&1 | logc fi @@ -1098,19 +1414,27 @@ return ${RC} # update SSH controls on a single host/client in slave mode function update2slave { -SERVER="$1" +(( ARG_DEBUG > 0 )) && set "${DEBUG_OPTS}" +typeset SERVER="$1" +typeset UPDATE_OPTS="" +typeset RC=0 # convert line to hostname SERVER=${SERVER%%;*} resolve_host ${SERVER} -if (( $? )) +if (( $? > 0 )) then warn "could not lookup host ${SERVER}, skipping" return 1 fi +# propagate the --debug flag +if (( ARG_DEBUG > 0 )) +then + UPDATE_OPTS="${UPDATE_OPTS} --debug" +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; +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 @@ -1124,7 +1448,12 @@ return ${RC} # update the 'fingerprints' file (must exist beforehand) function update_fingerprints { -FINGER_LINE="$1" +(( ARG_DEBUG > 0 )) && set "${DEBUG_OPTS}" +typeset FINGER_LINE="$1" +typeset FINGER_FIELDS=0 +typeset FINGER_USER="" +typeset FINGERPRINT="" +typeset FINGER_RC=0 # check for empty line [[ -z "${FINGER_LINE}" ]] && log "skipping empty line in keys file" && return 0 @@ -1139,15 +1468,15 @@ 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)" FINGER_RC=$? -if (( ! FINGER_RC )) +if (( FINGER_RC == 0 )) then case "${OS_NAME}" in - HP-UX) - FINGER_ENTRY="$(print ${FINGERPRINT} | awk '{print $1,$2,$4}')" - ;; - *) - FINGER_ENTRY="$(print ${FINGERPRINT} | awk '{print $1,$2,$5}')" - ;; + HP-UX) + FINGER_ENTRY="$(print ${FINGERPRINT} | awk '{print $1,$2,$4}')" + ;; + *) + FINGER_ENTRY="$(print ${FINGERPRINT} | awk '{print $1,$2,$5}')" + ;; esac log "${FINGER_USER}->${FINGER_ENTRY}" print "${FINGER_USER} ${FINGER_ENTRY}" >> "${LOCAL_DIR}/fingerprints" @@ -1177,26 +1506,29 @@ return 0 # wait for child processes to exit function wait_for_children { -WAIT_ERRORS=0 +(( ARG_DEBUG > 0 )) && set "${DEBUG_OPTS}" +typeset WAIT_ERRORS=0 +typeset PID="" +typeset RC=0 # 'endless' loop :-) while : do - (( ARG_DEBUG )) && print -u2 "child processes remaining: $*" + (( ARG_DEBUG > 0 )) && print -u2 "child processes remaining: $*" for PID in "$@" do shift # child is still alive? if kill -0 ${PID} 2>/dev/null then - (( ARG_DEBUG )) && print -u2 "DEBUG: ${PID} is still alive" + (( ARG_DEBUG > 0 )) && print -u2 "DEBUG: ${PID} is still alive" set -- "$@" "${PID}" # wait for sigchild, catching child exit codes is unreliable because # the child might have already ended before we get here (caveat emptor) else wait ${PID} RC=$? - if (( RC )) + if (( RC > 0 )) then warn "child process ${PID} exited [RC=${RC}]" WAIT_ERRORS=$(( WAIT_ERRORS + 1 )) @@ -1206,7 +1538,7 @@ do fi done # break loop if we have no child PIDs left - (($# > 0)) || break + (( $# > 0 )) || break sleep 1 # required to avoid race conditions done @@ -1216,11 +1548,14 @@ return ${WAIT_ERRORS} # ----------------------------------------------------------------------------- function warn { -NOW="$(date '+%d-%h-%Y %H:%M:%S')" +(( ARG_DEBUG > 0 )) && set "${DEBUG_OPTS}" +typeset NOW="$(date '+%d-%h-%Y %H:%M:%S')" +typeset LOG_LINE="" +typeset LOG_SIGIL="" if [[ -n "$1" ]] then - if (( ARG_LOG )) + if (( ARG_LOG > 0 )) then print - "$*" | while read LOG_LINE do @@ -1245,7 +1580,7 @@ then print "${NOW}: ${LOG_SIGIL}: [$$]:" "${LOG_LINE}" >>${LOG_FILE} done fi - if (( ARG_VERBOSE )) + if (( ARG_VERBOSE > 0 )) then print - "$*" | while read LOG_LINE do @@ -1276,21 +1611,21 @@ for PARAMETER in ${CMD_LINE} do case ${PARAMETER} in -a|-apply|--apply) - (( ARG_ACTION )) && { + (( ARG_ACTION > 0 )) && { print -u2 "ERROR: multiple actions specified" exit 1 } ARG_ACTION=1 ;; -b|-backup|--backup) - (( ARG_ACTION )) && { + (( ARG_ACTION > 0 )) && { print -u2 "ERROR: multiple actions specified" exit 1 } ARG_ACTION=9 ;; -c|-copy|--copy) - (( ARG_ACTION )) && { + (( ARG_ACTION > 0 )) && { print -u2 "ERROR: multiple actions specified" exit 1 } @@ -1300,21 +1635,21 @@ do ARG_DEBUG=1 ;; -distribute|--distribute) - (( ARG_ACTION )) && { + (( ARG_ACTION > 0 )) && { print -u2 "ERROR: multiple actions specified" exit 1 } ARG_ACTION=2 ;; -m|-make-finger|--make-finger) - (( ARG_ACTION )) && { + (( ARG_ACTION > 0 )) && { print -u2 "ERROR: multiple actions specified" exit 1 } ARG_ACTION=3 ;; -d|-discover|--discover) - (( ARG_ACTION )) && { + (( ARG_ACTION > 0 )) && { print -u2 "ERROR: multiple actions specified" exit 1 } @@ -1324,40 +1659,53 @@ do CAN_DISCOVER_KEYS=1 ;; -p|--preview-global|-preview-global) - (( ARG_ACTION )) && { + (( ARG_ACTION > 0 )) && { print -u2 "ERROR: multiple actions specified" exit 1 } ARG_ACTION=7 ;; -fix-local|--fix-local) - (( ARG_ACTION )) && { + (( ARG_ACTION > 0 )) && { print -u2 "ERROR: multiple actions specified" exit 1 } ARG_ACTION=5 ;; -fix-remote|--fix-remote) - (( ARG_ACTION )) && { + (( ARG_ACTION > 0 )) && { print -u2 "ERROR: multiple actions specified" exit 1 } ARG_ACTION=6 ;; + -r|-resolve-alias|--resolve-alias) + (( ARG_ACTION > 0 )) && { + print -u2 "ERROR: multiple actions specified" + exit 1 + } + ARG_ACTION=11 + ;; -s|-check-syntax|--check-syntax) - (( ARG_ACTION )) && { + (( ARG_ACTION > 0 )) && { print -u2 "ERROR: multiple actions specified" exit 1 } ARG_ACTION=8 ;; -u|-update|--update) - (( ARG_ACTION )) && { + (( ARG_ACTION > 0 )) && { print -u2 "ERROR: multiple actions specified" exit 1 } ARG_ACTION=4 ;; + -alias=*) + ARG_ALIAS="${PARAMETER#-alias=}" + ;; + --alias=*) + ARG_ALIAS="${PARAMETER#--alias=}" + ;; -create-dir|--create-dir) FIX_CREATE=1 ;; @@ -1367,6 +1715,12 @@ do --fix-dir=*) ARG_FIX_DIR="${PARAMETER#--fix-dir=}" ;; + -fix-user=*) + ARG_FIX_USER="${PARAMETER#-fix-user=}" + ;; + --fix-user=*) + ARG_FIX_USER="${PARAMETER#--fix-user=}" + ;; -local-dir=*) ARG_LOCAL_DIR="${PARAMETER#-local-dir=}" ;; @@ -1398,13 +1752,17 @@ do ARG_TARGETS="${PARAMETER#--targets=}" ;; -V|-version|--version) - print "INFO: $0: ${MY_VRF}" + print "INFO: $0: ${SCRIPT_VERSION}" exit 0 ;; \? | -h | -help | --help) display_usage exit 0 ;; + *) + display_usage + exit 0 + ;; esac done @@ -1429,10 +1787,18 @@ fi check_params && check_config && check_setup && check_logging # catch shell signals -trap 'do_cleanup; exit' 1 2 3 15 +trap 'do_cleanup; exit 0' 0 +trap 'do_cleanup; exit 1' 1 2 3 15 + +# set debugging options +if (( ARG_DEBUG > 0 )) +then + DEBUG_OPTS='-vx' + set "${DEBUG_OPTS}" +fi log "*** start of ${SCRIPT_NAME} [${CMD_LINE}] /$$@${HOST_NAME}/ ***" -(( ARG_LOG )) && log "logging takes places in ${LOG_FILE}" +(( ARG_LOG > 0 )) && log "logging takes places in ${LOG_FILE}" log "runtime info: LOCAL_DIR is set to: ${LOCAL_DIR}" @@ -1444,31 +1810,31 @@ case ${ARG_ACTION} in then check_root_user && die "must NOT be run as user 'root'" fi + + # build clients list + CLIENTS=$(resolve_targets) + if [[ -z "${CLIENTS}" ]] + then + die "no targets to process" + else + log "processing targets: $(print ${CLIENTS} | tr -s '\n' ' ' 2>/dev/null)" + fi + # start SSH agent (if needed) - if (( DO_SSH_AGENT && CAN_START_AGENT )) + if (( DO_SSH_AGENT > 0 && CAN_START_AGENT > 0 )) then start_ssh_agent - if (( $? )) + if (( $? > 0 )) then die "problem with launching an SSH agent, bailing out" fi fi - # build clients list (in array) - cat "${TARGETS_FILE}" | grep -v -E -e '^#' -e '^$' |\ - { - I=0 - set -A CLIENTS - while read LINE - do - CLIENTS[${I}]="${LINE}" - I=$(( I + 1 )) - done - } + # set max updates in background COUNT=${MAX_BACKGROUND_PROCS} - for CLIENT in "${CLIENTS[@]}" + print "${CLIENTS}" | while read -r CLIENT do - if (( DO_SLAVE )) + if (( DO_SLAVE > 0 )) then update2slave ${CLIENT} & else @@ -1493,7 +1859,7 @@ case ${ARG_ACTION} in wait_for_children ${PIDS} || \ warn "$? background jobs (possibly) failed to complete correctly" # stop SSH agent if needed - (( DO_SSH_AGENT && CAN_START_AGENT )) && \ + (( DO_SSH_AGENT > 0 && CAN_START_AGENT > 0 )) && \ stop_ssh_agent log "finished applying SSH controls remotely" ;; @@ -1504,29 +1870,29 @@ case ${ARG_ACTION} in then check_root_user && die "must NOT be run as user 'root'" fi + + # build clients list + CLIENTS=$(resolve_targets) + if [[ -z "${CLIENTS}" ]] + then + die "no targets to process" + else + log "processing targets: $(print ${CLIENTS} | tr -s '\n' ' ' 2>/dev/null)" + fi + # start SSH agent (if needed) - if (( DO_SSH_AGENT && CAN_START_AGENT )) + if (( DO_SSH_AGENT > 0 && CAN_START_AGENT > 0 )) then start_ssh_agent - if (( $? )) + if (( $? > 0 )) then die "problem with launching an SSH agent, bailing out" fi fi - # build clients list (in array) - cat "${TARGETS_FILE}" | grep -v -E -e '^#' -e '^$' |\ - { - I=0 - set -A CLIENTS - while read LINE - do - CLIENTS[${I}]="${LINE}" - I=$(( I + 1 )) - done - } + # set max updates in background COUNT=${MAX_BACKGROUND_PROCS} - for CLIENT in "${CLIENTS[@]}" + print "${CLIENTS}" | while read -r CLIENT do if (( DO_SLAVE )) then @@ -1553,7 +1919,7 @@ case ${ARG_ACTION} in wait_for_children ${PIDS} || \ warn "$? background jobs (possibly) failed to complete correctly" # stop SSH agent if needed - (( DO_SSH_AGENT && CAN_START_AGENT )) && \ + (( DO_SSH_AGENT > 0 && CAN_START_AGENT > 0 )) && \ stop_ssh_agent log "finished copying/distributing SSH controls" ;; @@ -1584,7 +1950,7 @@ case ${ARG_ACTION} in # check if ssh-keygen support fingerprint type FAIL_SSH_KEYGEN=$(ssh-keygen -E 2>&1 | grep -c "illegal") - if (( ! FAIL_SSH_KEYGEN )) + if (( FAIL_SSH_KEYGEN == 0 )) then SSH_KEYGEN_OPTS="-E ${FINGERPRINT_TYPE}" else @@ -1619,7 +1985,7 @@ case ${ARG_ACTION} in ) 2>&1 | logc # fetch return code from subshell RC="$(< ${TMP_RC_FILE})" - if (( RC )) + if (( RC > 0 )) then die "failed to apply SSH controls locally [RC=${RC}]" else @@ -1629,7 +1995,12 @@ case ${ARG_ACTION} in 5) # fix local directory structure/perms/ownerships log "ACTION: fix local SSH controls repository" check_root_user || die "must be run as user 'root'" - if (( FIX_CREATE )) + log "resetting ownerships to UNIX user ${SUDO_FIX_USER}" + if [[ ${SSH_FIX_USER} = "root" ]] + then + warn "!!! resetting ownerships to user root !!!" + fi + if (( FIX_CREATE > 0 )) then log "you requested to create directories (if needed)" else @@ -1637,7 +2008,7 @@ case ${ARG_ACTION} in fi # check if the SSH control repo is already there - if (( FIX_CREATE )) && [[ ! -d "${FIX_DIR}" ]] + if (( FIX_CREATE > 0 )) && [[ ! -d "${FIX_DIR}" ]] then # create stub directories mkdir -p "${FIX_DIR}/holding" 2>/dev/null || \ @@ -1654,7 +2025,7 @@ case ${ARG_ACTION} in if [[ -d "${FIX_DIR}/holding" ]] then chmod 2775 "${FIX_DIR}/holding" 2>/dev/null && \ - chown root:${SSH_OWNER_GROUP} "${FIX_DIR}/holding" 2>/dev/null + chown ${SSH_FIX_USER}:${SSH_OWNER_GROUP} "${FIX_DIR}/holding" 2>/dev/null fi if [[ -d "${FIX_DIR}/keys.d" ]] then @@ -1667,7 +2038,7 @@ case ${ARG_ACTION} in if [[ -f "${FIX_DIR}/holding/${FILE}" ]] then chmod 660 "${FIX_DIR}/holding/${FILE}" 2>/dev/null && \ - chown root:${SSH_OWNER_GROUP} "${FIX_DIR}/holding/${FILE}" 2>/dev/null + chown ${SSH_FIX_USER}:${SSH_OWNER_GROUP} "${FIX_DIR}/holding/${FILE}" 2>/dev/null fi done for FILE in manage_ssh.sh update_ssh.pl @@ -1675,14 +2046,14 @@ case ${ARG_ACTION} in if [[ -f "${FIX_DIR}/holding/${FILE}" ]] then chmod 770 "${FIX_DIR}/holding/${FILE}" 2>/dev/null && \ - chown root:${SSH_OWNER_GROUP} "${FIX_DIR}/holding/${FILE}" 2>/dev/null + chown ${SSH_FIX_USER}:${SSH_OWNER_GROUP} "${FIX_DIR}/holding/${FILE}" 2>/dev/null fi done # log file if [[ -f "${LOG_FILE}" ]] then chmod 664 "${LOG_FILE}" 2>/dev/null && \ - chown root:${SSH_OWNER_GROUP} "${LOG_FILE}" 2>/dev/null + chown ${SSH_FIX_USER}:${SSH_OWNER_GROUP} "${LOG_FILE}" 2>/dev/null fi # check for SELinux labels case ${OS_NAME} in @@ -1728,40 +2099,41 @@ case ${ARG_ACTION} in then check_root_user && die "must NOT be run as user 'root'" fi - # start SSH agent (if needed) - if (( DO_SSH_AGENT && CAN_START_AGENT )) - then - start_ssh_agent - if (( $? )) - then - die "problem with launching an SSH agent, bailing out" - fi - fi + # derive SSH controls repo from $REMOTE_DIR: # /etc/ssh_controls/holding -> /etc/ssh_controls FIX_DIR="$(print ${REMOTE_DIR%/*})" [[ -z "${FIX_DIR}" ]] && \ die "could not determine SSH controls repo path from \$REMOTE_DIR?" - # build clients list (in array) - cat "${TARGETS_FILE}" | grep -v -E -e '^#' -e '^$' |\ - { - I=0 - set -A CLIENTS - while read LINE - do - CLIENTS[${I}]="${LINE}" - I=$(( I + 1 )) - done - } + + # build clients list + CLIENTS=$(resolve_targets) + if [[ -z "${CLIENTS}" ]] + then + die "no targets to process" + else + log "processing targets: $(print ${CLIENTS} | tr -s '\n' ' ' 2>/dev/null)" + fi + + # start SSH agent (if needed) + if (( DO_SSH_AGENT > 0 && CAN_START_AGENT > 0 )) + then + start_ssh_agent + if (( $? > 0 )) + then + die "problem with launching an SSH agent, bailing out" + fi + fi + # set max updates in background COUNT=${MAX_BACKGROUND_PROCS} - for CLIENT in "${CLIENTS[@]}" + print "${CLIENTS}" | while read -r CLIENT do - if (( DO_SLAVE )) + if (( DO_SLAVE > 0 )) then - fix2slave ${CLIENT} "${FIX_DIR}" & + fix2slave ${CLIENT} "${FIX_DIR}" "${SSH_UPDATE_USER}" & else - fix2host ${CLIENT} "${FIX_DIR}" & + fix2host ${CLIENT} "${FIX_DIR}" "${SSH_UPDATE_USER}" & fi PID=$! log "fixing SSH controls on ${CLIENT} in background [PID=${PID}] ..." @@ -1782,7 +2154,7 @@ case ${ARG_ACTION} in wait_for_children ${PIDS} || \ warn "$? background jobs (possibly) failed to complete correctly" # stop SSH agent if needed - (( DO_SSH_AGENT && CAN_START_AGENT )) && \ + (( DO_SSH_AGENT > 0 && CAN_START_AGENT > 0 )) && \ stop_ssh_agent log "finished applying fixes to the remote SSH control repository" ;; @@ -1827,17 +2199,33 @@ case ${ARG_ACTION} in ;; 10) # gather SSH host keys log "ACTION: gathering SSH host keys ..." - if (( CAN_DISCOVER_KEYS )) + if (( CAN_DISCOVER_KEYS > 0 )) then - cat "${TARGETS_FILE}" | grep -v -E -e '^#' -e '^$' |\ - ${SSH_KEYSCAN_BIN} ${SSH_KEYSCAN_ARGS} -f - + CLIENTS=$(resolve_targets) + if [[ -z "${CLIENTS}" ]] + then + die "no targets to process" + else + log "processing targets: $(print ${CLIENTS} | tr -s '\n' ' ' 2>/dev/null)" + fi + print "${CLIENTS}" | ${SSH_KEYSCAN_BIN} ${SSH_KEYSCAN_ARGS} -f - 2>/dev/null fi log "finished gathering SSH host keys" ;; + 11) # resolve an alias + log "ACTION: resolving alias ${ARG_ALIAS} ..." + RESOLVE_ALIAS=$(resolve_alias "${ARG_ALIAS}" 0) + if (( $? > 0 )) && [[ -z "${RESOLVE_ALIAS}" ]] + then + die "alias ${ARG_ALIAS} did not resolve correctly" + else + log "alias ${ARG_ALIAS} resolves to: $(print ${RESOLVE_ALIAS} | tr -s '\n' ' ' 2>/dev/null)" + fi + log "finished resolving alias" + ;; esac -# finish up work -do_cleanup +exit 0 #****************************************************************************** # END of script diff --git a/update_ssh.pl b/update_ssh.pl index 83d3739..a7bca31 100644 --- a/update_ssh.pl +++ b/update_ssh.pl @@ -41,23 +41,25 @@ use Pod::Usage; #****************************************************************************** # ------------------------- CONFIGURATION starts here ------------------------- -# define the V.R.F (version/release/fix) -my $MY_VRF = "1.2.0"; +# define the version (YYYY-MM-DD) +my $script_version = "2018-11-03"; # 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) my $local_config_file = "update_ssh.conf.local"; +# maxiumum level of recursion for alias resolution +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', +my %selinux_contexts = ( '5' => 'sshd_key_t', + '6' => 'ssh_home_t', '7' => 'ssh_home_t'); -# ------------------------- CONFIGURATION ends here --------------------------- +# ------------------------- 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 (%options, @uname, @pwgetent, @accounts, %aliases, %keys, %access, @blacklist); my ($os, $hostname, $run_dir); -my ($selinux_status, $selinux_context, $linux_version, $has_selinux) = ("","","",0); +my ($selinux_status, $selinux_context, $linux_version, $has_selinux, $recursion_count) = ("","","",0,1); $|++; @@ -67,7 +69,7 @@ $|++; # ----------------------------------------------------------------------------- sub do_log { - + my $message = shift; if ($message =~ /^ERROR:/ || $message =~ /^WARN:/) { @@ -118,7 +120,7 @@ sub parse_config_file { } } } - + return (1); } @@ -132,7 +134,7 @@ sub resolve_aliases foreach $entry (@tmp_array) { if ($entry =~ /^\@/) { ($aliases{$entry}) - ? push (@new_array, @{$aliases{$entry}}) + ? push (@new_array, @{$aliases{$entry}}) : do_log ("WARN: unable to resolve alias $entry [$hostname]"); } else { ($entry) @@ -147,14 +149,14 @@ sub resolve_aliases sub set_file { my ($file, $perm, $uid, $gid) = @_; - - chmod ($perm, "$file") + + chmod ($perm, "$file") 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]") - and exit (1); - + and exit (1); + return (1); } @@ -180,12 +182,12 @@ if ( @ARGV > 0 ) { version|V )) || pod2usage(-verbose => 0); } -pod2usage(-verbose => 0) unless (%options); - +pod2usage(-verbose => 0) unless (%options); + # check version parameter if ($options{'version'}) { $verbose = 1; - do_log ("INFO: $0: version $MY_VRF"); + do_log ("INFO: $0: version $script_version"); exit (0); } # check help parameter @@ -202,7 +204,7 @@ if ($options{'preview'}) { $preview = 1; $verbose = 1; if ($global) { - do_log ("INFO: running in GLOBAL PREVIEW mode"); + do_log ("INFO: running in GLOBAL PREVIEW mode"); } else { do_log ("INFO: running in PREVIEW mode"); } @@ -234,7 +236,7 @@ do_log ("INFO: parsing configuration file(s) ..."); push (@config_files, "$run_dir/$global_config_file") if (-f "$run_dir/$global_config_file"); push (@config_files, "$run_dir/$local_config_file") if (-f "$run_dir/$local_config_file"); unless (@config_files) { - do_log ("ERROR: unable to find any configuration file, bailing out [$hostname]") + do_log ("ERROR: unable to find any configuration file, bailing out [$hostname]") and exit (1); } @@ -249,7 +251,7 @@ unless ($preview and $global) { if (-d $access_dir) { do_log ("INFO: host is under SSH control via $access_dir"); } else { - do_log ("ERROR: host is not under SSH keys only control [$hostname]") + do_log ("ERROR: host is not under SSH keys only control [$hostname]") and exit (1); } } @@ -259,14 +261,14 @@ 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); do_log ("INFO: keys blacklist file found with ".scalar (@blacklist)." entr(y|ies) on $hostname"); print Dumper (\@blacklist) if $debug; } else { - do_log ("WARN: no keys blacklist file found [$hostname]"); + do_log ("WARN: no keys blacklist file found [$hostname]"); } } @@ -276,7 +278,7 @@ $os = $uname[0]; # who am I? unless ($preview and $global) { if ($< != 0) { - do_log ("ERROR: script must be invoked as user 'root' [$hostname]") + do_log ("ERROR: script must be invoked as user 'root' [$hostname]") and exit (1); } } @@ -301,7 +303,7 @@ while (@pwgetent = getpwent()) { push (@accounts, $pwgetent[0]); } -# remove duplicates (which should not happen (!) but local, LDAP and accounts +# remove duplicates (which should not happen (!) but local, LDAP and accounts # from other sources might trample over each other) my %uniq_accounts = map { $_, 0 } @accounts; @accounts = keys %uniq_accounts; @@ -310,7 +312,7 @@ do_log ("INFO: ".scalar (@accounts)." user accounts found on $hostname"); print Dumper (\@accounts) if $debug; # ----------------------------------------------------------------------------- -# read aliases for teams, servers and users +# read aliases for teams, servers and users (and resolve group definitions) # result: %aliases # ----------------------------------------------------------------------------- @@ -321,7 +323,7 @@ open (ALIASES, "<", "${run_dir}/alias") while () { my ($key, $value, @values); - + chomp (); next if (/^$/ || /\#/); s/\s+//g; @@ -334,13 +336,45 @@ close (ALIASES); do_log ("DEBUG: dumping unexpanded aliases:"); print Dumper (\%aliases) if $debug; -# we can nest aliases one level deep, so do a one-level recursive sort of lookup -# of the remaining '@' aliases. Input should be passed as comma-separated -# string to resolve_aliases so don't forget to smash everything back together -# first. -foreach my $key (keys (%aliases)) { - - $aliases{$key} = [resolve_aliases (join (",", @{$aliases{$key}}))]; +# resolve aliases recursively to a maxium of $max_recursion +while ($recursion_count <= $max_recursion) { + # crawl over all items in the hash %aliases + foreach my $key (keys (%aliases)) { + # crawl over all items in the array @{aliases{$key}} + my @new_array; my @filtered_array; # these are the working stashes + do_log ("DEBUG: expanded alias $key before recursion $recursion_count [$hostname]"); + print Dumper (\@{$aliases{$key}}) if $debug; + foreach my $item (@{$aliases{$key}}) { + # is it a group? + if ($item =~ /^\@/) { + # expand the group if it exists + if ($aliases{$item}) { + # add current and new items to the working stash + if (@new_array) { + push (@new_array, @{$aliases{$item}}); + } else { + @new_array = (@{$aliases{$key}}, @{$aliases{$item}}); + } + # remove the original group item from the working stash + @filtered_array = grep { $_ ne $item } @new_array; + @new_array = @filtered_array; + } else { + do_log ("WARN: unable to resolve alias $item [$hostname]"); + } + # no group, just add the item as-is to working stash + } else { + push (@new_array, $item); + } + } + # filter out dupes + my %seen; + @filtered_array = grep { not $seen{$_}++ } @new_array; + # re-assign working stash back to our original hash key + @{$aliases{$key}} = @filtered_array; + do_log ("DEBUG: expanded alias $key after recursion $recursion_count [$hostname]"); + print Dumper (\@{$aliases{$key}}) if $debug; + } + $recursion_count++; } do_log ("INFO: ".scalar (keys (%aliases))." aliases found on $hostname"); @@ -364,18 +398,18 @@ 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 =~ /^\./); push (@key_files, "${run_dir}/keys.d/$key_file"); } - closedir (KEYS_DIR); + closedir (KEYS_DIR); } elsif (-f "${run_dir}/keys") { do_log ("INFO: local 'keys' are stored in a FILE on $hostname"); push (@key_files, "${run_dir}/keys"); } else { - do_log ("ERROR: cannot find any public keys in the repository! [$hostname]") + do_log ("ERROR: cannot find any public keys in the repository! [$hostname]") and exit (1); } @@ -390,7 +424,7 @@ foreach my $key_file (@key_files) { chomp (); next if (/^$/ || /\#/); - + # check for blacklisting my $key_line = $_; if (grep (/\Q${key_line}\E/, @blacklist)) { @@ -412,8 +446,8 @@ print Dumper(\%keys) if $debug; # ----------------------------------------------------------------------------- # read access definitions -# result: %access (hash of arrays). The keys are the accounts for which -# access control has been defined for this server. The values are an array +# result: %access (hash of arrays). The keys are the accounts for which +# access control has been defined for this server. The values are an array # with all the people who can access the account. # ----------------------------------------------------------------------------- @@ -424,7 +458,7 @@ open (ACCESS, "<", "${run_dir}/access") while () { my ($who, $where, $what, @who, @where, @what); - + chomp (); next if (/^$/ || /\#/); s/\s+//g; @@ -437,14 +471,14 @@ while () { do_log ("WARN: ignoring line $. in 'access' due to missing/non-resolving values [$hostname]"); next; } - + foreach my $account (sort (@what)) { - + my @new_array; - + foreach my $server (sort (@where)) { foreach my $person (sort (@who)) { - do_log ("DEBUG: adding access for $account to $person on $server in \%access") + do_log ("DEBUG: adding access for $account to $person on $server in \%access") if ($server eq $hostname); # add person to access list if the entry is for this host push (@new_array, $person) if ($server eq $hostname); @@ -477,7 +511,7 @@ if ($preview && $global) { while () { my ($who, $where, $what, @who, @where, @what); - + chomp (); next if (/^$/ || /\#/); s/\s+//g; @@ -490,14 +524,14 @@ if ($preview && $global) { do_log ("WARN: ignoring line $. in 'access' due to missing/non-resolving values [$hostname]"); next; } - + foreach my $account (sort (@what)) { - + my @new_array; - + foreach my $server (sort (@where)) { foreach my $person (sort (@who)) { - do_log ("$person|$server|$account") + do_log ("$person|$server|$account") } } } @@ -523,7 +557,7 @@ unless ($preview) { if ($selinux_status eq "Permissive" or $selinux_status eq "Enforcing") { do_log ("INFO: runtime info: detected active SELinux system on $hostname"); $has_selinux = 1; - } + } # figure out RHEL version (via lsb_release or /etc/redhat-release) $linux_version = qx#/usr/bin/lsb_release -rs 2>/dev/null | /usr/bin/cut -f1 -d'.'#; chomp ($linux_version); @@ -539,7 +573,7 @@ unless ($preview) { $release_string =~ m/release 6/i && do { $linux_version = 6; last SWITCH_RELEASE; - }; + }; $release_string =~ m/release 7/i && do { $linux_version = 7; last SWITCH_RELEASE; @@ -547,7 +581,7 @@ unless ($preview) { } } # use fall back in case we cannot determine the version - if (not (defined ($linux_version)) or $linux_version eq "") { + if (not (defined ($linux_version)) or $linux_version eq "") { $selinux_context = 'etc_t'; $linux_version = 'unknown'; } else { @@ -557,21 +591,21 @@ unless ($preview) { do_log ("INFO: runtime info: OS major version $linux_version, SELinux context $selinux_context on $hostname"); } else { do_log ("INFO: runtime info: OS major version $linux_version on $hostname"); - } - last SWITCH_OS; + } + last SWITCH_OS; }; } } -# only add authorized_keys for existing accounts, +# only add authorized_keys for existing accounts, # otherwise revoke access if needed foreach my $account (sort (@accounts)) { - + my $access_file = "$access_dir/$account"; - - # only add authorised_keys if there are access definitions + + # only add authorised_keys if there are access definitions if ($access{$account}) { - + unless ($preview) { open (KEYFILE, "+>", $access_file) or do_log ("ERROR: cannot open file for writing in $access_dir [$! $hostname]") @@ -583,11 +617,11 @@ foreach my $account (sort (@accounts)) { # only add authorized_keys if $person actually has a key if (exists ($keys{$person})) { # only add authorized_keys if $person actually has an account - print KEYFILE "$keys{$person}{keytype} $keys{$person}{key} $real_name\n" + print KEYFILE "$keys{$person}{keytype} $keys{$person}{key} $real_name\n" unless $preview; do_log ("INFO: granting access to $account for $real_name on $hostname"); } else { - do_log ("INFO: denying access (no key) to $account for $real_name on $hostname"); + do_log ("INFO: denying access (no key) to $account for $real_name on $hostname"); } } close (KEYFILE) unless $preview; @@ -597,12 +631,12 @@ foreach my $account (sort (@accounts)) { set_file ($access_file, 0644, 0, 0); # selinux labels SWITCH: { - $os eq "Linux" && do { + $os eq "Linux" && do { if ($has_selinux) { system ("/usr/bin/chcon -t $selinux_context $access_file") and do_log ("WARN: failed to set SELinux context $selinux_context on $access_file [$hostname]"); }; - last SWITCH; + last SWITCH; } } } @@ -621,14 +655,14 @@ foreach my $account (sort (@accounts)) { } # ----------------------------------------------------------------------------- -# alert on/remove extraneous authorized_keys files +# alert on/remove extraneous authorized_keys files # (access files for which no longer a valid UNIX account exists) # ----------------------------------------------------------------------------- do_log ("INFO: checking for extraneous access files ...."); opendir (ACCESS_DIR, $access_dir) - or do_log ("ERROR: cannot open directory $access_dir [$! $hostname]") + or do_log ("ERROR: cannot open directory $access_dir [$! $hostname]") and exit (1); while (my $access_file = readdir (ACCESS_DIR)) { next if ($access_file =~ /^\./); @@ -644,7 +678,7 @@ 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]") + or do_log ("ERROR: cannot remove extraneous access file(s) [$! $hostname]") and exit (1); do_log ("INFO: $count extraneous access files removed $hostname"); } @@ -667,20 +701,20 @@ update_ssh.pl - distributes SSH public keys in a desired state model. =head1 SYNOPSIS - update_ssh.pl[-d|--debug] - [-h|--help] + update_ssh.pl[-d|--debug] + [-h|--help] ([-p|--preview] [-g|--global]) | [-r|--remove] [-v|--verbose] [-V|--version] - + =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. -This script should be run on each host where SSH key authentication is the exclusive method of (remote) authentication. +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. -Alternatively key files may be stored as set of individual key files within a called sub-directory called F. +For update 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. =head1 CONFIGURATION @@ -693,7 +727,7 @@ B requires the presence of at least one of the following configur =item * F -=back +=back Use F for localized settings per host. Settings in the localized configuration file will always override other values. @@ -746,12 +780,12 @@ S< >Remove any extraneous 'authorized_keys' files (i.e. belonging to non-e =item -v | --verbose S< >Be verbose during exection. - + =item -V | --version S< >Show version of the script. -=back +=back =head1 NOTES @@ -761,16 +795,8 @@ S< >Show version of the script. =item * Options may be bundled (e.g. -vp) -=back +=back =head1 AUTHOR (c) KUDOS BVBA, Patrick Van der Veken - -=head1 history - -@(#) 2014-12-04: VRF 1.0.0: first version [Patrick Van der Veken] -@(#) 2014-12-16: VRF 1.0.1: added SELinux context, new config option 'selinux_context' [Patrick Van der Veken] -@(#) 2015-08-08: VRF 1.0.2: small fix for 'cut' command [Patrick Van der Veken] -@(#) 2015-08-15: VRF 1.1.0: replace uname/hostname syscalls, now support for FQDN via $use_fqdn, other fixes [Patrick Van der Veken] -@(#) 2015-08-26: VRF 1.2.0: replace read of /etc/passwd by pwgetent() call, small and not so small fixes [Patrick Van der Veken] \ No newline at end of file