Linux Password Enforcement with PAM

Hal Pomeranz, hal@deer-run.com

Many years ago I wrote an article on "Strong Password Enforcement with pam_cracklib". Since that time, there have been some changes in Linux:

So while the advice in my previous article is still valid for many Linux distributions, I wanted to develop new guidance based on the current set of available password enforcement modules. Testing for this article was done on a CentOS 7.1 system.

Basic PAM Configuration

On RedHat-based systems, password checks are enabled via configuration in the /etc/pam.d/system-auth and /etc/pam.d/password-auth files. Actually, these files are just symbolic links to /etc/pam.d/system-auth-ac and /etc/pam.d/password-auth-ac, respectively.

The default configuration looks like this:

password    requisite     pam_pwquality.so try_first_pass local_users_only retry=3 authtok_type=
password    sufficient    pam_unix.so sha512 shadow nullok try_first_pass use_authtok

The try_first_pass option tells a later module to try using the password entered for a previous module. In the configuration above, pam_pwquality will require the user to enter a strong password choice, and the try_first_pass option on pam_unix module tells pam_unix to try this choice. Similarly, use_authtok tells pam_unix to use the password from the stacked pam_pwquality module.

Frankly, the try_first_pass option is redundant with the use_authtok option on pam_unix. And try_first_pass isn't necessary for pam_pwquality since there are no modules stacked in front of it.

Other default options set on pam_pwquality above include local_users_only, which tells pam_pwquality to ignore users that are not in the local /etc/passwd file (for example, users whose accounts are in an LDAP or Active Directory database). The retry option is the number of tries a user gets to pick an acceptable password before the module returns an error.

By default, the prompt the user gets when entering their password is "New password:". If the administrator sets authtok_type=FOO, the prompt becomes "New FOO password:".

For pam_unix, the sha512 option means use a password hashing routine based on the SHA512 algorithm. blowfish is also supported along with several other, less secure, choices. The shadow option means maintain password hashes in a separate /etc/shadow file that is only readable by the root user. This option should always be set. nullok means allow user accounts that have null password entries. Personally, I would recommend removing this option.

Taking all of this advice together, a better default configuration for these modules would be:

password    requisite     pam_pwquality.so local_users_only retry=3
password    sufficient    pam_unix.so sha512 shadow use_authtok

Standard Checks

pam_pwquality performs a number of basic checks, just like the old pam_cracklib module:

However, pam_pwquality adds several other checks that can be enabled at the administrator's discretion (none of these checks are enabled by default):

gecoscheck
Do not allow any of the words in the user's "full name" (GECOS) field from /etc/passwd to be used in their password choice.

maxrepeat=N
Reject passwords that have more than N of the same characters in a row ("aaaaaa").

maxsequence=N
Reject passwords if there are more than N upper-case, lower-case, or number characters in a row. In other words, try to stop passwords like "QWERTY" or "12345".

maxclassrepeat=N
Like maxsequence above except that runs of punctuation characters ("!@#$%") are also rejected. I recommend ignoring maxsequence and just using maxclassrepeat.

Length and Strength

pam_pwquality uses a "scoring" system that combines password length requirements with a "credit" system based on the number of different types of characters used. This is identical to the old pam_cracklib module.

You start with the minlen=N parameter which sets the minimum acceptable length for a password. However, the user gets one "credit" each for using a lower-case letter, an upper-case letter, a number, and a punctuation character. So if minlen=15, the user could still use an 11 character password if it contained all four character classes.

It is important to note that even a monocase password gives the user one "length credit". So if you set minlen=15 you are really saying that you allow 14 character monocase passwords.

While users normally get a maximum of one credit per type of character, you can use the lcredit, ucredit, dcredit, and ocredit parameters to create a different scheme:

password    requisite     pam_pwquality.so minlen=19 lcredit=0 ucredit=1 dcredit=1 ocredit=2 ...

In the above example, no credits are given for lower-case letters (lcredit=0). Upper-case letters (ucredit=1) and digits (dcredit=1) can give up to one credit, which is the default. Users may receive credits for up to two punctuation characters (ocredit=2) if they are included in the password. Since minlen=19, the user must still come up with at least a 15 character password, even if they get all possible credits.

If you want to require users to use certain character classes, use negative values:

password    requisite     pam_pwquality.so minlen=15 lcredit=0 ucredit=0 dcredit=-1 ocredit=-1 ...

The above example requires a 15 character password with at least one digit and one punctuation-type character. Note that requiring the password to contain certain character types actually makes life easier for somebody who is trying to brute-force your user passwords, since they can skip testing strings that don't match your requirements.

pam_pwquality also supports a minclass=N parameter that requires characters from at least N of the four different character classes. This is probably a better way to go than specifically requiring a specific type of character.

On RedHat systems, all of the parameters we've been discussing in the last two sections can be set in /etc/security/pwquality.conf. This is probably easier than hacking the parameters into long lines in the PAM configuration files.

Dictionary Checks

