RHEL Basics #5: Users / Groups / Permissions — UID/GID, sudo, ACL
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:
- #1 What is RHEL — From Fedora to RHEL, plus AlmaLinux and Rocky Linux
- #2 Setup — Installing RHEL 9, Subscription Manager, first login
- #3 dnf and package management — repo, modules, AppStream
- #4 Intro to systemd — services, targets, journalctl
- #5 Users / groups / permissions — UID/GID, sudo, ACL ← this post
- #6 Filesystem basics — XFS, mount, /etc/fstab
- #7 Basic security — firewalld, SSH hardening
Where users live — /etc/passwd / /etc/shadow
#
Users on Linux are split across two files.
/etc/passwd
#
$ grep curtis /etc/passwd
curtis:x:1000:1000:Curtis Kim:/home/curtis:/bin/bashSeven colon-separated fields:
| Field | Meaning |
|---|---|
curtis | Username |
x | Password slot (used to live here; now in /etc/shadow) |
1000 | UID — the actual identifier |
1000 | Default GID |
Curtis Kim | GECOS — full name / notes (free-form) |
/home/curtis | Home directory |
/bin/bash | Login shell |
This file is readable by everyone — the username → UID mapping is needed all over the system.
/etc/shadow
#
$ 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 #
0 → root
1 ~ 999 → system users (services running under their own UID)
1000~ → regular usersuseradd 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
nginxorpostgressystem user is created automatically and the daemon runs as that user. Install the RHELnginxpackage andid nginxshows the UID.
Groups — /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 groupcurtisbelongs to. wheel is the sudo group on RHEL.
$ id
uid=1000(curtis) gid=1000(curtis) groups=1000(curtis),10(wheel)
$ groups
curtis wheelOne default group, multiple supplementary groups.
useradd / usermod / userdel #
$ sudo useradd -m -s /bin/bash -G wheel alice
$ sudo passwd aliceCommon useradd options:
| Option | Meaning |
|---|---|
-m | Create 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 |
-r | Create as a system user (UID < 1000) |
$ 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 # unlockWatch out —
-Galone replaces all supplementary groups. Always use-aG(--append --groups). One slip and the user is suddenly missing fromwheeland can’t sudo.
$ sudo userdel alice # delete user (home stays)
$ sudo userdel -r alice # also delete home and mail spoolIn production, be careful with userdel -r — there might be important files in the home directory. Usually usermod -L (lock) is enough.
Password policy #
$ 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 earlyFile permissions — what rwx means #
Each file or directory has three user classes (owner / group / other), each with three permission bits (r/w/x).
$ 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 - 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 charr / w / x mean different things on files vs. directories #
| Permission | File | Directory |
|---|---|---|
r | Read contents | List files inside (ls) |
w | Write contents | Create / delete / rename files inside |
x | Execute | Enter 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:
r = 4, w = 2, x = 1
rwx = 7 r-x = 5 r-- = 4 --- = 0Three classes (owner / group / other), one digit each:
$ 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 outsidersThere are 4–5 patterns you’ll run into over and over — get them into muscle memory.
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 #
$ 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.
| Bit | Octal | Use |
|---|---|---|
| SUID | 4 | Runs with the file owner’s permissions. Attached to commands like passwd |
| SGID | 2 | (On a directory) files created inside inherit the directory’s group |
| Sticky | 1 | (On a directory) only the file’s owner can delete it. Lives on /tmp |
$ 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 # stickyYou 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.
$ umask
0022umask is “bits to subtract from the maximum default.” The maximum is 666 for files and 777 for directories. Subtracting 022:
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 /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
#
$ 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::---$ sudo setfacl -m g:devs:r-x /var/data$ sudo setfacl -x u:alice /var/data # remove one entry
$ sudo setfacl -b /var/data # remove all ACL entriesA file with ACLs gets a + at the end of its permissions in ls -l.
$ ls -l /var/data
drwxrwx---+ 2 root data 6 Apr 13 11:00 /var/data
↑
indicates ACLs are attachedDefault ACLs — auto-applied to new files #
A directory’s default ACL gets auto-applied to anything created inside it.
$ sudo setfacl -d -m u:alice:rwx /var/data # -d = defaultOne 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:
%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.
$ sudo visudo # /etc/sudoers itself
$ sudo visudo -f /etc/sudoers.d/mycompany # a separate filevisudo 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.”
deploy ALL=(ALL) NOPASSWD: /bin/systemctl restart myapp, /bin/systemctl status myappNOPASSWD:— 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 #
$ 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
sudogroup. RHEL’s tradition iswheel. They play the same role under different names. Creating asudogroup on RHEL won’t do anything — usewheel.
Common commands at a glance #
| Command | What it does |
|---|---|
id [user] | UID / GID / groups |
who / w | Currently 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 file | Permissions (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 -l | sudo |
visudo [-f /etc/sudoers.d/<file>] | Safe sudoers editing |
Common pitfalls #
“Forgot -aG and replaced the groups”
#
$ 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” #
~/.ssh 700
~/.ssh/authorized_keys 600
~/.ssh/id_rsa 600Anything 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
-aGwith usermod, never-Galone — one mistake nukes sudo. rwxbits — the trap is directoryxmeans “you can enter it (and reach what’s inside)”.- Two notations for
chmod(octal and symbolic); be careful with-Ronchown/chgrp. - When standard permissions don’t fit, ACL —
setfacl -m u:alice:rwx, plus default ACL for auto-application. - Split sudo config into
/etc/sudoers.d/, edit only withvisudo. - The sudo group on RHEL is
wheel(notsudolike 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.