September 24, 2004

Hardening the PAM framework

In yesterday's article we began looking at how PAM can securely authenticate Windows users. Today we'll check the PAM framework, harden the basic services that we expect to authenticate to, and look at new PAM modules that might make our systems more secure.

This article is excerpted from the recently published book Hardening Linux, published by McGraw-Hill/Osborne, 2004, with permission from McGraw-Hill.

First make sure that /etc/pam.conf doesn't exist unless this is an old system running an older version of PAM.

Next, make sure that /etc/pam.d exists, and contains PAM configuration files.

The first user to log in at the console of a Linux box can get ownership of many hardware devices, depending on how PAM is configured. Traditionally, Unix systems let the superuser (root) own the hardware, but to make it easy for desktop users to access devices such as sound cards, CD drives, and the like, the first console user can be set up to have ownership of these devices. Ownership reverts to root when the console user logs out. The device list is in /etc/security/console.perms, and ownership is changed by the PAM module pam_console.so.

The console user is also allowed to access PAM-aware applications with their names in the /etc/security/console.apps directory. Halting and rebooting the machine are typically controlled by pam_console.so.

Ensure that the /etc/pam.d/other and /etc/pam.d/common-* or /etc/pam.d/system-* configuration files contain acceptable values. These are the files that are referenced for any PAM application where there isn't a specific configuration file, so they're like default settings for PAM services.

Different distributions may handle this differently, the /etc/pam.d/other file is the fallback name for PAM to use if it can't find a service name specifically. It may reference /etc/pam.d/common-* or /etc/pam.d/system-* a set of files for each interface, such as /etc/pam.d/common-auth, /etc/pam.d/system-auth, etc/pam.d/system-password, and so on.

Alternately, you can strengthen your Linux system to not allow unknown programs to authenticate. This is highly recommended. Here's a sample /etc/pam.d/other file that does not allow unknown services to authenticate a user:

auth required /lib/security/pam_deny.so
auth required /lib/security/pam_warn.so
account required /lib/security/pam_deny.so
account required /lib/security/pam_warn.so
password required /lib/security/pam_deny.so
password required /lib/security/pam_warn.so
session required /lib/security/pam_deny.so
session required /lib/security/pam_warn.so

This configuration allows you to follow the "default deny" rule of security for unknown services. As with all PAM changes, you'll want to ensure that /lib/security/pam_warn.so and /lib/security/pam_deny.so exist prior to implementing this configuration.

Traditional services

The /bin/login program is the traditional Unix console/terminal authentication mechanism. Linux's /bin/login uses PAM.

Here's the default /etc/pam.d/login from Red Hat Enterprise Linux AS 3.0:

#%PAM-1.0
auth required pam_securetty.so
auth required pam_stack.so service=system-auth
auth required pam_nologin.so
account required pam_stack.so service=system-auth
password required pam_stack.so service=system-auth
session required pam_stack.so service=system-auth
session optional pam_console.so

This configuration file calls four different modules: pam_securetty.so, pam_stack.so, pam_nologin.so, and pam_console.so. The pam_stack.so module is called for four different interfaces: auth, account, password, and session.

The authentication stack first checks to see if the user is logging on from a device listed in /etc/securetty. Next it uses the normal password authenticator, pam_stack. The service argument tells PAM to use the /etc/pam.d/system-auth configuration file, which looks like this:

#%PAM-1.0
# This file is auto-generated.
# User changes will be destroyed the next time authconfig is run.
auth required /lib/security/$ISA/pam_env.so
auth sufficient /lib/security/$ISA/pam_unix.so likeauth nullok
auth required /lib/security/$ISA/pam_deny.so
account required /lib/security/$ISA/pam_unix.so
password required /lib/security/$ISA/pam_cracklib.so retry=3
password sufficient /lib/security/$ISA/pam_unix.so nullok use_authtok md5 +
password required /lib/security/$ISA/pam_deny.so
session required /lib/security/$ISA/pam_limits.so
session required /lib/security/$ISA/pam_unix.so

For auth, we can see that the first thing that is required is passing through pam_env.so, which sets up the user's environment using variables specified in /etc/security/ pam_env.conf. Typically, it's not used, but could be used to have PAM set the DISPLAY environment variable, for instance. There are many commented-out examples in the default file. This module is required.

