Strong
Passwords with PAM
Hal Pomeranz
Standard Unix reusable passwords are not really a good authentication
system. However, the costs associated with migrating to an alternate
authentication system, such as two-factor token authentication or
smartcard-based systems, are too high for most enterprises. So sites
are generally left with the "lowest common denominator"
option provided by their vendors.
To improve the security of standard reusable passwords, "best
practices" tell us to require users to change their passwords
on a regular basis, enforce minimum lengths and good "rules"
for new passwords (e.g., requiring mixed case and non-alphanumeric
characters), and even keep a "history" of previous user
passwords so that users don't "repeat". Interestingly,
Unix systems have typically lagged behind other operating systems
in providing this functionality -- particularly when it comes
to rule-based systems for requiring strong passwords, as well as
functionality for keeping password history. In an effort to address
this shortcoming, the PAM module pam_cracklib was developed
for Linux systems.
I admit that for the longest time I thought pam_cracklib
was just about useless. But it turns out that it's not useless,
it's just really poorly documented. In an effort to correct
this problem, I present the following article based on my research
with the existing documentation, the pam_cracklib source
code (when in doubt, read the source), and my trusty Knoppix (Debian)
GNU/Linux system.
Enabling pam_cracklib
The pam_cracklib module is enabled via the system's
standard PAM configuration interface. On Debian systems, this is
the /etc/pam.d/common-password file (but it's /etc/pam.d/system-auth
on Red Hat-derived systems -- can't we all just get along?).
The typical configuration looks something like this:
password required pam_cracklib.so retry=3 minlen=6 diffok=3
password required pam_unix.so md5 use_authtok
The first line enables the pam_cracklib module and sets several
module parameters. "retry=3" means that users get
three chances to pick a good password before the passwd program
aborts. Users can always re-run the passwd program and start over
again, however. "minlen=6" sets the minimum number
of characters in the password. Actually, since Linux systems generally
use MD5 password hashes, which are not limited to eight-character
passwords like the old DES56 hashes, you should probably think about
increasing the "minlen" parameter to something longer.
I'll come back to this notion a bit later in the article. "diffok=3"
sets the minimum number of characters that must be different from
the previous password. If you increase "minlen",
you may also want to increase this value as well.
The second line invokes the standard pam_unix module. The
"md5" argument here enables standard Linux MD5
password hashes, although you have the option of using old-style
DES56 hashes for backward compatibility with legacy Unix systems.
"use_authtok" tells pam_unix not to bother
doing its own internal password checks, which duplicate many of
the checks in pam_cracklib, but instead to accept the password
that the user inputs after it has been thoroughly checked by pam_cracklib.
Simple Checks
By default, pam_cracklib performs a number of basic checks
on the new password:
- Is the new password just the old password with the letters
reversed ("password" vs. "drowssap") or rotated
("password" vs. "asswordp")?
- Does the new password only differ from the old one due to change
of case ("password" vs. "Password")?
- Are at least some minimum number of characters in the new password
not present in the old password? This is where the "diffok"
parameter comes into play.
These are the same checks you get in the pam_unix module
if you turn on the "obscure" flag, but because
we're already using pam_cracklib we don't need
to do this.
Length and Strength
Although the "minlen" parameter controls the
minimum password length, things are not as simple as they might
appear. This is because pam_cracklib combines the notion
of password length with password "strength" (the use of
mixed-case and non-letter characters).
"minlen" is actually the minimum required length
for a password consisting of all lowercase letters. But users get
"length credits" for using upper- and lowercase letters,
numbers, and non-alphanumeric characters. The default is normally
that you can only get a maximum of "1 credit" for each
type of character. So if the administrator sets "minlen=12",
a user could still have an eight-character password if they used
all four types of characters. Actually, since using a lowercase
letter gets you a credit, the real minimum length for an all lowercase
passwords is minlen-1.
The maximum credit for any particular class of characters is actually
customizable. The four parameters "lcredit", "ucredit",
"dcredit", and "ocredit" are used
to set the maximum credit for lowercase, uppercase, numeric (digit),
and non-alphanumeric (other) characters, respectively. For example,
you could add the following parameters on the pam_cracklib
line in the /etc/pam.d/common-password file:
lcredit=0 ucredit=1 dcredit=1 ocredit=2
In other words, lowercase characters aren't special at all, so
we give no credit there. But, we do give extra credit if a user puts
two or more non-alphanumeric characters in their password. One point
is still the max credit for uppercase characters and numbers. Note
that no matter what you set "minlen" to and no matter
how many "credits" you give to your users, pam_cracklib
will never let users pick passwords with fewer than six characters
-- this is a hard-coded internal minimum.
Play around with these values and find something that makes sense
for your site; as a starting point, I might recommend "minlen=12
diffok=4" for machines using MD5 password hashes. This
means that the smallest password a user could have is eight characters,
and that's only if they use all four character sets.
Dictionary Checks
pam_cracklib also checks the user's password against
its own internal dictionaries of easily guessed passwords. On Debian
systems, pam_cracklib's dictionaries live in /var/cache/cracklib
and are rebuilt nightly by the /usr/sbin/update-cracklib
script. Other Linux distros may have other mechanisms for updating
the dictionaries (as far as I can tell, Red Hat doesn't provide
any tools for doing this).
The update-cracklib script searches a number of directories
for input files, including /usr/local/dict and /usr/local/share/dict.
So, adding your own words is as easy as putting them in a file in
one of these directories and running update-cracklib or waiting
for cron to do it for you. Note that if you want to add other
directories to update-cracklib's search path, you can
do this by modifying the /etc/cracklib/cracklib.conf file
(at least on Debian systems).
Password "History"
pam_cracklib is capable of consulting a user's password
"history" and not allowing them to re-use old passwords.
However, the functionality for actually storing the user's
old passwords is enabled via the pam_unix module.
The first step is to make sure to create an empty /etc/security/opasswd
file for storing old user passwords. If you forget to do this before
enabling the history feature in the PAM configuration file, all
user password updates will fail because the pam_unix module
will constantly return errors from the password history code because
the file is missing.
Treat your opasswd file like your /etc/shadow file
because it will end up containing user password hashes (albeit for
old user passwords that are no longer in use):
touch /etc/security/opasswd
chown root:root /etc/security/opasswd
chmod 600 /etc/security/opasswd
Once you've set up the opasswd file, enable password history
checking by adding the option "remember=<x>"
to the pam_unix configuration line in the /etc/pam.d/common-password
file. Here's how I set up things on my Knoppix machine:
password required pam_cracklib.so retry=3 minlen=12 diffok=4
password required pam_unix.so md5 remember=12 use_authtok
The value of the "remember" parameter is the number
of old passwords you want to store for a user. It turns out that there's
an internal maximum of 400 previous passwords, so values higher than
400 are all equivalent to 400. Before you complain about this limit,
consider that even if your site forces users to change passwords every
30 days, 400 previous passwords represents more than 30 years of password
history. This is probably sufficient for even the oldest of legacy
systems.
Once you've enabled password history, the opasswd
file starts filling up with user entries that look like this:
hal:1000:<n>:<hash1>,<hash2>,...,<hashn>
The first two fields are the username and user ID. The <n>
in the third field represents the number of old passwords currently
being stored for the user -- this value is incremented by one
every time a new hash is added to the user's password history
until <n> ultimately equals the value of the "remember"
parameter set on the pam_unix configuration line. <hash1>,<hash2>,...,<hashn>
are actually the MD5 password hashes for the user's old passwords.
Password Expiration
At this point, you may be wondering how to get the system to automatically
force a user to change his password after some period of time. This
is not actually the job of pam_cracklib. Instead, these parameters
are set in the /etc/login.defs file on most Linux systems.
PASS_MAX_DAYS is how often users have to change their passwords.
PASS_MIN_DAYS is how long a user is forced to live with his
new password before he's allowed to change it again. PASS_WARN_AGE
is the number of days before the password expiration date that the
user is warned that his password is about to expire. The choice
of values for these parameters is entirely dependent on site policy.
Note that these parameters are only applied to new accounts created
with the default system useradd program. If you use some
other mechanism for creating accounts on the system, then you'll
have to use the chage command (this is not a typo) to manually
set these parameters on your user accounts. And, if you use a naming
service, such as LDAP or NIS, for account management, then you're
completely on your own.
By the way, if you've ever wondered what all those extra
fields in the /etc/shadow file were for, the answer is that
they store the password expiration/aging information for the user.
Conclusion
I hope I've shed a little light on some of the more mysterious
corners of pam_cracklib's functionality and the Linux
password system in general. If pam_cracklib appears limited
compared to the password enforcement routines on your particular
operating system variant, consider that some Unix-like operating
systems (such as Solaris -- at least through Solaris 9) don't
have any functionality of this type, other than the standard password
expiration/aging routines.
Hal Pomeranz (hal@deer-run.com) realizes that the wonderful
thing about Linux is that you can always refer back to the source
code, but would prefer to read a Unix manual page whenever possible. |