Friday, August 19, 2011

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 you'd 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 doesn't 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 you'd 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 "\n-u -h -k\tthese option are mandatory, they need to be specified"
  echo -e "-h <hostname>\tyou can specify multiple hostnames separated with \",\" or a filename containing hostnames"
  echo -e "-k <keypath>\tspecify directory where the keys are stored"
  echo -e "-i -a -r\tyou can use only one option at the same time"
  echo -e "-i\t\tenter info mode"
  echo -e "-a <keys>\tadd keys to the specified server, enter key filenames separated with \",\""
  echo -e "-r <keys>\tremove keys from the specified server, enter key filenames separated with \",\""
  echo -e "\nNote 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 " " "\n" | 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 '\n'Found file:
    echo $SSHOUT | tr " " "\n" | grep -n "";
    echo "Choose line number for file to edit:"; read LINE;
    SSHOUT="`echo $SSHOUT | tr " " "\n" | awk NR==$LINE`"
  fi

  echo -e '\n'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 '\n'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 '\n'Copying keys...
      cat $TMP/$FNAME-tmp.tar | ssh $USERNAME@$HOST "cd $FPATH; tar xvf -"
    elif [ "$1" == "rem" ]; then
      echo -e '\n'Removing keys...
      ssh $USERNAME@$HOST "cd $FPATH; rm $REMKEYS"
    fi
  fi
  echo DONE.

  #remove local temporary files
  echo -e '\n'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 there's 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!'\n'
    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 "\nINFO: 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\t$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\" doesn't 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 "\nINFO: 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\" doesn't exist."
      fi
    done

    #if some matching keys were found
    if [ ! -z "$lines" ]; then
      SORTED=`echo $lines | tr " " "\n" | sort -nr | uniq`;
      echo -e '\n'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 '\n'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 "," "\n" | tr " " "\n" | 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\t//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, don't 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 '\n'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 '\n'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



No comments:

Post a Comment