Tuesday, January 30, 2018
SSH public key distribution and management
SSH public key distribution and management
Recently I was working on a script which enables the distribution and management of public SSH keys to remote machines. If you need to distribute lots of keys or to delete lost of keys, this automation tool would be interesting for you.
This is the man page, which describes everything about this script:
NAME
sshkeys - add and remove public keys from remote servers
SYNOPSIS
sshkeys <-u username> <-h hostname> <-k keypath> (OPTION)
DESCRIPTION
This page documents the script sshkeys, a ssh public keys management tool designed to add, remove and check public keys on a remote server(s).
The script should be used from server with bash, as it is written in bash and uses advanced tools like awk and sed for key editing. Running the
script requires to specify the mandatory arguments: username, hostname and keypath. After these arguments you can choose one option at a time
according to operation youd like to perform.
Managing public keys according to the options.
-u username
here you can specify username to login with on a remote machine.
-h hostname
enter hostname or several hostnames separated with ,. You can also specify a file containing hostnames list.
-k keypath
enter the absolute path to the directory containing the public keys.
OPTION
choose one option according to what do you like to do. Known options are:
-i Info mode. This option doesnt require additional parameter. In this mode the proper authorized key file is found and compared with known
keys stored in the keypath. At the end you can continue with other operations:
[a]dd - add keys to the file, you can enter keys youd like to add to the file separated with or ,.
[r]emove - remove lines from the file, you can enter line numbers which will be removed.
[b]backup restore
- restore the backup in case something goes wrong.
[e]xit - exit the script. This will delete temporary files and end the script. This option is used by default.
[c]ancel - exit the script without removing any temporary files. Use this in case you like to preserved temporary files for
further usage.
-a keys
Add keys. This option takes as a parameter key file names separated with ,. These keys have to be stored in the keypath directory.
Specified keys will be added to the authorized keys file on remote servers.
-r keys
Remove keys. This option takes as a parameter key file names separated with ,. These keys have to be stored in the keypath directory.
Specified keys will be removed from the authorized keys file on remote servers.
EXAMPLES
sshkeys -u user -h host1.com,host2.com -k /here/are/keys/ -i
get information from the two hosts specified
sshkeys -u user -h host1.com,host2.com -k /here/are/keys/ -a user1.pub,user2.pub
add public keys user1.pub and user2.pub to specified hosts
sshkeys -u user -h /file/with/hosts.txt -k /here/are/keys/ -r user1.pub,user2.pub
remove public keys user1.pub and user2.pub from hosts specified in the file hosts.txt
AUTHOR
Written by KrisKo.
- the public keys in the pubkeys folder should be in SECSH Public Key File Format.
The script:
#!/bin/bash
#########################################################################
# Version 1.1
#
# Description:
# script for ssh public key management on remote machines allowing to
# add and remove keys
#########################################################################
#set the temp folder
TMP=/tmp;
#add description to comment e.g. # <keyname>.pub <your desription>
description="myDescription"
### usage()
# Display complete help and usage of this script
###
usage(){
echo "USAGE: $0 -u <username> -h <hostnames> -k <keypath> (-i | -a <keys> | -r <keys>)";
echo "Add and remove public keys from remote servers."
echo -e " -u -h -k these option are mandatory, they need to be specified"
echo -e "-h <hostname> you can specify multiple hostnames separated with "," or a filename containing hostnames"
echo -e "-k <keypath> specify directory where the keys are stored"
echo -e "-i -a -r you can use only one option at the same time"
echo -e "-i enter info mode"
echo -e "-a <keys> add keys to the specified server, enter key filenames separated with ",""
echo -e "-r <keys> remove keys from the specified server, enter key filenames separated with ",""
echo -e " Note thet this script was tested on RHEL and SLES linux. It does not work on Solaris!"
exit 0
}
### findfile()
# Connect to the host, find the correct file
###
findfile(){
#find all standard files on the remote host
SSHOUT="`ssh $USERNAME@$HOST "
if [ -f ~/.ssh/authorized_keys ];then
cd ~/.ssh;
echo ~/.ssh,authorized_keys
fi
if [ -f ~/.ssh2/authorization ]; then
cd ~/.ssh2;
echo ~/.ssh2,authorization
fi
if [ -f ~/.ssh2/authorized_keys ];then
cd ~/.ssh2;
echo ~/.ssh2,authorized_keys
fi
"`"
#if ssh connection failed, end this script
if [ $? == 255 ]; then
echo ERROR: Failed to connect to remote host.
exit 255
fi
LINE="`echo $SSHOUT | tr " " " " | grep -c ""`"
#notify if no standard file is found
if [ -z $SSHOUT ]; then
echo "ERROR: No standard file found!"
echo "Would you like to create standard file "~/.ssh/authorized_keys"? [y]"
read ans
if [ "x$ans" == "x" ] || [ "$ans" == "y" ]; then
echo "Creating file..."
#ssh $USERNAME@$HOST "touch ~/.ssh/authorized_keys"
SSHOUT="~/.ssh,authorized_keys_default"
else
echo "Quitting, you can create proper file on the remote host manually."
exit 1;
fi
#show found file (if there are more than one, let the user choose one)
elif [ $LINE -gt 1 ]; then
echo -e Found file:
echo $SSHOUT | tr " " " " | grep -n "";
echo "Choose line number for file to edit:"; read LINE;
SSHOUT="`echo $SSHOUT | tr " " " " | awk NR==$LINE`"
fi
echo -e INFO: Using file: $SSHOUT | tr "," "/"
}
### getfile()
# Download the file and store it in $TMPAUTH
###
getfile(){
#parse $SSHOUT given as the parameter $1 to file path and name
FPATH="`echo $1 | cut -d "," -f 1`"
FNAME="`echo $1 | cut -d "," -f 2`"
#check if the temporary file exists and warn
if [ -f $TMP/$FNAME.tmp ]; then
echo "WARNING: file $TMP/$FNAME.tmp exists, it will be overwritten!"
fi
if [ "$FNAME" == "authorized_keys_default" ]; then
FNAME="authorized_keys"
>$TMP/$FNAME.tmp
else
#cat the remote file into the local temp file
echo Downloading the file $FNAME.
ssh $USERNAME@$HOST "
cat $FPATH/$FNAME;
" > $TMP/$FNAME.tmp
fi
}
### writedirect()
# Write the prepared temp file (and keys) to the remote server and create backup
###
writedirect(){
echo -e The file "$FPATH/$FNAME" will be written to the server...;
#create backup of the existing file on remote host and cat the prepared temp file to the remote host and set proper permissions
cat $TMP/$FNAME.tmp | ssh $USERNAME@$HOST "
echo "INFO: Creating backupfile";
mkdir $FPATH 2>/dev/null;
mv $FPATH/$FNAME $FPATH/$FNAME.bak 2>/dev/null || echo "INFO: No file to backup.";
echo "INFO: Writing to $FPATH/$FNAME";
dd conv=notrunc > $FPATH/$FNAME;
chmod 600 $FPATH/$FNAME;"
#if we work with authorization file, copy/remove also the necessary keys
if [ "$FNAME" == "authorization" ]; then
if [ "$1" == "add" ]; then
echo -e Copying keys...
cat $TMP/$FNAME-tmp.tar | ssh $USERNAME@$HOST "cd $FPATH; tar xvf -"
elif [ "$1" == "rem" ]; then
echo -e Removing keys...
ssh $USERNAME@$HOST "cd $FPATH; rm $REMKEYS"
fi
fi
echo DONE.
#remove local temporary files
echo -e INFO: Removing temporary files.
if [ -f $TMP/$FNAME.tmp ]; then
rm $TMP/$FNAME.tmp;
echo $TMP/$FNAME.tmp;
fi;
if [ -f $TMP/$FNAME-tmp.tar ]; then
rm $TMP/$FNAME-tmp.tar;
echo $TMP/$FNAME-tmp.tar;
fi
echo "# # # DONE # # #";
}
### restore()
# Restore backup file
###
restore(){
#warn that keys are not restored by authorization file
if [ "$FNAME" == "authorization" ]; then
echo WARNING: restoring only file "autorization"! The keys are not being restored.
fi
#move existing file to .old and copy backup to the original file
ssh $USERNAME@$HOST "
if [ -f "$FPATH/$FNAME.bak" ]; then
echo "INFO: Found backupfile, restoring...";
mv $FPATH/$FNAME $FPATH/$FNAME.old;
echo "Created "$FNAME.old" from the original...";
cp $FPATH/$FNAME.bak $FPATH/$FNAME;
echo "Backup restored...";
chmod 600 $FPATH/$FNAME;
echo DONE!;
else echo "ERROR: Backup file not found.";
fi"
exit 0
}
### getkey()
# Create proper form of .pub key file
###
getkey(){
#when theres no newline at the end of a pub key file, add one
test "`tail -c 2 "$KEYPATH/$key"`" && echo "" >> "$KEYPATH/$key"
#convert the key to proper form and test if succesfull
SSHKEY="`ssh-keygen -i -f "$KEYPATH/$key"`";
if [ ! "$SSHKEY" ]; then
echo -n "INFO: "
#in case the test is not succesfull convert the key to unix format
dos2unix "$KEYPATH/$key"
fi
SSHKEY="`ssh-keygen -i -f "$KEYPATH/$key"`"
#if the conversion fails, show error message and continue
if [ ! "$SSHKEY" ]; then
echo -e ERROR: Conversion of "$key" failed!
SSHKEY="ERROR: Conversion failed"
fi
}
### adddirect()
# Adding directly to the remote server
###
adddirect(){
#$1=specified keys, $2=parameter direct/info according from where this function is called
for HOST in $( echo $HOST | sed -e s/,/ /g ); do
echo -e " INFO: Connecting to host $HOST"
#download the file if the function is called directly, otherwise the file is already downloaded
if [ "$2" == "direct" ]; then
#get the file (this will rewrite the existing file)
findfile;
getfile $SSHOUT;
fi
#prepare temporary file (and keys)
for key in $( echo $1 | sed -e s/,/ /g); do
if [ -f $KEYPATH/$key ]; then echo adding key: $key;
if [ "$FNAME" == "authorized_keys" ]; then
#create proper key form: ssh-rsa ABCKEY...
getkey $key
#test if the key is not already in the file (double entries check)
if [ "`cat $TMP/$FNAME.tmp | grep -n "" | grep --color "$SSHKEY"`" ]; then
echo WARNING: The key "$key" is already in the file! Skipping entry...
elif [ "`echo $SSHKEY | grep ERROR`" ]; then
echo WARNING: The key "$key" is not properly formatted or is corrupted! Skipping entry...
else
echo "# $key $description" >> $TMP/$FNAME.tmp;
echo "$SSHKEY" >> $TMP/$FNAME.tmp
fi
elif [ "$FNAME" == "authorization" ]; then
#before the pub key file will be tar-red, check the format (this will not skip the failed keys, they will be uploaded with others)
getkey $key
echo -e "Key $key" >> $TMP/$FNAME.tmp;
# tar keys and prepare to upload them
tar -r -C $KEYPATH -f $TMP/$FNAME-tmp.tar $key;
fi;
else echo "The key "$key" doesnt exist."
fi
done
writedirect "add";
done
exit 0
}
### removedirect()
# Direct removal of specified keys
###
removedirect(){
for HOST in $( echo $HOST | sed -e s/,/ /g ); do
echo -e " INFO: Connecting to host $HOST"
findfile;
#download the auth. file
getfile $SSHOUT;
#remove requested lines accorind to specified keys
lines=;
for key in $( echo $1 | sed -e s/,/ /g); do
echo "";
if [ -f $KEYPATH/$key ]; then
if [ "$FNAME" == "authorized_keys" ]; then
#prepare the pub key file to proper form
getkey $key
#find match and store line numbers to the LINENR
LINENR="`cat $TMP/$FNAME.tmp | grep -n "" | grep --color "$SSHKEY" | cut -d ":" -f 1`"
if [ "$LINENR" != "" ]; then
#try if the previous line is a comment
COMMENT="`cat $TMP/$FNAME.tmp | awk NR==$(($LINENR-1))`"
if [[ "$COMMENT" == #* ]]; then
lines="$lines"" $LINENR $(($LINENR-1))"
echo "$key" match:
cat $TMP/$FNAME.tmp | awk NR==$(($LINENR-1))
cat $TMP/$FNAME.tmp | awk NR==$LINENR
else
lines="$lines"" $LINENR"
echo "$key" match (without comment line):
cat $TMP/$FNAME.tmp | awk NR==$LINENR
fi
else echo No match for key "$key".;
fi
elif [ "$FNAME" == "authorization" ]; then
if [ "`cat $TMP/$FNAME.tmp | grep -n "" | grep --color "$key"`" != "" ]; then
echo "$key" match:
cat $TMP/$FNAME.tmp | grep -n "" | sed -e s/:/: / | grep --color "$key"
lines="$lines"" `cat $TMP/$FNAME.tmp | grep -n "" | grep --color "$key" | cut -d ":" -f 1`"
REMKEYS="`echo $REMKEYS $key`";
else echo No match for key "$key".;
fi
fi
else echo "The key "$key" doesnt exist."
fi
done
#if some matching keys were found
if [ ! -z "$lines" ]; then
SORTED=`echo $lines | tr " " " " | sort -nr | uniq`;
echo -e INFO: These lines will be removed: $SORTED
#additional info when authorization file is used
if [ "$FNAME" == "authorization" ]; then
echo INFO: These keys will be removed: $REMKEYS
fi
#remove specified/selected lines
echo "";
for line in $SORTED; do
echo Removing: $line: `awk NR==$line $TMP/$FNAME.tmp` ;
sed -i "$line"d $TMP/$FNAME.tmp;
done
#write to the file
writedirect "rem";
else
echo -e WARNING: No matching key found...;
echo INFO: Removing $TMP/$FNAME.tmp
rm $TMP/$FNAME.tmp
echo "# # # DONE # # # ";
fi
done
exit 0
}
### remove()
# Remove specified lines from file
###
remove(){
#sort entered numbers to remove lines in correct order
SORTED=`echo $1 | tr "," " " | tr " " " " | sort -nr | uniq`
echo These lines will be removed: $SORTED
if [ $(( `echo $SORTED | wc -w` % 2 )) != 0 ]; then
echo "WARNING: Entered line count is not even, are you sure to remove selected lines?"
fi
echo "Press enter to continue or CTRL+C to quit."
read w8;
#remove the specified lines, optionally mark keys which would be also removed
for line in $SORTED; do
echo Removing: $line: `awk NR==$line $TMP/$FNAME.tmp`
if [ "$FNAME" == "authorization" ]; then
THISKEY="`awk NR==$line $TMP/$FNAME.tmp | sed -e s/Key //g`"
REMKEYS="`echo $REMKEYS $THISKEY | tr " " ","`"
fi
sed -i "$line"d $TMP/$FNAME.tmp
done
#additional info when authorization file is used
if [ "$FNAME" == "authorization" ]; then
echo INFO: These keys will be removed: $REMKEYS
fi
#write to the file
writedirect "rem";
exit 0
}
### check()
# Info function; display keys, compare them, show options
###
check(){
#find the used authorization files, choose which one to use
findfile;
#when the file is found and chosen, download it
getfile $SSHOUT;
#compare the file with all keys, dont compare authorization file as it contains only keyfile names
if [ "$FNAME" != "authorization" ]; then
echo Matching keys:
for key in $( ls $KEYPATH ); do
#get the key
getkey $key
#print matching/known keys
if [ "`cat $TMP/$FNAME.tmp | grep -n "" | sed -e s/:/: / | grep --color -B 1 "$SSHKEY"`" != "" ]; then
echo -e Key: $key
cat $TMP/$FNAME.tmp | grep -n "" | sed -e s/:/: / | grep --color -B 1 "$SSHKEY"
KEY="$KEY"" `cat $TMP/$FNAME.tmp | grep -n "" | sed -e s/:/: / | grep --color -B 1 "$SSHKEY" | cut -d ":" -f 1`"
echo ""
fi
done
#show supported options
echo -e Would you like to [a]dd/[r]emove? [continue]; read opt;
case "$opt" in
"a" ) cd $KEYPATH; ls -l; echo -n "enter keys to add (key1 key2 ... or use * to select all files): "; read -e addkey;
adddirect "$addkey" "info";
;;
"r" ) echo -n "enter line numbers to remove (enter lines for comment and key) (nr nr ...): "; read remnr;
remove "$remnr";
;;
* ) echo NON-Matching keys:
;;
esac
#printf all other entries in file exept of known keys
LNCOUNT=`cat $TMP/$FNAME.tmp | wc -l`
ACTLN=1
while [ "$LNCOUNT" -ge "$ACTLN" ]; do
if [ ! "`echo $KEY | grep -w $ACTLN`" ]; then
echo $ACTLN: `cat $TMP/$FNAME.tmp | awk NR==$ACTLN`
fi
let ACTLN=$ACTLN+1
done
#else print the whole file
else cat $TMP/$FNAME.tmp | grep -n "" | sed -e s/:/: /;
fi
#show supported options
echo -e Would you like to [a]dd/[r]emove/[b]ackup restore/[e]xit/[c]ancel? [exit]; read opt;
case "$opt" in
"a" ) cd $KEYPATH; ls -l; echo -n "enter keys to add (key1 key2 ... or use * to select all files): "; read -e addkey;
adddirect "$addkey" "info";
;;
"r" ) echo -n "enter line numbers to remove(nr nr ...): "; read remnr;
remove "$remnr";
;;
"b" ) restore;
;;
"c" ) exit 0;
;;
* ) echo Removing temp file and exitting...
rm $TMP/$FNAME.tmp
exit 0;
;;
esac
exit 0
}
# if no parameters are specified, show them how it works
if [ $# -eq 0 ];then
usage;
fi
## check prerequisites
# check OS
if [ -f /etc/*release ]; then
OSversion="`cat /etc/*release | head -n 1 | cut -d -f 1,2 | tr [A-Z] [a-z]`"
case $OSversion in
"red hat") echo "INFO: your OS is supported.";;
"suse linux") echo "INFO: your OS is supported.";;
*) echo "ERROR: Unsupported OS, supported systems are SLES and RHEL."
echo "Press any key to ignore this error and continue."
read w8;;
esac
else
echo "ERROR: Unable to determine OS, supported systems are SLES and RHEL."
echo "Press any key to ignore this error and continue."
read w8
fi
# check tools used in this script
if [ `which sed 2>/dev/null 1>&2; echo $?` == 1 ]; then echo "FATAL ERROR: sed not found."; exit 1; fi
if [ `which cut 2>/dev/null 1>&2; echo $?` == 1 ]; then echo "FATAL ERROR: cut not found."; exit 1; fi
if [ `which ssh 2>/dev/null 1>&2; echo $?` == 1 ]; then echo "FATAL ERROR: ssh not found."; exit 1; fi
if [ `which ssh-keygen 2>/dev/null 1>&2; echo $?` == 1 ]; then echo "FATAL ERROR: ssh-keygen not found."; exit 1; fi
if [ `which tar 2>/dev/null 1>&2; echo $?` == 1 ]; then echo "ERROR: tar not found. Tar is needed when copying keys for autorization file"; fi
if [ `which dos2unix 2>/dev/null 1>&2; echo $?` == 1 ]; then echo "ERROR: dos2unix not found. This tool is needed to convert public keys to proper format."; fi
### chkopts()
# Check if all mandatory options are entered
###
chkopts(){
if [ "$USERNAME" == "" ] || [ "$HOST" == "" ] || [ "$KEYPATH" == "" ]; then
echo ERROR: You need to specify Username, Hostname and Keypath.
exit 1
fi
if [ ! -d $KEYPATH ]; then
echo ERROR: The key path is not valid!
exit 1
fi
}
#allowed options; in each case proper function is called with parameter (if specified)
while getopts "u:h:k:ia:r:" option; do
case "$option" in
u ) USERNAME=$OPTARG;;
h ) if [ -f "$OPTARG" ]; then HOST="`cat $OPTARG`"; else HOST=$OPTARG; fi;;
k ) KEYPATH=$OPTARG; cd $KEYPATH; KEYPATH=`pwd`;;
i ) chkopts; echo INFO MODE...; check;;
a ) chkopts; echo ADDING KEYS...;
ALLKEYS="`echo $OPTARG | tr " " ","`"
echo KEYS: $ALLKEYS
adddirect $ALLKEYS "direct";;
r ) chkopts; echo REMOVING KEYS...;
ALLKEYS="`echo $OPTARG | tr " " ","`"
echo KEYS: $ALLKEYS
removedirect $ALLKEYS;;
[?]) echo "Bad option."; usage;;
esac
done
exit 0