Next, we have a call to pam_unix, the PAM password authenticator. The first argument is likeauth, which makes the module return the same value for credential (password) changes as for authentication, ensuring equivalent security in both cases. The next argument, nullok, should be removed for a hardened system, as it allows null passwords to be set for accounts. This module is marked as sufficient so that the next module can be tested.

This brings us to pam_deny.so. This module disallows access, so this line is a "default deny" line unless the sufficientpam_unix passes success back.

Next, we have an account method, which uses the pam_unix.so module. This module checks to see if the user has a valid account and that the account hasn't expired. This module is required because we obviously don't want people authenticating who don't have accounts on the system.

Next, we have three password methods. The first, pam_cracklib.so, checks the password against common words and disallows them as passwords. We have it set to allow three attempts to find a non-dictionary password; this method is set to required. After checking the word, pam_unix is used with this method. For hardening a system, we'll want to remove nullok, which would allow a blank password. We're also set up for using an authentication token (already supplied password), md5, and shadow passwords. The use_authtok parameter tells the module to use the already supplied password instead of prompting for it. Since the user was already prompted for a password, this is obviously good behavior, and ensures that the password has been through pam_cracklib.so. The md5 parameter says that the system should have the password using the Message Digest 5 hashing function. This function is more secure than the traditional DES-based Unix password hashing method. Finally, the shadow parameter tells the module to store the password hash in the /etc/shadow file, which should only be accessible to the root user ID, rather than in the traditional /etc/password file, which is world-readable. This module is marked as sufficient, because it's followed by the "default deny" of pam_deny.so.

Next, we have the session methods pam_limits.so and pam_unix.so. The pam_limits module sets resource limits based upon the file /etc/security/limits.conf. By default, all the entries in /etc/security/limits.conf are commented out for most distributions, but you can control things like the size of core files, memory used, CPU used, number of processes, and maximum number of logins by user, group, or default. Finally, we see the pam_unix module for this method. This method updates the lastlog file with the last login time, and provides the last time the user logged in, which is printed by some programs like /bin/login. It is also set up to log the start and finish of a session at the INFO level in syslog. Common distributions set this to be /var/log /messages.

Now we're back to the original file again, but since the same pam_stack module was used for each method, we simply have to check out what pam_console does. As we discussed earlier, this module generally deals with device ownership for the user logged in at the physical console.

As you can see, there's a lot of complexity and intertwining dependencies when dealing with PAM. Before you modify your configuration, you should trace through the stack of modules to ensure you understand what happens normally for that service. Backing up the files before modification is also critical. I recommend changing only one configuration file at a time, and testing that change fully prior to making new changes. Testing should include testing a valid account with no password, an invalid password, and a correct password, and testing an invalid account with any password and no password. Whenever possible, testing should be done in a controlled network environment, or with ipchains or iptables filtering protecting the service if it's a network service.

You should examine login, passwd, sshd, su, and any other PAM modules you expect to use regularly to ensure they're set up correctly. In general, the distribution defaults are fine for the default modules, other than any special purpose modules you might want to add.

Now let's look at adding some PAM modules to enhance your system's security.

A BSD-like wheel group

The module pam_wheel.so is included by default in the PAM package. It allows you to specify which users may su to root. Typically, on BSD systems, only users who are in the wheel group, typically group 0, are permitted to use the su command to gain root privileges. This means that even if a user knows the root password, they're not permitted to use it to gain root access from their account. Obviously, enforcement of this requires that root be not allowed to directly access the system's login methods. Typically, this means not allowing root to use SSH remotely.

To enable wheel group in Linux, insert the following line to the top of your PAM configuration for su (/etc/pam.d/su):

auth required pam_wheel.so use_uid group=wheel

If you want to use the default group that's set up for root, with a group ID of 0, you don't need to use group=wheel. Renaming the group ID 0 group in /etc/group to wheel will make your configuration easier to understand for administrators used to BSD systems.

Per-user temporary directories

On multiuser systems, flaws in file handling are often exploited as race conditions. For systems where we expect multiple users, there's a PAM module, pam_mktemp, which makes a private per-user temporary directory and then assigns both the TEMP and TEMPDIR environment variables to that directory. Lack of a shared /tmp directory eliminates most of the exposure to symbolic link race conditions. Adding pam_mktemp to the module stack for interactive logins (ssh, login, telnetd, and so on) will help limit race condition exploitation for root-executed and user-executed processes.

Require strong passwords

Many administrators like to require strong passwords. It's a tradeoff that should be considered carefully. Weak passwords are often easy for users to remember, but may be attackable. First of all, if a service is network available, such as the SSH daemon, an attacker can try to brute force or dictionary attack that service by using a program to try known user IDs and dictionary word passwords. If the administrator isn't paying attention to failed authentication in the logs, this may gain the attacker enough time to get into the system. Also, if an attacker can get the password hashes for the system, then a dictionary attacking program for passwords can match the hashes in a very short period of time, allowing the attacker access to the system. With enough fast computers, the attacker may be able to match even a relatively strong hash in minutes. The San Diego Supercomputer Center's Teracrack paper is worth reading for both the methodology and the surprisingly difficult passwords generated by their attempts: "Teracrack: Password cracking using TeraFLOP and Petabyte Resources" is available at http://security.sdsc.edu/publications/teracrack.pdf.

Adding strong passwords that require many different types of characters, or even passphrases, will help negate dictionary attacks. However, the tradeoff is that users will likely write down the passwords in a convenient place or will regularly forget their password. Note that just adding digits or special characters to a word will not make it less dictionary attackable, as most password cracking programs do a very good job of enumerating obvious modifications.

So the password "password" is obviously easily crackable, but so are "password123" and "123password." Both "pass" and "word" are dictionary words, so "pass123word" is also easily cracked. Besides English words, most dictionaries used by attackers contain foreign words, common computer terms, and even common transformations, such as substituting the number 4 for an a character, or zero for an o character, so "p4ssw0rd" is no safer than "password." The string "I^34WxV2" is not normally considered dictionary attackable, but is difficult to remember (and may be added to a common dictionary once someone reads this).

The criteria for deciding if you want to enforce strong passwords should include what your primary purpose is for the passwords. If it's local, physical access to a system where there aren't network services available, then having the password written down is probably worse than allowing weak passwords (other than those easily guessed, such as "password," "secret," and the ID itself). That's because an attacker is more likely to be local to the system. For remote authentication where the authentication mechanism is open to a wide range of systems, like SSH open to the Internet, stronger passwords make much more sense.

Require strong passwords using the pam_passwdqc module

In any case, besides the pam_cracklib PAM module, there's a strong authentication module that allows you to require very strong non-guessable passwords from your users. That module is pam_passwdqc. This module is used for password changing programs like passwd, and it allows you to enforce strong passwords, and passphrases (a series of words, rather than just one), and can generate random passwords for users. This module is included in SUSE distributions, but must be added by the administrator for older Red Hat releases. Red Hat started including pam_passwdqc in Red Hat Enterprise Linux AS 3.0 in an update in May 2004.

Both pam_cracklib and pam_passwdqc should not be used in the same service configuration file. Therefore, pam_passwdqc should replace pam_cracklib in a configuration file if you want to use it.

The pam_passwfqc module should be inserted prior to pam_unix or pam_pwdb in the password method of a service's authentication stack. This module defines four character classes: digits, lowercase letters, uppercase letters, and all others. Non-ASCII characters are assumed to be non-digits. The module allows the administrator to set the minimum length limits for passwords made of each type of character, and disable completely a particular class. The parameter for minimum password length is min=N0,N1,N2,N3,N4, which defaults to min=disabled, 24,12,8,7.

  • N0 By default, this group is disabled. This group represents a password that consists of one character class, such as all lowercase letters.
  • N1 By default, this group requires 24 characters. This group represents a password that consists of two character classes and does not meet the requirements for a passphrase.
  • N2 By default, this group requires 12 characters. It is reserved for passphrases, which must contain a minimum number or words (see the passphrase option).
  • N3 By default, this group requires 8 characters. It represents a password that consists of three character classes.
  • N4 By default, this group requires 7 characters. It represents a password that contains four different character classes.

Note that this module does not count uppercase initial characters or trailing digits when counting the number of character classes. So Apassword99 will count only as one class, while ApaSSword99 counts as two, and A99PassworD counts as three.

The next parameter the module accepts is max=, which is the maximum password size. The default is max=40. The value 8 has a special meaning, and passwords used will be truncated to 8 characters if that value is used to ensure compatibility with older Unix programs, which can't deal with longer passwords. This should only be used if absolutely necessary.

