Automating Linux User Configuration Updates
Managing user configurations at scale is one of those sysadmin challenges that starts small and quietly grows into a maintenance headache. When you update /etc/skel or change password aging defaults in /etc/login.defs, only newly created accounts pick up those changes — every existing user is left behind. In this guide, we’ll build two Bash scripts that close that gap, schedule them intelligently, and wire up proper logging so you always know what changed and when.
Understanding the Problem
The Skeleton Directory
The /etc/skel directory holds the default dotfiles that are copied into every new user’s home directory at account creation time. Common files include:
| File | Purpose |
|---|---|
.bashrc |
Interactive shell configuration |
.profile |
Login environment settings |
.bash_profile |
Bash-specific login configuration |
.bash_logout |
Commands executed on logout |
The catch is that this copy happens once, at account creation. Updating /etc/skel/.bashrc today does nothing for the 50 users already on the system — their dotfiles are untouched copies from whenever their accounts were made.
Password Aging Policies
The same problem applies to password aging. Default policy values live in /etc/login.defs:
1
2
3
PASS_MAX_DAYS 99999
PASS_MIN_DAYS 0
PASS_WARN_AGE 7
Change these values and only future accounts inherit the new policy. Existing users keep whatever aging settings were in place when their accounts were created — which is often no aging policy at all.
The result: Two separate configuration gaps that silently widen with every policy update you make.
Automated Solution Architecture
Our approach covers both problems with two focused scripts and flexible scheduling options:
| Task | Challenge | Solution |
|---|---|---|
| Skeleton Updates | Changes to /etc/skel only affect new users |
Script copies updates to all existing home directories |
| Password Aging | Policy changes in /etc/login.defs only affect new users |
Script applies chage settings to all existing accounts |
| Scheduling | Manual execution is error-prone |
at command for one-time runs; cron for recurring automation |
Both scripts share common design principles: root-only execution, UID range filtering to skip system accounts, timestamped logging, and graceful error handling.
Part 1: Skeleton Directory Synchronization
Script: update_skel_to_users.sh
This script reads all regular user accounts from /etc/passwd, checks that their home directory exists, backs up any files it is about to replace, then copies the current /etc/skel contents with correct ownership and permissions.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#!/bin/bash
#######################################################
# Script: update_skel_to_users.sh
# Purpose: Sync /etc/skel updates to existing users
# Author: System Administrator
#######################################################
LOGFILE="/var/log/skel_sync.log"
SKEL_DIR="/etc/skel"
MIN_UID=1000 # Minimum UID for regular users
MAX_UID=60000 # Maximum UID for regular users
log_message() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOGFILE"
}
if [[ $EUID -ne 0 ]]; then
echo "Error: This script must be run as root"
exit 1
fi
log_message "=== Starting skeleton directory synchronization ==="
USERS=$(awk -F: -v min=$MIN_UID -v max=$MAX_UID \
'$3 >= min && $3 <= max && $7 !~ /nologin|false/ {print $1":"$6}' /etc/passwd)
UPDATED_COUNT=0
ERROR_COUNT=0
while IFS=: read -r username homedir; do
if [[ ! -d "$homedir" ]]; then
log_message "Warning: Home directory $homedir does not exist for user $username"
continue
fi
log_message "Processing user: $username ($homedir)"
for skelfile in "$SKEL_DIR"/.[!.]*; do
if [[ -f "$skelfile" ]]; then
filename=$(basename "$skelfile")
target="$homedir/$filename"
# Backup existing file if present
if [[ -f "$target" ]]; then
cp "$target" "${target}.backup.$(date +%Y%m%d)"
fi
if cp "$skelfile" "$target"; then
chown "$username:$username" "$target"
chmod --reference="$skelfile" "$target"
log_message " ✓ Updated: $filename"
(( UPDATED_COUNT++ ))
else
log_message " ✗ Error: Failed to copy $filename"
(( ERROR_COUNT++ ))
fi
fi
done
done <<< "$USERS"
log_message "=== Synchronization complete ==="
log_message "Files updated: $UPDATED_COUNT"
log_message "Errors encountered: $ERROR_COUNT"
exit 0
What the Script Does
-
UID filtering —
awkselects only accounts with UIDs between 1000 and 60000 and excludes shell-less accounts (nologin,false), so system accounts likenobodyorwww-dataare never touched. -
Automatic backups — before overwriting any file, a dated copy is created (e.g.
.bashrc.backup.20251009), giving you a recovery path if something goes wrong. -
Ownership and permission preservation —
chownresets ownership to the target user, andchmod --referencemirrors the exact permissions from the skeleton file. -
Comprehensive logging — every action is timestamped and written to
/var/log/skel_sync.log.
Tip: Run
ls -la ~/.bashrc.backup.*on any user account after the script runs to confirm backups were created before the first production use.
Part 2: Password Aging Policy Automation
Understanding Password Aging
The chage command manages the password aging fields stored in /etc/shadow. Each user has independent aging values, and the script applies a consistent policy set across all regular accounts.
| Parameter |
chage Option |
Description | Typical Value |
|---|---|---|---|
| Maximum Days | -M |
Days until password change is required | 90 days |
| Minimum Days | -m |
Days that must pass before user can change password | 1 day |
| Warning Days | -W |
Days of advance warning before expiry | 7 days |
| Inactive Days | -I |
Days of inactivity before account locks | 30 days |
Script: update_password_aging.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#!/bin/bash
#######################################################
# Script: update_password_aging.sh
# Purpose: Apply password aging policies to existing users
# Author: System Administrator
#######################################################
LOGFILE="/var/log/password_aging.log"
# Password aging policy settings (in days)
MAX_DAYS=90 # Maximum password age
MIN_DAYS=1 # Minimum days between password changes
WARN_DAYS=7 # Warning days before expiration
INACTIVE_DAYS=30 # Days of inactivity before account lock
MIN_UID=1000
MAX_UID=60000
log_message() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOGFILE"
}
if [[ $EUID -ne 0 ]]; then
echo "Error: This script must be run as root"
exit 1
fi
log_message "=== Starting password aging policy update ==="
log_message "Policy: MAX=$MAX_DAYS, MIN=$MIN_DAYS, WARN=$WARN_DAYS, INACTIVE=$INACTIVE_DAYS"
USERS=$(awk -F: -v min=$MIN_UID -v max=$MAX_UID \
'$3 >= min && $3 <= max && $7 !~ /nologin|false/ {print $1}' /etc/passwd)
SUCCESS_COUNT=0
ERROR_COUNT=0
while IFS= read -r username; do
log_message "Processing user: $username"
if chage -M "$MAX_DAYS" -m "$MIN_DAYS" -W "$WARN_DAYS" -I "$INACTIVE_DAYS" "$username" 2>>"$LOGFILE"; then
log_message " ✓ Updated aging policy for: $username"
(( SUCCESS_COUNT++ ))
CURRENT_SETTINGS=$(chage -l "$username" 2>/dev/null | grep -E "Maximum|Minimum|Warning")
log_message " Current settings:
$CURRENT_SETTINGS"
else
log_message " ✗ Error: Failed to update aging policy for $username"
(( ERROR_COUNT++ ))
fi
done <<< "$USERS"
log_message "=== Password aging update complete ==="
log_message "Users updated successfully: $SUCCESS_COUNT"
log_message "Errors encountered: $ERROR_COUNT"
exit 0
After each successful chage call, the script immediately reads back the applied settings with chage -l and writes them to the log — giving you a verifiable audit trail of exactly what was set for each user.
Installation and Setup
Step 1: Create the Scripts Directory
Store administrative scripts in /usr/local/sbin to keep them separate from package-managed binaries and ensure they are only accessible to root:
1
2
sudo mkdir -p /usr/local/sbin/user-management
cd /usr/local/sbin/user-management
Step 2: Create and Secure the Scripts
Create each script file, paste in the code from the sections above, then lock down permissions:
1
2
3
4
5
6
7
# Create the files
sudo nano update_skel_to_users.sh
sudo nano update_password_aging.sh
# Restrict to root only
sudo chmod 700 update_skel_to_users.sh update_password_aging.sh
sudo chown root:root *.sh
Important:
chmod 700ensures only root can read, write, or execute these scripts. Never make them world-readable since they contain policy logic and interact with sensitive system files.
Scheduling: Three Execution Options
Because skeleton files and password policies don’t change on a fixed schedule, you have three sensible approaches depending on your workflow.
Option 1: Manual Execution (Immediate)
Run scripts directly whenever you’ve made a change that needs propagating:
1
2
3
4
5
# After updating /etc/skel files
sudo /usr/local/sbin/user-management/update_skel_to_users.sh
# After changing password aging policy
sudo /usr/local/sbin/user-management/update_password_aging.sh
Option 2: One-Time Scheduling with at
The at command schedules a job to run once at a specified time — ideal for running maintenance during off-hours without setting up a recurring schedule:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Install 'at' if needed
sudo apt install at # Debian/Ubuntu
sudo yum install at # RHEL/CentOS
# Schedule both scripts for tonight at midnight
echo "/usr/local/sbin/user-management/update_skel_to_users.sh" | sudo at midnight
echo "/usr/local/sbin/user-management/update_password_aging.sh" | sudo at midnight + 5 minutes
# Schedule for a specific date and time
echo "/usr/local/sbin/user-management/update_skel_to_users.sh" | sudo at 23:00 February 15
# Review and manage pending jobs
sudo atq # List scheduled jobs
sudo atrm 1 # Remove job number 1
The 5-minute offset between jobs prevents them from competing for the same resources and makes logs easier to read.
Option 3: Recurring Schedule with Cron
For environments where skeleton files or password policies change frequently, add cron entries to root’s crontab:
1
sudo crontab -e
1
2
3
4
5
6
7
# Daily execution at midnight
0 0 * * * /usr/local/sbin/user-management/update_skel_to_users.sh
5 0 * * * /usr/local/sbin/user-management/update_password_aging.sh
# Or weekly, every Sunday at midnight
0 0 * * 0 /usr/local/sbin/user-management/update_skel_to_users.sh
5 0 * * 0 /usr/local/sbin/user-management/update_password_aging.sh
Cron field reference — fields are ordered minute hour day-of-month month day-of-week:
| Field | Range | Description |
|---|---|---|
| Minute | 0–59 | Minute of the hour |
| Hour | 0–23 | Hour of the day (0 = midnight) |
| Day of Month | 1–31 | Day of the month |
| Month | 1–12 | Month of the year |
| Day of Week | 0–6 | Day of the week (0 = Sunday) |
Choosing the Right Method
| Method | When to Use | Best For |
|---|---|---|
Manual (sudo ./script.sh) |
Run right now | Testing, one-time updates, immediate changes |
One-time (at command) |
Schedule once for off-hours | Running during maintenance windows |
Recurring (cron) |
Automatic daily/weekly schedule | Frequent policy changes, large organizations |
Recommendation: Most administrators use manual execution or one-time
atscheduling. Skeleton files and password policies rarely change on a predictable schedule, so a recurring cron job often runs unnecessarily. Reserve cron for environments with high user turnover or frequent policy updates.
Verification and Testing
Always test scripts manually before relying on scheduled execution.
Run Scripts and Watch the Output
1
2
3
4
5
6
# Run and observe real-time log output
sudo /usr/local/sbin/user-management/update_skel_to_users.sh
sudo tail -f /var/log/skel_sync.log
sudo /usr/local/sbin/user-management/update_password_aging.sh
sudo tail -f /var/log/password_aging.log
Verify Password Aging for a Specific User
1
sudo chage -l username
This displays all aging fields — last change date, expiry date, warning period, and inactive period — so you can confirm the policy was applied correctly.
Verify Cron Configuration
1
2
3
4
5
6
# List root's current cron jobs
sudo crontab -l
# Confirm cron daemon is running
sudo systemctl status cron # Debian/Ubuntu
sudo systemctl status crond # RHEL/CentOS
Log Management with Logrotate
Without rotation, /var/log/skel_sync.log and /var/log/password_aging.log grow indefinitely. Create a logrotate configuration to manage them automatically:
1
sudo nano /etc/logrotate.d/user-management
1
2
3
4
5
6
7
8
9
/var/log/skel_sync.log /var/log/password_aging.log {
daily
rotate 30
compress
delaycompress
missingok
notifempty
create 0640 root root
}
The key directives explained: rotate 30 keeps 30 days of history before deleting old files; compress gzips rotated logs to save disk space; delaycompress skips compressing the most recently rotated file in case it is still being written to; missingok suppresses errors if a log file has not been created yet; notifempty skips rotation of empty files.
Advanced: Email Notifications
For production environments, add email alerts so administrators are notified of errors without having to monitor logs manually:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Add near the top of either script
ADMIN_EMAIL="admin@example.com"
send_email_notification() {
local subject="$1"
local body="$2"
echo "$body" | mail -s "$subject" "$ADMIN_EMAIL"
}
# Add at the bottom of update_skel_to_users.sh
send_email_notification "Skeleton Sync Complete" \
"Updated: $UPDATED_COUNT files
Errors: $ERROR_COUNT
Log: $LOGFILE"
Other enhancements worth considering: a --dry-run flag that logs planned changes without applying them; an exclusion list for special service accounts; checksum-based differential updates that only copy files that have actually changed in /etc/skel.
Troubleshooting Common Issues
| Problem | Possible Cause | Solution |
|---|---|---|
| Scripts don’t execute | Missing execute permission | sudo chmod +x script.sh |
| Cron job not running | Cron daemon stopped | sudo systemctl start cron |
| Permission denied errors | Not running as root | Use sudo or add to root’s crontab |
| Files not updating | Incorrect UID range | Adjust MIN_UID and MAX_UID variables |
| No log output | Log directory missing | sudo mkdir -p /var/log |
Security Considerations
A few practices that should accompany any user management automation:
-
Backups are automatic but verify them before first production use — the
.backup.YYYYMMDDfiles in each home directory are your rollback path. - Test on non-production systems first, particularly the skeleton sync script, since it modifies files in every user’s home directory.
- Communicate password policy changes to users before enforcement — a 90-day maximum with no prior warning causes unnecessary support tickets.
- Review logs after every run until you have confidence in the scripts’ behavior on your specific system and user base.
-
UID range 1000–60000 covers standard regular users on most distributions, but verify this matches your environment with
awk -F: '$3 >= 1000 {print $1, $3}' /etc/passwd.
Quick Reference
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# Run skeleton sync
sudo /usr/local/sbin/user-management/update_skel_to_users.sh
# Run password aging update
sudo /usr/local/sbin/user-management/update_password_aging.sh
# Monitor logs in real time
sudo tail -f /var/log/skel_sync.log
sudo tail -f /var/log/password_aging.log
# Check aging policy for a user
sudo chage -l username
# List scheduled at jobs
sudo atq
# List root cron jobs
sudo crontab -l
# View skeleton directory
ls -la /etc/skel
# Check cron daemon status
sudo systemctl status crond # RHEL/CentOS
sudo systemctl status cron # Debian/Ubuntu
Conclusion
Skeleton directory synchronization and password aging enforcement are two of the most commonly overlooked gaps in Linux user management. The scripts in this guide close both gaps with consistent, logged, root-safe automation that respects existing files through automatic backups and skips system accounts through UID range filtering.
For most environments, manual or one-time at scheduling is the right approach — trigger propagation when you actually make a policy change rather than running nightly regardless. Reserve cron scheduling for high-turnover environments where policies shift frequently and the overhead of recurring runs is genuinely justified.
Pair these scripts with the logrotate configuration and you have a complete, low-maintenance solution for keeping all your users in sync with current policy.
Additional Resources
- chage(1) man page
- at(1) man page
- crontab(5) man page
- Red Hat — Managing Users and Groups
- logrotate(8) man page