CHAPTER 36
Password-Aging Notification

With security concerns reigning supreme, it is wise to keep a tight leash on your user accounts. Unfortunately, it can be difficult to control password aging in an environment with many users. It's definitely preferable to have user passwords set to expire automatically; however, if the account goes unused for a long period of time, even though the password on an account is expired the account will still be active. In some instances an account can still be accessed without the password being updated. Also, if a user logs in to their account infrequently, they may not know that their account is about to expire. Even when a user does use her account regularly, the expiration warning message may go unnoticed among the other items that typically scroll across the screen in the message of the day.

Requiring users to change their passwords after they have aged a certain number of days is a common practice. The logic behind it is that if a password is stolen or found out, it will be useful for only a limited amount of time. Another result of this practice is that users will tend to change their password in a way that least impacts their work so they don't have to learn a new password. One method might be to leave the main password in place but change only one or two characters such as a digit so the password is incremented but still basically the same.

A better solution might be to require strong passwords and then periodically run a utility such as John the Ripper1 against the encrypted passwords to ferret out any insecure ones. Implementing this solution combined with password aging might be even better albeit annoying to your user base.

The following script watches over the /etc/shadow file to determine how long it's been since the users have changed their passwords. The shadow file contains, among other things, account information such as encrypted user passwords and the day when a password was last changed. When the expiration date of an account approaches, the script starts annoying the user with a canned e-mail message letting them know they need to change their password and that the account will be locked if they don't. The script could easily be modified to support other notification methods.

Some NIS and HP-UX environments don't use shadow files to hold encrypted passwords and account-aging information. The users' encrypted passwords are held in the second field of the world-readable password file. If you think this isn't the best method to maintain security, you are correct. Also, without the shadow file, there is no information tracking the age of a password. In Chapter 37 I will demonstrate another script that can be used to create and maintain a pseudo–shadow file on such systems.

Script Initialization

First off, we have to set a bunch of environment variables. Originally these were set in a separate file accessed from our script. This makes configuration a bit more convenient, but to simplify this demonstration, I included the initialization of the variables in the script.

#!/bin/sh
HOME=/usr/local/pass_aging

The following line of code establishes the number of days that the user's password is valid before the account will be locked. The value could also be set dynamically by pulling the fifth field from the /etc/shadow file if the account has been configured appropriately. However, I have found that accounts on a system often are created by many different people and the fields in the /etc/shadow file are not always filled in correctly to include appropriate account-expiration settings.

VALID_DAYS=90

The ENVIRONMENT variable is used to customize the notifications with some meaningful information about the affected accounts. For example, you can assign Accounting or Development to the ENVIRONMENT variable. A notification might then read, "Your Development account is about to expire."

ENVIRONMENT="Scripting"

The following code shows the e-mail addresses used for administrative notification. The reports of account-password aging are sent to the ADMIN_EMAIL address. The DEBUG_EMAIL address is used for testing.

ADMIN_EMAIL=root
DEBUG_EMAIL=

Since this script has the potential for disrupting your environment by modifying passwd and shadow files, it would be wise to perform a lot of testing prior to running it. However, when the DEBUG_EMAIL variable is non-null, the shadow file will not be updated and users will not be notified. The notifications that would have been sent to the users will instead be sent to the DEBUG_EMAIL address. This may generate a lot of mail to that address, but this setting prevents potentially major problems and it is worth using.

Next the passwd and shadow files are configured to be used with this script. It would be wise to make backup copies of the real files. These definitions are useful in that you can configure the script to work with NIS since those files don't generally live in /etc.

shad=/usr/local/pass_aging/bin/shadow_copy
pswd=/usr/local/pass_aging/bin/passwd_copy

The exclude file, shown next, is a flat file containing a list of usernames not to be modified by the script. In some environments there may be userless accounts (such as apache or sendmail, which are associated with applications) that would break if the accounts were suddenly locked. You could improve this script by using a file that associates userless accounts with the e-mail addresses for the users responsible for those accounts. This would allow a notification to be sent to the account owner when expiration is approaching. It would also separate the management of this type of account from the general exclude list.

