๐Ÿ‘ฅ Users & Groups โ€” Advanced Management

Users & Groups Series: Part 1 โ€” Basics  |  Part 2 โ€” Advanced Management  |  Part 3 โ€” sudo & Privileges

Beyond the Basics

Part 2 covers the topics that separate a competent sysadmin from one who just knows the basic commands โ€” password aging policies, account auditing, bulk provisioning, login.defs tuning, /etc/skel customization, and understanding the shadow file format in depth. These are the skills needed when managing dozens or hundreds of accounts consistently.

Examples

1
Password Aging in Depth โ€” chage
# View complete aging info for a user
sudo chage -l alice

# Set comprehensive aging policy
sudo chage -m 1 -M 90 -W 14 -I 7 -E 2026-12-31 alice
# -m 1   = minimum 1 day between changes
# -M 90  = maximum 90 days before forced change
# -W 14  = warn 14 days before expiry
# -I 7   = lock account 7 days after expiry
# -E     = hard account expiry date

# Force password change on next login
sudo chage -d 0 alice

# Remove account expiry (never expires)
sudo chage -E -1 alice

# Apply aging policy to all regular users at once
for user in $(awk -F: '$3 >= 1000 && $3 < 65534 {print $1}' /etc/passwd); do
    sudo chage -M 90 -W 14 -I 7 "$user"
    echo "Policy applied: $user"
done

# Find accounts with passwords that never expire
sudo awk -F: '$5 == "" || $5 == "99999" {print $1}' /etc/shadow
chage -l alice output:
Last password change : Apr 25, 2026 Password expires : Jul 24, 2026 Password inactive : Jul 31, 2026 Account expires : Dec 31, 2026 Minimum number of days between change : 1 Maximum number of days between change : 90 Number of days of warning before expiry : 14
2
Understanding /etc/shadow

The shadow file contains hashed passwords and aging data. Reading it correctly is essential for auditing:

# View shadow file (root only)
sudo cat /etc/shadow | head -3
# Format: username:hash:lastchange:min:max:warn:inactive:expire:reserved
alice:$6$rounds=5000$saltsalt$hashedpassword...:19472:1:90:14:7:20088:
FieldExampleMeaning
usernamealiceLogin name
hash$6$...$6$=SHA-512, $5$=SHA-256, $y$=yescrypt, $1$=MD5 (weak)
lastchange19472Days since Jan 1 1970 of last password change
min1Minimum days between changes
max90Maximum days before forced change
warn14Days warning before expiry
inactive7Days after expiry before account locked
expire20088Days since epoch when account expires (empty = never)
# Find locked accounts (hash starts with !)
sudo awk -F: '$2 ~ /^!/ {print $1, "LOCKED"}' /etc/shadow

# Find accounts with no password set
sudo awk -F: '$2 == "" {print $1, "NO PASSWORD"}' /etc/shadow

# Find weak MD5 hashed passwords
sudo awk -F: '$2 ~ /^\$1\$/ {print $1, "MD5 - UPGRADE"}' /etc/shadow
โš ๏ธ Hash prefix meanings: $6$ = SHA-512 (good) ยท $5$ = SHA-256 (acceptable) ยท $y$ = yescrypt (best, RHEL 9+) ยท $1$ = MD5 (weak โ€” upgrade immediately) ยท ! or !! = locked ยท * = no password login (service accounts).
3
Defaults โ€” /etc/login.defs and /etc/default/useradd
# Key settings in /etc/login.defs
grep -E "^UID_MIN|^UID_MAX|^GID_MIN|^GID_MAX|^PASS_MAX|^PASS_MIN|^PASS_WARN|^CREATE_HOME|^ENCRYPT" \
    /etc/login.defs
Typical RHEL 9 output:
PASS_MAX_DAYS 90 PASS_MIN_DAYS 1 PASS_WARN_AGE 14 UID_MIN 1000 UID_MAX 60000 GID_MIN 1000 GID_MAX 60000 CREATE_HOME yes ENCRYPT_METHOD SHA512
# Changes to login.defs only affect NEW accounts
# Existing accounts need chage/usermod to update

# View useradd defaults
useradd -D

# Change useradd default shell
sudo useradd -D -s /bin/bash

