Password Aging
While it's clearly possible to use the /etc/passwd and /etc/shadow files in Solaris and other Unix systems without making use of the password aging features, you could be taking advantage of these features to encourage your users to practice better security -- and, with the right password aging values, you can configure a good password-changing policy into your system files while limiting the risk that your users will be locked out of their accounts.
In this week's column, we look at the various fields in the shadow file that govern password aging and suggest settings that might give you the right balance between user convenience and good password security.
The /etc/shadow File
To begin our review of how password aging works on a Solaris system, let's examine the format of the /etc/shadow file. Each colon-separated record looks like this:
johndoe:PaSsWoRdxye7d:13062:30:120:10:inactive:expire:
^ ^ ^ ^ ^ ^ ^ ^ ^
| | | | | | | | |
username:password:lastchg:min:max:warn:inactive:expire:flag
The first field is clearly the username. The next is the password encryption. The third is the date when the password was last changed expressed as the number of days since January 1, 1970. The min field is the number of days that a password MUST be kept after it is changed; this is used to keep users from changing their passwords and then immediately changing them back to their previous values (thereby invalidating the intended security). The max field represents the maximum number of days that any password can be used before it is expired. If you want your users to strictly change their passwords every 30 days, for example, you could set both of these fields to 30. Generally, however, the max field is set to a considerably larger value than min. The warn field specifies the number of days prior to a password expiration that a user is warned on login that his/her password is about to expire. This should not be too short a period of time since many users don't log in every day and the display of this message in the login messages is easy to overlook.
The inactive field sets the number of days that an account is allowed to be inactive. This value can help prevent idle accounts from being broken into. The expire field represents the absolute day (expressed as the number of days since January 1, 1970) that the password will expire. You might use this field if you want all of your users' passwords to expire at the end of the fiscal year or at the end of the semester. The last field, flag, is unused until Solaris 10 at which point it records the number of failed login attempts.
If the lines in your shadow file look like this:
sbob:dZlJpUNyyusab:12345::::::
The username and password are set and the date on which the password was last changed has been recorded, but no password aging is taking effect.
If it looks like this, the account is locked.
dumbo:*LK*:::::::
Various other combinations of the shadow file are possible, but the min, max and warn fields will only make sense if the lastchg field is set. For example:
jdoe:w0qjde84kr%p0:13062:60:::::
User must keep a password for 60 days once he changes it, but no password changes are required.
jdoe:w0qjde84kr%p0:13062::60::::
User must change his password every 60 days, but can change it at any time (including immediately changing it back to its previous value).
Choosing Min and Max Settings
If you want to turn on password aging, the combination of minimum (must keep) and maximum (invalid after) values enforces a practical password update scheme. Suggested settings depend in part on the security stance of your particular network. However, general consensus seems to be that passwords, once changed, should be kept for a month (min=30) and that passwords should be changed every three to six months (from max=90 to max=180).
Once a user has used a password for 30 days, he's probably not going to reset it back to its previous value. By then he should know it well enough to continue using it.
Changing a password more often than every month or so would probably make it hard for users to remember their passwords without writing them down.
The down side of min values is that this setting doesn't allow someone to change his password if he believes it has been compromised when the compromise happens within the "min" period. Whatever system you adopt should, therefore, make it painless for a user to request that his password be reset whenever he believes it may no longer be secure.
Wrap Up
We hear a lot about the tradeoff between security and convenience as it permeates so many of our decisions about how we manage our networks but, when it comes to passwords, we must be careful not to cross the line between securing logins and preventing them altogether. Locking our users too easily out of their accounts can reduce security as easily as enhance it. Using password aging with the proper settings can limit the risk that security constraints turn into unintended denials of service.
If you're starting with a group of users who have been active for a long time and not had their passwords aged, how should you go about introducing password aging?
To start, you might first take a look at the dates on which your users' passwords were last changed. To view the dates by themselves, you might use a command such as this (run as root):
# cat /etc/shadow | awk -F: '{print $3}' | sort -n | uniq -c
This command sorts the lastchg (last time the password was changed) field numerically and prints out the number of records with each particular date value.
Of course, the dates in this command's output are going to be presented to you as a list of numbers (rather than recognizable dates). You will see something that looks more or less like this:
7 6445
1 11289
2 11632
53 11676
5 11677
2 11683
1 11849
2 12038
23 12345
1 12881
1 13062
These numbers are a little hard to interpret, but the range of values and the "popular" values suggest that most users on this system have not changed their passwords in a very long time and that many of them might have last changed their passwords in response to a request to do so (since two groups of people changed their passwords on the same two days).
But let's try to pin these numbers down and get an idea what dates we are really looking at. How do you do this? Well, if you have the GNU date command installed on your system, you can view today's date with a command such as this:
% expr `date +%s` / 86400
Alternately, you can package this date conversion command in in a script such as that shown below, call it "today" and run it whenever you want to know what the current date looks like in the days-since-the-epoch format. If you're reading this column on the day that it was first published, that value would be 13062.
#!/usr/bin/perl -w
# today: a script to print date in days-since-epoch format
$now=`/usr/local/bin/date +%s`;
$_=$now / 86400;
($today)=/(\d+)./; # number of days since 01/01/1970
print "$today\n";
In both the command and the "today" script, we use the "date +%s" command to produce the current date/time as the number of seconds since midnight on January 1, 1970. We then divide this value by the number of seconds in a day (86,400) to convert this value to the number of days since January 1, 1970. The commented line lops off the digits on the right side of the decimal point (along with the decimal point itself). This gives us a value for today.
To determine how long ago one of the other dates in the lastchg list above happened to be, we can use an expr to calculate the number of days between today and the date the password was last changed. Let's choose the most popular value (line 4) for this:
# expr 13062 - 11676
1386
That's 1,386 days ago -- nearly four years! NOTE: The shadow records with 6445 in the lastchg field are disabled accounts and, thus, don't factor into our password aging concerns.
If the bulk of your users have the same last-set date, they have probably never changed their passwords -- or never changed them since they were last required to do so. Whenever you change a user's password or one of your users changes his own password, that field in the /etc/shadow file will be updated.
So, how do you introduce password aging in a situation such as this? If you add a max value when a user's password hasn't been reset for nearly four years, chances are that his password will already be expired and he will not be able to log in.
A better approach would be to initiate password aging by modifying the lastchg date in your shadow records and then selecting a max value that will give your users time to change their passwords before they run out of time. You should also publish notices explaining the change and focusing your users attention on the need to change their passwords from time to time.
For example, if you make the lastchg date of a record five months in the past and then require that the user change his password every six months, this would give him a month to change his password before he is locked out. And, from that point forward, he would need to change his password every six months.
Five months in the past would roughly put the (fictitious) lastchg date at 12912 (13062 - (5 * 30)). A shadow entry such as that shown below would, therefore, force sbob to change his password within the month and would give him a month's worth of warnings before he's locked out of his account:
sbob:dZlJpUNyyusab:12912:30:180:30:::
On login, sbob would see something like this:
Your password will expire in 30 days.
Last login: Wed Oct 6 16:28:34 2005 from corp.particles.com
Sun Microsystems Inc. SunOS 5.8 Generic February 2005
If you've never used password aging before, it's probably a good idea to get your users' attention to the fact that passwords are going to expire. The one-line warning above may not be enough to get your users' attention. Perhaps a notice like this in your /etc/motd file would be more effective:
>>> Passwords must be changed every 6 months <<<
>>> Look for password expiration information <<<
>>> in the system output above <<<
When a message like this is displayed on login for a month, your users are likely to notice and take action before their passwords expire.
You can also change the default settings for password aging in the /etc/default/passwd file. For example, if you want users to be required to keep a password for a month and change it every 6 months, your values might look like this:
MAXWEEKS=26 MINWEEKS=4 PASSLENGTH=6
In particular, we looked at the password aging fields in the /etc/shadow file and a script that displays the current day in the days-since-Jan-01-1970 (or "Unix Time") format used to record the day that a user's password was last changed.
In today's column, we're going to look at an efficient Perl command for expressing the current day in this format and a script that you can use to 1) list all users on a system whose passwords will soon be expiring and 2) print how many days are remaining along with the calendar date on which the password will expire.
The lastchg Date
If you changed your password on the day this column was first published, the value recorded in the /etc/shadow file would be 13076 and your shadow entry would look like something like this:
jdoe:jr85ys38dkrf9:13076:30:180:30:::
Since numbers such as 13076 are not the most human-friendly, we're going to look at a script for converting values such as these into calendar dates such as 11/04/2005.
Before we get into the new script, however, let's look at the Perl command for printing the current date in this Unix Time format. This Perl command first determines the current date/time in seconds since January 1, 1970 and then divides that number by the number of seconds in a day. The rest of the command then strips off the decimal point along with all of the digits following it, leaving only the 5-digit day number.
printf qq{%d\n},time/86400;
We'll use a similar command in our date conversion script. This command is more efficient that the script included in last week's column primarily because it doesn't make a call to the system to run the date command to derive a date in the seconds-since-1970 format. Instead, it uses the Perl time command. This also obviates the need for the GNU date command (not that you might not want it for other reasons) to print the date in this way.
Script for Converting Dates
The script shown below takes a Unix Time format date and changes it into a calendar date:
#!/usr/bin/perl
# caldate.pl: change Unix Time date into mm/dd/yyyy format
use strict;
use Time::Local;
my $inputDate = shift @ARGV;
if( !defined $inputDate )
{
print "Usage: $0 \n";
exit 5;
}
# date cannot exceed 24855 (01/19/2038)
if ( $inputDate > 24855 ) {
print "Error: Date entered exceeds limits on date calculations\n";
exit 5;
}
# calculate seconds since the epoch
$inputDate = $inputDate * 86400;
$ENV{TZ} = 'EST';
my($seconds,$mins,$hrs,$dom,$month,$year,$wday,$yday,$isdst) = localtime($inputDate);
$year = $year + 1900;
$month = $month + 1;
$dom="0${dom}" if($dom <>
$month = "0${month}" if($month <>
$hrs = "0${hrs}" if($hrs <>
$mins = "0${mins}" if($mins <>
print "$month/$dom/$year","\n";
Example
> ./caldate.pl 13076
10/20/2005
Notice that this script takes the date entered by the user (a date such as 13076) and multiplies it by 86400 to convert it to the number of seconds (rather than days) since the beginning of the Unix epoch. It then uses a series of commands to massage the provided date into a set of date attributes such as $month, $year and $dom (day of month).
Some of the date fields, such as $wday (weekday) and $isdst (tells whether daylight savings time is in effect) are not used in the date presented by the script but are included for completeness.
Script for Determining Expiration Dates
The following script looks through the shadow file for users with passwords that are expiring within the next two weeks and prints their usernames, days remaining and the dates on which the passwords will expire.
#!/usr/bin/perl -w
# daysleft.pl: list users w passwords expiring soon
use integer; # do integer math
@shadow=`cat /etc/shadow`;
$today = time/86400;
$soon = 14; # report passwords expiring within 14 days
foreach $record ( @shadow ) {
@shadfield=(split /:/, $record);
$username="$shadfield[0]";
$password=$shadfield[1];
$lastch=$shadfield[2];
$max=$shadfield[4];
next if ($password eq "*LK*");
next if ($lastch eq "");
next if ($max eq "");
$rem=$lastch + $max - $today;
$exp=$lastch + $max;
if ( $rem <= $soon ) {
print "$username: ";
printf qq{%d},$rem;
printf " days left, expiring on ";
$dt=`./caldate.pl $exp`;
print $dt;
}
}
Example:
# ./daysleft.pl
jdoe: 8 days left, expiring on 10/28/2005
sbob: 10 days left, expiring on 10/30/2005
jasper: 8 days left, expiring on 10/28/2005
The End of the Epoch
You might have noticed a couple oddities in our scripts. In the caldate.pl script, for example, we refused to work with Unix Time dates larger than 24,855. This leads us to some interesting observations about dates on Unix systems. Even if you know that Unix dates are stored as the number of seconds since midnight on January 1, 1970, you might not know that these dates can't extend beyond January 19, 2038.
To begin to understand why, we need to remember that dates are stored as four-byte signed numbers. The largest value that can be stored is, thus, 2**31 - 1, which we can calculate like so:
# expr 256 \* 256 \* 256 \* 128 - 1
2147483647
That's 24,855 days or roughly 68 years from January 1, 1970 -- the day on which the dates on Unix systems will presumably all reset to January 1, 1970 -- unless, of course, we solve the problem before then. This well-known problem is similar to Y2K problem.
You might have also noticed that we included a "use integer" command in our daysleft.pl script. This avoided having the result of our time/86400 calculation end up with a long string of digits following the decimal point and our subsequent calculation of days remaining before password expiration having to compensate for this partial day.
Thanks to Douglas Gray Stephens and John Gregory for their scripts and insights on processing Unix dates in Perl.
andra Henry-Stocker, ITworld.com
Before we move on to another topic, there are a few more things that we can do to improve our password management scripts. For one thing, we can modify our daysleft.pl script so that it does not need to be run from a particular directory in order to locate the secondary caldate.pl script. For another, we can generate email notifications to users with passwords that are soon to expire instead of leaving the job of notifying users to the sysadmins. Last, we can modify the format of our displayed date to make it more international. Let's take a look at how these changes can be implemented.
Directory Names in Perl
To use the equivalent of the Unix dirname command in Perl, we could use backticks and try to extract this information from the system, but using the Perl File::Basename module makes this task extremely easy. When we include the "use" statement in our script, we can determine the directory name associated with the script (whether /usr/local/bin or ".").
use File::Basename;
$dirname=dirname($0);
Once we establish the directory name ($dirname), we can then use it in our call to the caldate.pl script:
# report if less than 2 weeks remaining
if ( $rem <= 14 ) {
printf q{%s: %d days left, expiring on %s}, $username,$rem,
`$dirname/caldate.pl $exp`;
}
Now, if we run the script from / or some other location on the server by typing /usr/local/bin/daysleft.pl, the script will still find the caldate.pl script and everything will work the same as if we were in the /usr/local/bin directory and typed ./daysleft.pl.
Generating Email
If we want to send email notifications to users -- very helpful if they are unlikely to notice the password warnings that are printed when they log in or if they seldom log in, we can use Net::SMTP to provide easy commands for connecting to the mail server and sending the messages.
use Net::SMTP;
The Sys::Hostname module will allow us to easily include the name of the system on which the password is expiring in the email without hard-coding this in the script.
use Sys::Hostname;
Next, we'll set up the name of the mail server to which all of the email notifications will be sent and the name of the local system:
my $ServerName="mail.myorg.org";
my $host=hostname();
Assuming we want all the notifications to appear to be coming from the same email address, we assign an address:
my $MailFrom = "sysadmin\@$host.myorg.org";
Net::SMTP includes a number of commands for connecting to an SMTP server, sending a message and disconnecting. In the loop below, we use one command to make a new connection to the server and exit the script if the connection cannot be made.
if ( $rem <= $soon ) {
# Connect to the server
$smtp = Net::SMTP->new($ServerName);
die "Couldn't connect to server" unless $smtp;
We then send the mail server the email address of the user -- one per loop:
my $MailTo = "$username\@$mailserver";
Next, we send the sender and recipient information:
$smtp->mail($MailFrom);
$smtp->to($MailTo);
We then construct the notification with the number of days and password expiration date for that particular user:
$dt=`$dirname/caldate.pl $exp`;
$msg="Password expiring for $username: $rem days left, expiring on $dt";
Last, we send the message to the mail server and close the connection:
$smtp->data();
$smtp->datasend("Subject: $host password expiring\n");
$smtp->datasend("$msg");
$smtp->dataend();
$smtp->quit();
}
International Dates
As several readers pointed out, not everyone sees the same date when they see something like 10/12/2005. For some readers, this looks like Oct 12th while for others, it's Dec 10th. One way to make dates work for everyone (everyone who speaks English anyway) is to print them in a more obvious format, such as 12 Oct 2005.
To do this in our caldate.pl script, we'll add an array including the 3-letter month abbreviations:
month=(q{Jan},q{Feb},q{Mar},q{Apr},q{May},q{Jun},
q{Jul},q{Aug},q{Sep},q{Oct},q{Nov},q{Dec}
);
When we are ready to print our date, we can use a command like this:
printf qq{%02d %s %4d\n},$dom,$month[$month],$year;
Putting it all Together
First, the caldate.pl script:
#!/usr/bin/perl -w
use strict;
use Time::Local;
my $inputDate = shift @ARGV;
if( !defined $inputDate )
{
print "Usage: $0 \n";
exit 5;
}
my @month=(q{Jan},q{Feb},q{Mar},q{Apr},q{May},q{Jun},
q{Jul},q{Aug},q{Sep},q{Oct},q{Nov},q{Dec}
);
# date cannot exceed 24855 (01/19/2038)
if ( $inputDate > 24855 ) {
print "Error: Date entered exceeds limits on date calculations\n";
exit 5;
}
# calculate seconds since the epoch
$inputDate = $inputDate * 86400;
my($seconds,$mins,$hrs,$dom,$month,$year,$wday,$yday,$isdst) = localtime($inputDate);
$year+=1900;
$month++;
$dom="0${dom}" if($dom <>
#$month = "0${month}" if($month <>
$hrs = "0${hrs}" if($hrs <>
$mins = "0${mins}" if($mins <>
# print "$month/$dom/$year","\n";
printf qq{%02d %s %4d\n},$dom,$month[$month],$year;
Next, our daysleft.pl script:
#!/usr/bin/perl -w
use integer;
use Net::SMTP;
use File::Basename;
use Sys::Hostname;
my $host=hostname();
$domain="myorg.org";
# get password aging data
@shadow=`cat /etc/shadow`;
# get today in days-since-epoch time
$today = time/86400;
$soon = 14;
my $ServerName = "localhost";
my $MailFrom = "sysadmin\@$host.myorg.org";
my $dirname = dirname($0);
foreach $record ( @shadow ) {
@shadfield=(split /:/, $record);
# get fields from shadow record
$username="$shadfield[0]";
$password=$shadfield[1];
$lastch=$shadfield[2];
$max=$shadfield[4];
# skip record is aging not enabled
next if ($password eq "*LK*");
next if ($lastch eq "");
next if ($max eq "");
# calculate datys remaining and expiration date
$rem=$lastch + $max - $today;
$exp=$lastch + $max;
if ( $rem <= $soon ) {
# Connect to the server
$smtp = Net::SMTP->new($ServerName);
die "Couldn't connect to server" unless $smtp;
my $MailTo = "$recip\@$domain";
$smtp->mail( $MailFrom );
$smtp->to( $MailTo );
$dt=`$dirname/caldate.pl $exp`;
$msg="Password expiring for $username: $rem days left, expiring on $dt";
$smtp->data();
$smtp->datasend("Subject: $host password expiring\n");
$smtp->datasend("$msg");
$smtp->dataend();
$smtp->quit();
}
}