Portal | Level: L1: Foundations | Topics: Linux Users & Permissions, Linux Fundamentals, Linux Hardening | Domain: Linux
Linux Users and Permissions — Primer¶
Why This Matters¶
Every process on a Linux system runs as some user. Every file is owned by some user and some group. The permission model is the fundamental security boundary between "this process can read your SSH keys" and "this process cannot." Misunderstanding users and permissions leads to services that won't start, security holes wide enough to drive a truck through, and 3 AM pages that could have been prevented.
If you operate Linux systems in production, you will interact with the permission model daily. This primer covers the full landscape: identity, ownership, access control, and privilege escalation.
The Identity Model¶
Users and UIDs¶
Every user account maps to a numeric User ID (UID). The kernel doesn't care about usernames — it operates entirely on UIDs. The mapping between names and numbers lives in /etc/passwd.
# View your own identity
$ id
uid=1000(deploy) gid=1000(deploy) groups=1000(deploy),27(sudo),999(docker)
# View another user's identity
$ id nginx
uid=101(nginx) gid=101(nginx) groups=101(nginx)
Key UID ranges (varies by distro, but conventionally):
| Range | Purpose |
|---|---|
| 0 | root — the superuser |
| 1-999 | System/service accounts |
| 1000+ | Regular human users |
/etc/passwd¶
Each line represents one user account:
Fields (colon-separated):
1. Username — login name
2. Password placeholder — x means the password hash is in /etc/shadow
3. UID — numeric user ID
4. GID — primary group ID
5. GECOS — comment field (full name, phone, etc.)
6. Home directory — path to home
7. Login shell — shell started on login (/sbin/nologin or /usr/sbin/nologin for service accounts)
/etc/shadow¶
Contains the actual password hashes. Readable only by root (mode 000 or 640 depending on distro):
Fields:
1. Username
2. Password hash (or ! / * for locked accounts)
3. Days since epoch of last password change
4. Minimum days between changes
5. Maximum days before change required
6. Warning days before expiry
7. Inactive days after expiry before lockout
8. Account expiration date (days since epoch)
9. Reserved
The hash format $id$salt$hash indicates the algorithm: $1$ = MD5 (ancient, avoid), $5$ = SHA-256, $6$ = SHA-512 (standard modern default), $y$ = yescrypt (newer distros).
Groups and GIDs¶
Groups allow shared access between users. Every user has one primary group and zero or more supplementary groups.
# List all groups for current user
$ groups
deploy sudo docker
# View group membership
$ getent group docker
docker:x:999:deploy,ci-runner
Group information lives in /etc/group:
Fields: group name, password placeholder, GID, comma-separated member list.
Important: The member list in /etc/group only shows supplementary group members. Users whose primary group is set in /etc/passwd won't appear in the member list but still belong to the group.
/etc/gshadow¶
Group password hashes and administrators. Rarely used in practice but exists:
User Management Commands¶
Creating Users¶
# Create user with home directory and default shell
$ useradd -m -s /bin/bash newuser
# Create system user (no home, nologin shell, low UID)
$ useradd -r -s /sbin/nologin myservice
# Create user with specific UID, GID, and supplementary groups
$ useradd -m -u 1500 -g developers -G sudo,docker -s /bin/bash newdev
# Set password
$ passwd newuser
The -m flag creates the home directory from /etc/skel (skeleton directory). Whatever files exist in /etc/skel get copied to the new user's home.
Modifying Users¶
# Add user to supplementary group (APPEND — do not forget -a!)
$ usermod -aG docker deploy
# Change login shell
$ usermod -s /bin/zsh deploy
# Lock an account (prepends ! to password hash)
$ usermod -L baduser
# Unlock an account
$ usermod -U baduser
# Change home directory (and move files)
$ usermod -d /new/home -m deploy
# Set account expiry
$ usermod -e 2026-12-31 contractor
Critical: usermod -G docker deploy (without -a) replaces all supplementary groups with just docker. Always use -aG to append.
Deleting Users¶
# Delete user (keep home directory)
$ userdel olduser
# Delete user AND home directory
$ userdel -r olduser
Group Management¶
# Create a group
$ groupadd developers
# Create with specific GID
$ groupadd -g 2000 devteam
# Delete a group
$ groupdel oldgroup
# Switch your active group for the current session
$ newgrp docker
newgrp starts a new shell with the specified group as your primary group. Files you create in that shell will be owned by that group. Useful for shared project directories.
File Permissions¶
The Permission Model¶
Every file has three permission sets: owner, group, other. Each set has three bits: read (r), write (w), execute (x).
Breaking down -rw-r--r--:
| Position | Meaning |
|---|---|
- |
File type (- = regular, d = directory, l = symlink, etc.) |
rw- |
Owner can read and write |
r-- |
Group can read |
r-- |
Others can read |
For directories, the meanings shift:
- r = can list contents (ls)
- w = can create/delete files within
- x = can traverse (cd into) the directory
You need x on a directory to access anything inside it, even if you have permissions on the files themselves.
Numeric (Octal) Permissions¶
Each permission bit has a numeric value: r=4, w=2, x=1. Sum them per set:
| Octal | Binary | Permission |
|---|---|---|
| 7 | 111 | rwx |
| 6 | 110 | rw- |
| 5 | 101 | r-x |
| 4 | 100 | r-- |
| 3 | 011 | -wx |
| 2 | 010 | -w- |
| 1 | 001 | --x |
| 0 | 000 | --- |
So chmod 755 means: owner=rwx, group=r-x, other=r-x.
chmod — Changing Permissions¶
# Symbolic mode
$ chmod u+x script.sh # Add execute for owner
$ chmod g-w,o-r config.yaml # Remove write for group, read for others
$ chmod a+r public.html # Add read for all (a = all)
$ chmod u=rwx,g=rx,o= secret/ # Set exact permissions
# Numeric mode
$ chmod 750 myapp # rwxr-x---
$ chmod 644 config.yaml # rw-r--r--
$ chmod 600 ~/.ssh/id_rsa # rw-------
# Recursive
$ chmod -R 755 /var/www/html/ # Apply to all files and directories
chown — Changing Ownership¶
# Change owner
$ chown deploy /var/www/app
# Change owner and group
$ chown deploy:www-data /var/www/app
# Change group only
$ chown :www-data /var/www/app
# Recursive
$ chown -R deploy:deploy /home/deploy/
# Follow symlinks (or not)
$ chown -h deploy:deploy /var/www/current # Change the symlink itself
chgrp — Changing Group¶
umask — Default Permission Mask¶
The umask determines what permissions are removed from newly created files and directories. The default permission for new files is 666 (rw-rw-rw-) and for directories is 777 (rwxrwxrwx). The umask is subtracted.
$ umask
0022
# With umask 0022:
# New file: 666 - 022 = 644 (rw-r--r--)
# New directory: 777 - 022 = 755 (rwxr-xr-x)
Common umask values:
| umask | File result | Dir result | Use case |
|---|---|---|---|
| 0022 | 644 | 755 | Standard default |
| 0027 | 640 | 750 | Group-readable, no others |
| 0077 | 600 | 700 | Owner-only (private) |
| 0002 | 664 | 775 | Group-writable (shared projects) |
Set umask in /etc/profile, /etc/login.defs, or per-user shell rc files. For system services, set UMask= in the systemd unit file.
Special Permission Bits¶
SUID (Set User ID) — Octal 4000¶
When set on an executable, the process runs as the file owner rather than the invoking user.
The s in the owner execute position means SUID is set. /usr/bin/passwd runs as root so it can modify /etc/shadow, even when a regular user runs it.
Security warning: SUID on shell scripts is ignored by modern kernels (and was always dangerous). SUID should only be set on carefully audited compiled binaries.
SGID (Set Group ID) — Octal 2000¶
On executables: process runs with the file's group. On directories: new files inherit the directory's group (instead of the creator's primary group).
# SGID on a directory — essential for shared project directories
$ chmod g+s /opt/shared-project/
$ ls -ld /opt/shared-project/
drwxrwsr-x 2 root developers 4096 Mar 15 10:00 /opt/shared-project/
The s in the group execute position. Any file created inside /opt/shared-project/ will automatically belong to the developers group.
Sticky Bit — Octal 1000¶
On directories: only the file owner (or root) can delete or rename files within, even if others have write permission. Classic example: /tmp.
The t in the other execute position. Without the sticky bit, any user with write access to /tmp could delete anyone else's files.
Access Control Lists (ACLs)¶
Standard Unix permissions only support one owner and one group. ACLs provide fine-grained access for multiple users and groups.
Checking and Setting ACLs¶
# View ACLs
$ getfacl /var/www/html/
# file: var/www/html/
# owner: root
# group: www-data
# flags: -s-
user::rwx
user:deploy:rwx
group::r-x
group:ci-runner:rwx
mask::rwx
other::r-x
# Grant a specific user access
$ setfacl -m u:deploy:rwx /var/www/html/
# Grant a specific group access
$ setfacl -m g:ci-runner:rwx /var/www/html/
# Set default ACLs (inherited by new files in directory)
$ setfacl -d -m u:deploy:rwx /var/www/html/
# Remove a specific ACL entry
$ setfacl -x u:deploy /var/www/html/
# Remove all ACLs
$ setfacl -b /var/www/html/
# Recursive
$ setfacl -R -m u:deploy:rwx /var/www/html/
When a file has ACLs, ls -l shows a + after the permission string:
The ACL Mask¶
The mask entry limits the effective permissions for all named users and groups (not the owner or others). It acts as a ceiling:
# Even if deploy has rwx in the ACL, if mask is r-x, effective write is denied
$ setfacl -m m::rx /var/www/html/
Important: chmod on a file with ACLs modifies the mask, not the group permission. This catches people by surprise.
ACLs and Filesystem Support¶
ACLs require filesystem support. ext4, XFS, and Btrfs all support ACLs. Mount with acl option if not default (most modern distros enable it by default).
# Check if ACLs are supported
$ tune2fs -l /dev/sda1 | grep "Default mount options"
Default mount options: user_xattr acl
sudo and Privilege Escalation¶
sudo vs su¶
su (switch user) requires the target user's password. sudo requires your own password and checks if you're authorized.
# su — become root (need root's password)
$ su -
Password: [root's password]
# su — become another user
$ su - deploy
Password: [deploy's password]
# sudo — run one command as root (need YOUR password)
$ sudo systemctl restart nginx
# sudo — open a root shell
$ sudo -i
# sudo — run as a different user
$ sudo -u postgres psql
The - or -l flag with su starts a login shell (loads the target user's environment). Without it, you keep your current environment — which causes subtle bugs.
sudoers Configuration¶
The sudo policy lives in /etc/sudoers. Always edit with visudo — it validates syntax before saving, preventing lockouts.
sudoers syntax:
# User privilege specification
deploy ALL=(ALL:ALL) ALL
# Meaning:
# deploy = this user
# ALL= = on all hosts
# (ALL:ALL) = as any user:any group
# ALL = can run any command
Common patterns:
# Allow deploy to restart services without password
deploy ALL=(root) NOPASSWD: /usr/bin/systemctl restart nginx, /usr/bin/systemctl restart myapp
# Allow group %ops to run all commands
%ops ALL=(ALL:ALL) ALL
# Allow ci-runner specific commands only
ci-runner ALL=(root) NOPASSWD: /usr/bin/docker, /usr/bin/kubectl
# Deny a specific command (deny rules must come after allow)
deploy ALL=(ALL) ALL
deploy ALL=(ALL) !/usr/bin/su
Drop-in sudoers files¶
Modern systems use /etc/sudoers.d/ for modular configuration:
# Create a drop-in file
$ sudo visudo -f /etc/sudoers.d/deploy
deploy ALL=(root) NOPASSWD: /usr/bin/systemctl restart nginx
Files in /etc/sudoers.d/ must not contain . or ~ in the filename (they'll be silently ignored). Use names like 10-deploy, 20-ci.
sudo Logging¶
sudo logs every invocation to syslog (usually /var/log/auth.log or /var/log/secure):
Mar 15 10:45:22 webserver sudo: deploy : TTY=pts/0 ; PWD=/home/deploy ; USER=root ; COMMAND=/usr/bin/systemctl restart nginx
Failed sudo attempts also appear:
Mar 15 10:46:01 webserver sudo: baduser : user NOT in sudoers ; TTY=pts/1 ; PWD=/home/baduser ; USER=root ; COMMAND=/bin/bash
PAM (Pluggable Authentication Modules)¶
PAM provides a modular framework for authentication. When you log in, change your password, or sudo, PAM modules handle the actual verification.
PAM Configuration¶
PAM config lives in /etc/pam.d/, with one file per service:
A PAM config line:
Fields:
1. Type: auth (verify identity), account (check access), password (update credentials), session (setup/teardown)
2. Control: required (must pass, continue checking), requisite (must pass, fail immediately), sufficient (pass = stop, skip rest), optional
3. Module: the shared library implementing the check
4. Arguments: module-specific options
Common PAM Modules¶
| Module | Purpose |
|---|---|
pam_unix.so |
Standard Unix password authentication |
pam_deny.so |
Always deny (blackhole) |
pam_permit.so |
Always permit (dangerous) |
pam_wheel.so |
Restrict su to members of wheel group |
pam_limits.so |
Apply resource limits from /etc/security/limits.conf |
pam_faillock.so |
Lock accounts after N failed attempts |
pam_pwquality.so |
Password complexity requirements |
pam_tally2.so |
Legacy failed login counter (replaced by pam_faillock) |
pam_google_authenticator.so |
TOTP two-factor authentication |
Password Policies¶
Password quality is enforced via PAM. On modern systems, /etc/security/pwquality.conf:
# Minimum length
minlen = 12
# Require at least one of each class
dcredit = -1 # digit
ucredit = -1 # uppercase
lcredit = -1 # lowercase
ocredit = -1 # special character
# Reject passwords containing the username
usercheck = 1
# Maximum consecutive identical characters
maxrepeat = 3
Password aging is controlled in /etc/login.defs and per-user via chage:
# View password aging info
$ chage -l deploy
Last password change : Mar 15, 2026
Password expires : Jun 13, 2026
Password inactive : never
Account expires : never
Minimum number of days between changes : 1
Maximum number of days between changes : 90
Number of days of warning before expiry : 14
# Set maximum password age
$ chage -M 90 deploy
# Force password change on next login
$ chage -d 0 deploy
# Set account expiry
$ chage -E 2026-12-31 contractor
Practical Permission Patterns¶
Web Server Document Root¶
# Standard setup for nginx/apache
$ sudo chown -R www-data:www-data /var/www/html
$ sudo chmod -R 755 /var/www/html
$ sudo find /var/www/html -type f -exec chmod 644 {} \;
# If deploy user needs to update files:
$ sudo setfacl -R -m u:deploy:rwx /var/www/html
$ sudo setfacl -R -d -m u:deploy:rwx /var/www/html # Default for new files
Shared Project Directory¶
$ sudo mkdir /opt/project
$ sudo chown root:developers /opt/project
$ sudo chmod 2775 /opt/project # SGID + group writable
$ sudo setfacl -d -m g:developers:rwx /opt/project # Default ACL
SSH Key Permissions¶
SSH is extremely strict about permissions. Wrong permissions = silent refusal:
$ chmod 700 ~/.ssh
$ chmod 600 ~/.ssh/id_rsa # Private key
$ chmod 644 ~/.ssh/id_rsa.pub # Public key
$ chmod 600 ~/.ssh/authorized_keys
$ chmod 644 ~/.ssh/config
Service Account Setup¶
# Create service account
$ sudo useradd -r -s /sbin/nologin -d /opt/myservice -m myservice
# Set ownership of service files
$ sudo chown -R myservice:myservice /opt/myservice
$ sudo chmod 750 /opt/myservice
# Secrets readable only by service
$ sudo chown myservice:myservice /opt/myservice/secrets.env
$ sudo chmod 600 /opt/myservice/secrets.env
Systemd Service Hardening¶
Systemd provides additional permission controls beyond traditional Unix:
[Service]
User=myservice
Group=myservice
UMask=0027
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths=/var/lib/myservice
PrivateTmp=yes
Permission Debugging Checklist¶
When something can't access a file, check in this order:
- Who is the process running as?
ps aux | grep myprocessor check systemd unitUser= - What are the file permissions?
ls -la /path/to/file - What is the full path permission chain? Every directory in the path needs
xpermission - Are there ACLs?
getfacl /path/to/file - Is SELinux/AppArmor blocking?
getenforceoraa-status, checkaudit.log - Are there mount options restricting access?
mount | grep /path(look fornoexec,nosuid,ro) - Is the filesystem full?
df -h /path(writes fail on full filesystem)
# Quick diagnostic one-liner
$ namei -l /var/www/html/index.html
f: /var/www/html/index.html
drwxr-xr-x root root /
drwxr-xr-x root root var
drwxr-xr-x root root www
drwxr-xr-x www-data www-data html
-rw-r--r-- www-data www-data index.html
namei -l is your best friend for tracing the entire permission chain from root to the target file.
Summary¶
The Linux permission model is deceptively simple — three sets of three bits — but its interactions with SUID/SGID, sticky bits, ACLs, umask, PAM, and sudo create a rich and sometimes confusing landscape. Master the basics (chmod, chown, umask), understand the special bits, and know how to debug permission chains from the filesystem root to the target file. That covers 95% of what you'll encounter in production.
Wiki Navigation¶
Prerequisites¶
- Linux Ops (Topic Pack, L0)
Related Content¶
- SELinux & AppArmor (Topic Pack, L2) — Linux Fundamentals, Linux Hardening
- SSH Deep Dive (Topic Pack, L1) — Linux Fundamentals, Linux Hardening
- /proc Filesystem (Topic Pack, L2) — Linux Fundamentals
- Advanced Bash for Ops (Topic Pack, L1) — Linux Fundamentals
- Adversarial Interview Gauntlet (30 sequences) (Scenario, L2) — Linux Fundamentals
- Bash Exercises (Quest Ladder) (CLI) (Exercise Set, L0) — Linux Fundamentals
- Case Study: CI Pipeline Fails — Docker Layer Cache Corruption (Case Study, L2) — Linux Fundamentals
- Case Study: Container Vuln Scanner False Positive Blocks Deploy (Case Study, L2) — Linux Fundamentals
- Case Study: Disk Full Root Services Down (Case Study, L1) — Linux Fundamentals
- Case Study: Disk Full — Runaway Logs, Fix Is Loki Retention (Case Study, L2) — Linux Fundamentals