# Change default inactive period
sudo useradd -D -f 7
๐Ÿ’ก Set login.defs BEFORE creating accounts. Password aging, UID ranges, and encryption method set here apply to all new accounts automatically. No need to set them per-user if you configure defaults first.
4
Customizing /etc/skel

Files in /etc/skel/ are copied to every new user's home directory at creation โ€” the right place for org-standard shell configs and welcome material:

# View current skel contents
ls -la /etc/skel/

# Add standard aliases for all new users
sudo tee /etc/skel/.bash_aliases << 'EOF'
alias ll='ls -alF'
alias la='ls -A'
alias grep='grep --color=auto'
alias df='df -h'
alias du='du -h'
alias free='free -h'
alias ports='ss -tulpn'
alias myip='ip -br addr show'
EOF

# Add a welcome / orientation file
sudo tee /etc/skel/README.txt << 'EOF'
Welcome to Binghamton University ITS Linux Systems.
Documentation: http://bambustudio/LessonPlans/LearnLinux/
Help desk: x4357
EOF

# Test โ€” new user gets these files automatically
sudo useradd -m testuser
ls -la /home/testuser/
sudo userdel -r testuser
/etc/skel is powerful and often overlooked. Every file placed there appears in every new user's home directory. Standard .bashrc, .vimrc, org aliases โ€” set once, applied to everyone going forward. Existing users are not affected.
5
Account Security Audit Script
#!/bin/bash
# user-audit.sh โ€” security audit of local accounts

echo "========================================"
echo " User Account Audit โ€” $(hostname)"
echo " $(date)"
echo "========================================"

echo ""
echo "--- ACCOUNTS WITH UID 0 (root-equivalent) ---"
awk -F: '$3 == 0 {print $1}' /etc/passwd

echo ""
echo "--- ACCOUNTS WITH NO PASSWORD ---"
sudo awk -F: '$2 == "" {print $1}' /etc/shadow

echo ""
echo "--- LOCKED ACCOUNTS ---"
sudo awk -F: '$2 ~ /^!/ {print $1}' /etc/shadow

echo ""
echo "--- WEAK MD5 HASHED PASSWORDS ---"
sudo awk -F: '$2 ~ /^\$1\$/ {print $1, "UPGRADE NEEDED"}' /etc/shadow

echo ""
echo "--- INTERACTIVE ACCOUNTS (have login shell) ---"
awk -F: '$7 !~ /nologin|false|sync|halt|shutdown/ && $3 >= 1000 {print $1, $7}' /etc/passwd

echo ""
echo "--- RECENT LOGINS ---"
last | head -15

echo "========================================"
๐Ÿ’ก Run this audit monthly and diff the output against last month's. New UID-0 accounts, new interactive accounts, or newly unlocked accounts that weren't there before are red flags that warrant immediate investigation.
6
Bulk User Provisioning from a CSV
#!/bin/bash
# bulk-useradd.sh โ€” create users from a CSV file
# CSV format: username,fullname,department,shell
# Example: jsmith,John Smith,finance,/bin/bash

CSVFILE=$1
LOGFILE=/var/log/bulk-useradd-$(date +%Y%m%d).log

if [[ -z "$CSVFILE" || ! -f "$CSVFILE" ]]; then
    echo "Usage: $0 users.csv"
    exit 1
fi

echo "Bulk user creation started: $(date)" | tee -a "$LOGFILE"

while IFS=, read -r username fullname department shell; do
    # Skip header line and blank lines
    [[ "$username" == "username" || -z "$username" ]] && continue

    # Check if user already exists
    if id "$username" &>/dev/null; then
        echo "SKIP: $username already exists" | tee -a "$LOGFILE"
        continue
    fi

    # Create department group if needed
    if ! getent group "$department" &>/dev/null; then
        sudo groupadd "$department"
        echo "Created group: $department" | tee -a "$LOGFILE"
    fi

    # Create user
    sudo useradd \
        -m \
        -s "$shell" \
        -c "$fullname" \
        -G "$department" \
        "$username"

    # Force password change on first login
    sudo chage -d 0 "$username"

    echo "OK: $username ($fullname) dept=$department" | tee -a "$LOGFILE"

done < "$CSVFILE"

