PAM Explanation

The Pluggable Authentication Modules system allows an administrator to fully control how authentication is done on a system, and releaves a developer from implementing all kinds of authentication mechanisms.

The "old" way of doing authentication is through /etc/passwd, which contained the username, uid and password. As long as everybody used /etc/passwd there was nog problem, but when different schemes came into play, like NIS, Kerberos, LDAP, and even the shadow system, it meant that developers needed to support all these different ways in their product, which created a enormous amount of duplicated code and a lot of overhead for the developers. To overcome this issue PAM was created. PAM provides a single interface for the developer to talk to. It just tells an application if a user is allowed or not. Meaning that the developer only has to support PAM.

By means of modules the administrator can on the fly change the e.g. the login policy for a certain system from /etc/passwd to kerberos without the users or applications noticing the change. And as long as all programs on a certain system, responsible for user authentication, work with PAM all should be fine.

login ftp telnet ssh
PAM API
PAM library PAM configuration
PAM SPI
account checks authentication session management password management

As said PAM is a modular system, hence the name. The configuration of PAM can be done in two different ways. You could have one long configuration file, or you could have a /etc/pam.d directory which contains several files for the configuration. This document will only discuss the /etc/pam.d variant.

Within the /etc/pam.d directory there are files for every program that needs authentication. In each file there are rules for that specific service. Of course there would be a lot of duplication if your created rules specific for every service, since most services will use the same way of authentication. To solve this issue there is an include statement that you can use in the configuration files.

auth include file
which includes the auth sections from the mentioned file.

On Red Hat based systems the included file is often system-auth, while for Debian based system you have a common-* file per "type" in the configuration file.

The "type" mentioned is the first colomn in the configuration file. The complete syntax for the file is:

type  control  module-path  module-arguments
The type can be:
Type Function Description
account pam_acct_mgmt Tests if the user is allowed to access the service, meaning if the password is not expired, if the user is allowed during this time of day, if the load is not too high, etc.
auth pam_authenticate This is the actual authentication. In the good old fashioned way it means that the password is checked to see if the user is who he or she claims to be.
pam_setcred Sets UID, GID and limits
session pam_open_session Things that should be done when the user is authenticated, and thus logs in.
pam_close_session Things that should be done when the user logs off.
password pam_chauthtok Used when the user wants to change the authentication credentials (password). Check password length, strength, etc.

Per type you can have multiple lines. So you can have "stacked" modules that describe what should be done, or to what rules the username and credentials should comply, before a user is authenticated to the system.

The second column in our configuration file is the "control" column. The field tells PAM what it should do when the module reports a failure. This field can be:

[value1=action1 value2=action2 ...]

PAM started with some predefined actions, which are described below. The use of [...] in the control field is a later addition that gives you full control of PAMs actions. The list below is split in two parts, those that are relevant for system administrators and those that are needed for debugging modules. Within the remainder of this document we are only concerned about the administrators part.

For system administrators:

abort
Critical error (?module fail now request)
acct_expired
User account has expired
auth_err
Authentication failure
authinfo_unavail
Underlying authentication service can not retrieve authentication information
authtok_err
Authentication token manipulation error
authtok_expired
user's authentication token has expired
authtok_disable_aging
Authentication token aging disabled
authtok_recover_err
Authentication information cannot be recovered
cred_err
Failure setting user credentials
cred_expired
User credentials expired
cred_insufficient
Can not access authentication data due to insufficient credentials
cred_unavail
Underlying authentication service can not retrieve user credentials unavailable
default
all not explicitly mentioned values
ignore
Ignore underlying account module regardless of whether the control flag is required, optional, or sufficient
maxtries
An authentication service has maintained a retry count which has been reached. No further retries should be attempted
module_unknown
module is not known
new_authtok_reqd
New authentication token required. This is normally returned if the machine security policies require that the password should be changed beccause the password is NULL or it has aged
perm_denied
Permission denied
session_err
Can not make/remove an entry for the specified session
success
Successful function return
try_again
Preliminary check by password service
user_unknown
User not known to the underlying authenticaiton module

Debugging modules:

authtok_lock_busy
Authentication token lock busy
bad_item
Bad item passed to pam_*_item()
buf_err
Memory buffer error
conv_again
conversation function is event driven and data is not available yet
conv_err
Conversation error
incomplete
please call this function again to complete authentication stack. Before calling again, verify that conversation is completed
no_module_data
No module specific data is present
open_err
The module could not be loaded
service_err
Error in service module
symbol_err
Symbol not found
system_err
System error

The action part can be any of:

ignore
The return status will not contribute to the return code.
bad
The return status is set to fail.
die
The return status is set to fail and the stack is terminated immediately and the return status reported to the application
ok
If the modules fails, the total stack state will be fail, if the stack was already fail, the return code of this module will do nothing.
done
Some as ok, but with direct termination of the stack
reset
Clear all memory of the state of the module stack and start again with the next module.
requisite ([success=ok new_authtok_reqd=ok ignore=ignore default=die])
When the module reports failure, the user gets denied immediately. Meaning that e.g. a non-existend username can immediately be denied. The downside is that an attacker knows that the username is invalid.
required ([success=ok new_authtok_reqd=ok ignore=ignore default=bad])
When the module reports failure, the user gets denied after all other lines in the type-section are checked. The reason that even when the user is denied access all other lines are checked has to do with system reponse. By checking all other lines a possible attacked has no clue which module created the denial state, and thus makes it harder for the attacker to create an alternative attack method.
sufficient ([success=done new_authtok_reqd=done default=ignore])
If no status is set by a previous required module and this module reports success, the PAM framework returns success to the application immediately without trying any other modules. A failure means that the remaining lines are checked.
optional ([success=ok new_authtok_reqd=ok default=ignore])
According to the pam(8) manpage, will only cause an operation to fail if it's the only module in the stack for that facility

The third field in the configuration is the "module-path". This tells PAM the modules to use and most the times the path to find the module. According to the LFS, the modules should be located in /lib/security. However the PAM default is /usr/lib/security. For a complete overview of the different modules and their options see: http://www.kernel.org/pub/linux/libs/pam/Linux-PAM-html/sag-module-reference.html

The last field is the "module-arguments" which varies per module.

PAM examples

The examples below are a mix of Debian, Red Hat and CentOS system configurations mixed with additional features.

The following examples are tested with login and with sshd. Do know if you should replace system-auth (RHEL) or common-* (Debian) files with it.

Example: Be a minimal plain old Unix replacement

To act as a normal unix machine using /etc/passwd, /etc/shadow and /etc/group we use the pam_unix.so. We need this anyway to support the system accounts of our system like root.

# Per default the pam_unix.so module treats empty password fields as
# disabled accounts. The "nullok" option overrides this behaviour.
# To disable an account according to CERT policies, change the
# password field to * and set the login shell to /bin/false.
#
# The "md5" option enables MD5 passwords.  Without this option, the
# default is Unix crypt.
auth		sufficient	pam_unix.so nullok
auth		required	pam_deny.so

account		required	pam_unix.so
account		required	pam_permit.so

session		required	pam_unix.so

# NOT tested
password	sufficient	pam_unix.so shadow nullok md5
password	required	pam_deny.so

Example: plain old unix towards pam only control

Especially for the login functionality, there are a couple of "native" files that give a system administrator control of who is allowed to do what from where with which restrictions. The first ones that you will probably know are the hosts.allow and hosts.deny files. But also /etc/securetty, /etc/login.defs, and a couple more. If we want to control everything through pam we have to adjust our stack a little bit.

Let's start with the auth section:

# Load the /etc/security/pam_env.conf file. Just to be sure
auth		required	pam_env.so

# Enforce a minimal delay in case of failure (in microseconds).
# (Replaces the `FAIL_DELAY' setting from login.defs)
# Note that other modules may require another minimal delay. (for example,
# to disable any delay, you should add the nodelay option to pam_unix)
auth		optional	pam_faildelay.so delay=3000000

# Disallows other than root logins when /etc/nologin exists
# (Replaces the `NOLOGINS_FILE' option from login.defs)
auth		requisite	pam_nologin.so

