YubiKey, for multi-factor authentication on Linux.

Linux PAM for Compliance

Linux PAM

Linux uses PAM or the Pluggable Authentication Modules mechanism for security. This is because:

Authentication is crucial for security. If you can't trust user authentication, much of your information security is an unsolvable problem.

There is no single correct answer for "How should users be authenticated?" Different organizations have different concerns, and we should be able to fine-tune our user authentication to meet our needs.

Secure software is difficult to create. We need a diverse library of security modules written by trusted programmers, and examined by a community eager to find errors made by others.

This is why we have PAM.

Here is the architecture of PAM. At the top are the PAM-aware services. Some are continually-running programs listening for network connections (the SSH daemon, the Samba daemon), others are continually-running programs waiting for user interaction at the console (login and gdm), but others are programs which users may run whenever they choose (su and sudo). There are many more than the examples shown here.

In the center is the main PAM library, libpam.so.

Linux PAM architecture

The bottom half of the diagram shows the PAM modules, named pam_*.so. Those on the left have to do with mechanisms by which users can authenticate themselves — classic UNIX-style passwords, fingerprint scanners, Kerberos tickets, and a smart card or YubiKey device.

Some PAM modules shown at right impose restrictions — password quality and reuse, or locking an account after a specified number of authentication failures within a specified time. Other PAM modules are used for their side effects, such as reporting to the user when they last logged in, or when there was a recent login failure for their account. Other PAM modules trigger useful log records, or establish the user's environment for a new session.

Again, there are many more PAM modules than the examples shown here.

Programs are linked to the main PAM library in order to use the PAM architecture. Let's look at the login program. That's what you interact with while authenticating onto a text console. It uses libpam.so.0 and a related PAM library, and some other libraries obviously related to security.

$ which login
/usr/bin/login
$ ldd /usr/bin/login
	linux-vdso.so.1 (0x00007ffd1434d000)
	libpam.so.0 => /lib64/libpam.so.0 (0x00007fe0688a9000)
	libpam_misc.so.0 => /lib64/libpam_misc.so.0 (0x00007fe0686a5000)
	libaudit.so.1 => /lib64/libaudit.so.1 (0x00007fe06847b000)
	libselinux.so.1 => /lib64/libselinux.so.1 (0x00007fe068251000)
	libc.so.6 => /lib64/libc.so.6 (0x00007fe067e8d000)
	libdl.so.2 => /lib64/libdl.so.2 (0x00007fe067c89000)
	libcap-ng.so.0 => /lib64/libcap-ng.so.0 (0x00007fe067a83000)
	libpcre2-8.so.0 => /lib64/libpcre2-8.so.0 (0x00007fe0677ff000)
	/lib64/ld-linux-x86-64.so.2 (0x00007fe068cc2000)
	libpthread.so.0 => /lib64/libpthread.so.0 (0x00007fe0675df000) 

The main library collection is typically somewhere like /usr/lib64/ (as on RHEL and Oracle Linux) or /lib/x86_64-linux-gnu/ (as on Debian, Ubuntu, and Mint Linux).

The main library file is there, probably with a symbolic link named libpam.so.0 pointing to the actual file, with the same name with a version number appended. shared library files The PAM module files are in a subdirectory named security. On Oracle Linux 8 I see this:

$ ls -l /lib64/libpam.so*
lrwxrwxrwx 1 root root    16 Aug  1  2020 /lib64/libpam.so.0 -> libpam.so.0.84.2
-rwxr-xr-x 1 root root 70368 Aug  1  2020 /lib64/libpam.so.0.84.2
$ ls -F /lib64/security
pam_access.so*     pam_limits.so*          pam_shells.so*
pam_cap.so*        pam_listfile.so*        pam_stress.so*
pam_chroot.so*     pam_localuser.so*       pam_succeed_if.so*
pam_console.so*    pam_loginuid.so*        pam_systemd.so*
pam_cracklib.so*   pam_mail.so*            pam_time.so*
pam_debug.so*      pam_mkhomedir.so*       pam_timestamp.so*
pam_deny.so*       pam_motd.so*            pam_tty_audit.so*
pam_echo.so*       pam_namespace.so*       pam_umask.so*
pam_env.so*        pam_nologin.so*         pam_unix.so*
pam_exec.so*       pam_permit.so*          pam_unix_acct.so@
pam_faildelay.so*  pam_postgresok.so*      pam_unix_auth.so@
pam_faillock.so*   pam_pwhistory.so*       pam_unix_passwd.so@
pam_filter/        pam_pwquality.so*       pam_unix_session.so@
pam_filter.so*     pam_rhosts.so*          pam_userdb.so*
pam_ftp.so*        pam_rootok.so*          pam_usertype.so*
pam_group.so*      pam_securetty.so*       pam_warn.so*
pam_issue.so*      pam_selinux.so*         pam_wheel.so*
pam_keyinit.so*    pam_selinux_permit.so@  pam_xauth.so*
pam_lastlog.so*    pam_sepermit.so* 

The PAM configuration files are in the /etc/pam.d/ directory. Every PAM-aware program looks there for a configuration file with the same name. If such a file exists, it uses the contained authentication logic. If it doesn't, then the program does what it natively does by default as guided by its configuration files.

You will find other files in the PAM directory, depending on your distribution and which packages you have installed. For example, system-auth and password-auth on RHEL and Oracle Linux, or common-auth and common-password on Debian, Ubuntu, and Mint Linux.

As we will see below, PAM lets you configure a complex password quality and maintenance policy. Because of required password change schedules, a user might be forced to change their password at any authentication event, regardless of which service they are authenticating to.

You want the password quality policy to always be the same. You don't want one set of rules applied if the text login screen told the user it was time for a new password, a different set of rules if they were logging in to the graphical console, and a third set of rules if they were connecting in via SSH.

You could strive to maintain identical policy definitions in several files in /etc/pam.d/, but the details can very easily become inconsistent. A include mechanism was created to allow you to configure a general policy in one file, and pull that into multiple service-specific files. It's quite common for a PAM file to contain nothing but include statements, as that service just follows the general policies and adds no service-specific requirements.

A substack mechanism was added later. It's quite similar to include and when you are first looking at PAM, you probably should consider them to be equivalent. See the pam.conf(5) manual page for the details.

Now let's look at a specific complex set of requirements that can be achieved with PAM.

DISA STIGs

DISA STIG
Archive

DISA, the Defense Information System Agency, is part of the U.S. Department of Defense. DISA maintains the Security Technical Implementation Guide (or STIG) documents.

The STIG documents describe cybersecurity requirements for a wide range of computer operating systems, routers, and other computing systems. STIG details are based on concepts in NIST Special Publication 800-53, which specifies security controls for all U.S. federal information systems other than national security systems. SP 800-53 describes concepts like "Passwords should be adequately complex", while the STIGs specify how long, how many character classes, and so on.

There is no
STIG for Debian

DISA will only create a STIG for an operating system from a trusted provider. For example, Red Hat (and now IBM) for RHEL, Oracle for Oracle Linux, and Canonical for Ubuntu Linux. There is no STIG for Debian, nor will there be one, because there is no company controlling Debian releases and maintaining quality and patch control.

Example STIG
Content

Another page of mine shows you example STIG details. Let's see a practical solution: how to configure PAM on RHEL or Oracle Linux for STIG compliance.

Decide Now: Ease of Maintenance, or Complete Control

Linux distributions include programs to automatically modify the PAM files as you turn features on and off. RHEL and derivatives up through RHEL 7 include authconfig and authconfig-gtk. At version 8 those are replaced by authselect and authselect-gtk. Debian and Ubuntu provide pam-auth-update, which modifies the shared common-auth PAM file.

These overwrite your PAM files, making any attempts to manually configure PAM confusing and frustrating.

If you need to be compliant to the US DISA STIG, then you are going to have to modify your PAM files by hand and be very careful to never use the automated tools. Unfortunately, authconfig cannot be removed due to package dependencies, so your PAM configuration relies on all system administrators understand that it must never be used.