On RedHat systems, passwords are checked against a dictionary stored in /usr/share/cracklib/pw_dict.*. The files are in a database format that can be built using the create-cracklib-dict program. Use the cracklib-unpacker program to see the contents of the current system dictionary.

Debian systems typically place their dictionaries in /var/cache/cracklib. There's a nightly cron job that runs update-cracklib to rebuild the dictionary.

Password History

The password history checking code in pam_cracklib and pam_unix is being deprecated in favor of the new pam_pwhistory module. However, if you're familiar with the old way of doing things, you'll find that the paramaters used by pam_pwhistory are the same.

Here's a typical configuration stacking pam_pwhistory in with pam_pwquality and pam_unix:

password    requisite     pam_pwquality.so local_users_only retry=3
password    required      pam_pwhistory.so remember=400 use_authtok
password    sufficient    pam_unix.so sha512 shadow use_authtok

The remember=N parameter says how many old passwords to remember for each user. The default is 10, and 400 is an internal hard-coded maximum. Even if you were to force users to change passwords monthly, that still gives you more than 30 years of password history. So remember=400 is effectively infinite.

The old password hashes for users are stored in /etc/security/opasswd.

Password Expiration

Password expiration is still controlled by pam_unix. You get to control:

PASS_MAX_DAYS
The number of days a password may be used before change is forced.

PASS_WARN_AGE
How many days before the password expires do you start warning the user of the upcoming change?

PASS_MIN_DAYS
How many days must the user wait before being allowed to change their password again?

INACTIVE
How many days after the account is expired before the account is locked for inactivity? Set this to -1 to prevent lockouts.

You can set defaults for the first three parameters in /etc/login.defs. The default setting for INACTIVE is found in /etc/default/useradd. Note that these defaults are only used if you use the built-in useradd command to make user accounts.

You can manually set these parameters on a user's account using the chage command. You can view the settings for a user by inspecting their /etc/shadow entry (see "man 5 shadow" for which field is which).

Lockout on Failure

Constant brute-force password guessing attacks have made "lockout on failure" functionality a necessary evil. On modern Linux systems, this is handled by the pam_tally2 module.

For Redhat systems, add a line like this at the top of /etc/pam.d/system-auth-ac and password-auth-ac:

auth       required     pam_tally2.so deny=3 unlock_time=1800 even_deny_root

Accounts will be locked after three failures (deny=3) but automatically unlocked after 30 minutes (unlock_time=1800 uses seconds as the unit). If the unlock_time parameter is left off, then accounts stay locked until the administrator manually intervenes.

even_deny_root says to apply lockout on failure to the root account as well-- this is not the default. You can set a special timeout for the root account with the root_unlock_time=N parameter if you like. Generally speaking, you should not be allowing direct root logins to your system ("PermitRootLogin no" in your sshd_config file), so locking out the root account shouldn't be a factor.

Login failure and user lockout records are stored in /var/log/tallylog by default. You can change this with the file= option.

There is also a command-line program called pam_tally2. This is how admins query and unlock user accounts that have been locked out due to failures.

Putting It All Together

Here's a sample password-auth-ac file with a reasonable default configuration:

auth        required      pam_tally2.so deny=3 unlock_time=1800 even_deny_root
auth        required      pam_env.so
auth        sufficient    pam_unix.so nullok try_first_pass
auth        requisite     pam_succeed_if.so uid >= 1000 quiet_success
auth        required      pam_deny.so

account     required      pam_unix.so
account     sufficient    pam_localuser.so
account     sufficient    pam_succeed_if.so uid < 1000 quiet
account     required      pam_permit.so

password    requisite     pam_pwquality.so local_users_only retry=3 minlen=19 gecoscheck maxrepeat=3
password    required      pam_pwhistory.so remember=400 use_authtok
password    sufficient    pam_unix.so sha512 shadow use_authtok
password    required      pam_deny.so

session     optional      pam_keyinit.so revoke
session     required      pam_limits.so
-session     optional      pam_systemd.so
session     [success=1 default=ignore] pam_succeed_if.so service in crond quiet use_uid
session     required      pam_unix.so

Don't forget system-auth-ac:

auth        required      pam_tally2.so deny=3 unlock_time=1800 even_deny_root
auth        required      pam_env.so
auth        sufficient    pam_fprintd.so
auth        sufficient    pam_unix.so nullok try_first_pass
auth        requisite     pam_succeed_if.so uid >= 1000 quiet_success
auth        required      pam_deny.so

account     required      pam_unix.so
account     sufficient    pam_localuser.so
account     sufficient    pam_succeed_if.so uid < 1000 quiet
account     required      pam_permit.so

password    requisite     pam_pwquality.so local_users_only retry=3 minlen=19 gecoscheck maxrepeat=3
password    required      pam_pwhistory.so remember=400 use_authtok
password    sufficient    pam_unix.so sha512 shadow use_authtok
password    required      pam_deny.so

session     optional      pam_keyinit.so revoke
session     required      pam_limits.so
-session     optional      pam_systemd.so
session     [success=1 default=ignore] pam_succeed_if.so service in crond quiet use_uid
session     required      pam_unix.so