PAM Tutorial

 

© 2005–2018 by Wayne Pollock, Tampa Florida USA.  All Rights Reserved.

Background:

Many interactive commands are security sensitive.  An obvious example is passwd used to change a user's password.  Such commands require users to authenticate themselves even though they have successfully logged in to the system.  Also many server daemons carry out tasks on behalf of remote users, and most of these require the daemon to authenticate the remote user.

Early versions of Unix had all such programs (applications and daemons) directly read and parse the /etc/passwd file, so they could authenticate users.  This became a problem when the format of /etc/passwd changed to include aging information in the second field.  Every program that needed to authenticate users had to be updated and re-compiled.  Sometime after that, single (remote) user databases became common in large organizations, using technology such as NIS.  This meant a second set of such commands (all NIS commands start with the letters “yp”, as in yppasswd).  Soon shadow passwords became common, then Kerberos, then different password encryption (actually, hashing) algorithms (such as MD5), then LDAP, etc.  Every such change either requires custom versions of applications and daemons, or a re-write of the existing versions.  And for every program that needed authentication!

At some point, someone came up with the idea that programs that need authentication should use a standard library for that, which in turn could be configured to use different databases and/or new algorithms just by adding new DLLs (called shared object files in Linux or Unix).  This system became known as PAM (Pluggable Authentication Modules).  Using PAM, some new authentication DLL can be invoked by the PAM library just by editing a text configuration file (which says which authentication modules to use).  “PAM-ified” programs do not need to be changed in any way to use different authentication modules.  Only a text configuration file (one for each program) needs to be updated to change how some program authenticates users.

From the FreeBSD PAM documentation:  PAM was defined and developed in 1995 by Vipin Samar and Charlie Lai of Sun Microsystems, and has not changed much since.  In 1997, the Open Group published the X/Open Single Sign-on (XSSO) preliminary specification, which standardized the PAM API and added extensions for single (or rather integrated) sign-on.  PAM is widely used by BSD (OpenPAM), by Linux (PAM-Linux) and by Solaris (Solaris PAM).

To see if some program is “PAM-ified” or not, check if it has been compiled with the PAM library:

ldd cmd | grep libpam.so

Modern (and most legacy) applications and daemons that need authentication have been re-written (hopefully for the last time!) to use PAM.  There are many PAM modules (yes I know that's redundant but saying “PAMs” or “PA modules” is awkward) available for every system, each supporting a different authentication method.  New ones can be easily found on the Internet, or created by programmers.  A nice benefit of this design is that different programs can use different PAM modules for authentication, all on the same system.  (Each program's text configuration file may specify a different set of PAM modules to use.)

In addition to authentication, PAM modules can be used for session setup and tear-down, logging, and various other uses.  For example, there is a PAM module to display the MOTD file.  Another module changes the owner, group, and permissions of various files in /dev, to allow users logged in at the console permission to use sound or access removable media.

The PAM configuration file for some program can list more than one PAM module to try, and each is tried in the order listed.  So if the user fails to authenticate using the PAM module for (say) local files (/etc/passwd and /etc/shadow), then PAM will try the next module listed, which can attempt authentication using a different database such as NIS, LDAP, or even Windows AD.

PAM modules don't only allow or deny access by authenticating users.  They can check other things to determine access.  For instance, some PAM modules deny access if the resource is busy with too many users already.  Others can allow or deny users based on criteria such as the time of day.

Because of PAM's utility and flexibility, system administrators should become familiar with the system and the set of PAM modules available to them.  Then they can implement a wide range of security policies for the various applications and services on their hosts.

PAM is not the only such framework available, but it is the most widely used.  Others on Linux include GSAPI and SSSD.  Note that most of these frameworks include PAM modules, so even if some application uses one of them, they can still be configured through PAM.  Also, PAM usually includes modules to use these other frameworks, leading to the possibility of the application using PAM to use (say) GSAPI, which may be configured to use PAM.  Some care is needed to avoid this.  (See overlapping below for more details.)

Keep in mind that PAM, like all such frameworks, can only inform the programs that use it whether or not a user should be allowed access.  PAM cannot enforce that; a badly written program, or one infected with malware, might still perform its function even if a user is not authenticated or authorized.  Other security subsystems, such as SE Linux, can enforce policy.

PAM Overview:

In the PAM configuration file for some program (application or daemon), the administrator lists all the PAM modules that should be used to implement the access policy.  This list is called a stack.  When a program needs to authenticate a user, each PAM module is invoked in the order listed in the configuration file for that program.  Each module can return success or failure.  The results of all the modules are combined into a single result.  This process is controlled by the “control-flag” listed for each module.  Generally, if any one module “fails”, then PAM informs the application that access is denied.