exclude="$HOME/config/exclude_list"

The following code shows a few more variables to set up some paths, filenames, and various other items in our script. The ED variable defines the file that will contain editing changes to be made to the shadow file. The max variable represents the number of days a password is permitted to exist without change. The notify variable is used to start notifying users that their accounts are about to expire. The notifications start two weeks (14 days) prior to expiration. The remaining variable assignments specify files that will all contain parts of the final aging report that is sent to the administrator. Nothing too fancy here.

ED=ed.script
max=$VALID_DAYS
notify=$(($max-14))
OUTFILE=$HOME/aging
NOTEOUT=$HOME/notes
WARNOUT=$HOME/warnings
REPORT=$HOME/report
ARCHIVE=$HOME/archive
BIN="$HOME/bin"

Since this script is going to read and potentially modify the shadow file, it must be run as root. You have to ensure that this is the case when installing the script.

if [ "`id -un`" != "root" ]; then
  echo "This script must be run as root - exiting" >&2
  exit 1
fi

Then we have to clean up any old transient report files that exist.

for file in $OUTFILE $WARNOUT $NOTEOUT $REPORT
do
  if [ -f $file ]
  then
    rm $file
  fi
done

Next we determine the number of days that have passed since 1/1/1970. Password-aging information in the /etc/shadow file is held in the third field of each account entry. This field contains an integer that expresses the date on which the password was last changed as a number of days since 1/1/1970. Subtracting this number from the current number of days elapsed since that date gives us the age of the password.

seconds_since_epoch=$((`date +%s`))
seconds_per_day=86400
days_since_epoch=$(($seconds_since_epoch/$seconds_per_day))

We now back up the shadow file for safety. We want to be able to return to the original file to start over if necessary. Using cp -p will also preserve the original modification time and permissions on the new copy of the file.

backdate=`date +%m%d%y%H%M`
cp -p $shad $ARCHIVE/shadow.$backdate

The following code lets you clean the archive directory by removing old reports and backup shadow files more than seven days old.

find $ARCHIVE -mtime +7 -exec rm {} ;

Processing Begins

Now here is where the real script begins. We start a loop that iterates through all the usernames in the passwd file.

for user in `cut -d: -f1 $pswd`
do

To line up the text in the final report in columns and make it a little more readable, you might like to create some padding spaces based on the length of the username to be reported.

  padding=""
  user_length=`echo $user | awk '{print length}'`
  padding_len=$((15-$user_length))
  counter=1
  while [ $counter -lt $padding_len ]
  do
    padding="${padding} "
    counter=$(($counter+1))
  done

The code would also benefit from using printf to format the report appropriately instead of inserting spaces manually to line up the columns. The printf command is a print function that gives you a lot of control over how the output is formatted.

Next we populate some variables with the values from the current user's shadow entry. First is the number of days after which the user must change his password. The second is the number of days since January 1, 1970 that the password was last changed. These will be used to determine when an account should expire.

  exp_days=`grep "^${user}:" $shad | cut -d: -f5`
  pass_days=`grep "^${user}:" $shad | cut -d: -f3`

Depending on whether the encrypted password is located in the passwd or the shadow file, you have to use one of the following two lines:

  #pass_word=`grep "^${user}:" $pswd | cut -d: -f2`
  pass_word=`grep "^${user}:" $shad | cut -d: -f2`

Our implementation uses the second (uncommented) line; it gets its data from the shadow file. You probably think the script could simply check for the existence of the shadow file; in case it isn't there, it should default to the passwd file. This would work in most—but not all—situations. I have worked on some machines that have both files but use only the /etc/passwd. I have also seen systems that will use the /etc/passwd file unless an /etc/shadow exists. In that case the shadow file will be used. This script will, of course, need to be customized to fit your environment.

You also need to check for a special case: if the encrypted password consists of a single asterisk (*) character, you have to escape it with a backslash (). If the asterisk isn't escaped, it will be evaluated later as a wild card denoting all filenames residing in the current working directory (the traditional meaning of *). In a shell environment, another method to get around this problem would be to set noglob, which turns off that type of evaluation.

  if [ "$pass_word" = "*" ]
  then
    pass_word="*"
  fi

If the user account is incorrectly configured with a null password, the script creates a message explaining this state of affairs and appends it to the warning-output file. The contents of this file will be added to the final report at a later stage.

  if [ "$pass_word" = "" ]
  then
    echo "$user $padding WARN: $user has null password set,
      set password or lock account" >> $WARNOUT
  fi

Then we determine by checking the exclude file whether the current user is one of those exempted from password expiration. If the user is not exempt from password expiration, the script continues on to the core of the program.

  exempt=`grep "^${user}$" $exclude`
  if [ "$exempt" = "" ]
  then

Before proceeding we have to make sure the user's password has an expiration date set. If it doesn't, the else clause that follows will create a notification for the final report.

  if [ "$pass_days" != "" -a "$exp_days" != "" ]
  then

Determine Password Age

This is the beginning of the core of the script. First we determine how many days have passed since the password was last changed. Then we evaluate this number to determine if the user should start receiving notifications that his account is about to be disabled.

    days_since_change=$(($days_since_epoch-$pass_days))
    if [ $days_since_change -lt $notify ]
    then

A locked account is indicated by the first character of the password; when the script locks an account, it changes the user's encrypted password string to a string of the form *CLOSED_${the_date}*. If the password is still young, no action needs to be taken against the account. However, we should check to see if the account has already been locked, and if so, append a notice to the report.

      first_char=`echo $pass_word | cut -c1`
      if [ "$first_char" = "*" ]
      then
        echo "$user $padding $days_since_change Already locked"
          >> $OUTFILE
      else
        echo "$user $padding $days_since_change OK" >> $OUTFILE
      fi

The following code checks whether the password is still valid but the user-notification period has begun. If so, we determine how many days it will be before the account is locked. (The length of the notification period is configurable; recall that at the beginning of the script I set it to start 14 days prior to the password expiring.)

    elif [ $days_since_change -ge $notify -a $days_since_change -le $max ]
    then
      exp=$(($max-$days_since_change))

We then report warning notifications to the output file and to the debugging e-mail address, provided it has been configured.

      if [ "$DEBUG_EMAIL" != "" ]
      then
        echo "$user $padding $days_since_change Expires in $exp days ;
          Would have sent mail ; sent mail to $DEBUG_EMAIL" >> $OUTFILE
      else
        echo "$user $padding $days_since_change Expires in $exp days ;
        sending mail" >> $OUTFILE
      fi

Finally, we notify the user that the password will expire in the determined number of days. Please recall that if the DEBUG_EMAIL variable is not null, notifications will be sent only to the debug address specified in the configuration section. Only when this variable is null will notifications be sent to the specified user. This is handled in the send_email script called earlier; it also checks whether the debug e-mail address has been set. At a first glance it may seem that the notifications should always be sent to the user, but this is not the case. (The send_email script is not included here, but it should be simple to implement.)

      $BIN/send_email $user $days_since_change about_to_expire

In the following example the password has expired and the account needs to be locked. We first set some variables in anticipation of changing the user's password to a CLOSED string showing the date on which the user account was locked, as described earlier.

    else
      first_char=`echo $pass_word | cut -c1`
      the_date=`date +%y%m%d`
      CLOSED="*CLOSED_${the_date}*"

