Last active
January 9, 2023 05:14
-
-
Save rawiriblundell/51cd64f659be1637e1df17c013479e78 to your computer and use it in GitHub Desktop.
Regenerate your known_hosts file after rotating your keys
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
# Regenerate your known_hosts file after rotating your keys | |
# Generate a list of hosts that we're more likely to care about, divined from our shell history | |
# Adjust filtering in the final 'grep -Ev' to suit your needs | |
# n.b. typos and stale entries will fail in subsequent steps, so don't sweat this too much | |
get_historical_hosts() { | |
grep "^ssh " "${HOME}/.bash_history" | | |
tr " " "\n" | | |
grep -Ev '^ssh$|^-v+|^-[a-zA-Z]$|^.*.pem$|^".*.pem"$|^raw$|^$|^~|^.$' | | |
sed -e 's/.*@//g' -e 's/:.*//g' | | |
awk -F '.' '{ if ($1 ~ /[a-zA-Z]/){print $1} else {print $0} }' | | |
sort | | |
uniq | |
} | |
# Get the ssh fingerprints of a remote host | |
ssh-fingerprint() { | |
local fingerprint keyscanargs | |
fingerprint=$(mktemp) | |
trap 'rm -f "${fingerprint:?}" 2>/dev/null' RETURN | |
# Test if the local host supports ed25519 | |
# Older versions of ssh don't have '-Q' so also likely won't have ed25519 | |
# If you wanted a more portable test: 'man ssh | grep ed25519' might be it | |
ssh -Q key 2>/dev/null | grep -q ed25519 && keyscanargs=( -t "ed25519,rsa,ecdsa" ) | |
# If we have an arg "-a", "--add" or "--append", we add our findings to known_hosts | |
case "${1}" in | |
(-a|--add|--append) | |
shift 1 | |
ssh-keyscan "${keyscanargs[@]}" "${*}" > "${fingerprint}" 2> /dev/null | |
# If the fingerprint file is empty, then quietly fail | |
[[ -s "${fingerprint}" ]] || return 1 | |
# Otherwise, ensure that the fingerprint is added + deduplicated into known_hosts | |
cp "${HOME}"/.ssh/known_hosts{,."$(date +%Y%m%d)"} | |
cat "${fingerprint}" "${HOME}/.ssh/known_hosts.$(date +%Y%m%d)" | | |
sort | | |
uniq > "${HOME}"/.ssh/known_hosts | |
;; | |
(''|-h|--help) | |
printf -- '%s\n' "Usage: ssh-fingerprint (-a|--add|--append) [list of hostnames]" | |
return 1 | |
;; | |
(*) | |
ssh-keyscan "${keyscanargs[@]}" "${*}" > "${fingerprint}" 2> /dev/null | |
[[ -s "${fingerprint}" ]] || return 1 | |
ssh-keygen -l -f "${fingerprint}" | |
;; | |
esac | |
} | |
# Backup our known_hosts file before we start | |
rand_stamp=$(LC_CTYPE=C tr -dc "a-zA-Z0-9" < /dev/urandom | fold -w 8 | head -n 1) | |
cp "${HOME}/.ssh/known_hosts" "${HOME}/.ssh/known_hosts.${rand_stamp}.$(date +%Y%m%d)" | |
# Split known_hosts into hashed and unhashed files | |
grep '|1|' "${HOME}/.ssh/known_hosts" > "${HOME}/.ssh/known_hosts.hashed" | |
grep -v '|1|' "${HOME}/.ssh/known_hosts" | awk '{print $1}' | tr ',' '\n' > "${HOME}/.ssh/known_hosts.unhashed" | |
# If we do have hashed entries to cater for, then try to figure out each entry | |
# by matching to the output of 'get_historical_hosts()' | |
# Could-do/Won't-do: Count hashed matches and compare the linecount of the .hashed file | |
if [[ -s "${HOME}/.ssh/known_hosts.hashed" ]]; then | |
while read -r; do | |
# We can use ssh-keygen to test if a hostname exists in known_hosts, whether hashed or not | |
# ... And you now might want to reconsider any affection for the HashedKnownHosts directive... | |
if ssh-keygen -F "${REPLY}" >/dev/null 2>&1; then | |
# Test if the host isn't already unhashed, if not, append it to the list | |
if ! grep -q "${REPLY}" "${HOME}/.ssh/known_hosts.unhashed"; then | |
printf -- '%s\n' "${REPLY}" >> "${HOME}/.ssh/known_hosts.unhashed" | |
fi | |
fi | |
done < <(get_historical_hosts) | |
fi | |
# Now we smoosh the unhashed file and the output of 'get_historical_hosts()' together | |
cat "${HOME}/.ssh/known_hosts.unhashed" <(get_historical_hosts) | sort | uniq > "${HOME}/.ssh/known_hosts.sorted" | |
# Truncate our known_hosts file, bye bye old friend! | |
:> "${HOME}/.ssh/known_hosts" | |
# Now work our way through our sorted list of targets and rebuild the known_hosts file | |
while read -r target_host; do | |
printf -- '======> %s\n' "Processing ${target_host}..." | |
ssh-fingerprint --add "${target_host}" || printf -- '%s\n' "${target_host}" >> "${HOME}/.ssh/failed_fingerprinting" | |
done < "${HOME}/.ssh/known_hosts.sorted" | |
# Next pass, we work through our failed fingerprint attempts | |
if [[ -s "${HOME}/.ssh/failed_fingerprinting" ]]; then | |
printf -- '\n======> %s\n\n' "Now we're going to work through our list of failures from the previous step, this can be time-exhaustive" | |
while read -r; do | |
if ! ssh -n -o ConnectTimeout=3 -o BatchMode=yes -o StrictHostKeyChecking=accept-new "${REPLY}" true; then | |
printf -- '======> %s\n' "${REPLY} Unable to connect, please intervene manually" >&2 | |
fi | |
grep -q -- "^${REPLY}" "${HOME}/.ssh/known_hosts" && printf -- '======> %s\n' "${REPLY} added to known_hosts" | |
done < "${HOME}/.ssh/failed_fingerprinting" | |
fi | |
# Leave a note to cleanup - we don't do this programmatically because some of the generated files may be useful for any manual follow up | |
printf -- '\n======> %s\n\n' "Processing complete. Don't forget to cleanup this directory when you're done" | |
ls -1 "${HOME}/.ssh" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment