By Manny Fernandez

September 29, 2026

Linux User and Group Management and Permissions: The Practitioner’s Guide

Objective: Build, manage, and audit Linux identities and file access the right way, from /etc/passwd to default ACLs, with every command you need to verify the result.

Target audience: Network and security engineers, sysadmins, and blue-teamers who touch Linux boxes (jump hosts, syslog collectors, SIEM nodes, lab VMs) and want access control that holds up to an audit.

Every Linux access decision comes down to three questions: who are you (UID), what groups are you in (GIDs), and what do the file’s permission bits and ACLs say about those IDs. Get those three right and you have least privilege. Get them wrong and you have a shared service account with a world-writable home directory and NOPASSWD: ALL in sudoers. This guide walks through the full lifecycle: the identity files, creating and modifying users and groups, sudo delegation, standard permissions, special bits, ACLs, a real shared-directory build, and the audit commands that prove it all works.


1. Prerequisites and Architecture

Assumed knowledge

  • Comfortable in a Linux shell (navigation, cat, grep, editing a file)
  • Root or sudo access on a lab system
  • Basic understanding of what a process and a file are

Lab requirements

Component Requirement Notes
OS Ubuntu 24.04 LTS or RHEL/Rocky/Alma 9 Commands shown work on both unless noted
Privilege sudo-capable admin account Keep a second root session open while editing sudoers
Packages acl, passwd/shadow-utils, sudo acl is installed by default on both families
Filesystem ext4 or XFS Both support POSIX ACLs out of the box

The identity files

Linux identity lives in a handful of flat files. Every tool in this guide (useradd, usermod, gpasswd, chage) is just a safe editor for these.

File Contains Permissions Key fields
/etc/passwd User accounts 644 (world-readable) name:x:UID:GID:GECOS:home:shell
/etc/shadow Password hashes and aging 640 or 000 name:hash:lastchg:min:max:warn:inactive:expire
/etc/group Groups and supplementary members 644 name:x:GID:member1,member2
/etc/gshadow Group passwords and group admins 640 or 000 name:hash:admins:members
/etc/login.defs Defaults: UID ranges, password aging, UMASK 644 UID_MIN, PASS_MAX_DAYS, UMASK
/etc/default/useradd useradd defaults: shell, home base, skel 644 SHELL, HOME, SKEL
/etc/skel/ Template files copied into new home dirs 755 .bashrc, .profile

Read the files, never hand-edit them. Use vipw and vigr if you truly must edit directly; they lock the file and check syntax.

getent passwd <username>
getent group <groupname>
sudo getent shadow <username>

getent is the right way to query because it also resolves accounts from LDAP, SSSD, or FreeIPA, not just the local files.

UID and GID ranges

Range Purpose
0 root. Any account with UID 0 is root, regardless of its name
1 to 999 System and service accounts (sshd, www-data, nginx, wazuh)
1000 and up Regular human users (set by UID_MIN in /etc/login.defs)
65534 nobody / nfsnobody, the unprivileged overflow ID

2. Step-by-Step Implementation Workflow

Step 1: Create users

Goal: Create a human user with a home directory, a real shell, and a forced password change at first login.

Action: On both distro families, useradd is the low-level tool. On Debian/Ubuntu, adduser is an interactive Perl wrapper that fills in sane defaults. Script with useradd, because its behavior is identical everywhere.

sudo useradd -m -s /bin/bash -c "Jane Analyst" <username>
sudo passwd <username>
sudo chage -d 0 <username>
Flag Meaning
-m Create the home directory and copy /etc/skel into it (RHEL does this by default, Ubuntu does not)
-s Login shell. Use /usr/sbin/nologin for service accounts
-c GECOS comment (full name)
-u <UID> Pin a specific UID, useful for keeping NFS ownership consistent across hosts
-g <group> Primary group (otherwise a same-named private group is created)
-G <g1,g2> Supplementary groups at creation time
-e YYYY-MM-DD Account expiration date, ideal for contractors

chage -d 0 sets the last password change to the epoch, which forces a new password at next login.

