Creating Linux Users the Right Way: Beyond useradd

Bits Lovers
Written by Bits Lovers on
Creating Linux Users the Right Way: Beyond useradd

I’ve broken a production server twice by creating users wrong. Once by assigning the wrong UID. Once by not understanding how the primary group assignment works. Neither time was obvious until something else stopped working three months later.

This post covers user management on Linux from the perspective of someone who’s made the mistakes and learned from them.

The Commands You’ll Use

There are two main commands for creating users: useradd and adduser. The difference matters.

useradd is the low-level utility. It creates the user account with minimal defaults. adduser (on Debian/Ubuntu) is a Perl wrapper that’s more user-friendly and creates home directories and defaults automatically.

# Low-level: useradd (works everywhere)
sudo useradd username

# High-level: adduser (Debian/Ubuntu only)
sudo adduser username

The catch: if you’re writing scripts or Ansible playbooks that need to run across different Linux distributions, use useradd. adduser doesn’t exist on RHEL, CentOS, or Amazon Linux.

For a more complete example with all the options:

# Create user with explicit options
sudo useradd -m -s /bin/bash -c "John Doe,Platform Engineer" \
  -G sudo,docker,www-data -e 2027-12-31 -f 30 johndoe

# -m: create home directory
# -s: login shell
# -c: GECOS comment field (full name, room, phone, etc.)
# -G: supplementary groups
# -e: account expiry date (ISO format)
# -f: inactive days after password expires

Understanding the Files Behind the Scenes

When you create a user, Linux updates four files. Understanding what touches what explains a lot of confusing behavior.

# /etc/passwd - user account info (readable by everyone)
# Format: username:password:UID:GID:GECOS:home:shell
john:x:1001:1001:John Smith,Room 101:/home/john:/bin/bash

# /etc/shadow - password hashes (root only)
# Format: username:hash:last_change:min:max:warn:inactive:expire
john:$6$sOmErAnDoM:19523:0:90:7:14::

# /etc/group - group definitions
# Format: groupname:password:GID:member1,member2
sudo:x:27:john,alice

# /etc/gshadow - group password hashes (root only)

The x in /etc/passwd means the actual password hash is in /etc/shadow. That’s been the standard since the 1980s — the original Unix design stored encrypted passwords in /etc/passwd, which was world-readable.

The Default Settings File

useradd reads defaults from /etc/default/useradd. Here’s what it looks like on a typical Ubuntu system:

cat /etc/default/useradd
# Result:
# GROUP=100
# HOME=/home
# INACTIVE=-1
# EXPIRE=
# SHELL=/bin/sh
# SKEL=/etc/skel
# CREATE_MAIL_SPOOL=no

The SKEL directory matters. Every new user’s home directory is populated from /etc/skel. If you want all new users to get a specific .bashrc or .vimrc, put it there.

# Add a default .vimrc for all new users
echo 'set number' | sudo tee /etc/skel/.vimrc

# Now any new user gets it automatically
sudo useradd -m newuser
ls -la /home/newuser/
# You'll see .vimrc in the new home directory

UID and GID: The Numbers That Matter

Every user has a UID (User ID) and every group has a GID (Group ID). These numbers are how the kernel tracks permissions — not the usernames.

# Check UID and GID for a user
id john
# Output: uid=1001(john) gid=1001(john) groups=1001(john),27(sudo)

# Check just the UID
id -u john
# Output: 1001

# Check just the primary GID
id -g john
# Output: 1001

Most Linux distributions reserve UID 0-999 for system accounts and start regular users at UID 1000. You can verify this in /etc/login.defs:

grep -E "^UID_MIN|^UID_MAX|^GID_MIN|^GID_MAX" /etc/login.defs
# Output:
# UID_MIN 1000
# UID_MAX 60000
# GID_MIN 1000
# GID_MAX 60000

When UID Conflicts Break Things

Here’s a failure mode I ran into: we had a user deploy on two servers. Server A had UID 1001 for deploy. Server B had UID 1002. When we set up shared NFS storage, files created by deploy on server A had owner 1001, and deploy on server B couldn’t write to them.

The fix is to specify the UID explicitly when creating the user:

