How to enforce password complexity on Linux

Linux gives you lots of ways to create complexity in passwords that include a lot more than just length, such as mixing upper- and lower-case letters with numerals and punctuation marks along with other restrictions.

passwords
Thinkstock

Deploying password-quality checking on your Debian-based Linux servers can help ensure that your users assign reasonably secure passwords to their accounts, but the settings themselves can be a bit misleading.

For example, setting a minimum password length of 12 characters does not necessarily mean that all your users' passwords will actually have 12 or more characters.

Let's stroll down Complexity Boulevard and see how the settings work and examine some that are worth considering.

The files that contain the settings we're going to look at will be:

  • /etc/pam.d/common-password on Debian-base systems
  • /etc/security/pwquality.conf on RedHat

Complexity settings

Here's how it works. You can set a minimum password length to insure strength, but this might not work exactly as you’d expect. In fact, passwords with the most characters aren't necessarily the most secure or easy to use and remember. In fact your users can set themselves up with shorter passwords that are just as secure if they incorporate certain restrictions and categories of characters that make them harder to crack and get credit for doing so.

Here are complexity settings you can require in addition to length:

  • uppercase characters
  • lowercase characters
  • digits
  • other characters (e.g., punctuation marks)
  • a mix of the above
  • a restriction on the number of characters in any particular class (uppercase, lowercase, etc.)
  • a restriction on how many times the same character can be used
  • the number of characters that have to be different from those used in the previous password
  • restrictions on password re-use

The settings include:

  • minlen = minimum password length
  • minclass = the minimum number of character types that must be used (i.e., uppercase, lowercase, digits, other)
  • maxrepeat = the maximum number of times a single character may be repeated
  • maxclassrepeat = the maximum number of characters in a row that can be in the same class
  • lcredit = maximum number of lowercase characters that will generate a credit
  • ucredit = maximum number of uppercase characters that will generate a credit
  • dcredit = maximum number of digits that will generate a credit
  • ocredit = maximum number of other characters that will generate a credit
  • difok = the minimum number of characters that must be different from the old password
  • remember = the number of passwords that will be remembered by the system so that they cannot be used again
  • gecoscheck = whether to check for the words from the passwd entry GECOS string of the user (enabled if the value is not 0)
  • dictcheck = whether to check for the words from the cracklib dictionary (enabled if the value is not 0)
  • usercheck = whether to check if the password contains the user name in some form (enabled if the value is not 0)
  • enforcing = new password is rejected if it fails the check and the value is not 0
  • dictpath = path to the cracklib dictionaries. Default is to use the cracklib default.

These settings on a Red Hat system might look like this. The credit settings mean your users will get credits for using a mix of character types that can reduce the password length requirement.

$ grep "=" /etc/security/pwquality.conf

# difok = 1

minlen = 12

dcredit = -1

ucredit = 1

lcredit = 1

ocredit = 1

# minclass = 0

# maxrepeat = 0

# maxclassrepeat = 0

# gecoscheck = 0

# dictcheck = 1

# usercheck = 1

# enforcing = 1

# dictpath =

The same settings on a Debian system might look like this:

$ grep ^password common-password

password   requisite      pam_pwquality.so retry=3 minlen=12 difok=1 remember=3 lcredit=1 ucredit=1 ocredit=1 dcredit=-1

Note that, regardless of the value you set for minlen, passwords cannot have fewer than six characters. That is, even if you set minlen equal to 4 and give credit for many types of characters, passwords with fewer than six characters will be rejected.

Getting credit for complexity

The idea of "credits" (e.g., lcredit and ucredit) is very interesting. Basically, a shorter password might be acceptable if it's more complex with respect to the mix of characters.

As an example, a password like "hijlmqrazp" might pass a minlen=10 test. If dcredit is set to 2, on the other hand, the password "hijlmq99" would also pass. Why? Because you'd get two credits for the digits. So, eight characters plus credits is valued as highly as 10 characters without credits. If dcredit were set to 1, you would need an additional character. However, we can also grant credits for uppercase, lowercase, and non-alphanumeric characters like punctuation marks.

