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.
# 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/shadowchage -l alice output:
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
| Field | Example | Meaning |
|---|---|---|
| username | alice | Login name |
| hash | $6$... | $6$=SHA-512, $5$=SHA-256, $y$=yescrypt, $1$=MD5 (weak) |
| lastchange | 19472 | Days since Jan 1 1970 of last password change |
| min | 1 | Minimum days between changes |
| max | 90 | Maximum days before forced change |
| warn | 14 | Days warning before expiry |
| inactive | 7 | Days after expiry before account locked |
| expire | 20088 | Days 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
$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).
# 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:
# 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
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
.bashrc, .vimrc, org aliases โ
set once, applied to everyone going forward.
Existing users are not affected.
#!/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 "========================================"
#!/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:
#!/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"
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.phpgetfacl output:
ls -l shows a + after the permissions:
-rw-r--r--+. That's your signal to run
getfacl to see the full permission set.
| Command | What it does |
|---|---|
| chage -l user | Show full password aging info |
| chage -M 90 -W 14 -I 7 user | Set aging policy |
| chage -d 0 user | Force password change on next login |
| chage -E 2026-12-31 user | Set account expiry date |
| chage -E -1 user | Remove account expiry |
| grep ENCRYPT /etc/login.defs | Check password hash algorithm |
| useradd -D | Show useradd defaults |
| ls -la /etc/skel/ | Show skel template files |
| awk -F: '$3==0' /etc/passwd | Find UID-0 accounts |
| awk -F: '$2~/^!/' /etc/shadow | Find locked accounts |
| awk -F: '$2==""' /etc/shadow | Find passwordless accounts |
| getfacl file | Show ACL permissions |
| setfacl -m u:user:rw file | Grant user ACL permission |
| setfacl -m g:group:rx dir | Grant group ACL permission |
| setfacl -d -m g:group:rwx dir | Set default ACL for new files |
| setfacl -b file | Remove all ACLs |