# Create with explicit UID to ensure consistency across servers
sudo useradd -u 1001 -m deploy

This matters in any environment with shared storage (NFS, distributed filesystems, container volumes mounted across hosts). For larger environments, use LDAP or SSSD to centralize identity management across all hosts.

Group Management in Depth

Groups are about permissions sharing. Instead of granting access to individual users, you grant access to a group, then add users to that group.

# Create a new group
sudo groupadd developers

# Add a user to supplementary groups
sudo usermod -aG developers john
# -a means append (don't remove from other groups)
# -G means supplementary groups

# Add to multiple groups at once
sudo usermod -aG developers,analysts,sales alice

# Remove a user from a group
sudo gpasswd -d john developers

# List all members of a group
getent group developers

The Primary vs. Supplementary Group Confusion

Every user has one primary group (specified by the GID in /etc/passwd). When a user creates a file, the file is owned by their primary group by default.

# Check a user's groups
id john
# uid=1001(john) gid=1001(john) groups=1001(john),27(sudo),1002(developers)
# gid=1001 is the primary group
# groups=1001,27,1002 shows both primary and supplementary

# Change primary group
sudo usermod -g developers john
# Now john's primary group is developers, not john

The newgrp command lets users switch their primary group for a shell session:

# Switch primary group for this session
newgrp developers
# Now any files I create are group-owned by developers

System Users vs. Regular Users

System users run services. They typically don’t log in interactively and might not have home directories.

# Create a system user (no login, UID from system range)
sudo useradd -r mysql

# Compare with regular user
sudo useradd -m john

# Check the difference
id mysql
# uid=114(mysql) gid=127(mysql) groups=127(mysql)
# Note the lower UID (system range)

id john
# uid=1001(john) gid=1001(john) groups=1001(john)
# Note the higher UID (regular user range)

Here’s a mistake I made once: I created a service user with a home directory and a login shell, then wondered why SSH was failing because the shell didn’t exist in the container. System users should either use /sbin/nologin or /usr/sbin/nologin as their shell, or have no shell specified.

# Correct way to create a service user
sudo useradd -r -s /sbin/nologin -d /var/lib/mysql mysql

# Check what nologin does
cat /sbin/nologin
# Shows a message: "This account is currently not available."

Password Management That Actually Works

Creating a user without a password is a security hole waiting to happen. The user can’t log in until you set one, but the account isn’t locked either — someone who knows the username might be able to guess the password.

# Set password interactively
sudo passwd john

# Set password non-interactively (for scripts)
echo "john:MySecurePass123" | sudo chpasswd

# Lock an account (prevents login)
sudo passwd -l john

# Unlock an account
sudo passwd -u john

# Check account status
sudo chage -l john
# Output:
# Last password change      : May 18, 2021
# Password expires         : never
# Password inactive        : never
# Account expires          : never
# Minimum number of days between password change    : 0
# Maximum number of days between password change    : 99999
# Number of days of warning before password expires : 7

Password Expiration Policies

For systems with compliance requirements, password expiration is often mandatory. Here’s how to set it up:

# Force password change at next login
sudo passwd -e john

# Set password to expire in 90 days
sudo chage -M 90 john

# Set account to expire on a specific date (for contractors)
sudo chage -E 2026-12-31 contractor

# Require minimum days between password changes (prevent reuse)
sudo chage -m 1 john

# Set warning period (notify user before password expires)
sudo chage -W 14 john

Strong Password Policies with PAM

For systems requiring strong passwords, configure pam_pwquality in /etc/security/pwquality.conf:

# /etc/security/pwquality.conf
minlen = 14
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 = 2  # Max repeated characters
dictcheck = 1  # Check against dictionary
enforce_for_root = yes

Account lockout after failed attempts with pam_faillock:

# /etc/pam.d/system-auth (RHEL/CentOS)
auth required pam_faillock.so preauth silent audit deny=5 unlock_time=1800
auth [default=die] pam_faillock.so authfail audit deny=5 unlock_time=1800
account required pam_faillock.so

Sudo Access: The Right Way

There are three ways to give a user sudo access, and one of them is wrong.