Note, however, that you can only get credit for so many of the different characters. Maybe you will get credit for only one digit or two uppercase characters. Maybe you don't get any credit for lowercase characters. It all depends on your settings.

Mixing character classes

One other setting that comes into play is the minclass setting, which determines how many different classes of characters must be used for a password to be acceptable. If minclass is set to 2, a password containing all lowercase, all uppercase, all digits, or all any other class of characters wouldn't work. If set to 2, minclass would require you to use characters from two classes, like uppercase and lowercase, or lowercase and digits.

With minclass set to 4, passwords would have to include all four types of characters--like “howzit2B?”--and, if we get credit for uppercase, digits or other characters, we'd be OK even with the minlen set to 12.

You can also put a cap on the number of characters of any particular class. Set the maxclassrepeat setting to 4 and passwords cannot contain more than four lowercase, uppercase, digits, or other characters in succession.

The meaning of negative values

Setting any of the lcredit, ucredit, dcredit, or ocredit settings to a negative number means that you MUST have some of that type of character for a password to be acceptable. Setting dcredit to -1, for example, would mean that you have to include at least one digit.

Other passward-strength checks

Linux’s password-quality checking includes a number of other checks that help ensure that passwords are fairly secure. It can check to see if a password is a palindrome, like “racecar”, whether a new password is the same as the old password but with a change of case only, if the old and new passwords are too similar or rotations of each other, and whether a password contains the user's name. (It's getting to the point that it might actually be difficult to assign oneself a really poor password.)

For example, if a user doesn’t meet all the specified criteria, a password changing attempt might look like this:

$ passwd

Changing password for shs.

Current password:

New password:

BAD PASSWORD: The password is a palindrome

New password:

BAD PASSWORD: The password contains less than 1 uppercase letters

New password:

BAD PASSWORD: The password contains less than 1 non-alphanumeric characters

passwd: Have exhausted maximum number of retries for service

passwd: password unchanged

Password quality testing

If you change the settings in the top lines of the following Perl script, you will get a feel for the kind of passwords that will pass your quality tests. In this example, the minimum length for a password has been set to 12. One credit is given for lowercase and uppercase letters, but none for special characters (just to demonstrate the difference). In addition, a digit must be included (setting -1).

#!/usr/bin/perl -w

# -- set your complexity preferences here --

$minlen=12;

$lcredit=1;

$ucredit=1;

$dcredit=-1;

$ocredit=0;

# -- 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

# -- password MUST contain at least 6 characters

if ($score < 6) {

    print "password MUST contain at least 6 characters\n";

    exit;

}

# -- 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";

}

Notice that the password “2Good4me?” passes even though it’s only 9 characters long. This is because we got one credit each for the uppercase G, one for the use of lowercase letters and one for the digit. We’d have passed with a 13 if we had been given credit for the “?” as well. The “9 + 1 + 1 + 1 + 0” line displays the list of credits:

$ pwquality 2Good4me?

9 + 1 + 1 + 1 + 0

password passes with score of 12

Password complexity and PAM

Support for password complexity is provided through the pluggable authentication module (PAM). If you have a file named /etc/pam.d/system-auth on a RedHat system, look for lines that look like those shown below.

$ grep password /etc/pam.d/system-auth

password    requisite      pam_pwquality.so try_first_pass local_users_only

password    sufficient     pam_unix.so sha512 shadow nullok try_first_pass use_authtok

password    sufficient     pam_sss.so use_authtok

password    required       pam_deny.so

On Debian systems like Ubuntu, this command will show you whether PAM is installed and ready to be used:

$ apt-cache policy *pam-pwquality*

libpam-pwquality:

  Installed: 1.4.2-1build1

  Candidate: 1.4.2-1build1

  Version table:

 *** 1.4.2-1build1 500

        500 http://us.archive.ubuntu.com/ubuntu focal/main amd64 Packages

        100 /var/lib/dpkg/status

If the response on your system shows “Installed: (none)”, you can install it with this command:

$ sudo apt install libpam-pwquality

Join the Network World communities on Facebook and LinkedIn to comment on topics that are top of mind.

Copyright © 2020 IDG Communications, Inc.

IT Salary Survey: The results are in