Service accounts get no shell, no password, and no home directory login:

sudo useradd -r -s /usr/sbin/nologin -d /var/lib/<service> -M <service-account>

-r allocates from the system UID range and skips password aging.

Verification:

id <username>
getent passwd <username>
sudo chage -l <username>
ls -la /home/<username>

Step 2: Modify, lock, and remove users

Goal: Change account properties safely and disable access without destroying evidence.

Action:

# Add to a supplementary group (ALWAYS use -a with -G)
sudo usermod -aG <groupname> <username>

# Change shell, rename login, move home
sudo usermod -s /bin/zsh <username>
sudo usermod -l <newname> <oldname>
sudo usermod -d /home/<newname> -m <newname>

# Lock and unlock
sudo usermod -L <username>
sudo usermod -U <username>

# Fully disable: lock password, expire account, remove shell
sudo usermod -L -e 1 -s /usr/sbin/nologin <username>

usermod -L only prefixes the hash with !. The user can still log in with an SSH key. Expiring the account (-e 1) blocks every authentication path that goes through PAM account checks, including key-based SSH.

Password aging policy per user:

sudo chage -M 90 -m 1 -W 14 -I 30 <username>
Option Meaning
-M 90 Max 90 days between changes
-m 1 Min 1 day (stops users cycling back to an old password)
-W 14 Warn 14 days before expiry
-I 30 Lock the account 30 days after the password expires

Removing a user:

# Find everything they own BEFORE deleting
sudo find / -xdev -user <username> -ls 2>/dev/null

# Remove account, home dir, and mail spool
sudo userdel -r <username>

For offboarding, lock and expire first, then delete later once you have archived or reassigned their files. Files left behind by a deleted user show a raw number as owner, and the next user who gets that UID inherits them.

Verification:

sudo passwd -S <username>
sudo chage -l <username>

passwd -S shows L for locked, P for usable password, NP for no password.

Step 3: Create and manage groups

Goal: Use groups, not individual users, as the unit of access control.

Every user has exactly one primary group (the GID field in /etc/passwd, applied to new files they create) and zero or more supplementary groups (listed in /etc/group, used for access checks).

Action:

sudo groupadd <groupname>
sudo groupadd -g 5001 <groupname>       # pin the GID
sudo groupmod -n <newname> <oldname>    # rename
sudo groupdel <groupname>

gpasswd manages membership and lets you delegate group administration without handing out root:

sudo gpasswd -a <username> <groupname>   # add one member
sudo gpasswd -d <username> <groupname>   # remove one member
sudo gpasswd -M user1,user2 <groupname>  # set the full member list
sudo gpasswd -A <username> <groupname>   # make user a group admin

Verification:

getent group <groupname>
groups <username>
id -Gn <username>

Group membership is read at login. A logged-in user will not see a new group until they log out and back in, or start a new shell with newgrp <groupname> or su - <username>.

Step 4: Delegate privilege with sudo

Goal: Give admins exactly the root commands they need, logged and attributable.

The admin group differs by distro: sudo on Debian/Ubuntu, wheel on RHEL family.

sudo usermod -aG sudo <username>    # Ubuntu
sudo usermod -aG wheel <username>   # RHEL

Action: Never edit /etc/sudoers directly. Drop scoped rules into /etc/sudoers.d/ with visudo, which syntax-checks before saving. A broken sudoers file locks everyone out of sudo.

sudo visudo -f /etc/sudoers.d/netops
# Command aliases keep rules readable
Cmnd_Alias NET_DIAG = /usr/bin/tcpdump, /usr/sbin/ip, /usr/bin/ss
Cmnd_Alias SVC_SYSLOG = /usr/bin/systemctl restart rsyslog, \
                        /usr/bin/systemctl status rsyslog

# Group rule: members of netops get only these commands
%netops ALL=(root) NET_DIAG, SVC_SYSLOG

# Log every sudo session's I/O for audit
Defaults:%netops log_input, log_output

Rules to live by:

  • Always use full paths. tcpdump without a path lets a user drop a malicious tcpdump earlier in their PATH.
  • Avoid granting editors, pagers, find, tar, vim, less, or any interpreter. They all have shell escapes (see GTFOBins). Use sudoedit for file edits instead.
  • Avoid wildcards in arguments. /usr/bin/systemctl restart * also permits restart of any unit, plus extra arguments.
  • NOPASSWD belongs on automation accounts with tightly scoped commands, never on humans with ALL.

Verification:

sudo visudo -c
sudo -l -U <username>

visudo -c validates every file under /etc/sudoers.d/. sudo -l -U shows exactly what a user can run.

Step 5: Read and set standard permissions

Goal: Understand and control the nine permission bits on every file.

ls -l /etc/shadow
-rw-r----- 1 root shadow 1234 Sep 29 10:00 /etc/shadow
Position Meaning
- Type: - file, d directory, l symlink, c/b device, s socket, p pipe
rw- Owner (user) permissions
r-- Group permissions
--- Other (everyone else) permissions

The bits mean different things on files and directories. This is the single most misunderstood part of Linux permissions.

Bit On a file On a directory
r (4) Read contents List filenames (ls)
w (2) Modify contents Create, delete, and rename entries inside it
x (1) Execute as a program Enter it (cd) and access entries by name

Two consequences worth memorizing: you can delete a file you cannot write to if you have w on its directory, and r without x on a directory lets you see names but not open anything.

The kernel checks in order and stops at the first match: if you are the owner, only owner bits apply; else if you are in the group, only group bits apply; else other bits apply. An owner with --- is denied even if “other” has rwx.

Action: chmod accepts octal or symbolic modes.

chmod 640 <file>             # rw-r-----
chmod 750 <directory>        # rwxr-x---
chmod u+x,g-w,o= <file>      # symbolic: add, remove, set
chmod -R g+rX <directory>    # capital X: execute only on dirs
                             # and files already executable
Octal Symbolic Typical use
600 rw------- Private keys, ~/.ssh/authorized_keys
644 rw-r--r-- Config files, web content
640 rw-r----- Configs containing secrets, readable by a service group
700 rwx------ ~/.ssh, private scripts
750 rwxr-x--- Team directories, home directories
755 rwxr-xr-x Binaries, public directories

Change ownership with chown and chgrp:

sudo chown <user>:<group> <file>
sudo chown -R <user>: <directory>   # trailing colon: user's login group
sudo chgrp <group> <file>

Verification:

stat -c '%A %a %U:%G %n' <file>
namei -l /path/to/deep/file

namei -l shows the permissions on every directory in the path. When access fails and the file itself looks fine, it is almost always a parent directory missing x.

Step 6: Control default permissions with umask

Goal: Make new files secure by default instead of fixing them afterward.

New files start at 666 and directories at 777; the umask removes bits. The umask is subtracted bitwise, not arithmetically.

umask New file New directory Use
022 644 755 Common default
002 664 775 User private groups, collaborative work
027 640 750 Hardened servers, CIS recommended
077 600 700 Maximum privacy

Action:

umask            # show current (octal)
umask -S         # show current (symbolic)
umask 027        # set for this shell

Set it persistently with UMASK 027 in /etc/login.defs (applies via pam_umask), per-user in ~/.bashrc, or per-service with UMask=0027 in a systemd unit.

Verification:

touch /tmp/umask-test && stat -c '%a' /tmp/umask-test

Step 7: Use the special bits (setuid, setgid, sticky)

Goal: Know what the fourth permission digit does and where it belongs.

Bit Octal On a file On a directory Shown as
setuid 4000 Runs as the file’s owner (e.g., passwd runs as root) Ignored on Linux s in owner execute
setgid 2000 Runs with the file’s group New files inherit the directory’s group s in group execute
sticky 1000 Ignored Only the file owner (or dir owner, or root) can delete or rename entries t in other execute

A capital S or T means the special bit is set but the underlying execute bit is not, which is usually a mistake.