On a test system, however, these tools can be quite useful. You can use them to see what all is involved in enabling or disabling authentication to sssd, for example.

Red Hat and Oracle Linux system-auth

Usually system-auth-ac is the actual file, and system-auth is a symbolic link pointing to it.

The file system-auth on RHEL and Oracle Linux and derivatives is used by many services. It's provided with the distribution, and the main place where you extend or fine-tune overall system authentication policy. Here is the list of PAM configuration files, and then we see how many use system-auth.

$ ls /etc/pam.d
authconfig		login		   smtp@
authconfig-gtk		other		   smtp.postfix
authconfig-tui		passwd		   sshd
chfn			password-auth@	   sssd-shadowutils
chsh			password-auth-ac   su
config-util		pluto		   subscription-manager
crond			polkit-1	   subscription-manager-gui
fingerprint-auth@	postlogin@	   sudo
fingerprint-auth-ac	postlogin-ac	   sudo-i
gdm-autologin		remote		   su-l
gdm-fingerprint		rhn_register	   system-auth@
gdm-launch-environment	runuser		   system-auth-ac
gdm-password		runuser-l	   system-config-authentication
gdm-pin			screen		   systemd-user
gdm-smartcard		smartcard-auth@    vlock
liveinst		smartcard-auth-ac  xserver
$ grep -l system-auth /etc/pam.d/*
/etc/pam.d/chfn
/etc/pam.d/chsh
/etc/pam.d/config-util
/etc/pam.d/crond
/etc/pam.d/gdm-autologin
/etc/pam.d/gdm-launch-environment
/etc/pam.d/login
/etc/pam.d/passwd
/etc/pam.d/pluto
/etc/pam.d/polkit-1
/etc/pam.d/screen
/etc/pam.d/su
/etc/pam.d/sudo
/etc/pam.d/systemd-user
/etc/pam.d/vlock 

Let's look inside that crucial file. Below is the original content from a RHEL 7 installation.

The first field is the phase of authentication when the rule is applied.

The second is the logic. Both required and requisite must succeed. If a requisite rule fails, the entire stack immediately exits with failure. If a required rule fails, the stack is doomed to eventually fail, but the rule execution continues. If a sufficient rule succeeds, the stack immediately exits with success, unless an earlier required rule failed, in which case it's immediate stack failure.

The third field is the PAM module, with any parameters continuing on that line.

$ cat /etc/pam.d/system-auth
#%PAM-1.0
# This file is auto-generated.
# User changes will be destroyed the next time authconfig is run.
auth        required      pam_env.so
auth        required      pam_faildelay.so delay=2000000
auth        sufficient    pam_unix.so nullok try_first_pass
auth        requisite     pam_succeed_if.so uid >= 1000 quiet_success
auth        required      pam_deny.so

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

password    requisite     pam_pwquality.so try_first_pass local_users_only retry=3 authtok_type=
password    sufficient    pam_unix.so sha512 shadow nullok try_first_pass use_authtok
password    required      pam_deny.so

session     optional      pam_keyinit.so revoke
session     required      pam_limits.so
-session     optional      pam_systemd.so
session     [success=1 default=ignore] pam_succeed_if.so service in crond quiet use_uid
session     required      pam_unix.so 

The reason for the choices of all of required, requisite, and sufficient is to support various logical combinations.

Let's say that you wanted the use to be allowed onto the system if they could scan their recognized fingerprint or if they could type their password. Fingerprint is easy, try that first, but maybe the scanner is broken or dirty. Include this rule stack fragment. You are allowed in with either fingerprint or password, but denied access if you fail at both. pam_deny.so is a very simple module, it always fails!

[...other lines omitted...]
auth  sufficient  pam_fprintd.so
auth  sufficient  pam_unix.so
auth  required    pam_deny.so
[...other lines omitted...] 

On the other hand, maybe you want multi-factor authentication. The user must scan their recognized fingerprint and type their password. All or nothing! Include this:

[...other lines omitted...]
auth  required  pam_fprintd.so
auth  required  pam_unix.so
[...other lines omitted...] 

If you use required for both, then if either test fails the user is simply told that the authentication failed. They aren't told which part they failed. If you instead use requisite, the overall sequence exits when they first fail a test. Which of those approaches is best? Whichever you choose to implement! Unless, of course, someone like the Department of Defense says it must be done a certain way.

WARNING

Messing with PAM is an almost guaranteed way of locking yourself out of your system.

Keep a backup copy of the original /etc/pam.d/ so you can easily restore it.

Turn off session timeout and use virtual consoles to keep yourself logged in as root somewhere.

Compliance, Part I: Password Quality, Account Lock-Out, and Local versus Central Account Definitions

On top of everything else, most organizations (including DISA) seem to have a password-guessing threat model dating from 1990. Aggressive password rotation policies make things worse, with the result being powerful self-inflicted denial of service. Outside the Department of Defense, password apps on smartphones provide good security as long as you protect the device. Inside DoD and its contractors, phones are locked away in cabinets before you enter the facility. What little I've observed is that simple keyboard patterns dominate:
1qaz!QAZ2wsx@WSX
30 days later, shift one column:
2wsx@WSX3edc#EDC

The manual STIG documents specify explicitly configuring several details that would happen by default anyway. And, many automated tools apply those unneeded literal checks. The STIGs don't have to make sense, but we have to satisfy them.

Making things worse, some settings can be done with parameters on the PAM module line and with settings in module-specific configuration files.

As an example of this madness, current PAM modules require SHA-2-512 hashes of passwords by default. But the STIG says that you must explicitly specify SHA512 in the /etc/login.defs configuration file (rule RHEL-07-010210), and sha512 in the /etc/libuser.conf configuration file (rule RHEL-07-010220), and sha512 as a parameter to the pam_unix.so module in the PAM files (rule RHEL-07-010200).

Below is a system-auth PAM file with the needed changes and additions for password-only authentication. "Local users" are those defined in the local system's /etc/passwd and /etc/shadow files. This will be root and quite likely no one else.

Build a free Active Directory server equivalent with Samba

The majority, if not all, of the ordinary users will be defined through some LDAP / Kerberos / Winbind / etc network protocols, relying on FreeIPA or Active Directory or similar. The pam_sss.so module uses the sssd daemon, configured through /etc/sssd/sssd.conf, to talk to those network information and authentication services.

So, we need some logic like this:

if (user is defined in /etc/passwd) {
    password tests with pam_unix.so
} else {
    key/password/Kerberos tests with pam_sss.so
} 

Unfortunately, while the PAM configuration syntax is a programming language, it's a poor one. Its logic is limited to what you can build with a weak version of GOTO, which can only GOTO forward.

Making things worse, the needed pam_faillock.so has entirely different behavior depending on whether you use the preauth, authfail, or authsucc parameters. It's one module but it behaves in three totally different ways.

It would be very foolish to simply copy a block of code off some Internet page into your PAM configuration! Make sure you fully understand what this does. It very likely will burn down your facility if used as shown here.

PAM syntax is described here:
http://linux-pam.org/Linux-PAM-html/sag-configuration-file.html
You will need that reference to understand the square-bracketed descriptions often used in place of the simple required, sufficient, and so on.

Making things worse, distributions frequently omit the manual pages for some PAM modules. You may be able to find the missing ones at linux-pam.org. As of 2021 that site was still HTTP-only, and the bare-basics HTML landing page says it was last changed in 2018. Modules such as pam_gdm.so, included in Red Hat Linux, aren't mentioned either within the Red Hat distribution's manual pages or on the linux-pam.org site.

Here is system-auth for single-factor authentication. As best as I can tell, this satisfies the STIG requirements. I highlighted the local password authentication with light purple, the remote LDAP/Kerberos/FreeIPA/AD authentication with light orange, and comments are greyed out.

# Modified system-auth for password-only authentication

auth	 required    pam_env.so
# The logic of this next rule means:
#    "If defined in /etc/passwd, jump ahead 3 rules to
#    the faillock/pam_unix/faillock/faillock block.
#    Otherwise, flow into the following pam_sss.so section."
auth     [success=3 default=ignore] pam_localuser.so
# For the remote LDAP/Kerberos/FreeIPA/AD authentication, immediately fail and exit
# if it isn't an ordinary user with UID>=1000, then apply the pam_sss.so tests.
auth     requisite   pam_succeed_if.so uid >= 1000 quiet_success
auth     sufficient  pam_sss.so forward_pass
auth     required    pam_deny.so
# The main authentication block begins here.  Start by setting up the lock-on-failure
# mode.  With these numbers: lock the account after 3 failures within 20 minutes,
# then automatically unlock it one hour later.  Then apply the pam_unix.so
# password test.  If that fails, potentially lock the account.  But if it
# succeeds, skip ahead to the 3rd pam_faillock.so line.
auth	 required    pam_faillock.so preauth silent audit deny=3 even_deny_root fail_interval=1200 unlock_time=3600
auth	 [success=1 default=bad] pam_unix.so
auth	 [default=die] pam_faillock.so authfail audit deny=3 even_deny_root fail_interval=1200 unlock_time=3600
# If we hit the above rule, we have failed and the account should now be locked if
# it was the 3rd failure within 1200 seconds.  But if we have jumped to this next rule,
# we must have succeeded.  This next rule resets the lockout counter to zero.
auth	 required    pam_faillock.so authsucc silent deny=3 fail_interval=1200 unlock_time=3600

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

# At least 15 characters, all 4 classes (upper, lower, digit, other) with no more than
# 3 in a row of the same character, no more than 4 characters of the same class in
# a row, plus the usual heuristic and ad-hoc cracklib tests.
password required    pam_pwquality.so try_first_pass local_users_only retry=3 difok=4 minlen=15 ucredit=-1 lcredit=-1 dcredit=-1 ocredit=-1 maxrepeat=3 maxclassrepeat=4
# No using any of your past 24 passwords.
password required    pam_pwhistory.so use_authtok remember=24
password sufficient  pam_unix.so try_first_pass use_authtok sha512 shadow remember=24
password sufficient  pam_sss.so use_authtok
password required    pam_deny.so

session	 optional    pam_keyinit.so revoke
session	 required    pam_limits.so
# Despite this next rule being optional, gdm won't run correctly without it.
-session optional    pam_systemd.so
session	 [success=1 default=ignore] pam_succeed_if.so service in crond quiet use_uid
session	 required    pam_lastlog.so showfailed
session	 required    pam_unix.so
session  optional    pam_sss.so 

For STIG compliance you must also specify SHA-2-512 hashing of passwords in two configuration files.

$ grep 512 /etc/login.defs /etc/libuser.conf
/etc/login.defs:# SHA-2-512 for RHEL-07-010210
/etc/login.defs:ENCRYPT_METHOD SHA512
/etc/libuser.conf:# SHA-2-512 for RHEL-07-010220
/etc/libuser.conf:crypt_style = sha512 

You must also specify the same pam_pwquality.so parameters in a separate configuration file.

$ cat /etc/security/pwquality.conf
# Added for:
# RHEL-07-010170
# RHEL-07-010180
# RHEL-07-010190
# RHEL-07-010280
minlen=15
difok = 8
ucredit = -1
lcredit = -1
dcredit = -1
ocredit = -1
maxrepeat = 3
maxclassrepeat = 4
YubiKey device used for multi-factor authentication.

Compliance, Part II: Adding Multi-Factor Authentication

Warning: The pam_pkcs11.so module is well on the way out. This solution works on RHEL 7 and Oracle Linux 7. With version 8 of both of those, or on recent Debian and Ubuntu, pam_pkcs11.so is gone. Both pam_sss.so and pam_u2f.so offer ways forward.

We will add multi-factor authentication using a YubiKey device and the pam_pkcs11.so module. Above is a YubiKey.

They're quite small and easy to misplace. Connect a brightly colored cord! We're about to require possession of that specific device.

Our goal is multi-factor authentication. The user most both possess a specific YubiKey device and know their password. Here's our desired logic, with YubiKey work added in light blue:

if (user is defined in /etc/passwd) {
    if (service is text login or graphical login or a screensaver) {
        YubiKey tests with pam_pkcs11.so
    }
    password tests with pam_unix.so
} else {
    key/password/Kerberos tests with pam_sss.so
} 

Here is a system-auth file that seems to accomplish this. Remember that it would be very foolish to simply copy a block of code off some Internet page into your PAM configuration!

There is a required pam_pkcs11.so rule for the YubiKey, then a pam_unix.so whose customized logic is similar to required, except it adds in skipping the next rule if this one succeeds.

# Modified system-auth for multi-factor authentication

auth	 required    pam_env.so
# The logic of this next rule means:
#    "If defined in /etc/passwd, jump ahead 3 rules to
#    the faillock/pam_unix/faillock/faillock block.
#    Otherwise, flow into the following pam_sss.so section."
auth     [success=3 default=ignore] pam_localuser.so
# For the remote LDAP/Kerberos/FreeIPA/AD authentication, immediately fail and exit
# if it isn't an ordinary user with UID>=1000, then apply the pam_sss.so tests.
auth     requisite   pam_succeed_if.so uid >= 1000 quiet_success
auth     sufficient  pam_sss.so forward_pass
auth     required    pam_deny.so
# The main authentication block begins here.  Start by setting up the lock-on-failure
# mode.  With these numbers: lock the account after 3 failures within 20 minutes,
# then automatically unlock it one hour later.
auth	 required    pam_faillock.so preauth silent audit deny=3 even_deny_root fail_interval=1200 unlock_time=3600
# We only want to require the YubiKey for the text login, graphical display managers,
# and screensaver services.  So if the service is not one of those, skip pam_pkcs11.so.
auth     [success=1 default=ignore] pam_succeed_if.so service notin login:gdm:xdm:kdm:xscreensaver:gnome-screensaver:kscreensaver quiet use_uid
auth     requisite   pam_pkcs11.so wait_for_card debug
auth	 [success=1 default=bad] pam_unix.so
auth	 [default=die] pam_faillock.so authfail audit deny=3 even_deny_root fail_interval=1200 unlock_time=3600
# If we hit the above rule, we have failed and the account should now be locked if
# it was the 3rd failure within 1200 seconds.  But if we have jumped to this next rule,
# we must have succeeded.  This next rule resets the lockout counter to zero.
auth	 required    pam_faillock.so authsucc silent deny=3 fail_interval=1200 unlock_time=3600

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

password required    pam_pwquality.so try_first_pass local_users_only retry=3 difok=4 minlen=15 ucredit=-1 lcredit=-1 dcredit=-1 ocredit=-1 maxrepeat=3 maxclassrepeat=4
password required    pam_pwhistory.so use_authtok remember=24
password sufficient  pam_unix.so try_first_pass use_authtok sha512 shadow remember=24
password sufficient  pam_sss.so use_authtok
password required    pam_deny.so

session	 optional    pam_keyinit.so revoke
session	 required    pam_limits.so
# Despite this next rule being optional, gdm won't run correctly without it.
-session optional    pam_systemd.so
session	 [success=1 default=ignore] pam_succeed_if.so service in crond quiet use_uid
session	 required    pam_lastlog.so showfailed
session	 required    pam_unix.so
session  optional    pam_sss.so

With the above in place, we see something like the following on the text login screen:

Red Hat Enterprise Linux Server 7
Kernel 3.10.0 on an x86_64

localhost login: cromwell
Please insert your smart card.
Welcome cromwell!
Smart card PIN: ******
Password: _______________
Last login: Tue May 06 06:31:46 UTC 2025 on tty3
[cromwell@localhost ~]$