Way 1: Add to the sudo group (Debian/Ubuntu)

sudo usermod -aG sudo john

On Debian/Ubuntu, the sudo group is configured in /etc/sudoers to have full sudo access. This is clean and reversible.

Way 2: Edit /etc/sudoers with visudo

sudo visudo

Add a line like:

john ALL=(ALL:ALL) ALL

Or for passwordless sudo:

john ALL=(ALL) NOPASSWD:ALL

The visudo command is critical — it validates the file before saving. If you make a syntax error in /etc/sudoers, you can lock yourself out of sudo entirely.

Way 3: Drop-in files in /etc/sudoers.d/

The cleanest approach for managed systems:

# Create a sudoers drop-in file
echo "john ALL=(ALL) NOPASSWD:/usr/bin/systemctl restart nginx" | sudo tee /etc/sudoers.d/john-restrictive

# Set correct permissions (critical!)
sudo chmod 0440 /etc/sudoers.d/john-restrictive

# Verify the file is valid
sudo visudo -c
# Output: /etc/sudoers.d/john-restrictive: parsed OK

What Not to Do

# DON'T: Use wildcards for commands
john ALL=(ALL) NOPASSWD: /usr/bin/apt *

# This allows: apt install vim  (which drops to root shell)
# apt has an interactive shell option!

# DO: Specify exact paths and options
john ALL=(ALL) NOPASSWD: /usr/bin/systemctl status nginx, /usr/bin/systemctl restart nginx

SSH Keys: Better Than Passwords

For any server accessed remotely, SSH keys are the right approach. As of 2024-2026, ED25519 keys are the standard:

# Generate an ED25519 key (preferred over RSA)
ssh-keygen -t ed25519 -C "johndoe@workstation" -f ~/.ssh/id_ed25519

# Copy the public key to the server
ssh-copy-id -i ~/.ssh/id_ed25519.pub [email protected]

# On the server, the key lands in:
# /home/johndoe/.ssh/authorized_keys
# Permissions must be 600:
chmod 600 /home/johndoe/.ssh/authorized_keys

SSH Certificates: Scaling Key Management

For organizations with many servers, SSH certificates eliminate the need to copy keys to each server:

# 1. Set up a CA (Certificate Authority) key on your jump host
ssh-keygen -t ed25519 -C "SSH CA" -f /etc/ssh/ca

# 2. Configure the CA public key on all servers
# Add to /etc/ssh/sshd_config:
# TrustedUserCAKeys /etc/ssh/ca.pub

# 3. Sign a user's key with the CA
ssh-keygen -s /etc/ssh/ca -I "johndoe@workstation" \
  -n johndoe,developers -V +52w ~/.ssh/id_ed25519.pub

# The signed key works on any server with the CA configured
# No need to copy keys to individual servers
# Certificates expire automatically (-V +52w = 52 weeks)

The lock-out scenario and how to recover

The worst thing that can happen: you edit /etc/sudoers wrong and now neither you nor any sudo user can run sudo commands. Here’s how to recover.

On most Linux systems, there’s an emergency boot option or you can use the recovery mode:

# If you have physical access or can use a rescue disk:
# 1. Boot into recovery mode or rescue environment
# 2. Mount the root filesystem
mount /dev/sda1 /mnt

# 3. Fix the sudoers file
vim /mnt/etc/sudoers

# 4. Or just bypass sudo entirely for a single command
pkexec --user root bash

The safer approach is to always use visudo -c before saving, and test with sudo -k to invalidate your cached credentials before closing your session.

Managing Existing Users

You’ve created the user. Now you need to modify them.

# Change username (rare, but sometimes necessary)
sudo usermod -l newname oldname

# Change home directory location
sudo usermod -d /new/home -m john
# -m moves existing files to new location

# Lock account (instead of deleting)
sudo usermod -L john

# Unlock account
sudo usermod -U john

# Delete user (don't remove home directory)
sudo userdel john

# Delete user and home directory
sudo userdel -r john

# Delete user, home directory, AND mail spool
sudo userdel -r -f john

Auditing Who’s on Your System

# List all regular users (UID >= 1000)
getent passwd | awk -F: '$3 >= 1000 {print $1}'

