RHEL Basics #5: Users / Groups / Permissions — UID/GID, sudo, ACL

12 min read

In #4 we wrote a line like User=nobody in a systemd unit. How that user exists in the system, and which files they can reach, is the topic here. Half of operational problems are permissions issues, and half of those incidents stem from permissions that are too broad.

Where this post sits in the RHEL Basics series:

Where users live — /etc/passwd / /etc/shadow #

Users on Linux are split across two files.

/etc/passwd #

one line of /etc/passwd
$ grep curtis /etc/passwd
curtis:x:1000:1000:Curtis Kim:/home/curtis:/bin/bash

Seven colon-separated fields:

FieldMeaning
curtisUsername
xPassword slot (used to live here; now in /etc/shadow)
1000UID — the actual identifier
1000Default GID
Curtis KimGECOS — full name / notes (free-form)
/home/curtisHome directory
/bin/bashLogin shell

This file is readable by everyone — the username → UID mapping is needed all over the system.

/etc/shadow #

one line of /etc/shadow (root-only)
$ sudo grep curtis /etc/shadow
curtis:$6$abc...$xyz...:19457:0:99999:7:::

This is where the password hash and expiration policy live. Mode is 600 (root only), so regular users can’t see it.

What UIDs mean #

UID classification (RHEL 9)
   0          → root
   1 ~ 999    → system users (services running under their own UID)
   1000~      → regular users

useradd picks the next free regular UID, usually starting at 1000.

Why system users exist — daemons like nginx, postgres, mysql running as root mean a single break-in compromises everything. So when you install one of those packages, an nginx or postgres system user is created automatically and the daemon runs as that user. Install the RHEL nginx package and id nginx shows the UID.

Groups — /etc/group #

/etc/group
$ grep curtis /etc/group
wheel:x:10:curtis
curtis:x:1000:
  • curtis:x:1000:curtis’s default group (auto-created with the same GID as the UID — RHEL’s user private group pattern)
  • wheel:x:10:curtis — a supplementary group curtis belongs to. wheel is the sudo group on RHEL.
current user's groups
$ id
uid=1000(curtis) gid=1000(curtis) groups=1000(curtis),10(wheel)

$ groups
curtis wheel

One default group, multiple supplementary groups.

useradd / usermod / userdel #

add a user
$ sudo useradd -m -s /bin/bash -G wheel alice
$ sudo passwd alice

Common useradd options:

OptionMeaning
-mCreate the home directory (RHEL 9’s useradd does this by default)
-s <shell>Login shell
-G <group1>,<group2>Supplementary groups
-g <group>Default group (a user private group is auto-created if omitted)
-u <UID>Pin the UID
-rCreate as a system user (UID < 1000)
modify an existing user
$ sudo usermod -aG wheel alice          # add to wheel (without -a it OVERWRITES)
$ sudo usermod -s /bin/zsh alice        # change shell
$ sudo usermod -L alice                 # lock (disable password)
$ sudo usermod -U alice                 # unlock

Watch out-G alone replaces all supplementary groups. Always use -aG (--append --groups). One slip and the user is suddenly missing from wheel and can’t sudo.

delete
$ sudo userdel alice                    # delete user (home stays)
$ sudo userdel -r alice                 # also delete home and mail spool

In production, be careful with userdel -r — there might be important files in the home directory. Usually usermod -L (lock) is enough.

Password policy #

password aging — chage
$ sudo chage -l curtis
Last password change                         : Apr 12, 2026
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

$ sudo chage -M 90 -m 7 -W 14 curtis    # expire in 90 days, can't change for 7, warn 14 days early

File permissions — what rwx means #

Each file or directory has three user classes (owner / group / other), each with three permission bits (r/w/x).