ls -ld /tmp /usr/bin/passwd
drwxrwxrwt 18 root root 4096 Sep 29 10:00 /tmp
-rwsr-xr-x  1 root root 64152 Sep 29 10:00 /usr/bin/passwd

Action:

sudo chmod 2770 /srv/<project>     # setgid team directory
sudo chmod 1777 /srv/<dropbox>     # sticky shared drop area
sudo chmod u-s /path/to/binary     # strip setuid

Setuid binaries are a primary privilege-escalation target. Never set setuid on scripts (the kernel ignores it for interpreted files anyway), and inventory every setuid file you have. On modern systems, file capabilities (setcap cap_net_raw+ep /usr/bin/<tool>) are a far narrower alternative for tools that need one specific root power.

Verification:

stat -c '%A %a %n' /srv/<project>
getcap -r / 2>/dev/null

Step 8: Go beyond owner/group/other with ACLs

Goal: Grant access to a second group or a single user without restructuring ownership.

Standard permissions allow exactly one owner and one group. POSIX ACLs add named users and named groups.

Action:

# Grant a named group read-only
sudo setfacl -m g:<auditors>:rX /srv/<project>

# Grant a single user read/write
sudo setfacl -m u:<username>:rw /srv/<project>/<file>

# Default ACL: inherited by everything created inside
sudo setfacl -d -m g:<auditors>:rX /srv/<project>

# Apply recursively to existing content
sudo setfacl -R -m g:<auditors>:rX /srv/<project>

# Remove one entry, or strip all ACLs
sudo setfacl -x g:<auditors> /srv/<project>
sudo setfacl -b /srv/<project>

A + at the end of the ls -l mode string means an ACL is present:

drwxrws---+ 2 root devops 4096 Sep 29 10:00 /srv/project

The mask: Once ACLs exist, the group bits shown by ls -l become the mask, which caps every named user, named group, and the owning group. Running chmod g-w on a file with ACLs lowers the mask and silently removes write from every ACL entry. getfacl shows the result as #effective:.

Verification:

getfacl /srv/<project>
# file: srv/project
# owner: root
# group: devops
# flags: -s-
user::rwx
group::rwx
group:auditors:r-x
mask::rwx
other::---
default:user::rwx
default:group::rwx
default:group:auditors:r-x
default:mask::rwx
default:other::---

3. Worked Scenario: A Secure Shared Team Directory

Requirement: The devops team collaborates on /srv/project. Every file must stay group-owned by devops and group-writable no matter who creates it. The auditors group needs read-only access. Nobody else gets in, and team members cannot delete each other’s files.

# 1. Groups and members
sudo groupadd devops
sudo groupadd auditors
sudo usermod -aG devops <user1>
sudo usermod -aG devops <user2>
sudo usermod -aG auditors <auditor1>

# 2. Directory, ownership, setgid + sticky
sudo mkdir -p /srv/project
sudo chown root:devops /srv/project
sudo chmod 3770 /srv/project

# 3. Default ACLs force group rw on new content regardless of umask
sudo setfacl -m g:devops:rwx /srv/project
sudo setfacl -d -m g:devops:rwX /srv/project
sudo setfacl -m g:auditors:rX /srv/project
sudo setfacl -d -m g:auditors:rX /srv/project

What each piece does:

Setting Effect
root:devops ownership Team gets group access; no individual owns the directory
setgid (2000) New files and subdirs get group devops, not the creator’s primary group
sticky (1000) Members can edit each other’s files but only delete their own
770 Other gets nothing
Default ACL g:devops:rwX New files are group-writable even if a user’s umask is 022 or 027
ACL g:auditors:rX Read-only access for audit without joining the team

4. Verification and Validation

Test as the actual users, not as root. Root bypasses permission checks, so a test run as root proves nothing.

# As a devops member: create a file
sudo -u <user1> touch /srv/project/test1.txt
stat -c '%A %U:%G %n' /srv/project/test1.txt

Expected success output:

-rw-rw----+ user1:devops /srv/project/test1.txt
# Second devops member can write but not delete
sudo -u <user2> sh -c 'echo edit >> /srv/project/test1.txt' && echo WRITE_OK
sudo -u <user2> rm /srv/project/test1.txt

Expected:

WRITE_OK
rm: cannot remove '/srv/project/test1.txt': Operation not permitted
# Auditor can read, cannot write
sudo -u <auditor1> cat /srv/project/test1.txt
sudo -u <auditor1> touch /srv/project/audit.txt

Expected:

edit
touch: cannot touch '/srv/project/audit.txt': Permission denied
# Outsider gets nothing
sudo -u nobody ls /srv/project

Expected:

ls: cannot open directory '/srv/project': Permission denied

Audit commands for the whole system

# Accounts with UID 0 (should only be root)
awk -F: '$3 == 0 {print $1}' /etc/passwd

# Accounts with empty passwords
sudo awk -F: '$2 == "" {print $1}' /etc/shadow

# Human accounts with a login shell
awk -F: '$3 >= 1000 && $7 !~ /nologin|false/ {print $1, $7}' /etc/passwd

# Consistency check of passwd/shadow and group/gshadow
sudo pwck -r
sudo grpck -r

# Every setuid and setgid file
sudo find / -xdev -type f \( -perm -4000 -o -perm -2000 \) -ls 2>/dev/null

# World-writable files and dirs without sticky bit
sudo find / -xdev -type f -perm -0002 -ls 2>/dev/null
sudo find / -xdev -type d -perm -0002 ! -perm -1000 -ls 2>/dev/null

# Files with no valid owner or group (leftovers from deleted users)
sudo find / -xdev \( -nouser -o -nogroup \) -ls 2>/dev/null

# Recent logins and failed attempts
lastlog | grep -v "Never logged in"
sudo faillock --user <username>

5. Troubleshooting and Gotchas

Gotcha 1: usermod -G wiped all my groups

Symptom: After adding a user to one group, they lost sudo and every other group membership.

Cause: usermod -G <group> replaces the supplementary group list. Without -a, you just removed them from everything else, including sudo or wheel.

Diagnose and fix:

id <username>
sudo usermod -aG sudo,<group1>,<group2> <username>

Prefer gpasswd -a <username> <group>, which can only add. If you locked out your only admin, boot to single-user or recovery mode to repair.

Gotcha 2: User was added to a group but still gets “Permission denied”

Symptom: getent group shows the user as a member, ls -l shows the group has access, but access fails.

Cause: One of four things: the session predates the group change; a parent directory lacks x; an ACL mask is capping the group; or a mandatory access control layer (SELinux or AppArmor) is denying it.

Diagnose:

id                                 # groups of THIS session, not the account
namei -l /full/path/to/file        # missing x on a parent dir
getfacl /full/path/to/file         # look for #effective: limits from the mask
ls -Z /full/path/to/file           # SELinux context (RHEL)
sudo ausearch -m avc -ts recent    # SELinux denials
sudo journalctl -k | grep -i apparmor   # AppArmor denials (Ubuntu)

Resolution: Log out and back in (or newgrp), add x to the parent directory, restore the mask with setfacl -m m::rwx, or fix the SELinux context with restorecon -Rv or semanage fcontext.

Gotcha 3: Root cannot modify or delete a file

Symptom: rm: cannot remove 'file': Operation not permitted, even as root, with normal-looking permissions.

Cause: The file carries the immutable (i) or append-only (a) attribute, which sits below the permission layer. Attackers use chattr +i to protect persistence files, so treat an unexpected immutable flag as an indicator worth investigating.

Diagnose and fix:

lsattr /path/to/file
sudo chattr -i /path/to/file

Also check the mount: a filesystem mounted ro or noexec will block writes or execution regardless of permissions.

findmnt -T /path/to/file -o TARGET,OPTIONS

Quick Reference