The exact behavior of PAM in the event that one module fails can be changed in the configuration file, allowing for complex policies to be implemented.  For example you can configure some program to try to authenticate with LDAP first, and if that fails try local files.  Then allow access if either module succeeds.

Take for example an application such as hwbrowser.  This is written to use PAM to see who is allowed to run that command.  The exact set of modules is controlled by a text configuration (or policy) file in the /etc/pam.d directory; the configuration file is called hwbrowser.  (It is common but not required to use the application's name as the configuration file's name.)  By editing this configuration file, you can enforce any access policy desired.  Here's the default hwbrowser PAM configuration file from a Fedora Core Linux system:

/etc/pam.d# cat hwbrowser
#%PAM-1.0
auth       sufficient   pam_rootok.so
auth       sufficient   pam_timestamp.so
auth       required     pam_stack.so service=system-auth
session    required     pam_permit.so
session    optional     pam_xauth.so
account    required     pam_permit.so

As you can see, the file has a list of DLLs to use, and some of them are passed extra arguments.  (The syntax of PAM configuration files is discussed below.)

If an application can't find a configuration file for itself, it defaults to using the other PAM configuration file.  It is important for security reasons to have an “other” configuration file!  Typically this file should deny all access.  This is a deny by default policy.  The administrator will need to create (if missing) a PAM configuration file for any “PAM-ified” program to be used on the system.

By editing (or creating if missing) these files, a system administer implements access policy for the applications and servers on the host.  Don't forget that other security systems may also require configuration to implement a host's access policies, including firewalls, TCP Wrappers, file permissions and ACLs, group memberships, SE Linux policy files, service configuration files, SASL, and so on.

SASL is a framework for authentication mechanism negotiation.  A server can advertise one or more authentication mechanisms to clients, and the two can agree on which one to use.  (Most mail servers use this.)  It is not uncommon to have SASL configured to allow the use of PAM.  See Cyrus SASL for System Administrators and RFC-4422 for more information on SASL.

PAM modules are usually stored in the /lib/security or the /lib64/security directory.  Do an “ls” of this directory to see what PAM modules have been installed on your system.  Additional modules may be put anywhere, but if so the configuration file needs to list the complete pathname of the module.  Each PAM module is really just a DLL that defines one or more of the six standard functions that PAM will use.  These functions are grouped into four types, or contexts (I believe the official term is management group): account, auth, session, and password.  Any given PAM module will implement the functions needed for one or more of these types.  (So, a module that only implements session functions will do nothing if used in any other context.)

To see which functions a given PAM module implements, your first thought should be to consult the documentation.  When that is lacking, you can run the command: nm --dynamic --defined-only pam_module.so
to see which PAM functions are defined in that module.  An “auth” module will define the function pam_sm_authenticate(), an “account” module will define pam_sm_acct_mgmt(), a “session” module will define both pam_sm_open_session() and pam_sm_close_session(), and a “password” module will define pam_sm_chauthtok().  Any PAM module must define at least one set of these functions, and may define several.

Under the Hood of PAM — A Programmers Perspective:

A program written to use the PAM library calls a series of functions that in turn invoke the functioned defined in various PAM modules.  A typical application makes the following function calls to the PAM library:

...
pam_start(...);                Initializes the PAM library
...
if ( ! pam_authenticate(...) ) Authenticates using "auth" modules
   error_exit();
...
if ( ! pam-acct_mgmt(...) )    Checks for a valid, unexpired account and
                               verifies access restrictions with "account" modules
   error_exit();
...
pam_setcred(...)               Sets extra credentials, e.g. a Kerberos ticket
...
pam_open_session(...);         Sets up the session with "session" modules

do_stuff();

pam_close_session(...);        Tear-down session using the "session" modules
pam_end(...);

The only part of this a system administrator needs to understand is that a program that uses PAM will make function calls to the PAM library, and these in turn invoke functions in the related PAM modules.

PAM Service Names

The pam_start function is passed a service name as the first argument.  This name is nearly always the same as the program's name.  The PAM configuration files in /etc/pam.d are named by this service name.  So if the application or server daemon has a different filename, an administrator won't know which PAM configuration file it uses!

If the service name isn't obvious and you can't find it in the program's documentation (or don't trust that) you can determine this name yourself, by checking the string passed as the first argument to the pam_start function.  If you don't have access to the source code you can use tracing and debugging tools to determine the service name.

Here's an example on Linux to determine the service name of the vlock application:

$ ltrace /usr/bin/vlock 2>&1 1>/dev/null </dev/null |grep 'pam_start'
pam_start(0x8049ac0, 0x804a4c0, 0x804a210, 0xbf82adcc, 0x4554005a) = 0
            ^^^^^^^--> this is the RAM address of the service name
$ gdb /usr/bin/vlock
(gdb) printf "%s\n",0x8049ac0
vlock
(gdb) quit

