Users, Groups & UIDs
Every Linux process runs as a user, identified by a numeric UID (User ID). Files have an owning UID and GID. The kernel makes all access control decisions based on these numbers — names like "alice" and "sudo" are just human-readable labels stored in /etc/passwd and /etc/group.
| UID Range | Type | Examples |
|---|---|---|
| 0 | root — superuser, bypasses most permissions | root |
| 1–999 | System accounts — services and daemons | www-data, nobody, sshd, docker |
| 1000+ | Regular user accounts | alice, bob |
| 65534 | nobody — used for privilege separation | NFS anonymous, nobody |
# View your identity
id # uid, gid, and all supplementary groups
whoami # Just the username
id alice # View another user's IDs
# User and group databases
cat /etc/passwd # username:x:UID:GID:comment:homedir:shell
cat /etc/group # groupname:x:GID:members
cat /etc/shadow # Hashed passwords (root-readable only)
# Manage users
sudo useradd -m -s /bin/bash alice # Create user with home and shell
sudo passwd alice # Set password
sudo usermod -aG docker alice # Add alice to docker group
sudo userdel -r alice # Delete user and home directory
# Switch users
su - alice # Switch to alice (need alice's password)
sudo su - # Switch to root (need sudo password)
sudo -u alice ls # Run single command as alice
File Permission Bits
Every file has 9 permission bits (plus 3 special bits). The 9 bits are divided into three groups of 3: owner (u), group (g), and others (o). Each group has read (r=4), write (w=2), and execute (x=1) bits.
# Reading permission output from ls -la
ls -la /etc/passwd
# -rw-r--r-- 1 root root 2847 May 1 /etc/passwd
# ^ ^^^ ^^^ ^
# | | | filename
# | | +-- others: r-- = read only (004)
# | +------- group: r-- = read only (040)
# +---------- owner: rw- = read+write (600)
# Total octal: 644
# Common permission patterns
# 644 rw-r--r-- Regular files (readable by all, writable by owner)
# 755 rwxr-xr-x Directories and executables
# 600 rw------- Private files (SSH keys, secret configs)
# 700 rwx------ Private directories
# 777 rwxrwxrwx (Avoid! Anyone can modify/delete)
chmod & umask
# chmod — change file mode (permissions)
# Octal notation
chmod 644 file.txt # rw-r--r--
chmod 755 script.sh # rwxr-xr-x
chmod 600 ~/.ssh/id_rsa # rw------- (SSH key must be 600)
chmod -R 755 /var/www/html/ # Recursive
# Symbolic notation (u=user/owner, g=group, o=others, a=all)
chmod u+x script.sh # Add execute for owner
chmod go-w file.txt # Remove write from group and others
chmod a+r file.txt # Add read for everyone
chmod u=rwx,g=rx,o=r file # Set exact permissions
# umask — default permission mask for new files
umask # Show current umask (e.g., 0022)
# New files = 0666 & ~umask → 0666 & ~0022 = 0644
# New dirs = 0777 & ~umask → 0777 & ~0022 = 0755
umask 0077 # More secure: new files=600, dirs=700
umask 0022 # Standard: new files=644, dirs=755
chown & chgrp
# chown — change ownership
sudo chown alice file.txt # Change owner
sudo chown alice:alice file.txt # Change owner and group
sudo chown :www-data file.txt # Change group only
sudo chown -R alice:alice /var/www/ # Recursive
# chgrp — change group
sudo chgrp docker /var/run/docker.sock # Let docker group access socket
# View file ownership in numeric form
ls -lan /etc/shadow # Shows UID/GID numbers instead of names
Special Permission Bits
setuid & setgid
The setuid bit on an executable causes it to run with the owner's UID rather than the caller's UID. This is how passwd (owned by root) can write to /etc/shadow even when run by a non-root user.
# Find setuid/setgid executables on the system
find / -xdev -perm -4000 -type f 2>/dev/null # setuid (SUID)
find / -xdev -perm -2000 -type f 2>/dev/null # setgid (SGID)
# Common setuid programs
ls -la $(which passwd) # -rwsr-xr-x root root (s = setuid set)
ls -la $(which sudo) # -rwsr-xr-x root root
ls -la $(which ping) # -rwxr-xr-x (modern kernels use capabilities instead)
# How setuid appears in ls output
# -rwsr-xr-x s in owner's execute position = setuid + execute
# -rwSr-xr-x S in owner's execute position = setuid WITHOUT execute (unusual)
# Set setuid on a file (for academic understanding)
chmod u+s /tmp/test-binary # Adds setuid
chmod 4755 /tmp/test-binary # Equivalent: 4=setuid, 755=permissions
Sticky Bit
The sticky bit on a directory means: only the file's owner (or root) can delete or rename it, even if others have write permission on the directory. This is why /tmp is writable by everyone but you can only delete your own files there.
# /tmp has sticky bit set
ls -la / | grep " tmp"
# drwxrwxrwt — t at the end = sticky bit set (T = sticky without execute)
# Demonstrate sticky bit
chmod +t /shared-dir # Set sticky bit
chmod 1777 /shared-dir # Equivalent: 1=sticky, 777=permissions
# Without sticky bit: anyone with write access to directory can delete ANY file
# With sticky bit: only file owner can delete their files
Linux Capabilities
Linux capabilities break root's privileges into ~40 fine-grained units. Instead of "run as root" (all-or-nothing), you can grant just the specific capability a program needs.
| Capability | What It Grants | Example Use |
|---|---|---|
| CAP_NET_BIND_SERVICE | Bind to ports < 1024 | nginx on port 80 without root |
| CAP_NET_RAW | Use raw/packet sockets | ping, tcpdump |
| CAP_SYS_PTRACE | Trace other processes | strace, gdb |
| CAP_SYS_ADMIN | Wide-ranging admin operations | mount, cgroup ops (very broad!) |
| CAP_CHOWN | Change file ownership arbitrarily | Installation scripts |
| CAP_KILL | Send signals to any process | Process managers |
| CAP_NET_ADMIN | Network config (routes, interfaces) | ip command |
| CAP_DAC_OVERRIDE | Bypass file permission checks | Backup tools |
# View capabilities of a binary
getcap /usr/bin/ping # cap_net_raw=ep (e=effective, p=permitted)
getcap /usr/bin/tcpdump # cap_net_admin,cap_net_raw=eip
# View capabilities of a running process
cat /proc/$$/status | grep -i cap
# CapPrm, CapEff, CapBnd — in hex, decode with capsh
capsh --decode=$(cat /proc/1/status | grep CapEff | awk '{print $2}')
# Shows: cap_chown, cap_dac_override, cap_setuid...
# Grant capabilities to a binary (root required)
# sudo setcap cap_net_bind_service=+ep /usr/local/bin/myapp
# Now myapp can bind port 80 without running as root
# Docker capability management
# docker run --cap-drop ALL --cap-add NET_BIND_SERVICE nginx
ACLs — Fine-Grained Permissions
Standard Unix permissions only support one owner, one group, and others. ACLs (Access Control Lists) allow granting specific permissions to any number of users and groups on any file.
# View ACLs (if supported by filesystem)
getfacl /etc/passwd # Shows: owner, group, ACL entries
# Set ACL to grant bob read+write on a file owned by alice
touch /tmp/shared-file
sudo setfacl -m u:bob:rw /tmp/shared-file # User bob: rw
sudo setfacl -m g:devs:r /tmp/shared-file # Group devs: r
getfacl /tmp/shared-file
# user::rw- (owner)
# user:bob:rw- (bob's ACL)
# group::r-- (group)
# group:devs:r-- (devs ACL)
# other::r-- (others)
# Remove ACL entry
sudo setfacl -x u:bob /tmp/shared-file
sudo setfacl -b /tmp/shared-file # Remove all ACLs
Mandatory Access Control
Standard Linux permissions are discretionary (DAC) — the file owner decides who can access what. Mandatory Access Control (MAC) adds a second layer enforced by the kernel regardless of owner decisions, preventing even root from accessing certain resources.
SELinux (RHEL, CentOS, Fedora): Labels every file, process, and port with a security context. Policy rules define which contexts can access which. If nginx (labelled httpd_t) tries to read a file labelled user_home_t that SELinux policy forbids, the access is denied even if DAC permissions allow it.
AppArmor (Ubuntu, Debian): Profile-based — each program has a profile defining which files it can access and which capabilities it can use. Easier to understand than SELinux but less expressive.
# SELinux status
sestatus 2>/dev/null || echo "SELinux not installed"
getenforce 2>/dev/null # Enforcing / Permissive / Disabled
# View SELinux context of files
ls -Z /etc/passwd # system_u:object_r:passwd_file_t:s0
# View SELinux context of processes
ps -eZ | grep nginx # system_u:system_r:httpd_t:s0 nginx
# AppArmor status
sudo aa-status 2>/dev/null | head -20
# Lists profiles in enforce/complain mode
# View AppArmor profile for a binary
sudo cat /etc/apparmor.d/usr.sbin.nginx 2>/dev/null | head -20
# View AppArmor denials
sudo dmesg | grep "apparmor=\"DENIED\"" | tail -10
sudo journalctl | grep "apparmor" | grep DENIED | tail -10
How Docker Uses All These Mechanisms Together
Docker containers use a defence-in-depth combination of the mechanisms in this article: (1) UIDs — containers run as non-root users when configured with USER app in Dockerfile; (2) Capabilities — Docker drops all capabilities by default except ~14, and you can use --cap-drop ALL for minimal privilege; (3) Seccomp — a syscall filter (similar in spirit to capabilities) that blocks dangerous syscalls like ptrace, reboot, mount; (4) AppArmor — Docker applies the docker-default AppArmor profile; (5) Linux namespaces — UID namespace remapping makes container root (UID 0) map to an unprivileged host UID. This is why "rootless Docker" is the modern security recommendation.
Exercises
# Exercise 1: Read and decode file permissions
ls -la /usr/bin/sudo /usr/bin/passwd /tmp
# What do the 's' and 't' characters mean?
# Exercise 2: Find all setuid files on the system
find / -xdev -perm /4000 -type f 2>/dev/null | head -20
# Review which ones are truly necessary
# Exercise 3: Check capabilities of common binaries
for bin in ping traceroute tcpdump python3 node; do
path=$(which $bin 2>/dev/null)
[ -n "$path" ] && echo -n "$bin: " && (getcap $path || echo "no caps")
done
# Exercise 4: Create and test ACLs
mkdir /tmp/acl-test
touch /tmp/acl-test/secret.txt
echo "secret data" > /tmp/acl-test/secret.txt
chmod 700 /tmp/acl-test/secret.txt # Only owner can read
getfacl /tmp/acl-test/secret.txt
# Now grant read access to another user via ACL:
# setfacl -m u:www-data:r /tmp/acl-test/secret.txt
# getfacl /tmp/acl-test/secret.txt
# Exercise 5: Explore your process capabilities
cat /proc/$$/status | grep Cap # In hex
# To decode: capsh --decode=
Conclusion & Next Steps
Linux security is layered: UIDs for identity, permission bits for basic access control, setuid for controlled privilege escalation, capabilities for fine-grained privilege, ACLs for complex multi-user scenarios, and SELinux/AppArmor for mandatory, policy-enforced access control. Understanding this stack is essential for secure container configuration, cloud IAM integration, and privilege separation in application design.