Skip to content

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:

deploy:x:1000:1000:Deploy User:/home/deploy:/bin/bash

Fields (colon-separated): 1. Username — login name 2. Password placeholderx 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):

deploy:$6$rounds=5000$salt$hash...:19500:0:99999:7:::

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:

docker:x:999:deploy,ci-runner

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:

docker:!::deploy,ci-runner

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).

$ ls -la /etc/passwd
-rw-r--r-- 1 root root 2483 Mar 15 10:22 /etc/passwd

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

$ chgrp www-data /var/www/html
$ chgrp -R developers /opt/project/

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.

$ ls -l /usr/bin/passwd
-rwsr-xr-x 1 root root 68208 Mar 14 11:22 /usr/bin/passwd

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.

# Set SUID
$ chmod u+s /usr/local/bin/myapp
$ chmod 4755 /usr/local/bin/myapp

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.

$ ls -ld /tmp
drwxrwxrwt 15 root root 4096 Mar 15 10:30 /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.

$ chmod +t /var/shared/
$ chmod 1777 /tmp

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:

-rw-rwxr--+ 1 root www-data 1024 Mar 15 10:30 index.html

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.

$ sudo visudo

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:

$ ls /etc/pam.d/
common-auth  common-password  login  sshd  sudo  su  ...

A PAM config line:

auth    required    pam_unix.so    nullok

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:

  1. Who is the process running as? ps aux | grep myprocess or check systemd unit User=
  2. What are the file permissions? ls -la /path/to/file
  3. What is the full path permission chain? Every directory in the path needs x permission
  4. Are there ACLs? getfacl /path/to/file
  5. Is SELinux/AppArmor blocking? getenforce or aa-status, check audit.log
  6. Are there mount options restricting access? mount | grep /path (look for noexec, nosuid, ro)
  7. 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