User Management on RHEL 9
Going deep on every aspect of user and group management on RHEL 9. Not just the commands, but what they actually do under the hood.
- What happens when you create a user
- The useradd flags that matter
- Managing passwords properly
- System-wide password policies
- The skeleton directory. What new users get.
- Groups. Primary vs supplementary.
- Locking and disabling accounts
- Service accounts. Users that should never log in.
- Deleting users properly
- Sudo configuration deep dive
- Practical exercises
- Final checklist: monthly access review
The scenario: You’re managing a RHEL 9 server for a growing team. New people join, people leave, roles change, contractors need temporary access, service accounts need to exist but never log in, and someone always ends up with more permissions than they should have. User management sounds basic until you’re the one responsible for making sure the right people have the right access and nobody else does.
The problem: Most tutorials cover useradd and passwd and call it a day. But real user management is deeper than that. It’s password policies, account expiration, skeleton directories, group hierarchies, sudo scoping, locking accounts without deleting them, and understanding what happens under the hood when you create a user. If you don’t understand the system, you end up with orphaned files, security gaps, and that one contractor account from six months ago that still has sudo access.
What we’re going to do: Go deep on every aspect of user and group management on RHEL 9. Not just the commands but what they actually do to the system, which files they modify, and why each option exists. By the end you’ll manage users like someone who understands the machinery, not someone who memorized a cheat sheet.
What happens when you create a user
When you run useradd jmorales, it feels like one simple thing happened. A user was created. But under the hood, the system touched at least five different files and created a directory. Understanding this is the difference between managing users and actually knowing what you’re doing.
sudo useradd -m -s /bin/bash jmorales
Here’s everything that just happened:
1. /etc/passwd got a new line. This file stores basic user info. One line per user. No passwords here despite the name.
grep jmorales /etc/passwd # jmorales:x:1001:1001::/home/jmorales:/bin/bash
Let me break down each field separated by colons:
jmorales # username
x # password placeholder (actual hash is in /etc/shadow)
1001 # UID (user ID number)
1001 # GID (primary group ID number)
# GECOS field (comment/full name, empty here)
/home/jmorales # home directory
/bin/bash # login shell2. /etc/shadow got a new line. This is where the actual password hash lives. Only root can read this file.
sudo grep jmorales /etc/shadow # jmorales:!!:19837:0:99999:7:::
The !! means no password has been set yet. The user exists but can’t log in until you set a password. The other fields control password aging (minimum days, maximum days, warning period, etc.). We’ll dig into those later.
3. /etc/group got a new line. A private group was created with the same name as the user.
grep jmorales /etc/group # jmorales:x:1001:
This is called the User Private Group (UPG) scheme. Every user gets their own group by default. This matters for file creation permissions, which we’ll cover later.
4. /home/jmorales was created. The home directory was populated with files from /etc/skel.
ls -la /home/jmorales/ # drwx------. 2 jmorales jmorales 62 Apr 24 10:00 . # -rw-r--r--. 1 jmorales jmorales 18 Apr 24 10:00 .bash_logout # -rw-r--r--. 1 jmorales jmorales 141 Apr 24 10:00 .bash_profile # -rw-r--r--. 1 jmorales jmorales 492 Apr 24 10:00 .bashrc
5. /etc/gshadow got a new line. This stores group password info (rarely used but it’s there).
Five files, one directory, one command. Now you know what useradd actually does.
The useradd flags that matter
The bare useradd username command works but it often doesn’t give you what you need. Here are the flags you’ll use in real life and on the RHCSA:
# Full-featured user creation sudo useradd \ -m \ # create home directory -s /bin/bash \ # set login shell -G developers,wheel \ # supplementary groups -c "Juan Morales" \ # full name (GECOS field) -e 2026-12-31 \ # account expiration date -u 2001 \ # specific UID jmorales
Let me explain each one:
-m creates the home directory. On RHEL 9 this is the default behavior, but some systems don’t create it automatically. Always include it to be safe.
-s /bin/bash sets the login shell. Without this, the default is whatever’s in /etc/default/useradd, which might be /bin/sh or even nothing. For service accounts that should never log in, use -s /sbin/nologin.
-G developers,wheel adds the user to supplementary groups. Capital G. This is different from -g (lowercase) which sets the primary group. The primary group is created automatically with the UPG scheme. Supplementary groups are for shared access.
-c “Juan Morales” sets the GECOS field. This is the comment/description that shows up in /etc/passwd and when someone runs finger jmorales. It’s just metadata but it helps when you’re managing dozens of accounts and can’t remember who “jmorales” is.
-e 2026-12-31 sets an account expiration date. After this date, the user cannot log in. Perfect for contractors or temporary access. The account still exists but it’s locked.
-u 2001 sets a specific UID instead of letting the system auto-assign one. Use this when you need UIDs to match across multiple servers (NFS, for example, relies on matching UIDs).
Managing passwords properly
Setting passwords is straightforward. Managing them properly is where it gets interesting.
# Set a password interactively sudo passwd jmorales # Set a password non-interactively (for scripts) echo "jmorales:TempPass123!" | sudo chpasswd # Force password change on next login sudo chage -d 0 jmorales # Check password aging info sudo chage -l jmorales
The chage command is the one most people don’t know about. It controls all the password aging policies:
# Password must be changed at least every 90 days sudo chage -M 90 jmorales # Password can't be changed more than once per day (prevents cycling) sudo chage -m 1 jmorales # Warn user 14 days before password expires sudo chage -W 14 jmorales # Account expires on a specific date sudo chage -E 2026-12-31 jmorales # Check all settings sudo chage -l jmorales
The output of chage -l shows you everything:
Last password change : Apr 24, 2026 Password expires : Jul 23, 2026 Password inactive : never Account expires : Dec 31, 2026 Minimum number of days between changes : 1 Maximum number of days between changes : 90 Number of days of warning before expiry : 14
-m 1 (minimum 1 day between changes) prevents users from cycling through password changes to get back to their old password. If the system remembers the last 5 passwords and the minimum is 0, a clever user can change their password 5 times in a row and then set it back to the original. Setting a minimum of 1 day stops that.System-wide password policies
The chage command sets policies per user. For system-wide defaults that apply to every new user created, edit /etc/login.defs:
sudo vim /etc/login.defs
The key lines:
PASS_MAX_DAYS 90 # password expires after 90 days PASS_MIN_DAYS 1 # must wait 1 day between changes PASS_MIN_LEN 12 # minimum password length PASS_WARN_AGE 14 # warn 14 days before expiry UID_MIN 1000 # regular users start at UID 1000 UID_MAX 60000 # max UID for regular users CREATE_HOME yes # always create home directory
These defaults apply to every user created AFTER you change this file. Existing users keep their current settings unless you update them individually with chage.
The skeleton directory. What new users get.
When useradd -m creates a home directory, it copies everything from /etc/skel into it. This is the skeleton directory. Whatever you put in here, every new user gets.
ls -la /etc/skel/ # .bash_logout # .bash_profile # .bashrc
Want every new user to see a welcome message when they log in? Add it to the skeleton:
# Add a README to the skeleton sudo vim /etc/skel/README.txt
Welcome to dept-server01. Please change your password on first login. For access issues, contact the sysadmin team. Company IT policy: https://internal.company.com/it-policy
Want every new user to have a standard directory structure?
sudo mkdir -p /etc/skel/{projects,scripts,logs}
# Now when you create a new user:
sudo useradd -m -s /bin/bash testuser
ls /home/testuser/
# projects scripts logs README.txtThe skeleton directory is underrated. In a real environment, this saves you from manually setting up every new user’s home directory.
Groups. Primary vs supplementary.
Every user has exactly one primary group and zero or more supplementary groups. This distinction matters more than most people realize.
Primary group: Determines the default group ownership of files the user creates. When jmorales creates a file, it belongs to jmorales:jmorales (user:primary_group).
Supplementary groups: Determine what shared resources the user can access. Being in the developers group lets you read and write to /opt/projects. Being in the wheel group gives you sudo access.
# Create groups sudo groupadd developers sudo groupadd dbadmin sudo groupadd monitoring # Add a user to supplementary groups at creation sudo useradd -m -s /bin/bash -G developers,monitoring kpatel # Add an existing user to more groups (APPEND, don't replace) sudo usermod -aG dbadmin kpatel # Check the result id kpatel # uid=1002(kpatel) gid=1002(kpatel) groups=1002(kpatel),1001(developers),1003(dbadmin),1004(monitoring)
-a flag in usermod -aG means APPEND. If you forget the -a and just run usermod -G dbadmin kpatel, you will REPLACE all supplementary groups with just dbadmin. The user will be removed from developers and monitoring. This is the single most common usermod mistake. Always use -aG together.Remove a user from a group:
sudo gpasswd -d kpatel dbadmin # Removing user kpatel from group dbadmin
Locking and disabling accounts
When someone leaves the company, your first instinct might be to delete their account. Don’t. At least not right away. There might be files, running processes, or audit requirements tied to that account. Lock it first, investigate, then delete if appropriate.
# Lock the account (adds ! to the password hash, preventing login) sudo usermod -L jmorales # Verify the lock sudo grep jmorales /etc/shadow # jmorales:!$6$hash...:19837:... # The ! prefix means locked # Also expire the account immediately sudo chage -E 0 jmorales # Verify sudo chage -l jmorales # Account expires: Jan 01, 1970 (effectively immediately) # Change shell to nologin as a belt-and-suspenders measure sudo usermod -s /sbin/nologin jmorales
Now the account is triple-locked: password is locked, account is expired, and the shell prevents interactive login. Even if someone somehow bypasses one layer, the other two stop them.
To unlock later:
sudo usermod -U jmorales # unlock password sudo chage -E -1 jmorales # remove expiration (-1 means never) sudo usermod -s /bin/bash jmorales # restore shell
Service accounts. Users that should never log in.
Not every user account is for a human. Applications, daemons, and services often need their own user to run under. These accounts should never have a password and should never be able to log in interactively.
# Create a service account sudo useradd \ -r \ # system account (UID below 1000) -s /sbin/nologin \ # no interactive login -d /opt/myapp \ # home directory is the app directory -M \ # do NOT create home directory (it already exists) -c "MyApp Service" \ # description myapp
The -r flag creates a system account with a UID below 1000. This is a convention that separates human users (UID 1000+) from service/system accounts (UID below 1000). Tools like getent and login screens often filter based on this.
The -s /sbin/nologin flag means if anyone tries to SSH in as this user or su to it, they get a message saying “This account is currently not available” and the session closes. The service can still run as this user, it just can’t be used for interactive access.
Deleting users properly
When it’s finally time to delete an account, don’t just userdel username. Think about what files that user owned.
# Find all files owned by the user BEFORE deleting sudo find / -user jmorales -type f 2>/dev/null > /tmp/jmorales-files.txt wc -l /tmp/jmorales-files.txt # Check if any processes are running as this user ps -u jmorales # Delete the user AND their home directory sudo userdel -r jmorales # Without -r, the home directory stays behind as an orphan # With -r, home directory and mail spool are removed
After deleting a user, any files they owned outside their home directory become orphaned. They’ll show a UID number instead of a username when you ls -l them. Clean these up:
# Find orphaned files (owned by a UID that no longer exists)
sudo find / -nouser -type f 2>/dev/null
# Reassign them to another user or delete them
sudo find /opt/projects -nouser -exec chown rchen:developers {} \;Sudo configuration deep dive
The wheel group gives blanket sudo access. That’s fine for sysadmins. But for everyone else you want targeted permissions. Let’s go deeper than the basics.
sudo visudo -f /etc/sudoers.d/custom-roles
# Developers: restart web server only %developers ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart httpd, /usr/bin/systemctl status httpd # DB admins: manage MySQL only %dbadmin ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart mysqld, /usr/bin/systemctl status mysqld, /usr/bin/mysqldump # Monitoring team: read logs only %monitoring ALL=(ALL) NOPASSWD: /usr/bin/journalctl, /usr/bin/tail /var/log/* # Specific user: can run one specific script kpatel ALL=(ALL) NOPASSWD: /opt/scripts/deploy.sh
Each line follows the pattern: WHO WHERE=(AS_WHOM) WHAT. The % prefix means it’s a group. ALL=(ALL) means from any host, as any user. NOPASSWD: skips the password prompt. The command paths must be absolute.
Verify what a specific user can do:
sudo -l -U kpatel
Practical exercises
Exercise 1: Create a complete user with all options
# Create user with specific UID, groups, shell, expiration, and full name sudo useradd -m -s /bin/bash -G developers,monitoring -c "Test User" -e 2026-12-31 -u 2050 testuser1 # Set password and force change on first login echo "testuser1:Welcome123!" | sudo chpasswd sudo chage -d 0 testuser1 # Verify everything id testuser1 sudo chage -l testuser1 ls -la /home/testuser1/ grep testuser1 /etc/passwd
Exercise 2: Set up password policies
# Set max age 60 days, min age 2 days, warning 7 days sudo chage -M 60 -m 2 -W 7 testuser1 # Verify sudo chage -l testuser1 # Lock the account sudo usermod -L testuser1 # Verify the lock in /etc/shadow (look for ! prefix) sudo grep testuser1 /etc/shadow # Unlock it sudo usermod -U testuser1
Exercise 3: Create a service account
# Create a system account for an application sudo useradd -r -s /sbin/nologin -c "Monitoring Agent" monitoragent # Verify the UID is below 1000 id monitoragent # Try to log in as this user (should fail) su - monitoragent # This account is currently not available.
Exercise 4: Group management and the usermod trap
# Add testuser1 to three groups sudo usermod -aG developers,dbadmin,monitoring testuser1 id testuser1 # Should show all three supplementary groups # Now do it WRONG on purpose (omit the -a flag) sudo usermod -G developers testuser1 id testuser1 # dbadmin and monitoring are GONE # Fix it by adding them back sudo usermod -aG dbadmin,monitoring testuser1 id testuser1 # All three are back
Exercise 5: Full lifecycle. Create, configure, lock, delete.
# 1. Create the user
sudo useradd -m -s /bin/bash -G developers -c "Temp Contractor" -e 2026-06-30 contractor1
echo "contractor1:Temp999!" | sudo chpasswd
sudo chage -d 0 contractor1
# 2. Verify everything
id contractor1
sudo chage -l contractor1
# 3. The contractor's project ends early. Lock the account.
sudo usermod -L contractor1
sudo chage -E 0 contractor1
sudo usermod -s /sbin/nologin contractor1
# 4. Find all files they own
sudo find / -user contractor1 -type f 2>/dev/null
# 5. Reassign their project files
sudo find /opt/projects -user contractor1 -exec chown rchen:developers {} \;
# 6. Delete the account and home directory
sudo userdel -r contractor1
# 7. Verify they're gone
grep contractor1 /etc/passwd
# No output, they're gone
# 8. Check for orphaned files
sudo find / -nouser -type f 2>/dev/nullFinal checklist: monthly access review
Run this monthly. Seriously. Accounts accumulate access over time and nobody notices until something goes wrong.
# 1. All users with login shells (should recognize every name) getent passwd | awk -F: '$7 ~ /bash|sh/' | cut -d: -f1 # 2. All users with sudo access (should be minimal) sudo grep -r 'ALL=(ALL)' /etc/sudoers /etc/sudoers.d/ # 3. Users who haven't logged in recently (30+ days = investigate) sudo lastlog | grep -v "Never logged in" | grep -v Username # 4. Locked accounts (should match people who left) sudo awk -F: '$2 ~ /^!/' /etc/shadow | cut -d: -f1 # 5. Accounts expiring soon sudo awk -F: '$8 != "" && $8 != -1' /etc/shadow | cut -d: -f1,8 # 6. Orphaned files (files owned by deleted users) sudo find /opt /srv /var/www -nouser -type f 2>/dev/null # 7. Group memberships (review who's in privileged groups) getent group wheel getent group developers getent group dbadmin
If any of those checks reveals something unexpected, investigate. A dormant account with wheel access is a security incident waiting to happen. An orphaned file in a web directory could contain sensitive data. Access review is boring but it’s one of the most important things you can do as a sysadmin.