diff --git a/README.md b/README.md
index e778bb4..0243c31 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,14 @@

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