# List all users with login shells (interactive users)
getent passwd | awk -F: '/\/bin\/bash|\/bin\/sh/ {print $1}'

# Check last login times
lastlog | head -20

# Check currently logged in users
who

# Show user activity history
last john

# Review auth logs for login attempts (Debian/Ubuntu)
sudo tail -100 /var/log/auth.log

# Review auth logs for login attempts (RHEL/CentOS)
sudo tail -100 /var/log/secure

What Changed Recently (2024-2026)

systemd-homed brought encrypted home directories. Introduced in systemd v255+ (2023-2024), systemd-homed manages user accounts with automatic home directory encryption using FIDO2, PKCS#11, or SSH keys. It’s replacing traditional /etc/passwd and /etc/shadow management for workstations:

# Create a user with systemd-homed
homectl create johndoe --real-name="John Doe" --shell=/bin/bash \
  --member-of=developers --lifetime=30d
homectl activate johndoe

# Remove when no longer needed
homectl remove johndoe

SSSD became the enterprise standard. For environments using Active Directory or LDAP, SSSD (System Security Services Daemon) replaced nsswitch-based setups as the standard for centralized identity management. It handles caching, offline login, and cross-forest trusts.

sudo 1.9.14+ improved audit logging. Newer sudo versions support rule expiry dates and better session logging, making compliance auditing easier for regulated environments.

SSH certificates replaced key distribution. Instead of copying authorized_keys to every server, SSH CA certificates (TrustedUserCAKeys) became the preferred approach for organizations managing dozens or hundreds of servers. No more distributing keys when employees join or leave.

Common Gotchas (2024-2026)

  • UID/GID conflicts on NFS still bite. Use LDAP or SSSD to centralize identity. Manual UID assignment across servers will eventually conflict — it’s not a matter of if, but when.
  • NOPASSWD in production is a risk. Only use it for CI/CD automation where no human is involved. For interactive sessions, require the password.
  • Home directory permissions default to 755. Set 700 in /etc/login.defs (HOME_MODE 0700) to make new home directories private by default.
  • authorized_keys must be 600. SSH daemon silently ignores keys with wrong permissions. Always verify after copying.
  • passwd -l locks password auth only. SSH key-based auth still works unless keys are removed. To fully lock an account, also remove the key from authorized_keys or set PermitRootLogin without-password.
  • Mixing AndroidX and Support Library causes ClassNotFound at runtime. Complete the migration to AndroidX before targeting API 34+.

What This Looks Like in Ansible

If you’re managing multiple servers, here’s how to translate this to Ansible:

# Create user with consistent UID
- name: Create deploy user
  user:
    name: deploy
    uid: 1001
    group: deploy
    groups: docker,www-data
    shell: /bin/bash
    home: /home/deploy
    create_home: yes
    skeleton: /etc/skel
    password: ""
    password_expire_max: 90
    append: yes

# Add sudo access via group membership
- name: Add deploy user to sudo group
  user:
    name: deploy
    groups: sudo
    append: yes

# Create sudoers file for restricted commands
- name: Deploy user sudoers file
  copy:
    content: |
      deploy ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart nginx, /usr/bin/systemctl status nginx
    dest: /etc/sudoers.d/deploy
    mode: 0440

The Checklist Before You Ship

  • UID consistent across all servers (critical for shared storage)
  • Home directory created with correct permissions (0700)
  • Login shell set (don’t leave as /bin/sh unless that’s actually what you want)
  • Password set, not expired, not locked
  • Supplementary groups assigned correctly
  • Sudo access configured (if needed)
  • Password expiration policy set (if required)
  • Account expiration date set (if temporary access)
  • SSH key authorized (if remote access required)
  • Primary group set correctly (not users or 100 accidentally)

User management is one of those things that seems simple until something breaks because you forgot to specify the UID or set the wrong primary group. The rules are straightforward — the details matter.

For more on Linux system administration, the post on timezone configuration on EC2 covers server-level time settings, and changing file permissions covers the other side of the access control equation.

Bits Lovers

Bits Lovers

Professional writer and blogger. Focus on Cloud Computing.

Comments

comments powered by Disqus