* 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)
This commit is contained in:
Patrick Van der Veken 2020-12-30 17:06:02 +01:00
parent b8004afe62
commit 77a332e324
4 changed files with 321 additions and 157 deletions

View File

@ -1,5 +1,14 @@
<p align="center"><img src="logo.png" alt="SSH Controls Logo"></p>
## 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.

View File

@ -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 <<EOT
sftp ${SFTP_ARGS} "${SSH_TRANSFER_USER}@${TRANSFER_HOST}" >/dev/null <<EOT
cd ${REMOTE_DIR}
put ${SOURCE_FILE}
chmod ${TRANSFER_PERMS} ${SOURCE_FILE}
EOT
SFTP_RC=$?
else
sftp ${SFTP_ARGS} ${SSH_TRANSFER_USER}@${TRANSFER_HOST} >/dev/null <<EOT
sftp ${SFTP_ARGS} "${SSH_TRANSFER_USER}@${TRANSFER_HOST}" >/dev/null <<EOT
cd ${REMOTE_DIR}
put ${SOURCE_FILE}
EOT
SFTP_RC=$?
fi
cd ${OLD_PWD} || return 1
cd "${OLD_PWD}" || return 1
return ${SFTP_RC}
}
@ -1310,6 +1333,7 @@ else
return 1
else
log "SSH agent started on ${HOST_NAME}:"
# shellcheck disable=SC2086
log "$(ps -fp ${SSH_AGENT_PID})"
fi
fi
@ -1338,7 +1362,9 @@ if [[ -n "${SSH_AGENT_PID}" ]]
then
# SIGTERM
log "stopping (TERM) process on ${HOST_NAME} with PID: ${SSH_AGENT_PID}"
# shellcheck disable=SC2086
log "$(ps -fp ${SSH_AGENT_PID})"
# shellcheck disable=SC2086
kill -s TERM ${SSH_AGENT_PID}
sleep 3
@ -1346,7 +1372,9 @@ then
if (( $(pgrep -u "${USER}" ssh-agent | grep -c "${SSH_AGENT_PID}") ))
then
log "stopping (KILL) process on ${HOST_NAME} with PID: ${SSH_AGENT_PID}"
# shellcheck disable=SC2086
log "$(ps -fp ${SSH_AGENT_PID})"
# shellcheck disable=SC2086
kill -s kill ${SSH_AGENT_PID}
fi
sleep 3
@ -1375,8 +1403,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"
@ -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"

View File

@ -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

View File

@ -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 (<CONF_FD>) {
@ -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 = <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 (<ALIASES>) {
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 (<KEYS>) {
@ -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 (<ACCESS>) {
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 (<ACCESS>) {
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<update_ssh.pl> distributes SSH keys to the appropriate files (.e. 'authorized_keys') into the C<$access_dir> repository based on the F<access>, F<alias> and F<keys> files.
Alternatively B<update_ssh.pl> can distribute public keys to the location specified in the AuthorizedkeysFile setting of F<sshd_config> (allowing public keys to be distributed
to the traditional location in a user's HOME directory). See C<key_location> setting in F<update_ssh.conf[.local]>for 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<keys> file within the same directory as B<update_ssh.pl> script.
Orginally SSH public keys must be stored in a generic F<keys> file within the same directory as B<update_ssh.pl> script.
Alternatively key files may be stored as set of individual key files within a called sub-directory called F<keys.d>.
Both methods are mutually exclusive and the latter always take precedence.
@ -739,6 +827,8 @@ Following settings must be configured:
=item * B<access_dir> : target directory for allowed SSH public key files
=item * B<key_location> : whether or not to use AuthorizedkeysFile setting in sshd_config for overriding $access_dir
=item * B<blacklist_file> : 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<keys> 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<keys> file(s) during SSH controls updates, an alert will be shown on STDOUT and the key will be ignored for the rest.
Examples: