PAM and Linux Authentication
PAM (Pluggable Authentication Modules) is the framework that controls how Linux processes authenticate users. When you log in via SSH, sudo, su, or a GUI, PAM decides whether to grant or deny access - and it does so through a stack of configurable modules. Understanding PAM is essential for enforcing password policies, integrating MFA, and troubleshooting authentication failures.
The Password Files
Section titled “The Password Files”/etc/passwd
Section titled “/etc/passwd”Stores basic account information. World-readable by design (needed for username→UID resolution).
Format: username:x:UID:GID:GECOS:home:shell
alice:x:1001:1001:Alice Smith:/home/alice:/bin/bash│ │ │ │ │ │ ││ │ │ │ │ │ └─ Login shell│ │ │ │ │ └─ Home directory│ │ │ │ └─ GECOS field (full name, optional info)│ │ │ └─ Primary GID│ │ └─ UID (1000+ = regular users; 0 = root)│ └─ 'x' = password stored in /etc/shadow└─ Username# View account informationgetent passwd alice # query NSS (works for LDAP users too)id alice # UID, GID, groupsfinger alice # verbose user info (if installed)
# System accounts (login disabled)grep '/sbin/nologin\|/bin/false' /etc/passwd # can't log in interactively
# Add a user (high-level)useradd -m -s /bin/bash -c "Alice Smith" -G sudo alicepasswd alice # set password
# Modify a userusermod -aG docker alice # add to docker group (-a = append; don't remove existing)usermod -L alice # lock account (prepends ! to shadow hash)usermod -U alice # unlock accountusermod -s /sbin/nologin alice # disable interactive login
# Delete a useruserdel alice # removes user, keeps home directoryuserdel -r alice # removes user AND home directory/etc/shadow
Section titled “/etc/shadow”Stores hashed passwords and aging policy. Readable only by root.
Format: username:hash:lastchange:min:max:warn:inactive:expire:reserved
alice:$6$salt$hash...:19800:0:90:14:7:20000:│ │ │ │ │ │ │ ││ │ │ │ │ │ │ └─ Account expiry date (days since epoch)│ │ │ │ │ │ └─ Days inactive before account disabled│ │ │ │ │ └─ Warn N days before password expires│ │ │ │ └─ Password max age (days)│ │ │ └─ Password min age (days, prevents rapid cycling)│ │ └─ Days since epoch when password was last changed│ └─ Hashed password└─ Username
Hash format: $1$ = MD5 (obsolete, do not use) $5$ = SHA-256 $6$ = SHA-512 (current standard) $y$ = yescrypt (modern; Fedora/Debian default) ! = account locked (prepended to hash) * = no password (cannot log in via password)# Change passwordpasswd alice # as root: change any user's passwordpasswd # as user: change own password
# Check password expiry statuschage -l alice # list password aging infochage -M 90 alice # set max age to 90 dayschage -W 14 alice # warn 14 days before expirychage -I 7 alice # disable account 7 days after password expireschage -E 2026-12-31 alice # set account expiry datechage -d 0 alice # force password change on next login
# Audit: find accounts with no password (empty hash)awk -F: '($2 == "" || $2 == "!" ) { print $1 }' /etc/shadow
# Audit: find UID 0 accounts other than root (privilege escalation risk)awk -F: '($3 == 0) { print $1 }' /etc/passwd/etc/group and /etc/gshadow
Section titled “/etc/group and /etc/gshadow”/etc/group format: groupname:x:GID:member1,member2,.../etc/gshadow: groupname:hash:admins:members
# List groups a user belongs togroups aliceid alice
# Create and manage groupsgroupadd -g 1100 developersusermod -aG developers alicegpasswd -d alice developers # remove alice from developers group
# Check who is in a groupgetent group sudogetent group dockerPAM Architecture
Section titled “PAM Architecture”Login request (SSH, sudo, console, graphical) │ ▼ PAM framework reads: /etc/pam.d/<service> │ ▼ Processes the module stack in order:
TYPE CONTROL MODULE auth required pam_unix.so ← check /etc/shadow hash auth required pam_google_authenticator.so ← check TOTP account required pam_nologin.so ← check if logins are disabled account required pam_unix.so ← check account expiry session required pam_limits.so ← apply resource limits session optional pam_lastlog.so ← show last login infoPAM Module Types
Section titled “PAM Module Types”| Type | What it does |
|---|---|
| auth | Authenticate the user (verify password, OTP, key) |
| account | Check account validity (expired? locked? allowed to log in?) |
| password | Handle password changes and policy enforcement |
| session | Set up/tear down the session (mount home dir, set limits, logging) |
PAM Control Flags
Section titled “PAM Control Flags”| Flag | Meaning |
|---|---|
| required | Must succeed; failure always causes final AUTH FAIL (other modules still run) |
| requisite | Must succeed; failure causes immediate AUTH FAIL (no more modules run) |
| sufficient | If succeeds (and no prior required failure), grant access immediately |
| optional | Failure only matters if it’s the only module of this type |
| include | Include another PAM file’s rules inline |
PAM Configuration Files
Section titled “PAM Configuration Files”# Key files in /etc/pam.d/ls /etc/pam.d/# common-auth ← shared auth stack (included by sshd, sudo, etc.)# common-account ← shared account checks# common-password ← shared password update rules# common-session ← shared session setup# sshd ← SSH-specific overrides# sudo ← sudo-specific stack# login ← console login# su ← su command/etc/pam.d/common-auth (Debian/Ubuntu)
Section titled “/etc/pam.d/common-auth (Debian/Ubuntu)”auth [success=1 default=ignore] pam_unix.so nullokauth requisite pam_deny.soauth required pam_permit.soauth optional pam_cap.soAdding TOTP MFA to SSH
Section titled “Adding TOTP MFA to SSH”# See ssh-hardening.mdx for full setup# /etc/pam.d/sshd - add before other auth lines:auth required pam_google_authenticator.so nullok
# /etc/ssh/sshd_config:ChallengeResponseAuthentication yesAuthenticationMethods publickey,keyboard-interactive # key + TOTPPassword Complexity Policy with pam_pwquality
Section titled “Password Complexity Policy with pam_pwquality”# Installapt install libpam-pwquality
# /etc/pam.d/common-password:password requisite pam_pwquality.so retry=3
# /etc/security/pwquality.conf:minlen = 14 # minimum lengthminclass = 3 # minimum character classes (upper, lower, digit, special)maxrepeat = 3 # max consecutive same charactersmaxsequence = 4 # max sequential characters (abcd, 1234)dcredit = -1 # require at least 1 digit (-N = require N chars of type)ucredit = -1 # require at least 1 uppercaselcredit = -1 # require at least 1 lowercaseocredit = -1 # require at least 1 special characterdifok = 5 # minimum characters that must differ from old passwordgecoscheck = 1 # reject passwords that match user's GECOS fielddictcheck = 1 # reject dictionary words
# Test a password against the policypasswd alice# Or test directly:python3 -c "import subprocessresult = subprocess.run(['pwscore'], input='MyPassword123!', capture_output=True, text=True)print(result.stdout) # quality score 0-100"Account Lockout with pam_faillock
Section titled “Account Lockout with pam_faillock”# /etc/pam.d/common-auth - add BEFORE pam_unix.so:auth required pam_faillock.so preauth silent deny=5 unlock_time=900 fail_interval=300auth [success=1 default=ignore] pam_unix.so nullokauth [default=die] pam_faillock.so authfail deny=5 unlock_time=900
# /etc/pam.d/common-account:account required pam_faillock.so
# /etc/security/faillock.conf (modern alternative to inline args):deny = 5 # lock after 5 failuresunlock_time = 900 # unlock after 15 minutesfail_interval = 300 # count failures within 5 minute windowadmin_group = sudo # exempt sudo group from lockout
# Check lockout statusfaillock --user alice
# Manually unlock a locked accountfaillock --user alice --reset
# List all locked accountsfaillocksudo Configuration
Section titled “sudo Configuration”sudo allows users to run commands as root (or another user) with controlled access.
/etc/sudoers
Section titled “/etc/sudoers”visudo # opens /etc/sudoers in your default EDITOR with validation
# Or create a drop-in file (preferred - avoids editing sudoers directly)visudo -f /etc/sudoers.d/alicesudoers Syntax
Section titled “sudoers Syntax”# Format: USER HOST=(RUNAS) NOPASSWD:COMMAND
# Grant alice full sudo on all hosts (dangerous - avoid)alice ALL=(ALL:ALL) ALL
# Grant alice sudo without password (for automation - dangerous)alice ALL=(ALL) NOPASSWD: ALL
# Grant alice sudo for specific commands onlyalice ALL=(ALL) /bin/systemctl restart nginx, /bin/systemctl status *
# Grant a group sudo%sudo ALL=(ALL:ALL) ALL # % = group (this is the default for sudo group)%developers ALL=(ALL) /usr/bin/docker
# No password for specific commandalice ALL=(ALL) NOPASSWD: /sbin/ip addr add *, /sbin/ip addr del *
# Restrict to specific target user (run as www-data, not root)alice ALL=(www-data) /var/www/html/deploy.sh
# Use NOPASSWD carefully - only for commands with no destructive capability# Test what alice can do with sudosudo -l -U alice # as root: list alice's sudo capabilitiessudo -l # as alice herself: what can I do?
# Run a single command as rootsudo systemctl restart nginx
# Switch to root shell (use sparingly)sudo -i # root login shell (reads root's .profile)sudo -s # root shell but keeps current environment
# Run a command as a specific user (not root)sudo -u www-data php /var/www/maintenance.php
# See who has used sudo recentlyjournalctl -u sudo # systemd logsgrep sudo /var/log/auth.logsudo Audit Logging
Section titled “sudo Audit Logging”# Every sudo command is logged to /var/log/auth.log / journaldgrep "COMMAND=" /var/log/auth.log | tail -20# Format: alice : ... COMMAND=/usr/bin/apt install ...
# Auditd: log all sudo escalationscat >> /etc/audit/rules.d/sudo.rules << 'EOF'-a always,exit -F arch=b64 -F path=/usr/bin/sudo -S execve -k sudo_execEOFauditctl -R /etc/audit/rules.d/sudo.rulesausearch -k sudo_exec -ts recentsu vs sudo - When to Use Each
Section titled “su vs sudo - When to Use Each”| Factor | su | sudo |
|---|---|---|
| Authentication | Requires target user’s password (e.g., root password) | Requires your own password |
| Audit trail | No per-command logging | Every command logged with your username |
| Granularity | All or nothing (full shell as target user) | Can limit to specific commands |
| Root password | Must share root password with admins | No root password needed; no need to share |
| Best use | Legacy systems; switching to non-root user interactively | Almost all legitimate escalation use cases |
# su - switch to root (requires root password)su - # root login shell
# su to another usersu - alice # requires alice's password (or root's password if you're root)
# sudo vs su equivalent:sudo -i # equivalent to: su - root (but uses YOUR password, and logs it)sudo su - # equivalent but runs su through sudo (audit-visible)
# Best practice: disable direct root login and root passwordsudo passwd -l root # lock root password (prevents su - root entirely)# All escalation must go through sudo - ensures audit trailResource Limits - /etc/security/limits.conf
Section titled “Resource Limits - /etc/security/limits.conf”PAM’s pam_limits module enforces per-user or per-group resource limits:
# /etc/security/limits.conf (read by pam_limits)# Format: domain type item value
# Limit max open files for all users* soft nofile 1024 # soft limit (user can raise up to hard)* hard nofile 65536 # hard limit (absolute ceiling)
# Limit processes for a specific user (prevent fork bombs)alice hard nproc 100 # max 100 processesalice hard fsize 1048576 # max file size 1GB (in KB)
# Unlimited limits for a privileged user (e.g., database)postgres soft nofile 65536postgres hard nofile 65536postgres soft nproc unlimitedpostgres hard nproc unlimited
# Lock memory (for security-sensitive apps) - prevent swap write@developers hard memlock 1048576 # max 1GB locked# Check current limits for a processcat /proc/PID/limits
# Check limits for current shell sessionulimit -a # show allulimit -n # open files limitulimit -u # max user processes
# Set limits for current session (up to the hard limit)ulimit -n 4096 # increase open files for this session