# Disallows root logins except on tty's listed in /etc/securetty
# (Replaces the `CONSOLE' setting from login.defs)
auth       [success=ok ignore=ignore user_unknown=ignore default=die]  pam_securetty.so

# Check if the users shell exists
# (Uses /etc/shells)
auth		required	pam_shells.so

# Outputs an issue file prior to each login prompt
# (Replaces the ISSUE_FILE option from login.defs).
auth		optional	pam_issue.so issue=/etc/issue

# This allows certain extra groups to be granted to a user
# based on things like time of day, tty, service, and user.
# Please edit /etc/security/group.conf to fit your needs
# (Replaces the `CONSOLE_GROUPS' option in login.defs)
auth		optional	pam_group.so

auth            sufficient      pam_unix.so nullok
auth            required        pam_deny.so

Next we adjust the account section:

# Edit /etc/security/time.conf if you need to set time
# restrainst on logins.
# (Replaces the `PORTTIME_CHECKS_ENAB' option from login.defs
# as well as /etc/porttime)
account		requisite	pam_time.so

# Edit /etc/security/access.conf if you need to set
# access limits.
# (Replaces /etc/login.access file)
account		required	pam_access.so

account         required        pam_unix.so
account         required        pam_permit.so

Then the session section:

# This module parses environment configuration file(s)
# and also allows you to use an extended config
# file /etc/security/pam_env.conf.

# Backwards compatibility for /etc/environment
session		required	pam_env.so readenv=1 envfile=/etc/environment

# Setting the locale or i18n settings
# Debian: locale variables are also kept into /etc/default/locale in etch
#         reading this file *in addition to /etc/environment* does not hurt
# RHEL:   locale variables are kept in /etc/sysconfig/i18n
#
# Debian: session       required   pam_env.so readenv=1 envfile=/etc/default/locale
# RHEL: session       required   pam_env.so readenv=1 envfile=/etc/sysconfig/i18n

# Sets up user limits according to /etc/security/limits.conf
# (Replaces the use of /etc/limits in old login)
session		required	pam_limits.so

# Sets the umask
# (Replaces UMASK setting in login.defs)
# Does not seem to have any influence on the umask...
# needs more testing
session		optional	pam_umask.so umask=0077

# The following two options report some additional
# information when a user logs in. sshd also reports
# this information, so to prevent duplicate messages
# set in sshd_config:
# PrintLastLog no
# PrintMotd no
# (Replaces the `LASTLOG_ENAB' and `MOTD_FILE' options
# from login.defs)
session		optional	pam_lastlog.so
session		optional	pam_motd.so

# Prints the status of the user's mailbox upon succesful login
# (Replaces the `MAIL_CHECK_ENAB' option from login.defs). 
#
# This also defines the MAIL environment variable
# However, userdel also needs MAIL_DIR and MAIL_FILE variables
# in /etc/login.defs to make sure that removing a user 
# also removes the user's mail spool file.
# See comments in /etc/login.defs
session		optional	pam_mail.so standard

# Create home dir if it does not exist on login
session		required	pam_mkhomedir.so skel=/etc/skel/ umask=0022

# SELinux needs to intervene at login time to ensure that the process
# starts in the proper default security context.
# Uncomment the following line to enable SELinux
# session required pam_selinux.so select_context
# Did NOT test this:
# session         required        pam_unix.so

session		required	pam_unix.so

And last the password section:

# Alternate strength checking for password. Note that this
# requires the libpam-cracklib package to be installed.
# You will need to comment out the password line above and
# uncomment the next two in order to use this.
# (Replaces the `OBSCURE_CHECKS_ENAB', `CRACKLIB_DICTPATH')
#

# This is NOT tested

password	required	pam_cracklib.so retry=3 minlen=6 difok=3
password	required	pam_unix.so use_authtok nullok md5
password        required        pam_deny.so

Example: migrate to ldap

This section builds on the previous one, but adds LDAP support. We assume that users having a UID above 500 are in LDAP and all others are in the default files (passwd, shadow, group). The password for the users in LDAP is also placed in LDAP.

One extra feature supported is the fact that we need to be able to login to our servers with a normal unix account (root) when there is trouble with LDAP.

Let's start with the auth section:

auth		required	pam_env.so
auth		optional	pam_faildelay.so delay=3000000
auth		requisite	pam_nologin.so
auth       [success=ok ignore=ignore user_unknown=ignore default=die]  pam_securetty.so
auth		required	pam_shells.so
auth		optional	pam_issue.so issue=/etc/issue
auth		optional	pam_group.so

# We assume that UIDs above 500 are in LDAP
# If LDAP fails we want to still be able to login through local accounts
auth            sufficient      pam_unix.so nullok
auth		requisite	pam_succeed_if.so uid >= 500 quiet
auth		sufficient	pam_ldap.so use_first_pass
auth            required        pam_deny.so

Next we adjust the account section:

account		requisite	pam_time.so
account		required	pam_access.so

# If the user id is below 500 end the account section, if LDAP failes
# we can still login with a local account
account         required        pam_unix.so
account		sufficient	pam_succeed_if.so uid < 500 quit
account	[default=bad success=ok user_unknown=ignore] pam_ldap.so
account         required        pam_permit.so

Then the session section:

session		required	pam_env.so readenv=1 envfile=/etc/environment
session		required	pam_env.so readenv=1 envfile=/etc/sysconfig/i18n
session		required	pam_limits.so
session		optional	pam_umask.so umask=0077
session		optional	pam_lastlog.so
session		optional	pam_motd.so
session		optional	pam_mail.so standard
session		required	pam_mkhomedir.so skel=/etc/skel/ umask=0022

session		required	pam_unix.so

And last the password section:

# This is NOT tested
# We need pam_ldap.so to set the password in LDAP
# Additional rules we might need:
# password    sufficient    pam_unix.so md5 obscure min=4 max=8 nullok try_first_pass
# password    sufficient    pam_ldap.so

password	required	pam_cracklib.so retry=3 minlen=6 difok=3
password	sufficient	pam_unix.so use_authtok md5
password	required	pam_ldap.so use_authtok
password        required        pam_deny.so

Example: add kerberos support

Only tested with LDAP, kerberos still needs testing.

This example expands the above one, with kerberos. The users above UID 500 are still in LDAP, but their password is stored in kerberos.

NOTE: Debian supplies: http://www.eyrie.org/~eagle/software/pam-krb5/
RHEL supplies: http://people.redhat.com/nalin/pam_krb5/

auth		required	pam_env.so
auth		optional	pam_faildelay.so delay=3000000
auth		requisite	pam_nologin.so
auth [success=ok ignore=ignore user_unknown=ignore default=die]  pam_securetty.so
auth		required	pam_shells.so
auth		optional	pam_issue.so issue=/etc/issue
auth		optional	pam_group.so

# pam_ldap.so is in here for migration purposes, when all your
# users are kerberized you can remove the pam_ldap.so line
auth		sufficient	pam_unix.so nullok try_first_pass
auth		requisite	pam_succeed_if.so uid >= 500 quiet
auth            sufficient      pam_ldap.so use_first_pass
auth		sufficient	pam_krb5.so use_first_pass
auth		required	pam_deny.so

account		requisite	pam_time.so
account		required	pam_access.so

account		sufficient	pam_unix.so broken_shadow
account		sufficient	pam_succeed_if.so uid < 500 quiet
account		required	pam_ldap.so
account	[default=bad success=ok user_unknown=ignore] pam_krb5.so
account		required	pam_permit.so

session		required	pam_env.so readenv=1 envfile=/etc/environment
session		required	pam_env.so readenv=1 envfile=/etc/sysconfig/i18n
session		required	pam_limits.so
session		optional	pam_umask.so umask=0077
session		optional	pam_lastlog.so
session		optional	pam_motd.so
session		optional	pam_mail.so standard
session		required	pam_mkhomedir.so skel=/etc/skel/ umask=0022

# pam_ldap.so for session?
session		optional	pam_keyinit.so revoke
session		required	pam_unix.so
session		optional	pam_krb5.so minimum_uid=500

# Set password in krb database
password	requisite	pam_cracklib.so try_first_pass retry=3
password	sufficient	pam_unix.so md5 shadow nullok use_authtok
password	required	pam_krb5.so use_authtok clear_on_fail
password	required	pam_deny.so