The next parameter is passphrase, which defaults to passphrase=3. This is the minimum number of words allowed in a passphrase. A value of 0 disables passphrase support.

Next, we have match, which defaults to match=4. This is a case and direction insensitive substring-matching function that is used to determine if a substring is common. Passwords aren't rejected for having common substrings, the substring value is just removed from consideration when calculating the number of different character classes, just as leading capitals and trailing digits are.

Next, we have similar, which can be either similar=permit or similar=deny. The default is deny, which means the new password isn't allowed to be similar to the old password. Passwords are subject to the common substring test from match, and rejected if they're too similar. This stops users from using passwords similar to ones that may have been compromised. So, if the user's previous password was "mypass1" they wouldn't be allowed to use "mypass2" as their next password.

Next, we have random, which defaults to random=42. This is the default length for randomly generated passwords. Random passwords are allowed no matter what other restrictions are in place. A 0 value will disable this feature, and the string ,only appended to the length will disallow user-chosen passwords.

The next setting is enforce. This can have a value of none, users, or everyone. The default is enforce=everyone. none will warn of weak passwords only, but not enforce their rejection. users will enforce the restrictions for all non-root users on the system. everyone will enforce the restrictions for all users, including root.

Next, we have the non-unix option. This tells the module to not use the traditional getpwnam function call to get the user information. It's useful for services that don't use /etc/passwd for their authentication, such as some POP3 e-mail daemons. By default, this option is not specified.

The next parameter is retry, and defaults to retry=3. This is the number of chances the user gets to enter a sufficiently strong password.

The last four parameters all default to not being specified. This is because the module expects to normally be used in conjunction with the pre-existing configuration.

The ask_oldauthok parameter has the module prompt for the old password during the preliminary check. If it's specified as ask_oldauthok=update, then the old password is asked for during the update phase.

The check_oldauthok parameter has the module check that the old password is valid prior to performing the update. This is usually left to other modules, but something in the module stack should do this, otherwise anyone could walk up to an already logged- in account and change the password.

The next two options are mutually exclusive: use_first_pass and use_authtok. Both of these options tell the module to use the password provided by a prior module in the stack, instead of interacting with the user. The use_first_pass option is incompatible with ask_oldauthok, otherwise they're functionally the same.

Using this module, the default options are acceptable for everything other than randomly generated passwords. Long random passwords will almost require the user to write down the password, and should only be used in situations where they're necessary.

The following line before the pam_unix.so password line will add pam_passwdqc to a service:

password required pam_passwdqc.so enforce=users ask_oldauthok=update check_oldauthok

Modern distributions have lots of interdependencies for PAM, so for example, on Red Hat, the default system-auth service, which is referenced by many services through the pam_stack module, is auto-generated when the system is installed. Let's look at an example of replacing pam_cracklib with pam_passwdqc. Here's the original /etc/pam/d/system-auth file:

#%PAM-1.0
# This file is auto-generated.
# User changes will be destroyed the next time authconfig is run.
auth required /lib/security/$ISA/pam_env.so
auth sufficient /lib/security/$ISA/pam_unix.so likeauth nullok
auth required /lib/security/$ISA/pam_deny.so
account required /lib/security/$ISA/pam_unix.so
password required /lib/security/$ISA/pam_cracklib.so retry=3
password sufficient /lib/security/$ISA/pam_unix.so nullok use_authtok md5 +
password required /lib/security/$ISA/pam_deny.so
session required /lib/security/$ISA/pam_limits.so
session required /lib/security/$ISA/pam_unix.so

This is the replacement:

#%PAM-1.0
# This file is auto-generated.
# User changes will be destroyed the next time authconfig is run.
auth required /lib/security/$ISA/pam_env.so
auth sufficient /lib/security/$ISA/pam_unix.so likeauth nullok
auth required /lib/security/$ISA/pam_deny.so
account required /lib/security/$ISA/pam_unix.so

password required pam_passwdqc.so enforce=users ask_oldauthok=update check_oldauthok +
password sufficient /lib/security/$ISA/pam_unix.so nullok use_authtok md5 +
password required /lib/security/$ISA/pam_deny.so
session required /lib/security/$ISA/pam_limits.so
session required /lib/security/$ISA/pam_unix.so

Click Here!