If the account has already been locked in the following conditional statement, the echo statement reports that fact to the output file. (The logic for account-checking could be reorganized so that the same checks wouldn't have to be done multiple times; here I have broken down the cases for easier understanding, at the cost of some repeated code.)

      if [ "$first_char" = "*" ]
      then
        echo "$user $padding $days_since_change Already locked"
          >> $OUTFILE
      else

To lock the account, we will construct and run an ed script that modifies the passwd or shadow file. First we remove any existing ed scripts created from previous runs.

        if [ -f $HOME/$ED ]
        then
          rm $HOME/$ED
        fi

Once again, if the debug e-mail address has been set, we only append to the output file a report that the account is expired.

        if [ "$DEBUG_EMAIL" != "" ]
        then
          echo "$user $padding $days_since_change
            Would have locked account ; sent mail to
            $DEBUG_EMAIL" >> $OUTFILE
        else

Otherwise we append to the output file the notification that the user account is now being locked.

          echo "$user $padding $days_since_change Locking account
            *CLOSED_${the_date}*" >> $OUTFILE

The ed script contains instructions to substitute the user's encrypted password with the CLOSED string. When you construct this code, you have to use a backslash to escape all occurrences of the following special characters in the encrypted password string: ., /, $, and *. See Chapters 24 and 25 for more information on escaping special characters. These sed statements check for the specified special characters in the password string and replace all instances with the same character preceded with a backslash. This is so the shell ignores their special meanings and treats them simply as any other character.

          pass_word=`echo $pass_word | sed -e s/\./\\\\./g`
          pass_word=`echo $pass_word | sed -e s/\*/\\\\*/g`
          pass_word=`echo $pass_word | sed -e s/\$/\\\\$/g`
          pass_word=`echo $pass_word | sed -e s/\\//\\\\\//g`

We now have to cut off the last two characters of the modified password string. These are extraneous and were introduced by the preceding transformations. The $ character, which is a valid character in an encrypted password string, also denotes the special end-of-line character in a string. This is the nature of strings on a UNIX or Linux system. Since all of the $ characters of the encrypted string were replaced with $, the replacement action also included the end-of-line character that is now escaped and shouldn't be included in the encrypted string; both the trailing and $ characters need to be removed. This form of the sed command is explained in more detail in Chapter 24.

          pass_word=`echo $pass_word | sed -e 's/(.*)(.)(.)$/1/'`

Now we create the ed script that will replace the encrypted password string with the CLOSED string.

          echo "/$user:$pass_word/s/$pass_word/$CLOSED" > $HOME/$ED
          echo "w" >> $HOME/$ED
          echo "q" >> $HOME/$ED

To run the ed script, use one of the following two lines, depending on whether your password entries live in the passwd or the shadow file. The same ed script can be used to replace the encrypted password within the passwd or shadow file—whichever applies. As you can see, my example uses the shadow file. More information on how to modify files using ed can be found in Chapter 25.

          # ed -s $pswd < $HOME/$ED > /dev/null
          ed -s $shad < $HOME/$ED > /dev/null
        fi

Now we send an e-mail notification to the account stating that it has been locked. Once again, if the debug e-mail is set, the message will go to that address and not to the real user.

        if [ "$DEBUG_EMAIL" = "" ]
        then
          $BIN/send_email $user $days_since_change account_locked
        fi
    fi
  fi

The next else matches the line far earlier in the loop, where the check was performed to see whether the user account is exempt from expiration. If that test fails, we append a message about the nonexpiring account to the output file for the final report. This completes the main loop.

  else
    echo "$user $padding WARN: $user password not set to expire.
      Fix shadow entry" >> $WARNOUT
  fi
  else
    echo "$user $padding Note: $user is exempt from password
      expiring" >> $NOTEOUT
  fi
done

The script then collects all notification output files and sorts them by the age of the passwords. Then all transient files are removed. The final report will list any warnings in order of importance; for example, users without a password will appear first.

  for file in $WARNOUT $OUTFILE $NOTEOUT
  do
    if [ -f $file ]
    then
      sort -rn +1 $file >> $REPORT
      rm $file
    fi
  done

Finally, the report is sent to the administrator and archived for later reference.

cat $REPORT | mail -s "$ENVIRONMENT password aging report" $ADMIN_EMAIL
mv $REPORT $ARCHIVE/report.$backdate

Here is a sample of the output from a final report:


fred         122 Already locked
rbpeters     105 Locking account *CLOSED_041124*
yabbadmin    14  OK
xfs          14  OK
webalizer    14  OK
vinmaster    14  OK
vcsa         14  OK
uucp         9   OK

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.133.140.79