User & Group Management
Managing users
# Create a user with home directory and default shell
useradd -m -s /bin/bash alice
# Create a user with a comment (full name) and specific UID
useradd -m -s /bin/bash -c "Alice Smith" -u 1500 alice
# Create a user and add to a primary group
useradd -m -s /bin/bash -g developers alice
# Modify an existing user
usermod -s /bin/zsh alice # change shell
usermod -c "Alice Smith" alice # change comment/full name
usermod -d /home/newdir -m alice # change home dir and move files
usermod -l newname alice # rename the login name
usermod -aG wheel alice # add to supplementary group (append, don't replace)
usermod -L alice # lock account (disable password login)
usermod -U alice # unlock account
# Delete a user (keeps home directory)
userdel alice
# Delete a user AND their home directory
userdel -r alice
-a with -G when adding a user to a group: usermod -aG groupname username. Without -a, -G replaces all supplementary groups, removing the user from groups they were already in.
Checking user info
# Show all groups the current user belongs to
id
# uid=1000(alice) gid=1000(alice) groups=1000(alice),10(wheel),1001(developers)
# Show info for a specific user
id alice
# Show user's login name and shell
getent passwd alice
# List all users on the system
getent passwd | cut -d: -f1
Managing groups
# Create a group
groupadd developers
groupadd -g 2000 developers # with specific GID
# Delete a group
groupdel developers
# Rename a group
groupmod -n devs developers
# Add a user to a group (alternative to usermod -aG)
gpasswd -a alice developers
# Remove a user from a group
gpasswd -d alice developers
# List members of a group
getent group developers
# developers:x:2000:alice,bob,charlie
# Show all groups on the system
getent group | cut -d: -f1
Passwords
# Set password interactively
passwd alice
# Set password non-interactively (scripted) — use with care
echo "alice:newpassword" | chpasswd
# Force user to change password at next login
chage -d 0 alice
# Set password expiry (days)
chage -M 90 alice # password expires in 90 days
chage -m 1 alice # minimum 1 day between changes
chage -W 14 alice # warn 14 days before expiry
# View password aging info
chage -l alice
# Lock / unlock an account
passwd -l alice # lock (prepends ! to shadow hash)
passwd -u alice # unlock
/etc/passwd, /etc/shadow, /etc/group
# /etc/passwd — one record per user (world-readable)
# username:x:UID:GID:comment:home:shell
alice:x:1000:1000:Alice Smith:/home/alice:/bin/bash
# The x in field 2 means the password hash is in /etc/shadow
# UID 0 = root; UIDs 1-999 are typically system accounts
# /etc/shadow — password hashes (root-readable only)
# username:hash:lastchange:min:max:warn:inactive:expire:reserved
alice:$6$rounds=...:19500:0:90:14:::
# /etc/group — one record per group
# groupname:x:GID:member-list
developers:x:2000:alice,bob
# /etc/gshadow — group passwords (rarely used)
# Look up entries with getent (works for local files AND SSSD/LDAP)
getent passwd alice
getent group developers
sudo and sudoers
sudo lets a regular user run commands as root (or another user). Configuration is in /etc/sudoers — always edit it with visudo, which validates syntax before saving.
# Edit sudoers safely — validates before writing
visudo
# Core sudoers syntax:
# WHO WHERE=(AS_WHOM) WHAT
# Allow alice to run any command as root
alice ALL=(ALL) ALL
# Allow alice to run any command without a password
alice ALL=(ALL) NOPASSWD: ALL
# Allow alice to run only specific commands
alice ALL=(ALL) /usr/bin/systemctl restart nginx, /usr/bin/systemctl status nginx
# Allow the wheel group full sudo access (standard RHEL setup)
%wheel ALL=(ALL) ALL
# Allow the sudo group full access (standard Debian setup)
%sudo ALL=(ALL) ALL
Add users to the wheel group (RHEL) or sudo group (Debian) for standard admin access, rather than writing individual sudoers rules. These groups are already defined with full sudo access by default.
Add a user to wheel/sudo group
# RHEL — add to wheel group
usermod -aG wheel alice
# Debian — add to sudo group
usermod -aG sudo alice
# Verify (user must log out and back in for group change to take effect)
id alice
/etc/sudoers.d/
Rather than editing /etc/sudoers directly, drop files in /etc/sudoers.d/. This keeps custom rules separate from distribution defaults and makes them easier to manage with Ansible.
# Create a sudoers.d file for deploy user
visudo -f /etc/sudoers.d/deploy
# Contents of /etc/sudoers.d/deploy:
deploy ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart myapp, \
/usr/bin/systemctl reload nginx
# Permissions must be exactly 0440
chmod 0440 /etc/sudoers.d/deploy
# Validate without saving (dry run)
visudo -c -f /etc/sudoers.d/deploy
visudo -c to validate a sudoers file before applying it. A syntax error in sudoers can lock everyone out of sudo, including root escalation via sudo.
Service accounts
Service accounts are users created for running services, not for human login. They have no password, no interactive shell, and often no home directory.
# Create a system account (low UID, no login)
useradd -r -s /sbin/nologin -d /var/lib/myapp -c "myapp service" myapp
# -r = system account (UID below 1000)
# -s /sbin/nologin = cannot log in interactively
# -d = home directory (can be /nonexistent if the service doesn't need one)
# Verify the account
id myapp
getent passwd myapp
# Create a directory owned by the service account
mkdir -p /var/lib/myapp
chown myapp:myapp /var/lib/myapp
chmod 750 /var/lib/myapp
# In a systemd unit, run as this account
# [Service]
# User=myapp
# Group=myapp
Ansible
---
# Create a user
- name: Create deploy user
ansible.builtin.user:
name: deploy
comment: Deployment service account
shell: /bin/bash
create_home: true
groups:
- deploy
append: true # don't remove from other groups
# Create a system service account
- name: Create myapp service account
ansible.builtin.user:
name: myapp
system: true
shell: /sbin/nologin
home: /var/lib/myapp
create_home: true
comment: myapp service account
# Create a group
- name: Create developers group
ansible.builtin.group:
name: developers
gid: 2000
state: present
# Add user to groups
- name: Add alice to developers and wheel
ansible.builtin.user:
name: alice
groups:
- developers
- wheel
append: true # IMPORTANT: preserve existing group memberships
# Deploy a sudoers.d rule
- name: Deploy sudoers rule for deploy user
ansible.builtin.template:
src: sudoers-deploy.j2
dest: /etc/sudoers.d/deploy
owner: root
group: root
mode: '0440'
validate: visudo -c -f %s
Troubleshooting
# User can't log in
# Check if account is locked
passwd -S alice # LK = locked, PS = has password, NP = no password
chage -l alice # check expiry dates
# User not in expected groups after usermod -aG
# Groups only update after the user's session ends and starts again
# Check current session groups vs /etc/group
id alice # session groups
getent group wheel # check wheel group membership in /etc/group
# "sudo: PAM account management error"
# SSSD or PAM misconfiguration — check sssd.conf and /etc/pam.d/sudo
journalctl -u sssd
# "sudo: alice is not in the sudoers file. This incident will be reported."
# Add to wheel/sudo group, or create a sudoers.d rule
usermod -aG wheel alice # RHEL
usermod -aG sudo alice # Debian
# visudo reports syntax error after edits
# The error line number is shown — fix and run visudo again
# Emergency recovery: boot to single-user mode and edit manually
useradd / visudo is for standalone servers, service accounts, and bootstrap situations.
faillock — account unlock after PAM lockout
When PAM's pam_faillock (or its predecessor pam_tally2) is configured, repeated failed logins lock the account automatically. The faillock command shows and clears the failure count.
# Show all accounts with recorded failures
faillock
# Show failures for a specific user
faillock --user alice
# Example output:
# alice:
# When Type Source Valid
# 2024-03-15 10:23:01 TTY ssh V
# 2024-03-15 10:23:05 TTY ssh V
# 2024-03-15 10:23:09 TTY ssh V ← locked after this
# Reset (unlock) a user's failure count
faillock --user alice --reset
# Verify the counter is cleared
faillock --user alice # should now show no entries
The lockout settings (max tries, unlock time) are configured in /etc/security/faillock.conf or in /etc/pam.d/system-auth. Common settings: deny = 5 (lock after 5 failures), unlock_time = 600 (10 minute auto-unlock). Manual unlock with faillock --reset works immediately regardless of the unlock_time timer.
--maxfail and --lockouttime password policy. Use ipa user-unlock alice to unlock in that case.
/etc/login.defs
/etc/login.defs sets system-wide defaults that affect useradd, password aging, UID/GID ranges, and home directory creation. Many of these defaults are applied at user creation time and do not retroactively affect existing accounts.
# Key directives in /etc/login.defs
# Password aging defaults (applied to new accounts)
PASS_MAX_DAYS 90 # password expires after 90 days
PASS_MIN_DAYS 0 # minimum days before password can be changed
PASS_WARN_AGE 7 # warn user 7 days before expiry
# Minimum password length (PAM usually enforces this too)
PASS_MIN_LEN 8
# UID/GID ranges for normal users (useradd picks from these)
UID_MIN 1000
UID_MAX 60000
# UID/GID ranges for system accounts (useradd --system)
SYS_UID_MIN 201
SYS_UID_MAX 999
# Default umask for user home directories
UMASK 022
# Create home directory by default
CREATE_HOME yes
# Encrypt passwords with SHA-512
ENCRYPT_METHOD SHA512
Changes to /etc/login.defs only affect future useradd commands and future password changes. To apply new aging settings to existing accounts, use chage per-user.
pwck and grpck
After bulk user or group modifications (especially manual edits to /etc/passwd, /etc/shadow, or /etc/group), run these utilities to check for inconsistencies.
# Check /etc/passwd and /etc/shadow for consistency
pwck
# Read-only check (report only, no fixes)
pwck -r
# Check /etc/group and /etc/gshadow for consistency
grpck
# Read-only check
grpck -r
What pwck checks:
- Every user in
/etc/passwdhas a matching entry in/etc/shadow - Home directories exist
- Login shells listed in
/etc/passwdexist in/etc/shells - UID/GID values are valid numbers
What grpck checks:
- Every group in
/etc/grouphas a matching entry in/etc/gshadow - All users listed as group members exist in
/etc/passwd - Duplicate GIDs
# If pwck finds fixable issues, it will prompt to remove them
# Run non-interactively and just report
pwck -r 2>&1 | grep -v "^$"
useradd / visudo is for standalone servers, service accounts, and bootstrap situations.
umask: login.defs vs profile.d
Two different layers control a shell's default umask, and they disagree often enough to cause "new files keep landing with the wrong group/world bits" support calls. Always check both before concluding anything is broken.
# Layer 1: /etc/login.defs — consumed by PAM's pam_umask.so during LOGIN
# - Applied when a shell is started through login, sshd, or su -
# - One value for the whole system, set at session creation
UMASK 022 # world-readable new files (0644) — traditional default
UMASK 027 # group-readable, world-denied (0640) — hardened default
USERGROUPS_ENAB yes # if user == primary group, drop group bit from umask
# so 022 effectively becomes 002 for that user
# Layer 2: /etc/profile.d/*.sh — consumed by bash/sh at SHELL STARTUP
# - Runs AFTER pam_umask; can override it
# - Only affects interactive shells that source /etc/profile
cat > /etc/profile.d/umask.sh <<'EOF'
# Enforce a stricter umask for all interactive shells
if [ "$(id -u)" -ge 1000 ]; then
umask 027
fi
EOF
chmod 644 /etc/profile.d/umask.sh
Who wins:
- pam_umask (login.defs) sets the session umask first. This is what non-interactive cron jobs, systemd services, and scp/sftp sessions inherit — they never touch
/etc/profile.d/. - profile.d fires only for interactive login shells and overrides the PAM value for the rest of that shell's life. Non-interactive subprocesses (a script run from cron, a process spawned by systemd) do not see it.
- ~/.bashrc / ~/.bash_profile can override both per user — check these if one user's files are unexpectedly permissive.
For fleet-wide policy set UMASK in /etc/login.defs — it's the only layer that catches every session type (cron, systemd, SSH non-interactive, SFTP). Use /etc/profile.d/ only to tighten interactive shells further, never as the sole control. Verify with su - alice -c 'umask' and sudo -u alice bash -c 'umask'; they should both return the expected value.
PAM faillock vs SSSD lockout
On a FreeIPA- or AD-joined host there are two independent lockout systems running at the same time, and either one can block a login. Knowing which one locked a user is half the ticket.
| System | Scope | State lives in | Unlock command |
|---|---|---|---|
pam_faillock |
This one host only. Local counter, local reset. | /var/run/faillock/<user> (tally files on disk). |
faillock --user alice --reset |
| SSSD / IPA lockout | Realm-wide. Enforced by the KDC / LDAP directory, so a locked user cannot authenticate to any enrolled host. | IPA password policy (ipa pwpolicy-show), counter stored on the IPA master. |
ipa user-unlock alice (AD: Unlock-ADAccount) |
Diagnostic flow when alice reports "I can't log in":
# 1. Is alice locked locally on THIS host?
sudo faillock --user alice
# → if there are recent entries near the deny= threshold, PAM has her locked
# 2. Is alice locked in the directory (realm-wide)?
ipa user-status alice
# → shows "Kerberos pre-authentication failed" count per IPA replica
# → any replica at or above the policy max = realm lockout
# 3. Clear the right side:
sudo faillock --user alice --reset # if local
ipa user-unlock alice # if realm-wide
# 4. If both were locked, clear both — SSSD caches the directory lockout briefly,
# so also expire the cache so the next login sees the fresh unlock:
sudo sss_cache -u alice
Rule of thumb: local faillock locks one host, SSSD/IPA lockout locks the whole fleet. If a user can't log in anywhere, it's the directory side; if they can log in to some hosts but not one particular server, it's that host's local faillock. Don't chase the wrong one — the counters are separate and unlocking the wrong system has no effect on the other.