This method won't work if the binary has been stripped (or is not readable by the current user).  A better method is to use a tracing tool, such as strace on Linux, to see which files the command opens:

# strace -uwpollock -e open -o strace.out chfn
# grep /etc/pam.d strace.out  # Hint: not "other" or "system-auth".
# rm strace.out

So a PAM program starts.  The correct configuration file is found and read.  The program then calls pam_authenticate() which in turn calls a specific function (pam_sm_authenticate()) in each of the “auth” modules listed in the configuration file, in order.  The results are combined according to the control-flags for those modules.  Finally pam_authenticate() returns a result back to the program.  The other PAM functions used by the program work the same way.

If some “auth” module wants to ask the user to re-enter a password, the module invokes a function in the application to prompt the user and to get the input.  This is called a “call-back” function.

PAM Configuration (Policy) File Syntax:

This is documented in the pam.conf man page on your system.  Older versions of PAM used one large pam.conf file, with each line starting with the name of the service the line applies to.  In modern PAM, each service has its own file in /etc/pam.d.  The syntax is the same except that first field is omitted, even though the man page may still mention it.

If both the /etc/pam.conf file and the /etc/pam.d/ directory exist, Linux PAM will ignore the file.

Each line contains a module context (or type), the control flag, the module, and module options (if any).  Blank lines and comment lines (starting with “#”) are also allowed.  (Some implementations of PAM allow for long lines to be continued, using the convention of ending a line with a backslash.  Don't rely on this unless you are certain your implementation supports it!)  The context says when the module is used: for authentication, for password updates, or for session setup/cleanup.  The control flag tells PAM how to react to the module's result (e.g., pass or fail).

For determining authorization to run a command, only the lines that start with “auth” and “account” matter.  The others (“session” and “password”) can be ignored.  (They have other uses, such as initializing the environment, logging command's use, or approving a new password.)  The auth modules determine who the user is and if that user has a valid account on this machine (authentication).  The poorly-named account modules determine if the user is allowed access (authorization).  Some possible “account” policies you can test for include: the account and password are not expired, must be a system account, only a certain number of users may run this command (say FTP) at once, access only allowed during certain hours, access from the console only (i.e. not a remote user), and others.

The relevant modules are run in order listed in the file.  Most software checks authentication (“auth”) modules first.  Once the user is authenticated, the authorization (“account”) modules are then run.  If all is well, “session” modules may be run next, if any.  If the software allows a user to update their credentials (passwords), any “password” modules are then run.  When the task is finished, the software will run the “session” modules again, to close the session.

Some PAM modules that logically should be in the session setup are run as “auth” modules.  The reason is that some environment settings have security implications, or affect the user interface (when asking for a password for example), and need to run first.

Modules are passed information about the request and user, plus any module options listed on the end of the line.  Each module returns a result such as pass or fail.  The modules are run until an overall decision is reached, which is either pass or fail.  (In more sophisticated configurations, many different return values can occur, and the configuration file can say what to do for each.)  The final result is passed back to the application.

Locating PAM Module Documentation:

A description of most standard PAM modules can be found in the on-line PAM Administrator's Guide, but often the name alone suggests what a module does.  Not every module is listed in this one guide.  However many PAM modules have man pages (and many don't), so if not listed in the guide try “man pam_module”.  (I.e. to find info on pam_console.so module try “man pam_console”.)  If this doesn't work you must look on the Internet for information.  A good place to start is The Linux-PAM web site.  Failing that you can always try a Google search.  For example, a search for “pam_timestamp” easily finds a man page for pam_timestamp.

PAM Walk-through (I):

Enough with the theory!  Examine the first line from the hwbrowser example above:

auth       sufficient   pam_rootok.so

A description of this “pam_rootok” module can be found in the on-line PAM Administrator's Guide, but the name alone suggests what it does:  If the user is root, then that is sufficient to allow access to this command.

The “sufficient” control-flag means that if the module passes, that is if the user is root, no further (auth) modules need to be tried.  But if the user is not root, PAM must try the other auth modules listed (if any) to decide whether to allow access.

The next “auth” line is:

auth       sufficient   pam_timestamp.so

What does this mean?  Looking up this module in the guide we find... nothing!  In this case the man page is locally installed and we find this information there: “... In a nutshell, pam_timestamp caches successful authentication attempts, and allows you to use a recent successful attempt as the basis for authentication.  ...

This means that if you have successfully authenticated recently say by supplying your password, that will be remembered for several minutes and you can use the command without authenticating yourself again.  (This module was invented for the use of the sudo command.  The timestamp is the modification time on a file, by default in /var/run/sudo/username/*.  You can also determine for how long after that time your credentials remain valid; default is 5 minutes.)

Now look at the last auth line:

auth       required     pam_stack.so service=system-auth

The “required” control-flag means that if the module is used, it must pass or the overall result will be fail, regardless of the status of other modules.

The pam_stack module runs all the modules in the file listed after service= (another PAM config file), and returns whatever result that file's modules return.

Modern Linux PAM no longer uses pam_stack.  Instead two new keywords are available for use in the configuration files, include and the similar substack (They differ in their handling of the sub-module's sufficient success (“done”) and requisite failure (“die”); see the pam.conf man page for details).  On a modern system, the above line would look like this:

auth       include    system-auth

The pam_stack.so line says that the user can use the hwbrowser command if they pass all the auth modules listed in the PAM configuration file “system-auth”.  This is a Red Hat thing.  The folks at Red Hat decided to centralize a lot of security policies into that one file.  (Other systems use the same idea, but a different file name.)  If you look at some other configuration files in /etc/pam.d on a Red Hat based system, you will see the same line.  (Note the system-auth file is over-written each time the “authconfig” command is run, so it is important to keep a backup copy of this file handy if you edit it manually.)  Centralizing your default access policy in one PAM configuration file is a good idea, so if and when it changes you need only update that one file.

Now look at the last line:

account    required     pam_permit.so

A quick check of this module says it merely always returns pass.  This is here because if there is no module for some type, PAM may return fail if an application tries to invoke it.  You can ignore such lines as they have no effect on PAM's overall result.

Putting these lines together we get this policy: A user can run the hwbrowser command: if the user ID is zero (root), or if the user recently authenticated, or (if neither of those) if the user passes the common policy in the system-auth file.  (The default system-auth file is discussed below.)  If none of those pass, access is denied and the attempt is logged.

Suppose the policy on your site changes so root and only root should be allowed to use the command hwbrowser.  To implement this new policy, an administrator need only comment out the second and third “auth” lines in the configuration file.

Control-flags and Module Return Values:

As mentioned above, the “sufficient” control-flag means that if the module passes, that is enough and the remaining modules in the same context (“auth”) are ignored.  On the other hand, if the module fails, that doesn't mean an overall result of fail.  If a subsequent sufficient passes, or if all subsequent required modules pass, then the overall result is pass.  (You can think of sufficient as if no failures so far, stop checking and return success.)

The “required” control-flag means it isn't enough to pass this one module to be allowed to run the command.  All required modules tried must say “pass” before access is granted by PAM.  If any required module fails, the remaining required modules are still tried so that hackers won't know exactly which one failed, but it won't matter if any of them pass.  (Note that a sufficient modules that passes will prevent any following required modules from running, as PAM will have stopped at that point.)

The “requisite” control-flag means the same as the required flag, except when the module fails.  As with required, the overall result is fail.  The difference is that with requisite, no further modules are tried.  (It is a fail-fast version of required.)

The “optional” control-flag means that the success or failure of that module has no effect.  Generally, this flag is used for session modules only.

A missing module acts like a “fail”, and the error is logged (via the system logging daemon, usually syslog).

If a line in the configuration file starts with a dash, the error isn't logged.  This can be useful for modules that may not be present, for example, a module for fingerprint authentication may not be present, but if it is, it should be used.

To prevent authentication failure when a module is missing, such modules should be used with sufficient rather than required, such as:

-auth   sufficient   pam_fingerprintd.so

The order of the modules is significant.  If a prior required module fails, and a later sufficient module passes, access will be denied.  For example:

auth     required     pam_moduleA
auth     sufficient   pam_moduleB
auth     required     pam_moduleC

What policy does this implement?  It says access is allowed: if both modules A and B pass, or if both modules A and C pass.  On the other hand, the policy implemented with:

auth     sufficient   pam_moduleB
auth     required     pam_moduleA
auth     required     pam_moduleC

says to allow access: if module B passes, or if modules A and C both pass.

But, what does pass mean exactly?  Each PAM module examines information provided by the program requesting authentication (usually the user's ID and a supplied password), plus other information found elsewhere (often there are per-module configuration files found in /etc/security).  PAM then decides if the current user passes the authentication test and meets the required account policies.  If so the module returns “pass”; otherwise it returns “fail”.

Actually PAM modules can return several different status values and not simply pass or fail.  The control flag can say to do this if the status is one thing, and to do that if the status is something else.  In practice the older scheme of just pass and fail, with the control-flag keywords of sufficient and required, is flexible enough for most policies, and is commonly used.

You might occasionally see other control-flags listed in some configuration files.  The “pam” man page and PAM Administrator's Guide describe everything that can go there in gory detail.  But for most policies, you only need to understand the required and sufficient control-flags.

It is not specified what happens when the PAM config file for some application is empty, or all of the (say) AUTH modules have been commented out, or listed as optional.  By reading the source for Linux PAM (especially pam_dispatch.c) and running some experiments, I have determined that Linux PAM does the safe action: it fails in these cases.  But system administrators should not rely on this undocumented behavior.  Always have some required (or sufficient) module for each type, even if only pam_deny.so or pam_permit.so.

In short, all modules of the correct type (context) are tried in the order listed, except when a sufficient module passes, or a requisite module fails.  In those two cases, the remaining modules are not run and PAM returns a result immediately.

PAM Walk-through (II):

Back to our story!  Recall the PAM configuration file for hwbrowser above also requires the common policy in system-auth to pass.  The auth and account lines (remember you can ignore the rest) from the /etc/pam.d/system-auth file look like this (the default file on an old Fedora Linux system):

auth        required      pam_env.so
auth        sufficient    pam_unix.so nullok try_first_pass
auth        requisite     pam_succeed_if.so uid >= 500 quiet
auth        required      pam_deny.so

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

Each of these modules should be looked up in the PAM reference documentation.  That would tell you what each is for and what the module arguments such as “nullok” mean.  Basically this (system default) policy authorizes a user to run some command if the user provides a valid password.  (You've probably have seen this behavior when you try any command with a similar access policy (hwbrowser, chfn, and passwd are some examples); as root you don't get asked a password to run hwbrowser, but as a regular user you must supply a password to run it.)

Note the requisite control-flag.  As mentioned previously, this is the same as required except that if the module fails no further modules are tried.  In other words requisite is a “fail fast” version of required.  So this auth section says access is permitted if the user supplies a valid password.  If they don't, fail quietly if this is a system account, otherwise, pam_deny fails with a log message.

The pam_unix.so and pam_succeed_if.so modules are interesting because they are used in two different contexts, account and auth!  The explanation is that modules have different set of functions that get invoked depending from which context they are called.  Take the pam_unix.so module for example:  In the auth context this module checks the user's name and password, but in the account context this same module checks the account's aging and expiration information instead.  Many modules have multiple purposes, depending on the context.  When reading the documentation for some module, be sure to refer to the part that describes the purpose of the module, for the context in which that the module is used.

On Linux, pam_unix.so does not currently check if a user's account is locked!  It logically should do so in the account context, but apparently the assumption is that a locked account won't pass the auth context, since locking accounts on Unix and Linux is done by altering the password hash.  However, if a user uses biometrics or SSH keys to authenticate, the mangled password hash is never examined!

And what if you don't use the shadow suite, but something like LDAP instead?  Then you just use a different module.  This is the power of PAM: an easy way to change which authentication methods are used without re-writing all your applications, or changing the configuration of each application separately.

The pam_unix.so module (and similar modules on other systems such as the Solaris 8 pam_pwdb.so module) use standard system calls to check users and passwords.  On modern systems these system calls can use different account databases depending on settings in the /etc/nsswitch.conf file.  So, to use (say) NIS you either use the pam_nis.so module (if one is available on your system), or use pam_unix.so and configure the nsswitch.conf entries for passwd, shadow, and group to use NIS.  A recently developed module uses SSSD, a common service for all remote authentication, which can cache credentials for when the network is unavailable.

All these modules are nice, but the overlap in functionality means there are many, slightly different ways to use the same authentication service (single sign-on).  To use LDAP for example, you could:

The bottom line is that there is more than one way to configure a given access policy!  However, there can be subtle differences in the various modules.

The account section is interesting.  The first line says the user must have a valid (unexpired) user account.  The second says it is sufficient if that user is listed in the local /etc/passwd and /etc/shadow files, while the third line says it is sufficient if you are a system account (that is, an account with a UID of less than 500).  But the second through fourth account lines don't seem to do anything!  You can comment out those three lines and the result is apparently the same policy.  (What policy do you think they were trying to implement here?)

The explanation may be that (as mentioned above) pam_unix uses the standard system calls to check for users and passwords, and those system calls may not be checking any local files.  So the intended policy is probably to allow a user if it is a standard system account, or if it is a valid account in the local /etc/passwd and /etc/shadow files, or if it is a valid account in the standard (usually networked) database.  (What changes would you make to correctly implement this policy?)

(Solaris Note:  Solaris 10 and newer no longer support pam_unix or pam_pwdb.  These have been replaced with a bunch of better modules.  See the Solaris pam.conf(4) man page for more information.)

Making Policy Changes:

While you could restrict the use of hwbrowser to root by changing the permissions on the program (or change group membership or by adding ACLs), this is not a good way to do control access.  Such changes get lost when updating commands, are hard to remember, and do not log violations.  So you should use PAM to enforce your policies whenever possible.

Suppose you decide to change the hwbrowser configuration so that only root is allowed to run the command.  Take another look at the hwbrowser configuration file's auth lines:

auth       sufficient   pam_rootok.so
auth       sufficient   pam_timestamp.so
auth       required     pam_stack.so service=system-auth

What would happen if you changed “sufficient” to “required” in the rootok line?  Now root access would be required.  However PAM would still try pam_timestamp and run through the modules listed in the “system-auth” file.  If those lines were commented out as well as changing “sufficient” to “required” in the first line, then you get the behavior you want: only root can run the command.  Changing sufficient to requisite would work too.  (To comment out a module means adding a “#” in the front of the line listing that module.)

(Note that just commenting out the second and third auth lines will work too; if there are no required modules, the failure of all the sufficient modules will cause PAM to` refuse authorization.  But this is undocumented PAM behavior, so I suggest you change sufficient to required as well as commenting out the second and third lines.)

Other PAM changes are simpler.  To modify “su” to use the “wheel” group membership for sufficient or required permission to run a command, examine the /etc/pam.d/su file.  To change the minimum required password length, look at the /etc/pam.d/passwd file, only in that case you will only see references to the system-auth file.  So go look at that file again.  If you want a different policy for some command, don't use include in that command's PAM configuration file.

Many system administrators mistakenly think they can set minimum password length in other configuration files such as /etc/default/login, /etc/login.defs, or /etc/libuser.conf.  But changes to those files may or may not have any effect!  It depends on your version of Unix or Linux used, as well as the version of the passwd command on your system and which database you are using (e.g., shadow suite, NIS, etc.).

You should always set the password policy using PAM.  If you do set a minimum length in some other file(s) and those settings are used on your system, then a candidate password must meet the criteria from both the file(s) and from PAM.  So you might as well only set your policy with PAM.

When changing passwords (or other user credentials), the “password” modules are used to determine password policies such as which database of passwords to use (e.g., /etc/shadow, RADIUS, LDAP, etc.) and password constraints (e.g., minimal length, types of characters required such as a mix of letters and digits, etc.). The “auth” modules are still used to make sure the current user has permission to change the password, but it is the password modules that determine acceptable passwords.

If you look up the password modules found in the system-auth file in the on-line PAM reference, you will quickly discover that only the “pam_cracklib” module controls password constraints such as minimum password length, at least on Linux.  (Newer distros of Linux use the newer but compatible “pam_pwquality” module instead.)  This is done in a complex way so you really need to read that module's description and the provided examples carefully.  (See some notes regarding pam_cracklib and pam_pwquality below.)

Not all systems provide the exact same PAM modules, but usually there are similar ones to the PAM-Linux distribution.  If your system doesn't seem to have pam_cracklib or pam_pwquality, see if there is another module with similar functionality.  On Solaris for example, you can use pam_authtok_check instead.

Some Security Issues of Policy Changes:

You need to be careful!  Modules with the same name may work differently on different systems.  Always check the documentation and verify your PAM configuration files implement the policies you think they do.  A good way to do that is to run experiments to test your policies.

The default policies (the ones controlled by PAM anyway) aren't always the most secure, and you should carefully review them on any upgraded system as part of the post-install process.  A larger organization can have standardized PAM configurations that can be applied as a patch or RPM (or similar) package.

The Linux pam_unix module in a PAM configuration file with the context (module type) of “account” checks that an account exists and isn't expired.  It does not check if an account has been locked or has an invalid shell!  (Apparently pam_unix assumes the auth component will fail if the account is locked.)  This means that users using SSH keys to log in will be allowed to do, so even if you lock their accounts with the “passwd -l” command (since sshd doesn't use the PAM auth modules in this case)!  Setting an invalid shell is checked with the Linux pam_shells module, but that is usually included only in the configuration files for FTP servers.  (I've been locking users out this way for years and I've never realized the danger.  Now I always add pam_shells in the account part of my PAM configuration (as required), and lock accounts by specifying an invalid shell such as /bin/false, for login, sshd, and other remote access services.)

Solaris 10 and newer no longer use pam_unix.  It has been replaced it with other modules that each do part of what pam_unix used to do.  The new pam_unix_account module does check for locked accounts.

Some versions of sshd do check for locked accounts, but only when configured to not use PAM.  When configured to use PAM, and sshd uses keys to authenticate the user, sshd apparently only uses the PAM account and session modules and not the auth modules.  While Linux PAM doesn't contain any(!) account modules that can check for locked accounts, you may be able to find one you can use from non-standard PAM modules found on the Internet, or create one yourself.  (Note that not all *nix systems have this security bug.)

The best way to prevent account access is to expire the account.  That is checked by default in all PAM stacks.  Access can be restored by setting a new expiration date (in the future).

PAM Advanced Syntax:

As mentioned previously PAM modules are not limited to returning only pass or fail.  Linux PAM defines over 30 different return values that a module might return (listed below).  Instead of one of the original control flags (required, sufficient, requisite, and optional) you can use the new syntax with square braces, like this:

type    [value=action value=action ...] module options

You can list any one of seven different actions for each possible return type.  That's a lot of flexibility!  The value is one of the following PAM module return values:

ABORT
Critical error, immediate abort,
ACCT_EXPIRED
User account has expired,
AUTHINFO_UNAVAIL
Authentication service cannot retrieve authentication info,
AUTHTOK_DISABLE_AGING
Authentication token aging disabled,
AUTHTOK_ERR
Authentication token manipulation error,
AUTHTOK_EXPIRED
Authentication token expired,
AUTHTOK_LOCK_BUSY
Authentication token lock busy,
AUTHTOK_RECOVERY_ERR
Authentication information cannot be recovered,
AUTH_ERR
Authentication failure,
BUF_ERR
Memory buffer error,
CONV_ERR
Conversation failure,
CRED_ERR
Failure setting user credentials,
CRED_EXPIRED
User credentials expired,
CRED_INSUFFICIENT
Insufficient credentials to access authentication data,
CRED_UNAVAIL
Authentication service cannot retrieve user credentials,
IGNORE
The return value should be ignored by PAM dispatch,
INCOMPLETE
Please call this function again to complete authentication stack; before calling again, verify that conversation is completed,
MAXTRIES
Have exhausted maximum number of retries for service,
MODULE_UNKNOWN
Module is unknown,
NEW_AUTHTOK_REQD
Authentication token is no longer valid; a new one required,
NO_MODULE_DATA
No module specific data is present,
OPEN_ERR
Failed to load module,
PERM_DENIED
Permission denied,
SERVICE_ERR
Error in service module,
SESSION_ERR
Cannot make/remove an entry for the specified session,
SUCCESS
Success,
SYMBOL_ERR
Symbol not found,
SYSTEM_ERR
System error,
TRY_AGAIN
Failed preliminary check by password service,
USER_UNKNOWN
User not known to the underlying authentication module,
default
All return values not explicitly listed.

The action says how to combine this return value for the module in the overall pass or fail value that is returned to the application for the whole stack.  It is one of:

ok
This tells PAM that the administrator thinks this return code should contribute directly to the return code of the full stack of modules.  In other words, if the state of the stack so far would lead to a return of SUCCESS, the module's return code will override this value.  If the former state of the stack holds some value that is indicative of a modules failure, this “ok” value will not be used to override that value.  (This is essentially the same as the old “required” flag when the module returns SUCCESS, and is indeed often used as “[success=ok...]”.)
done
Equivalent to ok with the side effect of terminating the module stack and PAM immediately returning to the application.  (Similar to “sufficient”.)
bad
This action indicates that the return code should be thought of as indicative of the module failing.  If this module is the first in the stack to fail, its status value will be used for that of the whole stack.  (This is similar to the “required” flag when the module did not return SUCCSSS.  It is often used as “[... default=bad]”.)
die
Equivalent to bad with the side effect of terminating the module stack and PAM immediately returning to the application.  (Similar to “requisite”.)
ignore
The module's return status will not contribute to the return code the application obtains.  (Similar to “optional”.)
reset
Clear all memory of the state of the stack and start again with the next stacked module.
n
(An unsigned integer) Skip the next n modules in the stack.  (This provides a primitive form of if ... then control flow.)

Each of the four control-flag keywords (required, requisite, sufficient, and optional) have an equivalent expression in terms of the [...] syntax:

required
[success=ok new_authtok_reqd=ok ignore=ignore default=bad]
requisite
[success=ok new_authtok_reqd=ok ignore=ignore default=die]
sufficient
[success=done new_authtok_reqd=done default=ignore]
optional
[success=ok new_authtok_reqd=ok default=ignore]

As an example from an old Fedora system, consider the auth lines from the smartcard-auth PAM file:

auth        required      pam_env.so
auth        [success=done ignore=ignore default=die] pam_pkcs11.so nodebug wait_for_card
auth        required      pam_deny.so

Some Notes Regarding pam_cracklib and pam_pwquality:

The cracklib and pwquality PAM modules are very similar; pwquality is a modern replacement for cracklib.  For both, the documentation uses the term minimum length or “minlen”, but it's really a “score” of complexity, and not just the length of a password.  A password is acceptable if it passes certain checks (for example, not based on a dictionary word or your account name), and has a complexity score equal to or greater than minlen.  This score for a candidate password is computed as follows when using the default settings (cracklib and older pwquality):

With pam_cracklib and older pam_pwquality, the default values for lcredit, ucredit, dcredit, and ocredit are 1 (one).  The defaults for modern pam_pwquality are different: 0 (zero) for all credits and minlen defaults to 8.  (These defaults are almost certainly no longer sufficient to prevent password cracking by brute-force, so consider increasing minlen on your systems.)

The DoD/DISA recommendation (2015) is stricter:
minlen=14 dcredit=-1 ucredit=-1 ocredit=-1 lcredit=-1
which requires at least one each of a digit, an uppercase letter, a lowercase letter, and a symbol.  (This recommendation changes over time.  You can find some recommendations in DoD Changes to UCR 2008, section 5.4.6.2.1.2, part 1b subsection 8, (PDF), I think from 2015; DoD STIG for Red Hat Enterprise Linux 7; or from “Passwords do not meet complexity or strength.” STIG teleconference (2014).

If the score is less than the value for minlen, the password is not acceptable (it “fails” the module's “simplicity” test.)  Here's an example to illustrate how the complexity is calculated:

Suppose minlen=8 is used with pam_cracklib and pam_pwquality.  If the user tries to set a password of “foobar” then cracklib/pwquality will not allow it.  The score for this password is 6 + 1 = 7 (assuming lcredit=1) but the minimum allowed score is 8.  The score for “Foobar” would be 6 + 1 + 1 = 8, and that would “pass” this test.  The score for “aB1$” would be 4 + 1 + 1 + 1 + 1 = 8.

You don't have to use such a complex test if you don't want to.  If you set the credit for each type of character to zero then only the length of the password will matter.  (This is in fact the default with pam_pwquality.so.)

Besides this strength/complexity test for a minimum “length”, pam_cracklib/pwquality has a hardcoded minimum number of bytes (characters) in the password of 4.  (Perhaps because the U.S. government specifies that as a minimum length, in FIPS-112.)  In addition the system password changing code often defines a minimum length (number of characters, not minimum strength), whether or not you use pam_cracklib/pam_pwquality.  For Solaris, a length is defined in /etc/default/passwd.  For Linux (when using the “shadow” suite) it is defined in /etc/login.defs and possibly other files (e.g. /etc/libuser.conf).

Setting the credit for (say) digits to a number greater than one or to a negative value allows more complex passwords to be required.  Say you set dcredit=3.  Then you will add zero to the score if no digits are in the candidate password, add one if there is one digit, two if there is two digits, and three if there is three or more digits.  Basically the value is a maximum amount of extra credit you can have by using that type of character in your passwords.  Setting the four credit values to 0 (zero) means only the length of the password matters.

A negative credit number “-N” means the same as a positive number except that you must have at least that many or the candidate password will “fail” regardless of its overall score.  For example, setting:

   password required pam_cracklib.so dcredit=-1 ucredit=-1 minlen=8

or

   password required pam_pwquality.so dcredit=-1 ucredit=-1 minlen=8

(and leaving lcredit and ocredit at the default value of 1) will require all passwords to have at least one digit and at least one uppercase letter.  So “Foobar” will fail even though its score is 8 (6 + 1 + 1).  But “Fo0bar” (note the zero) will pass this test (its score is 6+1+1+1=9, and it has the required 1 digit).

Another cracklib/pwquality setting is “difok” (note the single “f”).  It determines how many characters must be different (inserted, removed, or replaced) from your old password.  The default is 1 for pam_pwquality on Fedora (although the man page says it is “5”), which means your new password must have at least one character not present in the old password or be one character shorter.  (It knows you previous password since you enter it when running the passwd command.  For older passwords, it only knows their hashes, so this setting only looks at your current password.)

The difok default can be set in the /etc/security/pwquality.conf file.  Note a bug(?) in these modules won't allow you to set a new password the same as your current one, even if the value of difok is zero.

To make the system remember old passwords, you need to add an argument of “remember=N” to the pam_unix.so module (the one for the “password” context, not the one for “auth”.)  (Modern systems often have a pam_pwhistory module, but either seems to work.)  Try adding “remember=3” and then change your password a few times.  Then examine the /etc/security/opasswd file.  (Note, if using a single sign-on system such as LDAP or Kerberos, password history doesn't work since the old hashes are only saved on the local system.  Making this work properly would require a lot of changes; this is unlikely to happen anytime soon.)

Cracklib/pwquality has a number of options that can be used to support different policies, such as reject_username, maxrepeat, and minclass.  See the Linux PAM documentation for cracklib/pwquality for a list of all options you can use.  (Also, test that your policy works as expected!)

Summary:

PAM is powerful but difficult, which is why it is so important to understand how to use it correctly to enforce security policies.  By controlling which modules are used, their options, the order of modules, and which control-flags are used, you have the flexibility to enforce any security policy you can imagine.  You can use the more complex configuration file syntax for even more precise control.  (You can read about that if you want to go beyond the simple syntax discussed in this tutorial.)  You can even write your own modules (or you can hire someone to do so).

Defining, implementing, and auditing security policies is a common and vital task of the Unix/Linux system administrator.  You should become familiar with the standard PAM modules on your system so you will have an idea of the sort of policy you can use PAM to enforce.  From there you should read the other documentation found at linux-pam.org.  (This was formerly kept at www.kernel.org/pub/linux/libs/pam/.)