Skip to content

Instantly share code, notes, and snippets.

@rawiriblundell
Last active January 9, 2023 05:14
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rawiriblundell/51cd64f659be1637e1df17c013479e78 to your computer and use it in GitHub Desktop.
Save rawiriblundell/51cd64f659be1637e1df17c013479e78 to your computer and use it in GitHub Desktop.
Regenerate your known_hosts file after rotating your keys
#!/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