Thursday, May 17, 2012

How to enforce password complexity on Linux

http://www.itworld.com/endpoint-security/275056/how-enforce-password-complexity-linux

On most Linux systems, you can use PAM (the "pluggable authentication module") to enforce password complexity. If you have a file named /etc/pam.d/system-auth on RedHat (/etc/pam.d/common-password on Debian systems), look for lines that look like those shown below.
$ grep password /etc/pam.d/system-auth
password    requisite     pam_cracklib.so try_first_pass retry=3
password    sufficient    pam_unix.so md5 shadow nullok try_first_pass use_authtok
password    required      pam_deny.so
That's what you should expect to see on a new system.
By default, passwords must have at least six characters (see /etc/login.defs for possible changes). This is hardly long enough by current standards to consider passwords to be secure. You will have a much stronger password complexity policy if you change the first line to something like this, requiring longer passwords and ensuring a degree of complexity as well.
password requisite pam_cracklib.so try_first_pass retry=3 minlength=12 lcredit=1
ucredit=1 dcredit=1 ocredit=1 difok=4
Here's what each of the available parameters does:
try_first_pass = sets the number of times users can attempt setting a good
  password before the passwd command aborts
minlen = establishes a measure of complexity related to the password length
  (more in a moment on this)
lcredit = sets the minimum number of required lowercase letters
ucredit = sets the minimum number of required uppercase letters
dcredit = sets the minimum number of required digits
ocredit = sets the minimum number of required other characters
difok = sets the number of characters that must be different from those in the
   previous password
That said, minlen is actually a measure of complexity, not simply length. It specifies a complexity score that must be reached for a password to be deemed as acceptable. If each character in a password added one to the complexity count, then minlen would simply represent the password length but, if some characters count more than once, the calculation is more complex. So let's see how this works.
The minlen complexity measure is calculated in a number of steps:
  • every character in a password yields one point, regardless of the type of character
  • every lowercase letter adds one point, but only up to the value of lcredit
  • every uppercase letter adds one point, but only up to the value of ucredit
  • every digit adds one point, but only up to the value of dcredit
  • every special character adds one point, but only up to the value of ocredit
If lcredit, ucredit, dcredit and ocredit were all set to 0, only the password length would be used to determine if it's acceptable. No characters would add extra points to the complexity score.
When you set any of the lcredit, ucredit, dcredit or ocredit parameters to a negative number, then you MUST have at least that number of characters for each character class for the password to pass the complexity test.
Setting minlen to 12 and difok to 4 are generally good settings. However, if you want to require 12 character passwords and complexity too, you will need a larger minlen setting. With minlen set to 12 and one point given for including each of a lowercase, uppercase, digit and special character (the defaults), you could get by with passwords that have only eight characters -- even with minlen set to 12! A stronger policy would be required by this:
password requisite pam_cracklib.so try_first_pass retry=3 minlength=16
lcredit=-1 ucredit=-1 dcredit=-1 ocredit=-1 difok=4
These settings would ensure that your passwords have 12 characters, including at least one characters in each of the four classes.
If you'd like to experiment with the password length and complexity settings, try the script below. It's a bit cumbersome, but it should capture all the requirements aside from the difok (differences) criteria. Change the settings in the first section to match those you want to evaluate.
#!/usr/bin/perl -w

# -- set your complexity preferences here --
$minlen=16;
$lcredit=-1;
$ucredit=-1;
$dcredit=-1;
$ocredit=-1;

# -- initialize the counters --
$score=0;
$lcase=0;
$ucase=0;
$digits=0;
$other=0;

# -- set fail to false --
$fail=0;

# -- check for argument --
if ( $#ARGV < 0 ) {
    print "argument expected\n";
    exit;
} else {
    $password=$ARGV[0];
}

# -- determine if any character settings are mandatory (if negative)
if ($lcredit < 0) {             # needed # of lowercase characters
    $lneeded=-1 * $lcredit;
    $lextra=$lneeded;
} else {
    $lneeded=0;
    $lextra=$lcredit;
}
if ($ucredit < 0) {             # needed # of uppercase characters
    $uneeded=-1 * $ucredit;
    $uextra=$uneeded;
} else {
    $uneeded=0;
    $uextra=$ucredit;
}
if ($dcredit < 0) {             # needed # of digits
    $dneeded=-1 * $dcredit;
    $dextra=$dneeded;
} else {
    $dneeded=0;
    $dextra=$dcredit;
}
if ($ocredit < 0) {             # needed # of special characters
    $oneeded=-1 * $ocredit;
    $oextra=$oneeded;
} else {
    $oneeded=0;
    $oextra=$ocredit;
}

$score=length($password);               # 1 point for each character

# -- count the characters of each type --
foreach $char (split //, $password) {

    if ($char =~ /\d/) {
        $digits++;                      # digits
    } elsif ($char !~ /\w/) {
        $other++;                       # special characters
    } elsif ($char eq lc($char)) {
        $lcase++;                       # lowercase
    } elsif ($char eq uc($char)) {
        $ucase++;                       # uppercase
    } else {
        print "Error: unrecognized character. Please fix this script!\n";
    }
}

if ($lcase < $lneeded) {
    print "password failure: need $lneeded lowercase character(s)\n";
    $fail=1;
}
if ($ucase < $uneeded) {
    print "password failure: need $uneeded uppercase character(s)\n";
    $fail=1;
}
if ($digits < $dneeded) {
    print "password failure: need $dneeded digit(s)\n";
    $fail=1;
}
if ($other < $oneeded) {
    print "password failure: need $oneeded special character(s)\n";
    $fail=1;
}
if ($fail > 0) {
    exit;
}

# -- reduce credits to number allowed --
if ($lcase > $lextra) {
    $lcase=$lextra;
}
if ($ucase > $uextra) {
    $ucase=$uextra;
}
if ($digits > $dextra) {
    $digits=$dextra;
}
if ($other > $oextra) {
    $other=$oextra;
}

print "$score + $lcase + $ucase + $digits + $other\n";
$score=$score + $lcase + $ucase + $digits + $other;

if ($score >= $minlen) {
    print "password passes with score of $score\n";
} else {
    print "password fails with score of $score\n";
}
Check out /etc/login.defs for password expiration parameters -- another component of good password security.
And please note that cracklib ensures that users can't just reverse their prior passwords or rotate letters to avoid making significant password changes. So, p4ssw0rd cannot be replaced with dr0wss4p or 4ssw0rdp.

No comments:

Post a Comment