341 lines
12 KiB
Bash
Executable File
341 lines
12 KiB
Bash
Executable File
#!/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__
|