#!/usr/bin/env bash # Rotate and deploy admin keys # base folder for all data BASE_FOLDER=$(dirname "$(readlink -f "$0")")"/"; # config folder CONFIG_BASE="${BASE_FOLDER}../config/"; # timestamps of last rotate per user/host LAST_ROTATE="${BASE_FOLDER}../last-rotate/"; # ssh-keys temp holder SSH_PRIVATE_KEYS="${BASE_FOLDER}../ssh-keys/"; # ssh public keys from current and last SSH_PUBLIC_KEYS_PREVIOUS="${BASE_FOLDER}../ssh-public-keys/admin-previous/"; SSH_PUBLIC_KEYS_CURRENT="${BASE_FOLDER}../ssh-public-keys/admin-current/"; # list of admin user names, if username does not match this only update the user entry ADMIN_USERS=(admin ubuntu ec2-user) DRY_RUN=0; FORCE=0; FORCE_CREATE=0; GO=0; HOST_ONLY=""; USER_ONLY=""; # while getopts ":h:u:nfcg" opt; do case "${opt}" in h) # hostname HOST_ONLY="${OPTARG}"; ;; u) # username USER_ONLY="${OPTARG}"; ;; n) # dry-run DRY_RUN=1; ;; f) # force FORCE=1; ;; c) # force-create FORCE_CREATE=1 ;; g) # go GO=1; ;; \?) echo -e "\n Option does not exist: ${OPTARG}\n"; echo "-h override single host name"; echo "-u override user name for a host"; echo "-f force key change"; echo "-c force create new key even if old key exists"; echo "-n dry run"; echo "-g flag for actual change call"; echo "" exit 1; ;; esac done if [ ! -d "${SSH_PRIVATE_KEYS}" ]; then echo "Missing ssh private keys folder: ${SSH_PRIVATE_KEYS}"; exit; fi if [ ! -d "${SSH_PUBLIC_KEYS_CURRENT}" ]; then echo "Missing ssh public keys current folder: ${SSH_PUBLIC_KEYS_CURRENT}"; exit; fi if [ ! -d "${SSH_PUBLIC_KEYS_PREVIOUS}" ]; then echo "Missing ssh public keys previous folder: ${SSH_PUBLIC_KEYS_PREVIOUS}"; exit; fi # load config if [ ! -f "${CONFIG_BASE}settings.ini" ]; then echo "Missing 'settings.ini' file in ${CONFIG_BASE}"; exit; fi # shellcheck source=../config/settings.ini # shellcheck disable=SC1094 source <(grep "=" "${CONFIG_BASE}settings.ini" | sed 's/ *= */=/g') if [ -z "${key_age}" ]; then echo "A minimnum key age in days must be set in the settings"; exit; fi if [ -z "${server_list}" ]; then echo "No server list is defined in the settings"; exit; fi; # we must have "server_list" set and file must be in config folder if [ ! -f "${CONFIG_BASE}${server_list}" ]; then echo "Cannot find ${server_list} file in the config folder"; exit; fi # If the path is under "" then the ~ replacement is needed # PEM folder for server __PEM_SERVER="${server_pem_folder}"; if [ -z "${server_pem_folder##~\/*}" ]; then server_pem_folder="${server_pem_folder/~\//"${HOME}"\/}"; fi; PEM_SERVER="${server_pem_folder}/"; if [ ! -d "${PEM_SERVER}" ]; then echo "Cannot found PEM server key folder: ${PEM_SERVER}"; exit; fi # PEM archive folder __PEM_ARCHIVE="${server_pem_archive_folder}/$(date +%F)/"; if [ -z "${server_pem_archive_folder##~\/*}" ]; then server_pem_archive_folder="${server_pem_archive_folder/~\//"${HOME}"\/}"; fi PEM_ARCHIVE="${server_pem_archive_folder}"; if [ ! -d "${PEM_ARCHIVE}" ]; then echo "Cannot found PEM server key archive folder: ${PEM_ARCHIVE}"; exit; fi # add todays date PEM_ARCHIVE="${PEM_ARCHIVE}/$(date +%F)/" # abort if go not set if [ ${GO} -eq 0 ] && [ ${DRY_RUN} -eq 1 ]; then GO=1; elif [ ${GO} -eq 0 ]; then echo "No -g (go) parameter set. aborting. For testing set -n for dry run" exit; fi # default ssh command # -t is needed for systens when "Defaults requiretty" is set SSH="ssh -a -x -n"; # Add the SSH Key to an auth file if it does not exist yet and the auth file does exist # Build bash command to run this # @Params # AUTH_KEY_FILE {1}: the auth key file where to add the key # PUB_KEY_FILE {2}: Public key file name add_ssh_key() { AUTH_KEY_FILE="${1}"; PUB_KEY_FILE="${2}"; AUTH_KEY_SETTINGS="${3}"; RMV_CHATTR_I="chattr -i" ADD_CHATTR_I="chattr +i" RMV_CHMOD_UW="chmod u-w" ADD_CHMOD_UW="chmod u+w" # check if the auth file exists and the key is not yet in the auth file # the -z `tail ...` checks for a trailing newline. The echo adds one if was missing (from ssh-copy-id) # PROBLEM: # for grep from pipe, the left data is removed. we also can't cat from pipe # into a var as that would go through a pipe and not be visible # so we get the pub key file name and read it here pub_key=$(cat "${PUB_KEY_FILE}"); # if we have auth key settings, prefix them to the pub key # Note that the check key "pub_key" ignores any prefixes, but we add with settings prefix if [ -n "${AUTH_KEY_SETTINGS}" ]; then pub_key_write="${AUTH_KEY_SETTINGS} ${pub_key}"; else pub_key_write="${pub_key}"; fi INSTALLKEYS_SH=$(tr '\t\n' ' ' <<-EOF if [ -f "${AUTH_KEY_FILE}" ] && ! grep "${pub_key}" "${AUTH_KEY_FILE}" >> /dev/null; then ${RMV_CHATTR_I} "${AUTH_KEY_FILE}"; ${ADD_CHMOD_UW} "${AUTH_KEY_FILE}"; { [ -z \`tail -1c ${AUTH_KEY_FILE} 2>/dev/null\` ] || echo >> "${AUTH_KEY_FILE}" || exit 1; } && echo "${pub_key_write}" >> "${AUTH_KEY_FILE}" || exit 1; ${RMV_CHMOD_UW} "${AUTH_KEY_FILE}"; ${ADD_CHATTR_I} "${AUTH_KEY_FILE}"; else echo "[!] Already added"; fi; EOF ); # to defend against quirky remote shells: use 'exec sh -c' to get POSIX; printf "exec sudo sh -c '%s'" "${INSTALLKEYS_SH}" } # install call # @Params # HOSTNAME {1} hostname to access # USERNAME {2} username to use # PUB_KEY_FILE {3} public key file to add # AUTH_KEY_FILE {4} auth key file where to add the public key install_ssh_key() { HOSTNAME="${1}"; USERNAME="${2}"; PUB_KEY_FILE="${3}"; AUTH_KEY_FILE="${4}"; AUTH_KEY_SETTINGS="${5}"; echo "[.] Add to auth file: ${AUTH_KEY_FILE}"; if [ ${DRY_RUN} -eq 0 ]; then ${SSH} "${USERNAME}"@"${HOSTNAME}" "$(add_ssh_key "${AUTH_KEY_FILE}" "${PUB_KEY_FILE}" "${AUTH_KEY_SETTINGS}")" else echo "${SSH} \"${USERNAME}\"@\"${HOSTNAME}\" \"\$(add_ssh_key \"${AUTH_KEY_FILE}\" \"${PUB_KEY_FILE}\" \"${AUTH_KEY_SETTINGS}\")\""; fi } while read -r line; do if [[ "${line}" =~ ^\# ]]; then continue; fi # hostname is on pos 1 hostname=$(echo "${line}" | cut -d "," -f 1); # if hostname opt set and not matching skip if [ -n "${HOST_ONLY}" ] && [ "${HOST_ONLY}" != "${hostname}" ]; then continue; fi # login user name username=$(echo "${line}" | cut -d "," -f 2); # if username opt set and not matching skip if [ -n "${USER_ONLY}" ] && [ "${USER_ONLY}" != "${username}" ]; then continue; fi # check if force or if last rotaet in valid range if [ "${FORCE}" -eq 0 ] && [ -f "${LAST_ROTATE}${hostname}_${username}.last-rotate" ]; then # holds unix timestamp, if now - this timestamp is < key_age => skip last_rotate=$(cat "${LAST_ROTATE}${hostname}_${username}.last-rotate"); current_timestamp=$(date +%s) age=$(( current_timestamp - last_rotate )) days_left=$(( (age)/(3600*24) )) if [ $days_left -le "$key_age" ]; then echo "[!] Last rotate for ${username}@${hostname} was ${days_left} days ago, minimum is ${key_age}"; echo "[_] ............... SKIP"; continue; fi fi # flags: (not used at the moment) # Possible: U (add to .ssh/authorized_keys) flags=$(echo "${line}" | cut -d "," -f 3); # auth key settings (in front of auth key) auth_key_settings=$(echo "${line}" | cut -d "," -f 4); # name for the SSH key files SSH_KEY_FILE="${hostname}_${username}.pem"; SSH_KEY_PUB_FILE="${hostname}_${username}.pem.pub"; # if current exist, skip creation # if pem or pub missing, but not both, alert and skip # else create new CREATE_NEW_KEY=0; # if we have force, override this all if [ ${FORCE_CREATE} -eq 1 ]; then CREATE_NEW_KEY=1; elif [ -f "${SSH_PRIVATE_KEYS}${SSH_KEY_FILE}" ] || [ -f "${SSH_PUBLIC_KEYS_CURRENT}${SSH_KEY_PUB_FILE}" ]; then # if we miss private key -> alert skip if [ ! -f "${SSH_PRIVATE_KEYS}${SSH_KEY_FILE}" ]; then # extract public key from ${PEM_SERVER}${SSH_KEY_FILE} and check if same to public key if [ ! -f "${PEM_SERVER}${SSH_KEY_FILE}" ]; then echo "[!] There are no master pem file to extract public key from for ${username}@${hostname}"; echo "[_] ............... SKIP"; continue; else __COMP_PUB_KEY=$(ssh-keygen -y -f "${PEM_SERVER}${SSH_KEY_FILE}"); __CURRENT_PUB_KEY=$(cat "${SSH_PUBLIC_KEYS_CURRENT}${SSH_KEY_PUB_FILE}"); if [ "${__COMP_PUB_KEY}" != "${__CURRENT_PUB_KEY}" ]; then echo "[!] Current PEM public key does not match existing for ${username}@${hostname}"; echo "[!] Current Public: ${__CURRENT_PUB_KEY}"; echo "[!] Master Public : ${__COMP_PUB_KEY}"; echo "[_] ............... SKIP"; continue; fi fi fi else CREATE_NEW_KEY=1 fi # create name NEW_KEY_CREATED=0; if [ ${CREATE_NEW_KEY} -eq 1 ]; then echo "[+] Create new key for: ${username}@${hostname} with flags '${flags}' as: ${SSH_KEY_PUB_FILE}"; # previous still exists? alert and abort if [ -f "${SSH_PUBLIC_KEYS_PREVIOUS}${SSH_KEY_PUB_FILE}" ]; then echo "[!] Previous public key still exists, was the remote key removed for ${username}@${hostname}"; continue; fi # Move all current to last if [ -f "${SSH_PUBLIC_KEYS_CURRENT}${SSH_KEY_PUB_FILE}" ]; then echo "[>] Move current public key to the previous folder"; if [ ${DRY_RUN} -eq 0 ]; then mv "${SSH_PUBLIC_KEYS_CURRENT}${SSH_KEY_PUB_FILE}" "${SSH_PUBLIC_KEYS_PREVIOUS}"; else echo "mv \"${SSH_PUBLIC_KEYS_CURRENT}${SSH_KEY_PUB_FILE}\" \"${SSH_PUBLIC_KEYS_PREVIOUS}\";"; fi fi # only create if not dry run if [ ${DRY_RUN} -eq 0 ]; then # <<< $'\ny' ssh-keygen -q -t ed25519 -N "" -C "${username}@${hostname}: $(date +%F)" -f "${SSH_PRIVATE_KEYS}${SSH_KEY_FILE}" # move the public key to the current folder mv "${SSH_PRIVATE_KEYS}${SSH_KEY_PUB_FILE}" "${SSH_PUBLIC_KEYS_CURRENT}"; # flag new key creation for move else echo "ssh-keygen -q -t ed25519 -N \"\" -C \"${username}@${hostname}: $(date +%F)\" -f \"${SSH_PRIVATE_KEYS}${SSH_KEY_FILE}\";"; echo "mv \"${SSH_PRIVATE_KEYS}${SSH_KEY_PUB_FILE}\" \"${SSH_PUBLIC_KEYS_CURRENT}\";"; fi NEW_KEY_CREATED=1; else echo "[~] Deploy current key for: ${username}@${hostname} with flags '${flags}': ${SSH_KEY_PUB_FILE}"; fi # deploy public key to server if [[ ${ADMIN_USERS[*]} =~ $username ]]; then # - master admin file install_ssh_key "${hostname}" "${username}" "${SSH_PUBLIC_KEYS_CURRENT}${SSH_KEY_PUB_FILE}" "/etc/ssh/authorized_keys--master" "${auth_key_settings}"; fi # - admin ssh config auth file install_ssh_key "${hostname}" "${username}" "${SSH_PUBLIC_KEYS_CURRENT}${SSH_KEY_PUB_FILE}" "/etc/ssh/authorized_keys/${username}" "${auth_key_settings}"; if [ ${NEW_KEY_CREATED} -eq 1 ]; then # - copy local PEM file to archive folder if [ -f "${PEM_SERVER}${SSH_KEY_FILE}" ]; then # create new archive folder local, one time action if [ ! -d "${PEM_ARCHIVE}" ]; then echo "[+] Create ${PEM_ARCHIVE}": if [ ${DRY_RUN} -eq 0 ]; then mkdir -p "${PEM_ARCHIVE}"; else echo "mkdir -p \"${PEM_ARCHIVE}\";"; fi fi echo "[>] Move old PEM key to archive folder: ${__PEM_ARCHIVE}"; if [ ${DRY_RUN} -eq 0 ]; then cp "${PEM_SERVER}${SSH_KEY_FILE}" "${PEM_ARCHIVE}"; else echo "cp \"${PEM_SERVER}${SSH_KEY_FILE}\" \"${PEM_ARCHIVE}\";"; fi fi echo "[>] Move PEM key '${SSH_KEY_FILE}' to .ssh folder: ${__PEM_SERVER}"; if [ ${DRY_RUN} -eq 0 ]; then # - copy to local ssh folder mv "${SSH_PRIVATE_KEYS}${SSH_KEY_FILE}" "${PEM_SERVER}"; else echo "mv \"${SSH_PRIVATE_KEYS}${SSH_KEY_FILE}\" \"${PEM_SERVER}\";"; fi fi # post roate write timestamp into rotate file if [ ${DRY_RUN} -eq 0 ]; then "$(date +%s)" > "${LAST_ROTATE}${hostname}_${username}.last-rotate"; else echo "\"$(date +%s) > \"${LAST_ROTATE}${hostname}_${username}.last-rotate\";"; fi echo "[=] ............... DONE"; done <<<"$(sed 1d "${CONFIG_BASE}${server_list}")"; # __END__