echo "Done: $(date)" | tee -a "$LOGFILE"
echo "Log: $LOGFILE"
Example users.csv:
username,fullname,department,shell jsmith,John Smith,finance,/bin/bash mmiller,Mary Miller,sysadmin,/bin/bash bwilson,Bob Wilson,developers,/bin/bash
7
Offboarding โ€” Safe Account Removal
#!/bin/bash
# offboard.sh โ€” safely disable and archive a departing user

USERNAME=$1
ARCHIVE_DIR=/archive/users

if [[ -z "$USERNAME" ]]; then
    echo "Usage: $0 username"
    exit 1
fi

if ! id "$USERNAME" &>/dev/null; then
    echo "ERROR: User $USERNAME does not exist"
    exit 1
fi

echo "Offboarding: $USERNAME"

# Step 1: Lock the account immediately
sudo usermod -L "$USERNAME"
sudo usermod -s /sbin/nologin "$USERNAME"
echo "Account locked."

# Step 2: Kill any active sessions
sudo pkill -u "$USERNAME" 2>/dev/null
echo "Sessions terminated."

# Step 3: Archive home directory
sudo mkdir -p "$ARCHIVE_DIR"
sudo tar -czf "$ARCHIVE_DIR/${USERNAME}-$(date +%Y%m%d).tar.gz" \
    -C /home "$USERNAME" 2>/dev/null
echo "Home archived to $ARCHIVE_DIR/${USERNAME}-$(date +%Y%m%d).tar.gz"

# Step 4: Find all files owned by user on system
echo "Files owned by $USERNAME outside home:"
sudo find / -path /home -prune -o -user "$USERNAME" -print 2>/dev/null

# Step 5: Remove cron jobs
sudo crontab -r -u "$USERNAME" 2>/dev/null
echo "Cron jobs removed."

# Step 6: Remove the account (keep home for 30 days then delete)
sudo userdel "$USERNAME"
echo "Account removed. Home directory preserved for 30 days."
echo "To remove home: sudo rm -rf /home/$USERNAME"
8
ACLs โ€” Access Beyond Traditional Permissions

When standard owner/group/other permissions aren't flexible enough, POSIX ACLs let you grant permissions to specific users or groups without changing ownership:

# Install ACL tools if needed
sudo dnf install -y acl    # RHEL
sudo apt install -y acl    # Debian/Ubuntu

# View current ACLs on a file or directory
getfacl /var/www/myapp/

# Grant a specific user read/write access
sudo setfacl -m u:alice:rw /var/www/myapp/config.php

# Grant a group read/execute on a directory
sudo setfacl -m g:developers:rx /var/www/myapp/

# Set default ACL (new files inherit these permissions)
sudo setfacl -d -m g:developers:rwx /var/www/myapp/

# Apply recursively
sudo setfacl -R -m g:developers:rx /var/www/myapp/

# Remove a specific ACL entry
sudo setfacl -x u:alice /var/www/myapp/config.php

# Remove ALL ACL entries (back to standard permissions)
sudo setfacl -b /var/www/myapp/config.php
getfacl output:
# file: /var/www/myapp/ # owner: root # group: www-data user::rwx user:alice:rw- group::r-x group:developers:rwx mask::rwx other::--- default:group:developers:rwx
The + in ls output: When a file has an ACL, ls -l shows a + after the permissions: -rw-r--r--+. That's your signal to run getfacl to see the full permission set.

Quick Reference

CommandWhat it does
chage -l userShow full password aging info
chage -M 90 -W 14 -I 7 userSet aging policy
chage -d 0 userForce password change on next login
chage -E 2026-12-31 userSet account expiry date
chage -E -1 userRemove account expiry
grep ENCRYPT /etc/login.defsCheck password hash algorithm
useradd -DShow useradd defaults
ls -la /etc/skel/Show skel template files
awk -F: '$3==0' /etc/passwdFind UID-0 accounts
awk -F: '$2~/^!/' /etc/shadowFind locked accounts
awk -F: '$2==""' /etc/shadowFind passwordless accounts
getfacl fileShow ACL permissions
setfacl -m u:user:rw fileGrant user ACL permission
setfacl -m g:group:rx dirGrant group ACL permission
setfacl -d -m g:group:rwx dirSet default ACL for new files
setfacl -b fileRemove all ACLs

← Back to Users-Groups Index ↑ Back to EXPANDED