reading ls -l
$ ls -l
-rw-r--r--. 1 curtis curtis  256 Apr 13 10:00 notes.txt
drwxr-xr-x. 2 curtis curtis    6 Apr 13 09:55 docs
dissecting the bits
   - rw- r-- r-- .
   │ │   │   │   └── SELinux context indicator (.) — touched in [#7]
   │ │   │   └────── other: r-- (read only)
   │ │   └────────── group: r-- (read only)
   │ └────────────── owner: rw- (read + write)
   └──────────────── type: - regular / d directory / l symlink / b block / c char

r / w / x mean different things on files vs. directories #

PermissionFileDirectory
rRead contentsList files inside (ls)
wWrite contentsCreate / delete / rename files inside
xExecuteEnter the directory (cd) — and reach the files inside

The directory x is the one that surprises people. Without x on the directory, the files inside are unreachable no matter their own permissions. “Permissions look open but I still can’t read it” is 99% this.

chmod — two notations #

Octal (numeric) #

Each permission as a bit:

rwx in octal
r = 4, w = 2, x = 1
rwx = 7   r-x = 5   r-- = 4   --- = 0

Three classes (owner / group / other), one digit each:

patterns you'll use
$ chmod 644 file.txt          # rw-r--r--   default for regular files
$ chmod 600 secret.key        # rw-------   private (SSH keys, etc.)
$ chmod 755 script.sh         # rwxr-xr-x   executable script
$ chmod 700 ~/.ssh            # rwx------   the SSH directory
$ chmod 750 dir               # rwxr-x---   owner + group access, blocked from outsiders

There are 4–5 patterns you’ll run into over and over — get them into muscle memory.

Symbolic #

symbolic
$ chmod u+x script.sh         # add x for the owner
$ chmod g-w file              # remove w from the group
$ chmod o= file               # remove all permissions for other
$ chmod a+r file              # add r for everyone
$ chmod u=rw,g=r,o= file      # absolute (= sets exactly these bits)

A bit more human-friendly when you only want to flip one bit.

chown / chgrp — change owner #

change owner / group
$ sudo chown alice file              # owner only
$ sudo chown alice:devs file         # owner and group at once
$ sudo chgrp devs file               # group only
$ sudo chown -R alice:devs /var/app  # recursive over a tree

-R is common in production but dangerous. One slip and a system directory has the wrong owner everywhere — sometimes unbootable. Double-check the path before pressing enter.

Special bits — SUID / SGID / Sticky #

When rwx isn’t enough, three more bits exist.

BitOctalUse
SUID4Runs with the file owner’s permissions. Attached to commands like passwd
SGID2(On a directory) files created inside inherit the directory’s group
Sticky1(On a directory) only the file’s owner can delete it. Lives on /tmp
examples in the wild
$ ls -l /usr/bin/passwd
-rwsr-xr-x. 1 root root ... /usr/bin/passwd       # rws — SUID set

$ ls -ld /tmp
drwxrwxrwt. ... /tmp                               # rwt — sticky set

$ chmod 4755 myprog          # SUID + 755
$ chmod g+s shared-dir       # SGID on a directory
$ chmod +t /shared           # sticky

You rarely set SUID yourself in production. The basic security hygiene is periodically auditing the existing SUID binaries with something like find / -perm -4000.

umask — default permissions for new files #

The reason new files default to 644 and new directories to 755 is umask.

current umask
$ umask
0022

umask is “bits to subtract from the maximum default.” The maximum is 666 for files and 777 for directories. Subtracting 022:

math
files:       666 - 022 = 644 (rw-r--r--)
directories: 777 - 022 = 755 (rwxr-xr-x)

On servers, tighten further with umask 027 or 077 so new files are invisible to “other.” Set it in /etc/profile, /etc/bashrc, or each user’s ~/.bashrc.

ACL — when rwx isn’t enough #

Standard rwx only handles one user and one group. Two groups with different permissions, or giving extra access to one specific user, doesn’t fit. That’s where ACL (Access Control List) comes in.

Inspect — getfacl #

getfacl
$ getfacl /var/data
# file: var/data
# owner: root
# group: data
user::rwx
group::r-x
other::---

Without ACLs added, the output mirrors the regular rwx info.

Set — setfacl #

add permission for a specific user
$ sudo setfacl -m u:alice:rwx /var/data
$ getfacl /var/data
# file: var/data
# owner: root
# group: data
user::rwx
user:alice:rwx              ← newly added
group::r-x
mask::rwx
other::---
add permission for a group
$ sudo setfacl -m g:devs:r-x /var/data
remove
$ sudo setfacl -x u:alice /var/data           # remove one entry
$ sudo setfacl -b /var/data                   # remove all ACL entries

A file with ACLs gets a + at the end of its permissions in ls -l.

the + indicator
$ ls -l /var/data
drwxrwx---+ 2 root data 6 Apr 13 11:00 /var/data
                    indicates ACLs are attached

Default ACLs — auto-applied to new files #

A directory’s default ACL gets auto-applied to anything created inside it.

default ACL
$ sudo setfacl -d -m u:alice:rwx /var/data    # -d = default

One line and every file later created inside automatically gets alice’s permissions. The core trick for shared-directory operation.

sudo — delegating privilege #

A tool that lets specific users run specific commands as root without sharing the root password. The default RHEL 9 sudoers policy:

the central line in /etc/sudoers (RHEL 9)
%wheel  ALL=(ALL)       ALL

“Members of wheel can run any command as any user from any host.” Just usermod -aG wheel alice and alice has sudo.

/etc/sudoers and /etc/sudoers.d/ #

Don’t edit /etc/sudoers directly. Use visudo.

safe editing
$ sudo visudo                              # /etc/sudoers itself
$ sudo visudo -f /etc/sudoers.d/mycompany  # a separate file

visudo validates syntax before saving. If you save a broken file, sudo itself breaks and recovery becomes painful. vi /etc/sudoers directly is risky.

These days everyone splits things into /etc/sudoers.d/ with one file per package or role.

Restricted sudo #

In production, you typically narrow it to “this user, only these commands.”

/etc/sudoers.d/deploy
deploy  ALL=(ALL) NOPASSWD: /bin/systemctl restart myapp, /bin/systemctl status myapp
  • NOPASSWD: — no password prompt for these commands (for automation)
  • Just two commands listed — anything else, no sudo

The pattern: an automated deploy tool SSHes into the box and only needs to run sudo systemctl restart myapp. Nothing else is exposed.

Common sudo commands #

real life
$ sudo whoami                   # → root (fastest "is sudo working?" check)
$ sudo -i                       # drop into a root shell (similar to `sudo su -`)
$ sudo -u alice cmd             # run cmd as alice
$ sudo -l                       # list what sudo lets me run
$ sudo -k                       # invalidate the auth cache (next sudo will prompt)

wheel vs sudo group — Some other distros (Ubuntu, etc.) use a sudo group. RHEL’s tradition is wheel. They play the same role under different names. Creating a sudo group on RHEL won’t do anything — use wheel.

Common commands at a glance #

CommandWhat it does
id [user]UID / GID / groups
who / wCurrently logged-in users
useradd -m -s /bin/bash -G wheel <u>Create a user with sudo
usermod -aG <group> <user>Add to a group (always use -aG)
usermod -L/-U <user>Lock / unlock
userdel [-r] <user>Delete (also home with -r)
passwd [user]Set a password
chage -l <user> / chage -M N -m N -W N <user>Aging policy
chmod 644/755/600 <file>Permissions (octal)
chmod u+x file / chmod g-w filePermissions (symbolic)
chown user:group <file>Owner / group
getfacl <file> / setfacl -m u:<u>:rwx <file>ACL
setfacl -d -m u:<u>:rwx <dir>Default ACL
sudo -i / sudo -u <u> cmd / sudo -lsudo
visudo [-f /etc/sudoers.d/<file>]Safe sudoers editing

Common pitfalls #

“Forgot -aG and replaced the groups” #

oops
$ sudo usermod -G devs curtis   # ← curtis just lost wheel; no more sudo!

Always -aG (append). If it happens, log in as root and usermod -aG wheel curtis again.

“Directory is locked down but the files inside look open” #

The directory wins. Without x on the parent directory, files inside are unreachable no matter how open they look. When auditing, start from directory permissions.

“I chmod 777’d to debug and forgot to revert” #

The pattern: opening up chmod -R 777 /var/app mid-incident to confirm something works. If you forget to revert, SELinux and audit policies catch it later — a bigger incident. Always revert; in production, set the right permissions from the start.

“SSH keys rejected because permissions are too loose” #

permissions OpenSSH enforces
~/.ssh             700
~/.ssh/authorized_keys   600
~/.ssh/id_rsa            600

Anything looser and SSH refuses key-based auth. The most common trip-up for someone moving from Ubuntu to RHEL.

“ACL is set but SELinux blocks me anyway” #

Even past ACL, RHEL has SELinux as another layer. ACL OK but Permission denied? Likely an SELinux label. Covered in Intermediate #1 SELinux.

Wrap-up #

The picture from this post:

  • Users live in /etc/passwd (public) + /etc/shadow (private). UID 1000+ are regular users.
  • The standard new-user pattern is useradd -m -s /bin/bash -G wheel <u>. wheel is the sudo group.
  • Use -aG with usermod, never -G alone — one mistake nukes sudo.
  • rwx bits — the trap is directory x means “you can enter it (and reach what’s inside)”.
  • Two notations for chmod (octal and symbolic); be careful with -R on chown/chgrp.
  • When standard permissions don’t fit, ACLsetfacl -m u:alice:rwx, plus default ACL for auto-application.
  • Split sudo config into /etc/sudoers.d/, edit only with visudo.
  • The sudo group on RHEL is wheel (not sudo like Ubuntu).

Next — the filesystem #

The stage on which permissions live — the filesystem itself — is the next topic. How disks get mounted into the tree, and what to record when adding one.

#6 Filesystem basics — XFS, mount, /etc/fstab covers RHEL 9’s default filesystem XFS and how it differs from ext4, viewing disks with lsblk / df / du, the full cycle of adding a new disk, partitioning it, formatting with mkfs.xfs, and mounting it, registering permanently in /etc/fstab with UUID, and managing swap.

X