Task Command
Create user with home and shell useradd -m -s /bin/bash <user>
Add user to group safely usermod -aG <group> <user> or gpasswd -a <user> <group>
Lock and expire account usermod -L -e 1 <user>
Force password change chage -d 0 <user>
Show user identity id <user>
Scoped sudo rule visudo -f /etc/sudoers.d/<file>
What can a user sudo sudo -l -U <user>
Set permissions chmod 750 <path>
Change owner and group chown <user>:<group> <path>
Team directory chmod 2770 <dir>
Grant named group access setfacl -m g:<group>:rX <path>
Inherited ACL setfacl -d -m g:<group>:rwX <dir>
Show ACLs getfacl <path>
Trace path permissions namei -l <path>
Find setuid files find / -xdev -perm -4000 -type f

Permissions are not a set-and-forget control. Build access around groups, delegate root with scoped sudo rules, default to a tight umask, and run the audit commands above on a schedule. The config that works is the one you verified as the user who actually needs it.

Recent posts

  • If you've spent any time configuring user authentication on... Full Story

  • DNS is one of those technologies that quietly underpins... Full Story

  • BGP issues on FortiGate firewalls usually trace back to... Full Story

  • Every time your laptop talks to your router, a... Full Story

  • If you've spent any time configuring NAT on a... Full Story

  • If you have spent any time configuring firewall policies... Full Story

  • High availability on FortiGate is one of those features... Full Story

  • If you've configured SD-WAN on a FortiGate, you've almost... Full Story

  • FortiLink is the management protocol that turns a FortiSwitch... Full Story

  • FortiSwitches are pretty rock solid from Mean Time Between... Full Story

  • This is a quicky tip.  Have you ever gone... Full Story

  • DNS is one of those quiet pieces of internet... Full Story

  • This article is an updated version of the previous... Full Story

  • You will add ns2 as a secondary (slave) BIND9... Full Story

  • In the process of deploying my lab, I needed... Full Story

  • RFC 8805, used to be known as Self-Correcting IP... Full Story

  • Years back, I wrote an article about certificate pinning. ... Full Story

  • FortiGates have the ability to send alerts to Microsoft... Full Story

  • In this post, I am going to walk through... Full Story

  • Troubleshooting VoIP on a FortiGate can feel like trying... Full Story

  • Prior to FortiOS 7.0, there were three commands to... Full Story

  • In this post, I am going to go over... Full Story

  • What we are going to do:  We are going... Full Story

  • Choosing between FGCP (FortiGate Clustering Protocol) and FGSP (FortiGate... Full Story

  • Creating a VLAN on macOS (The "Pro" Move) A... Full Story

  • This blog post explores the logic behind how macOS... Full Story

  • Pretty Fly for a Wi-Fi Tell My Wi-Fi Love... Full Story

  • Part of my daily gig is creating BoMs (Bill-of-Materials)... Full Story

  • ICMP introduces several security risks, but careful filtering, rate... Full Story

  • The command diag debug application dhcps -1 enables full... Full Story

  • In the world of FortiOS, execute tac report is... Full Story

  • LLDP; What is it The Link Layer Discovery Protocol... Full Story

  • What it actually does When you run diagnose fdsm... Full Story

  • Monkey Bites are bite-sized, high-impact security insights designed for... Full Story

  • I have run macOS in macOS with Parallels but... Full Story

  • Don't be confused with my other FortiNAC posts where... Full Story

  • This is the third session in a multi-part article... Full Story

  • Today I was configuring key-based authentication on a FortiGate... Full Story

  • Netcat, often called the "Swiss Army knife" of networking,... Full Story

  • At its core, IEEE 802.1X is a network layer... Full Story

  • In case you did not see the previous FortiNAC... Full Story

  • This is our 5th session where we are going... Full Story

  • Now that we have Wireshark installed and somewhat configured,... Full Story

  • The Philosophy of Packet Analysis Troubleshooting isn't about looking... Full Story

  • Objective: Strip blank and whitespace-only lines out of config... Full Story

  • In this guide Executive Summary Prerequisites and Architecture How... Full Story

  • Objective: Build, manage, and audit Linux identities and file... Full Story