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

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