Why User Management Is the Foundation of Linux Security
Every file on your server has an owner. Every process runs as a user. Every web application executes under an account with specific privileges. Linux multi-user architecture is not just a design choice. It is the primary mechanism that prevents a compromised PHP script from deleting your entire system, or a rogue developer from reading another client database.
Yet user management is where most server admins cut corners. Root SSH enabled everywhere. Developers sharing a single account. Service daemons running as root because it is easier. These shortcuts compound over time until one incident exposes exactly how much risk was quietly accumulating.
This guide covers everything: user and group creation, modification, deletion, file ownership, sudo configuration, ACLs, and the security practices that separate well-run servers from ones waiting for a breach.
1. Understanding Linux Users
UID Ranges: Who Is Who
Every user on a Linux system has a numeric User ID (UID). The kernel works with UIDs. The name-to-UID mapping happens in /etc/passwd.
- UID 0 — root. The superuser. No restrictions apply.
- UID 1-999 — System users. Created by packages for services (nginx, mysql, www-data, postgres). They typically have no login shell and no home directory in /home/.
- UID 1000+ — Regular users. Real humans, developers, or isolated application accounts.
The Four User Files
/etc/passwd — The user database. World-readable. Seven colon-separated fields:
username:x:UID:GID:comment:home_dir:shell
# Example:
deployer:x:1001:1001:Deploy User:/home/deployer:/bin/bash
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
- username — Login name
- x — Password placeholder (actual hash is in /etc/shadow)
- UID — Numeric user ID
- GID — Primary group ID
- comment — Full name or description (GECOS field)
- home_dir — Home directory path
- shell — Login shell (/bin/bash, /bin/sh, or /usr/sbin/nologin to block login)
/etc/shadow — Password hashes. Root-readable only. Nine fields:
username:HASH:last_change:min_age:max_age:warn_days:inactive:expire:reserved
- hash — The actual password hash (algorithm prefix: $6$=SHA-512, $y$=yescrypt)
- last_change — Days since epoch when password was last changed
- min_age — Minimum days before password can be changed again
- max_age — Maximum days before password must be changed (99999 = never)
- warn_days — Days before expiry to warn user
- inactive — Days after expiry before account is locked
- expire — Absolute expiry date (days since epoch)
/etc/group — Group database:
groupname:x:GID:member1,member2,member3
/etc/gshadow — Group passwords and administrators. Rarely used in practice.
Useful User Inspection Commands
# Show current user UID, GID, and all groups
id
# Show another user info
id username
# Who you are right now
whoami
# Who is currently logged in
who
# Who is logged in and what they are doing
w
# Login history (last 20)
last -20
# Last login for all users
lastlog
2. Creating Users
useradd: The Low-Level Tool
useradd is available on all Linux distributions and gives you full control. Unlike adduser, it does not create a home directory by default unless you ask it to.
# Minimal: no home dir, no shell, useful for service accounts
useradd username
# Full user with home directory and bash shell
useradd -m -s /bin/bash username
# Full user, added to sudo group, with comment
useradd -m -s /bin/bash -G sudo -c "John Smith" john
All useradd flags:
| Flag | Description | Example |
|---|---|---|
-m | Create home directory | useradd -m alice |
-s SHELL | Set login shell | -s /bin/bash |
-G groups | Supplementary groups (comma-separated) | -G sudo,docker |
-u UID | Set specific UID | -u 1500 |
-g GID | Set primary group by GID or name | -g www-data |
-d DIR | Set home directory path | -d /var/app/alice |
-e DATE | Account expiry date (YYYY-MM-DD) | -e 2026-12-31 |
-c COMMENT | Full name / description | -c "Web Deploy Bot" |
-r | Create system user (UID under 1000, no home) | useradd -r myservice |
-M | Do NOT create home directory | useradd -M -r daemon |
adduser: The Interactive Helper (Debian/Ubuntu)
adduser is a higher-level Perl wrapper that creates a home directory, sets up skeleton files from /etc/skel/, prompts for a password, and asks for full name and other details. It is friendlier for interactive use.
adduser alice
# Prompts: password, full name, room, phone, etc.
useradd vs adduser
| useradd | adduser | |
|---|---|---|
| Type | Binary (all distros) | Script (Debian/Ubuntu) |
| Home directory | Only with -m | Always created |
| Password | Not set (use passwd after) | Prompts during creation |
| Scripting | Better (non-interactive) | Harder (interactive) |
| Skeleton copy | Yes (with -m) | Yes |
Setting and Managing Passwords
# Set password interactively
passwd username
# Set password non-interactively (scripts)
echo "username:newpassword" | chpasswd
# Check password aging info
chage -l username
# Force password change on next login
chage -d 0 username
# Set maximum password age to 90 days
chage -M 90 username
# Set warning 14 days before expiry
chage -W 14 username
# Set account expiry
chage -E 2026-12-31 username
3. Modifying Users
After creation, usermod handles all user modifications.
# Add user to supplementary group (ALWAYS use -a to append!)
usermod -aG docker alice
usermod -aG sudo,www-data alice
# Change login shell
usermod -s /bin/zsh alice
# Disable shell (user cannot log in interactively)
usermod -s /usr/sbin/nologin alice
# Lock account (prepends ! to password hash)
usermod -L alice
# Unlock account
usermod -U alice
# Move home directory to new location
usermod -d /new/home/alice -m alice
# Rename user (does NOT rename home directory automatically)
usermod -l newname oldname
# Set account expiry date
usermod -e 2026-06-30 alice
# Change primary group
usermod -g newgroup alice
# Change UID
usermod -u 1500 alice
Critical Warning: -G without -a destroys group memberships
usermod -G docker aliceremoves alice from ALL other groups and adds only docker.
usermod -aG docker aliceappends docker to alice existing groups.
Always use-aGwhen adding to groups, never-Galone.
4. Deleting Users
Before You Delete
Deleting a user is irreversible. Before running userdel, check for:
# Find all files owned by this user
find / -user alice 2>/dev/null
# Check for running processes
ps aux | grep alice
# Check for cron jobs
crontab -u alice -l
# Check mail spool
ls -la /var/mail/alice
The Commands
# Remove user account only (home directory and files remain)
userdel alice
# Remove user + home directory + mail spool
userdel -r alice
# Force deletion even if user is logged in (dangerous)
userdel -f alice
Orphaned Files
After deleting a user, files they owned become orphaned and show a numeric UID instead of a name in ls -la. Find and reassign or delete them:
# Find all files with no valid owner
find / -nouser 2>/dev/null
# Find all files with no valid group
find / -nogroup 2>/dev/null
# Change orphaned files to a new owner
find / -nouser 2>/dev/null -exec chown newowner {} \;
5. Groups
Primary vs Supplementary Groups
Every user has one primary group (set in /etc/passwd) and can belong to many supplementary groups. New files get the primary group as their group owner. Supplementary groups grant access to resources owned by those groups.
# Create a group
groupadd developers
# Create group with specific GID
groupadd -g 2000 developers
# Rename a group
groupmod -n newname oldname
# Delete a group (fails if it is a primary group for any user)
groupdel developers
# Add user to group
gpasswd -a alice developers
# Remove user from group
gpasswd -d alice developers
# Show user groups
groups alice
# List all members of a group
getent group developers
Common Groups and Their Purpose
| Group | Purpose |
|---|---|
sudo | Full sudo access (Ubuntu/Debian) |
wheel | Full sudo access (RHEL/CentOS/Fedora) |
www-data | Web server processes (nginx, apache) |
docker | Docker socket access (no sudo needed) |
adm | Read system logs in /var/log/ |
systemd-journal | Read journald logs |
ssh | SSH login (if AllowGroups configured) |
shadow | Read /etc/shadow |
disk | Raw disk access (use carefully) |
6. sudo: Controlled Root Access
The Golden Rule: Never Edit /etc/sudoers Directly
A syntax error in /etc/sudoers locks everyone out of sudo. Always use visudo, which validates syntax before saving.
# Open sudoers safely
visudo
# Edit with a specific editor
EDITOR=nano visudo
Sudoers Syntax
# Format: WHO WHERE=(AS_WHO) COMMANDS
# WHO: username or %groupname
# WHERE: hostname or ALL
# Full sudo for a user
alice ALL=(ALL:ALL) ALL
# Full sudo without password (only for automation accounts)
deployer ALL=(ALL) NOPASSWD: ALL
# Group-based sudo
%developers ALL=(ALL:ALL) ALL
# Specific commands only: best practice for service accounts
capistrano ALL=(ALL) /usr/bin/nginx -s reload
Drop-in Files (Preferred Approach)
Instead of editing the main sudoers file, create files in /etc/sudoers.d/. Cleaner, easier to audit, and mistakes only affect that one file:
# Create a drop-in file safely
visudo -f /etc/sudoers.d/alice
# Permissions must be 440 (world-unreadable)
chmod 440 /etc/sudoers.d/alice
Useful sudo Commands
# What can I do with sudo?
sudo -l
# Run as another user
sudo -u www-data php artisan migrate
# Get a root shell
sudo -i
# Repeat last command with sudo
sudo !!
# Run with preserved environment
sudo -E command
# Invalidate cached sudo credentials
sudo -k
7. File Ownership and Permissions
For a deeper look at exactly why 755 and 644 are the defaults you see everywhere, see Linux File Permissions Explained.
Reading ls -la Output
drwxr-xr-x 2 alice developers 4096 Mar 15 10:23 project/
-rw-r--r-- 1 alice developers 1234 Mar 15 10:20 config.yml
-rwxr-x--- 1 root www-data 8192 Mar 10 09:15 deploy.sh
The permission string drwxr-xr-x breaks down as:
- d — Type: d=directory, -=file, l=symlink, b=block device
- rwx — Owner permissions (read, write, execute)
- r-x — Group permissions
- r-x — Others permissions
Changing Ownership
# Change owner
chown alice file.txt
# Change owner and group
chown alice:developers file.txt
# Change group only
chgrp developers file.txt
# Recursive change
chown -R alice:developers /var/www/mysite/
chmod: Numeric and Symbolic
# Numeric (octal)
chmod 755 script.sh # rwxr-xr-x
chmod 644 config.yml # rw-r--r--
chmod 600 .ssh/id_rsa # rw-------
chmod 700 /home/alice/ # rwx------
# Symbolic
chmod u+x script.sh # Add execute for owner
chmod g-w file.txt # Remove write from group
chmod o-r private.txt # Remove read from others
chmod a+r public.txt # Add read for all
# Recursive
chmod -R 755 /var/www/
Numeric Permissions Reference
| Number | Binary | Permissions | Meaning |
|---|---|---|---|
| 7 | 111 | rwx | Read + Write + Execute |
| 6 | 110 | rw- | Read + Write |
| 5 | 101 | r-x | Read + Execute |
| 4 | 100 | r-- | Read only |
| 3 | 011 | -wx | Write + Execute |
| 2 | 010 | -w- | Write only |
| 1 | 001 | --x | Execute only |
| 0 | 000 | --- | No access |
Common Permission Patterns
| Pattern | Use Case | Why |
|---|---|---|
755 | Directories, scripts, web roots | Owner full access, others can read and traverse |
644 | Config files, HTML, PHP files | Owner can edit, others read-only |
600 | Private keys, .env files, secrets | Owner only, nothing leaks to others |
700 | Home directories | Complete privacy from other users |
640 | Config with group access | Owner edits, group reads, others nothing |
775 | Shared team directories | Owner and group full, others read only |
| 777 | Never use this | Gives everyone full access, a security hole |
Special Permission Bits
SUID (4000) — When set on an executable, it runs as the file owner (not the calling user). Used by /usr/bin/passwd to write /etc/shadow as root.
# Set SUID
chmod u+s /usr/bin/myprogram
chmod 4755 /usr/bin/myprogram
# Security audit: find all SUID files on the system
find / -perm -4000 2>/dev/null
SGID (2000) — On a directory, new files inherit the directory group instead of the creator primary group. Essential for team-shared directories.
# Set SGID on shared directory
chmod g+s /var/www/shared/
chmod 2775 /var/www/shared/
Sticky Bit (1000) — On a directory, only the file owner (or root) can delete or rename files, even if others have write permission. This is how /tmp works.
# Set sticky bit
chmod +t /tmp/shared/
chmod 1777 /tmp/
umask: Default Permission Mask
# Check current umask
umask
# 0022 = new files get 644, new directories get 755
# Change umask for current session
umask 0027
# 0027 = new files get 640, new directories get 750
# Set persistently for all users
echo "umask 0027" >> /etc/profile
8. ACLs: Access Control Lists
Standard Unix permissions allow exactly one owner and one group per file. ACLs extend this to grant specific access to additional users and groups without changing ownership.
# Install ACL tools if needed
apt install acl
# View ACLs on a file or directory
getfacl /var/www/project/
# Grant specific user read+write access
setfacl -m u:bob:rw /var/www/project/config.yml
# Grant group execute access
setfacl -m g:developers:rx /var/scripts/
# Set default ACL on directory (inherited by new files)
setfacl -d -m u:deployer:rwx /var/www/project/
# Remove ACL for a user
setfacl -x u:bob /var/www/project/config.yml
# Remove ALL ACLs from file
setfacl -b /var/www/project/config.yml
# Copy ACL from one file to another
getfacl source_file | setfacl --set-file=- target_file
When ACLs are set, ls -la shows a + at the end of the permission string, indicating extended ACLs are present on that file or directory.
9. Security Best Practices
Disable Root SSH Login
This pairs directly with proper SSH key authentication — user accounts and SSH access control are two sides of the same problem.
# Edit /etc/ssh/sshd_config and set:
# PermitRootLogin no
# PasswordAuthentication no
# AllowUsers alice bob deployer
# AllowGroups sshusers
# Then reload the SSH daemon configuration
Lock Unused Accounts
# Disable shell for service accounts
usermod -s /usr/sbin/nologin www-data
usermod -s /usr/sbin/nologin mysql
usermod -s /usr/sbin/nologin nginx
# Immediately expire old contractor account
usermod -e 1 legacy_contractor
# Check which users can actually log in
awk -F: '$7 != "/usr/sbin/nologin" && $7 != "/bin/false" {print $1, $7}' /etc/passwd
Password Complexity with libpam-pwquality
# Install
apt install libpam-pwquality
# In /etc/security/pwquality.conf:
# minlen = 12
# dcredit = -1 (at least 1 digit)
# ucredit = -1 (at least 1 uppercase)
# lcredit = -1 (at least 1 lowercase)
# ocredit = -1 (at least 1 special character)
# maxrepeat = 3 (no more than 3 consecutive same chars)
Account Lockout After Failed Attempts
# Ubuntu 22.04+ uses pam_faillock
# In /etc/security/faillock.conf:
# deny = 5 (lock after 5 failures)
# unlock_time = 900 (unlock after 15 minutes)
# fail_interval = 900
# Check locked status
faillock --user alice
# Unlock manually
faillock --user alice --reset
Security Audit Commands
# Find all UID 0 accounts (should only be root)
awk -F: '$3 == 0 {print $1}' /etc/passwd
# Find accounts with empty passwords (critical vulnerability)
awk -F: '$2 == "" {print $1}' /etc/shadow
# List all sudoers entries
grep -E "^[^#]" /etc/sudoers
# Login history
last -20
# Failed login attempts
lastb -20
# Last login for all users (spot dormant accounts)
lastlog | grep -v "Never"
10. Practical Scenarios
Scenario 1: Set Up a Web Developer
# Create user with SSH access and web group membership
useradd -m -s /bin/bash -G www-data -c "Jane Developer" jane
passwd jane
# Set up SSH key authentication
mkdir -p /home/jane/.ssh
chmod 700 /home/jane/.ssh
# Paste the developer public key:
# echo "ssh-rsa AAAA..." >> /home/jane/.ssh/authorized_keys
chmod 600 /home/jane/.ssh/authorized_keys
chown -R jane:jane /home/jane/.ssh
# Give jane ownership of her site directory
chown -R jane:www-data /var/www/jane-site/
chmod -R 755 /var/www/jane-site/
chmod g+s /var/www/jane-site/ # New files inherit www-data group
Scenario 2: Create a Service Account for a Daemon
# System user: no home dir, no shell, low UID
useradd -r -s /usr/sbin/nologin -c "My App Service" myapp
# Create required directories
mkdir -p /var/lib/myapp /var/log/myapp /var/run/myapp
chown myapp:myapp /var/lib/myapp /var/log/myapp /var/run/myapp
chmod 750 /var/lib/myapp /var/log/myapp
# In the systemd unit file, set:
# User=myapp
# Group=myapp
Scenario 3: Shared Directory for a Team
# Create a group for the team
groupadd webteam
usermod -aG webteam alice
usermod -aG webteam bob
usermod -aG webteam charlie
# Create the shared directory
mkdir -p /var/shared/project
chown root:webteam /var/shared/project
# SGID: new files inherit webteam group
# Sticky: only owner can delete their own files
chmod 2775 /var/shared/project
chmod +t /var/shared/project
Scenario 4: Restrict User to SFTP Only (No Shell)
# Create user with no login shell
useradd -m -s /usr/sbin/nologin ftpuser
passwd ftpuser
# In /etc/ssh/sshd_config, add:
# Match User ftpuser
# ForceCommand internal-sftp
# ChrootDirectory /var/www/ftpuser
# PasswordAuthentication yes
# AllowAgentForwarding no
# AllowTcpForwarding no
# Chroot directory must be owned by root (SSH requirement)
mkdir -p /var/www/ftpuser
chown root:root /var/www/ftpuser
chmod 755 /var/www/ftpuser
# Create a writable subdirectory for the user
mkdir -p /var/www/ftpuser/uploads
chown ftpuser:ftpuser /var/www/ftpuser/uploads
Scenario 5: Audit Who Logged In Last Month
# Last 30 days login history
last -s -30days
# Count logins per user
last -s -30days | awk '{print $1}' | sort | uniq -c | sort -rn
# Failed attempts last 30 days
lastb -s -30days 2>/dev/null | awk '{print $1}' | sort | uniq -c | sort -rn
# All logins from a specific IP
last | grep "203.0.113.42"
Quick Reference Cheat Sheet
User Commands
| Command | Purpose |
|---|---|
useradd -m -s /bin/bash username | Create user with home and bash |
passwd username | Set or change password |
usermod -aG group username | Add user to group (safe append) |
usermod -s /usr/sbin/nologin username | Disable login shell |
usermod -L username | Lock account |
usermod -U username | Unlock account |
userdel -r username | Delete user and home directory |
chage -l username | Show password expiry info |
chage -M 90 username | Set 90-day password expiry |
id username | Show UID, GID, and groups |
last -20 | Last 20 logins |
Group Commands
| Command | Purpose |
|---|---|
groupadd groupname | Create group |
groupmod -n newname oldname | Rename group |
groupdel groupname | Delete group |
gpasswd -a user group | Add user to group |
gpasswd -d user group | Remove user from group |
groups username | List user groups |
getent group groupname | List group members |
Permission Commands
| Command | Purpose |
|---|---|
chmod 755 file | Set permissions (numeric) |
chmod u+x file | Add execute for owner |
chmod -R 644 dir/ | Recursive permissions |
chown user:group file | Change owner and group |
chown -R user:group dir/ | Recursive ownership change |
chmod g+s dir/ | SGID: inherit group on new files |
chmod +t dir/ | Sticky: only owner can delete |
getfacl file | View ACL entries |
setfacl -m u:user:rwx file | Set ACL for specific user |
umask | View default permission mask |
find / -nouser | Find orphaned files |
find / -perm -4000 | Find SUID files (security audit) |
Frequently Asked Questions
What is the difference between useradd and adduser?
useradd is a low-level binary available on every Linux distribution and does not create a home directory unless you pass -m. adduser is a friendlier Perl wrapper (Debian/Ubuntu) that always creates a home directory, copies skeleton files, and prompts interactively for a password and details.
Why should I always use usermod -aG instead of usermod -G?
usermod -G replaces all of a user's supplementary group memberships with only the groups listed. Forgetting one existing group silently removes their access to it. usermod -aG appends the new group without touching existing memberships, which is what almost everyone actually wants.
What permission should I use for a private SSH key or .env file?
600 (rw-------). Only the owner can read or write the file, and no other user or group has any access at all. SSH will actually refuse to use a private key if its permissions are more permissive than this.
Do I need ACLs if I already use standard Unix permissions?
Only when a file needs different access for more than one user or group beyond the standard owner/group/other model. For example, granting one specific contractor read access to a directory without changing its group ownership. Most servers never need ACLs; they matter most on shared team directories with mixed access requirements.
User Management Is Your First Line of Defense
Most breaches do not start with exotic zero-days. They start with a compromised account that had too much access, an orphaned service running as root, or a developer who was never removed after they left. User management is operational security. It is the difference between a contained incident and a full system compromise.
The principles are straightforward (and pair well with our broader 30-step server hardening checklist):
- Every user and service gets exactly the access it needs, nothing more
- Root access is earned through sudo, not handed out wholesale
- Unused accounts are locked or deleted
- File permissions enforce isolation between users and services
- Regular audits catch what creeps in over time
If you manage a multi-tenant server, hosting sites for multiple clients or teams, these principles need to be enforced at the system level, not left to convention. Panelica builds this isolation in by default: every user gets their own Linux user account with cgroups v2 limits, namespace isolation, SSH chroot jails, and PHP-FPM pools. The kind of configuration you would spend hours setting up manually, automated and maintained for you.
User management done right is not overhead. It is the foundation everything else stands on.