Key Takeaways
- The mental model: Every file on Linux has an owner (a user), a group, and a permission string like
-rwxr-xr--. The string has 10 characters: file type + 3 bits for owner + 3 bits for group + 3 bits for everyone else. - chmod in 60 seconds:
chmod 755 filesets owner=rwx (7), group=r-x (5), others=r-x (5).chmod +x script.shadds execute for everyone.chmod -R 644 /var/wwwsets 644 recursively. - chown in 60 seconds:
sudo chown www-data:www-data /var/www/htmlgives the Nginx user ownership of the web root.sudo chown -R youruser:youruser /opt/myappclaims ownership of a whole directory. - umask explained: Your umask (usually
0022) is subtracted from666for files and777for directories. Result: new files get644, new directories get755. Change it to077for private-by-default files.
Introduction: Why File Permissions Break Everything
Direct Answer: How do Linux file permissions work — chmod, chown, and umask explained for 2026?
Linux file permissions control who can read, write, and execute every file and directory on the system. Each file has three permission sets: owner (the user who created it), group (a shared group of users), and others (everyone else). Each set has three bits: read (r=4), write (w=2), execute (x=1). The ls -la command shows the full permission string — -rwxr-xr-- means a file where the owner can read/write/execute, the group can read/execute, and others can only read. chmod changes permissions using either numeric mode (chmod 755 file) or symbolic mode (chmod u+x file). chown changes ownership (sudo chown www-data:www-data /var/www/html). umask sets the default permissions for new files — umask 022 means new files get 644 (rw-r—r—) and new directories get 755 (rwxr-xr-x). On Ubuntu 24.04 LTS, file permission errors are the single most common cause of “Permission denied” failures in web deployments, Docker volumes, and SSH configurations.
“Every ‘Permission denied’ error in Linux has a root cause that takes under 30 seconds to diagnose once you understand the permission model. Most developers spend hours guessing because they never learned it properly.”
This guide does not stop at the theory. After explaining the model, it covers the 12 real-world scenarios every developer and system administrator encounters — from SSH key permissions to Nginx web root ownership to Docker volume access and CI/CD pipeline execute bits.
Part 1: Reading Permissions with ls -la
Before changing permissions, you need to read them accurately. The ls -la command shows everything.
# Create some test files to work with
mkdir -p ~/permissions-demo
cd ~/permissions-demo
touch file.txt
mkdir mydir
cp /usr/bin/bash script.sh
# View permissions in detail
ls -la ~/permissions-demo/
Expected output:
total 1212
drwxr-xr-x 3 youruser youruser 4096 Apr 15 09:00 .
drwxr-x--- 12 youruser youruser 4096 Apr 15 08:55 ..
-rw-r--r-- 1 youruser youruser 0 Apr 15 09:00 file.txt
drwxr-xr-x 2 youruser youruser 4096 Apr 15 09:00 mydir
-rwxr-xr-x 1 youruser youruser 1234016 Apr 15 09:00 script.sh
Anatomy of a permission string — reading -rwxr-xr-- character by character:
- rwx r-x r--
│ │ │ │
│ │ │ └── Others: r-- = read only (4)
│ │ └─────── Group: r-x = read + execute (5)
│ └──────────── Owner: rwx = read + write + execute (7)
└──────────────── File type: - = regular file
d = directory
l = symbolic link
b = block device
c = character device
File type characters:
| Character | Meaning | Example |
|---|---|---|
- | Regular file | config.txt, script.sh |
d | Directory | /var/www/html |
l | Symbolic link | /usr/bin/python → python3.14 |
b | Block device | /dev/sda |
c | Character device | /dev/tty |
The full ls -la column breakdown:
-rwxr-xr-x 1 youruser youruser 1234016 Apr 15 09:00 script.sh
│ │ │ │ │ │ │
│ │ │ │ │ │ └── Filename
│ │ │ │ │ └────────────── Last modified
│ │ │ │ └───────────────────────── Size in bytes
│ │ │ └────────────────────────────────── Group owner
│ │ └──────────────────────────────────────────── User owner
│ └─────────────────────────────────────────────────── Hard link count
└─────────────────────────────────────────────────────────── Permission string
Use stat for even more detail:
stat ~/permissions-demo/file.txt
Expected output:
File: /home/youruser/permissions-demo/file.txt
Size: 0 Blocks: 0 IO Block: 4096 regular empty file
Device: fd01h/64769d Inode: 131073 Links: 1
Access: (0644/-rw-r--r--) Uid: ( 1001/youruser) Gid: ( 1001/youruser)
Access: 2026-04-15 09:00:00.000000000 +0000
Modify: 2026-04-15 09:00:00.000000000 +0000
Change: 2026-04-15 09:00:00.000000000 +0000
Birth: 2026-04-15 09:00:00.000000000 +0000
The Access: (0644/-rw-r--r--) line shows both the octal value (0644) and the symbolic representation — the most useful line for debugging permission issues.
Part 2: chmod — Changing Permissions
chmod has two modes: numeric (octal) and symbolic. Both do the same thing — use whichever is clearer for the task at hand.
Numeric (Octal) Mode
Each permission type has a numeric value:
| Permission | Symbol | Value |
|---|---|---|
| Read | r | 4 |
| Write | w | 2 |
| Execute | x | 1 |
| No permission | - | 0 |
Add the values together for each group:
| Combination | Calculation | Result |
|---|---|---|
| rwx | 4+2+1 | 7 |
| rw- | 4+2+0 | 6 |
| r-x | 4+0+1 | 5 |
| r— | 4+0+0 | 4 |
| -wx | 0+2+1 | 3 |
| -w- | 0+2+0 | 2 |
| —x | 0+0+1 | 1 |
| --- | 0+0+0 | 0 |
The 9 permission values you’ll use 95% of the time:
cd ~/permissions-demo
# 755 — directories, scripts, executables (owner: rwx, group: r-x, others: r-x)
chmod 755 mydir
chmod 755 script.sh
# 644 — regular files, web content (owner: rw-, group: r--, others: r--)
chmod 644 file.txt
# 600 — private files, config files with secrets (owner: rw-, group: ---, others: ---)
touch private.key
chmod 600 private.key
# 700 — private directories, private executables (owner: rwx, group: ---, others: ---)
mkdir private-dir
chmod 700 private-dir
# 750 — group-accessible directories (owner: rwx, group: r-x, others: ---)
mkdir group-dir
chmod 750 group-dir
# 664 — group-writable files (owner: rw-, group: rw-, others: r--)
touch shared-file.txt
chmod 664 shared-file.txt
# 400 — read-only secrets, certificates (owner: r--, everyone else: ---)
touch secret.pem
chmod 400 secret.pem
# 444 — read-only for everyone (owner: r--, group: r--, others: r--)
touch readme.txt
chmod 444 readme.txt
Verify your work:
ls -la ~/permissions-demo/
Expected output:
total 1236
drwxr-xr-x 4 youruser youruser 4096 Apr 15 09:05 .
drwxr-x--- 12 youruser youruser 4096 Apr 15 08:55 ..
-rw-r--r-- 1 youruser youruser 0 Apr 15 09:00 file.txt
drwxr-x--- 2 youruser youruser 4096 Apr 15 09:05 group-dir
drwxr-xr-x 2 youruser youruser 4096 Apr 15 09:00 mydir
drwx------ 2 youruser youruser 4096 Apr 15 09:05 private-dir
-rw------- 1 youruser youruser 0 Apr 15 09:05 private.key
-r-------- 1 youruser youruser 0 Apr 15 09:05 secret.pem
-rw-rw-r-- 1 youruser youruser 0 Apr 15 09:05 shared-file.txt
-rwxr-xr-x 1 youruser youruser 1234016 Apr 15 09:00 script.sh
-r--r--r-- 1 youruser youruser 0 Apr 15 09:05 readme.txt
Symbolic Mode
Symbolic mode uses letters instead of numbers. More readable for targeted changes.
Syntax: chmod [who][operator][permissions] file
| Who | Meaning |
|---|---|
u | User (owner) |
g | Group |
o | Others |
a | All (u+g+o) |
| Operator | Meaning |
|---|---|
+ | Add permission |
- | Remove permission |
= | Set exactly (replace) |
# Add execute permission for the owner only
chmod u+x deploy.sh
# Remove write permission from group and others
chmod go-w config.yml
# Give owner full permissions, group read+execute, others nothing
chmod u=rwx,g=rx,o= deploy.sh
# Add execute for everyone (owner, group, others)
chmod a+x run.sh
# Remove all permissions for others
chmod o= sensitive-data.txt
# Add write for group only
chmod g+w shared-project/
# Set owner to rwx, group to r-x, others to nothing — same as 750
chmod u=rwx,g=rx,o= private-dir/
Verify changes with ls:
ls -la deploy.sh config.yml 2>/dev/null || \
echo "(test files — run the commands above first)"
Recursive chmod with -R
Apply permissions to a directory and everything inside it:
# Set all files and directories under /var/www to 755
sudo chmod -R 755 /var/www/html/
# PROBLEM: chmod -R 755 sets execute on files too — wrong for web content
# Files should be 644, directories should be 755
# The correct approach uses find:
# Set all directories to 755
find /var/www/html -type d -exec chmod 755 {} +
# Set all files to 644
find /var/www/html -type f -exec chmod 644 {} +
Expected output:
(no output — clean execution produces no terminal output)
Verify the results:
# Check a sample of files and directories
find /var/www/html -maxdepth 2 | head -10 | xargs ls -ld
Expected output:
drwxr-xr-x 3 www-data www-data 4096 Apr 15 09:10 /var/www/html
drwxr-xr-x 2 www-data www-data 4096 Apr 15 09:10 /var/www/html/css
-rw-r--r-- 1 www-data www-data 2048 Apr 15 09:10 /var/www/html/index.html
-rw-r--r-- 1 www-data www-data 512 Apr 15 09:10 /var/www/html/css/style.css
Directories at 755, files at 644 — correct web server permissions.
Common error: chmod: changing permissions of '/var/www/html/': Operation not permitted
Fix: You need sudo for files not owned by your user: sudo chmod 755 /var/www/html/
Part 3: chown — Changing Ownership
chmod changes what the permissions are. chown changes who those permissions apply to. Most “Permission denied” errors in web server and Docker deployments come from wrong ownership, not wrong permissions.
Basic chown syntax
# Change owner only
sudo chown alice file.txt
# Change owner and group
sudo chown alice:developers file.txt
# Change group only (note the colon with no user before it)
sudo chown :developers file.txt
# Recursive — change ownership of directory and all contents
sudo chown -R alice:developers /opt/myproject/
Verify ownership changes:
# Before chown
ls -la file.txt
Expected output (before):
-rw-r--r-- 1 root root 0 Apr 15 09:15 file.txt
sudo chown alice:developers file.txt
ls -la file.txt
Expected output (after):
-rw-r--r-- 1 alice developers 0 Apr 15 09:15 file.txt
chgrp — Change Group Only
chgrp is a dedicated command for changing only the group:
# Change group without touching the owner
sudo chgrp www-data /var/www/html/uploads/
# Recursive group change
sudo chgrp -R developers /opt/project/
The --reference flag — copy ownership from another file
# Make file2.txt have the same owner:group as file1.txt
sudo chown --reference=file1.txt file2.txt
Verify:
stat file1.txt file2.txt | grep "Uid\|Gid"
Expected output:
Access: (0644/-rw-r--r--) Uid: ( 1001/alice) Gid: ( 1002/developers)
Access: (0644/-rw-r--r--) Uid: ( 1001/alice) Gid: ( 1002/developers)
Both files now have identical ownership.
Common error: chown: invalid user: 'www-data' when the user doesn’t exist.
Fix: Verify the user exists: id www-data or getent passwd www-data. On a fresh server without Nginx installed, www-data won’t exist yet — install Nginx first.
Part 4: umask — Default Permissions for New Files
umask controls the permissions assigned to every new file and directory you create. Understanding it prevents unexpected permission problems that appear without any chmod command being run.
How umask works
Linux uses base permissions as the starting point:
- Files: base
666(rw-rw-rw-) - Directories: base
777(rwxrwxrwx)
The umask value is subtracted (masked out) from the base:
File permissions = 666 - umask
Dir permissions = 777 - umask
Check your current umask:
umask
Expected output (Ubuntu 24.04 default):
0022
What umask 022 produces:
Files: 666 - 022 = 644 (rw-r--r--)
Directories: 777 - 022 = 755 (rwxr-xr-x)
Prove it:
# Set umask explicitly to 022
umask 022
# Create a file and directory
touch umask-test-file
mkdir umask-test-dir
# Check resulting permissions
ls -la umask-test-file umask-test-dir
Expected output:
-rw-r--r-- 1 youruser youruser 0 Apr 15 09:20 umask-test-file
drwxr-xr-x 2 youruser youruser 4096 Apr 15 09:20 umask-test-dir
644 for the file, 755 for the directory — exactly 666 - 022 and 777 - 022.
Common umask values and when to use them
# umask 022 — Ubuntu default (files: 644, dirs: 755)
# Use for: normal user work, web content
umask 022
# umask 027 — security-conscious servers (files: 640, dirs: 750)
# Use for: production servers where others should have zero access
umask 027
# umask 077 — maximum privacy (files: 600, dirs: 700)
# Use for: servers with sensitive data, private key generation
umask 077
# umask 002 — collaborative environments (files: 664, dirs: 775)
# Use for: shared development directories where group can write
umask 002
Test each umask:
for mask in 022 027 077 002; do
umask $mask
touch test-$mask.txt
mkdir test-$mask-dir
done
ls -la test-*.txt test-*-dir/
Expected output:
-rw-r--r-- 1 youruser youruser 0 Apr 15 09:22 test-022.txt
-rw-r----- 1 youruser youruser 0 Apr 15 09:22 test-027.txt
-rw------- 1 youruser youruser 0 Apr 15 09:22 test-077.txt
-rw-rw-r-- 1 youruser youruser 0 Apr 15 09:22 test-002.txt
test-022-dir:
drwxr-xr-x
test-027-dir:
drwxr-x---
test-077-dir:
drwx------
test-002-dir:
drwxrwxr-x
Each umask produces a distinctly different default permission set.
Making umask permanent
# For your user only — add to ~/.bashrc
echo "umask 022" >> ~/.bashrc
# For all users system-wide — add to /etc/profile.d/
echo "umask 022" | sudo tee /etc/profile.d/umask.sh
# Verify it will load
cat /etc/profile.d/umask.sh
Expected output:
umask 022
Part 5: Special Permission Bits — SUID, SGID, Sticky Bit
Beyond the basic rwx model, Linux has three special permission bits that control advanced access behaviour. They appear in the execute position of the permission string.
SUID — Set User ID (4xxx)
When SUID is set on an executable, it runs with the file owner’s privileges instead of the caller’s.
The classic example — the passwd command:
ls -la /usr/bin/passwd
Expected output:
-rwsr-xr-x 1 root root 59976 Apr 8 10:00 /usr/bin/passwd
Notice the s in position 4 (rws instead of rwx) — that’s SUID. A regular user can change their own password even though the password database (/etc/shadow) is owned by root. The passwd command runs with root privileges via SUID.
# Set SUID on a file
chmod u+s executable
chmod 4755 executable # numeric equivalent
# Verify
ls -la executable
Expected output:
-rwsr-xr-x 1 youruser youruser 1234016 Apr 15 09:25 executable
Security warning: Never set SUID on shell scripts — it is silently ignored by Linux for security reasons. Only binary executables honour SUID. Find all SUID files on your system:
sudo find / -perm -4000 -type f 2>/dev/null
SGID — Set Group ID (2xxx)
On files: Runs with the group’s privileges (similar to SUID but for groups).
On directories: New files created inside inherit the directory’s group instead of the creator’s primary group — essential for shared project directories.
# Create a shared directory with SGID
sudo mkdir /opt/shared-project
sudo chown youruser:developers /opt/shared-project
sudo chmod 2775 /opt/shared-project # g+s = SGID
# Verify
ls -la /opt/ | grep shared-project
Expected output:
drwxrwsr-x 2 youruser developers 4096 Apr 15 09:30 shared-project
The s in the group execute position (rws) is SGID. Now every file created inside /opt/shared-project inherits the developers group automatically — no manual chown needed for collaborative work.
# Test SGID inheritance
touch /opt/shared-project/newfile.txt
ls -la /opt/shared-project/
Expected output:
-rw-rw-r-- 1 youruser developers 0 Apr 15 09:31 newfile.txt
developers group was automatically assigned — SGID is working.
Sticky Bit (1xxx)
On a directory, the sticky bit means only the file’s owner can delete it, even if others have write permission to the directory. This is why /tmp is safe to be world-writable.
ls -la / | grep tmp
Expected output:
drwxrwxrwt 12 root root 4096 Apr 15 09:32 tmp
The t at the end (rwt) is the sticky bit. Everyone can write to /tmp, but you can only delete your own files.
# Set sticky bit on a directory
sudo mkdir /opt/shared-uploads
sudo chmod 1777 /opt/shared-uploads # world-writable with sticky bit
sudo chmod a+t /opt/shared-uploads # symbolic equivalent
# Verify
ls -la /opt/ | grep shared-uploads
Expected output:
drwxrwxrwt 2 root root 4096 Apr 15 09:33 shared-uploads
Summary of special bit numeric prefixes:
| Prefix | Bit | Applies to | Effect |
|---|---|---|---|
4xxx | SUID | Executables | Run as file owner |
2xxx | SGID | Files / Directories | Run as file group / inherit group |
1xxx | Sticky | Directories | Only owner can delete |
6xxx | SUID+SGID | Executables | Both effects |
7xxx | All three | — | All three effects |
Part 6: Real-World Scenarios
This is what other guides skip. These are the exact permission scenarios you’ll hit in production.
Scenario 1: Nginx/Apache Web Root — “403 Forbidden” Fix
# PROBLEM: Nginx returns 403 Forbidden after deploying your site
# DIAGNOSIS:
ls -la /var/www/html/
# Files owned by root:root — Nginx (www-data) can't read them
# FIX: Give ownership to www-data
sudo chown -R www-data:www-data /var/www/html/
# Set correct permissions
find /var/www/html -type d -exec chmod 755 {} +
find /var/www/html -type f -exec chmod 644 {} +
# Verify Nginx can now read
sudo -u www-data cat /var/www/html/index.html | head -3
Expected output:
<!DOCTYPE html>
<html lang="en">
<head>
Nginx (running as www-data) can now read the file.
Scenario 2: SSH Keys — “Permission denied (publickey)”
SSH key permissions are strict by design — SSH refuses to use keys that are too permissive.
# PROBLEM: ssh -i ~/.ssh/id_ed25519 user@server gives "Permission denied"
# DIAGNOSIS:
ls -la ~/.ssh/
# authorized_keys is 664 — too permissive (SSH rejects it)
# FIX: Correct SSH directory and key permissions
chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_ed25519 # Private key — owner read/write only
chmod 644 ~/.ssh/id_ed25519.pub # Public key — readable by others is fine
chmod 600 ~/.ssh/authorized_keys # Must be 600 or SSH ignores it
chmod 600 ~/.ssh/config # SSH config file
# Verify
ls -la ~/.ssh/
Expected output:
drwx------ 2 youruser youruser 4096 Apr 15 09:35 .
-rw------- 1 youruser youruser 399 Apr 15 09:35 authorized_keys
-rw------- 1 youruser youruser 411 Apr 15 09:35 config
-rw------- 1 youruser youruser 432 Apr 15 09:35 id_ed25519
-rw-r--r-- 1 youruser youruser 103 Apr 15 09:35 id_ed25519.pub
# Confirm SSH accepts the key now
ssh -o PasswordAuthentication=no -i ~/.ssh/id_ed25519 youruser@localhost \
"echo 'SSH key auth working'" 2>&1
Expected output:
SSH key auth working
Scenario 3: Docker Volume — “Permission denied” Inside Container
# PROBLEM: Docker container can't write to a bind-mounted volume
# The container runs as UID 1000, but the host directory is owned by root
# DIAGNOSIS:
ls -la /opt/myapp-data/
# drwxr-xr-x 2 root root 4096 Apr 15 09:40 /opt/myapp-data/
# Container user (UID 1000) has no write permission
# FIX: Give ownership to the container's UID
sudo chown -R 1000:1000 /opt/myapp-data/
# OR: Give your user ownership and add group write access
sudo chown -R youruser:docker /opt/myapp-data/
sudo chmod -R 775 /opt/myapp-data/
# Verify container can write
docker run --rm \
-v /opt/myapp-data:/data \
ubuntu:24.04 \
bash -c "touch /data/test-write && echo 'Write successful'" 2>&1
Expected output:
Write successful
Scenario 4: CI/CD Pipeline — “Permission denied” Running a Script
# PROBLEM: GitHub Actions or Gitea CI fails with "Permission denied" on a deploy script
# DIAGNOSIS: The script is not executable
ls -la deploy.sh
# -rw-r--r-- 1 youruser youruser 512 Apr 15 09:45 deploy.sh
# FIX: Add execute permission
chmod +x deploy.sh
# Or more precisely — owner executes, others read
chmod 755 deploy.sh
# Verify
ls -la deploy.sh
Expected output:
-rwxr-xr-x 1 youruser youruser 512 Apr 15 09:45 deploy.sh
# Also ensure scripts committed to Git retain execute bit
# Git tracks the execute bit — check with:
git ls-files --stage deploy.sh
Expected output (if execute bit is tracked):
100755 abc123def456 0 deploy.sh
100755 means the execute bit is stored in Git. 100644 would mean it’s not. Set it with:
git update-index --chmod=+x deploy.sh
git commit -m "Make deploy.sh executable"
Scenario 5: PostgreSQL Data Directory
# PROBLEM: PostgreSQL fails to start — "data directory has wrong ownership"
# This happens after restoring a backup or creating the directory manually
# DIAGNOSIS:
ls -la /var/lib/postgresql/
# drwxr-xr-x 3 root root 4096 Apr 15 09:50 16
# FIX: PostgreSQL data directory must be owned by the postgres user
sudo chown -R postgres:postgres /var/lib/postgresql/
# The data directory itself must be exactly 700
sudo chmod 700 /var/lib/postgresql/16/main/
# Verify
ls -la /var/lib/postgresql/
Expected output:
drwx------ 19 postgres postgres 4096 Apr 15 09:50 16
# Restart PostgreSQL
sudo systemctl restart postgresql
sudo systemctl status postgresql --no-pager | grep "Active:"
Expected output:
Active: active (running) since Tue 2026-04-15 09:51:00 UTC; 2s ago
Scenario 6: Shared Development Directory with SetGID
# GOAL: A /opt/team-project directory where everyone in the 'developers' group
# can read, write, and create files — and all new files automatically
# belong to the 'developers' group (not each person's primary group)
# Create the directory
sudo mkdir -p /opt/team-project
# Set ownership
sudo chown root:developers /opt/team-project
# Set permissions: owner rwx, group rwx, others ---
# The 2 prefix sets SGID (new files inherit the group)
sudo chmod 2775 /opt/team-project
# Verify
ls -la /opt/ | grep team-project
Expected output:
drwxrwsr-x 2 root developers 4096 Apr 15 09:55 team-project
# Test: Create a file as youruser — it should get 'developers' group
touch /opt/team-project/shared-file.txt
ls -la /opt/team-project/
Expected output:
-rw-rw-r-- 1 youruser developers 0 Apr 15 09:56 shared-file.txt
developers group assigned automatically — no manual chgrp needed.
Part 7: Access Control Lists (ACLs) — Beyond the Basic Model
The standard rwx model only supports one user and one group per file. ACLs allow you to grant permissions to additional users and groups without changing ownership.
# Install ACL tools (usually pre-installed on Ubuntu 24.04)
sudo apt-get install -y acl
# Check if ACLs are supported on your filesystem
mount | grep "acl\|ext4\|xfs"
Expected output:
/dev/sda1 on / type ext4 (rw,relatime)
ext4 and xfs both support ACLs on Ubuntu 24.04. ACLs are enabled by default.
# Grant read access to user 'bob' on a file you own — without changing ownership
setfacl -m u:bob:r-- secretfile.txt
# Grant read+write access to group 'contractors' on a directory
setfacl -m g:contractors:rw- /opt/project/
# View current ACLs
getfacl secretfile.txt
Expected output:
# file: secretfile.txt
# owner: youruser
# group: youruser
user::rw-
user:bob:r--
group::r--
mask::r--
other::---
# Remove an ACL entry
setfacl -x u:bob secretfile.txt
# Remove all ACLs (revert to standard permissions)
setfacl -b secretfile.txt
# Set default ACLs on a directory (applies to all new files created inside)
setfacl -d -m g:developers:rw- /opt/project/
# Verify default ACLs
getfacl /opt/project/
Expected output:
# file: project/
# owner: youruser
# group: developers
user::rwx
group::rwx
other::r-x
default:user::rwx
default:group::rwx
default:group:developers:rw-
default:other::r-x
Note the + at the end of permission strings when ACLs are present:
ls -la /opt/project/
Expected output:
drwxrwxr-x+ 2 youruser developers 4096 Apr 15 10:00 project/
The + indicates ACLs are active on this directory.
Part 8: Finding and Auditing Permissions
Security audits require finding files with specific or dangerous permissions.
# Find all world-writable files (potential security risk)
sudo find /var/www -type f -perm -002 2>/dev/null
# Find all SUID files on the system (known risk — audit regularly)
sudo find / -perm -4000 -type f 2>/dev/null | sort
Expected output (standard Ubuntu 24.04 SUID files):
/usr/bin/chfn
/usr/bin/chsh
/usr/bin/gpasswd
/usr/bin/mount
/usr/bin/newgrp
/usr/bin/passwd
/usr/bin/su
/usr/bin/sudo
/usr/bin/umount
/usr/lib/openssh/ssh-keysign
/usr/sbin/pam_extrausers_chkpwd
/usr/sbin/unix_chkpwd
Any file in this list that you don’t recognise should be investigated — SUID files are a common privilege escalation target.
# Find files not owned by any existing user (orphaned files — security concern)
sudo find / -nouser -type f 2>/dev/null | head -10
# Find directories writable by others but not world-writable
sudo find /etc -perm -022 -not -perm -002 -type f 2>/dev/null | head -10
# Check permissions on sensitive files that must be exact
stat -c "%a %n" /etc/passwd /etc/shadow /etc/sudoers /etc/ssh/sshd_config
Expected output (correct permissions for sensitive files):
644 /etc/passwd
640 /etc/shadow
440 /etc/sudoers
600 /etc/ssh/sshd_config
If any of these differ from the expected values, your system may have been misconfigured or compromised.
Part 9: The Sovereignty Layer — Quick Reference Audit
Verify the permissions on the most security-sensitive locations on a sovereign Ubuntu 24.04 server:
echo "=== SOVEREIGN PERMISSIONS AUDIT ==="
echo ""
echo "[ SSH Keys ]"
stat -c "%a %n" ~/.ssh/authorized_keys ~/.ssh/id_ed25519 2>/dev/null | \
awk '{
if ($1 == "600") print " ✓ " $2 " (" $1 ")"
else print " ✗ " $2 " (" $1 ") — SHOULD BE 600"
}'
echo ""
echo "[ SSH Directory ]"
stat -c "%a %n" ~/.ssh 2>/dev/null | \
awk '{
if ($1 == "700") print " ✓ " $2 " (" $1 ")"
else print " ✗ " $2 " (" $1 ") — SHOULD BE 700"
}'
echo ""
echo "[ Sensitive System Files ]"
for f in /etc/shadow /etc/sudoers /etc/ssh/sshd_config; do
perm=$(stat -c "%a" $f 2>/dev/null)
case "$f" in
/etc/shadow) expected="640" ;;
/etc/sudoers) expected="440" ;;
/etc/ssh/sshd_config) expected="600" ;;
esac
if [ "$perm" = "$expected" ]; then
echo " ✓ $f ($perm)"
else
echo " ✗ $f ($perm) — SHOULD BE $expected"
fi
done
echo ""
echo "[ SUID File Count ]"
count=$(sudo find / -perm -4000 -type f 2>/dev/null | wc -l)
echo " $count SUID files found (run: sudo find / -perm -4000 -type f to review)"
echo ""
echo "[ World-Writable Files in /etc ]"
count=$(sudo find /etc -perm -002 -type f 2>/dev/null | wc -l)
if [ "$count" = "0" ]; then
echo " ✓ No world-writable files in /etc"
else
echo " ✗ $count world-writable files found in /etc — investigate"
sudo find /etc -perm -002 -type f 2>/dev/null
fi
Expected output on a clean Ubuntu 24.04 server:
=== SOVEREIGN PERMISSIONS AUDIT ===
[ SSH Keys ]
✓ /home/youruser/.ssh/authorized_keys (600)
✓ /home/youruser/.ssh/id_ed25519 (600)
[ SSH Directory ]
✓ /home/youruser/.ssh (700)
[ Sensitive System Files ]
✓ /etc/shadow (640)
✓ /etc/sudoers (440)
✓ /etc/ssh/sshd_config (600)
[ SUID File Count ]
12 SUID files found (run: sudo find / -perm -4000 -type f to review)
[ World-Writable Files in /etc ]
✓ No world-writable files in /etc
All checks green — your permission baseline is sovereign.
Quick Reference: chmod Values Cheat Sheet
VALUE SYMBOLIC USE CASE
─────────────────────────────────────────────────────────────────────
400 r-------- Read-only secrets (SSL private keys, .pem files)
444 r--r--r-- Read-only for everyone (public certs, docs)
600 rw------- Private files (SSH keys, .env files, DB passwords)
640 rw-r----- Config files readable by group only
644 rw-r--r-- Standard files (HTML, CSS, PHP, Python scripts as data)
664 rw-rw-r-- Group-collaborative files
700 rwx------ Private executables, private directories
710 rwx--x--- Directory: owner full, group can traverse only
750 rwxr-x--- Group-accessible directories
755 rwxr-xr-x Standard directories, public scripts, executables
770 rwxrwx--- Group-collaborative directories
775 rwxrwxr-x Group-writable, world-readable directories
777 rwxrwxrwx NEVER USE IN PRODUCTION — security vulnerability
SPECIAL BITS
─────────────────────────────────────────────────────────────────────
4755 rwsr-xr-x SUID executable (runs as file owner)
2755 rwxr-sr-x SGID directory (new files inherit group)
1777 rwxrwxrwt Sticky world-writable dir (/tmp pattern)
2775 rwxrwsr-x SGID + group write (shared project dirs)
Troubleshooting
Permission denied — but permissions look correct
Cause: The directory containing the file lacks execute permission. Execute on a directory means “allow traversal” — you need execute on every directory in the path.
Fix:
# Check every directory in the path, not just the file
namei -l /path/to/your/file
# Example: if /var/www is drwx------, Nginx can't even reach /var/www/html/
# Fix the parent directory too:
sudo chmod 755 /var/www
Expected output of namei:
f: /var/www/html/index.html
d /
d var
d www ← check this has execute (x) for www-data
d html
- index.html
chmod: changing permissions of 'file': Operation not permitted
Cause: You don’t own the file — even if you have write permission on the directory, only the file’s owner and root can change its permissions. Fix:
# Option 1: Use sudo
sudo chmod 644 file
# Option 2: Take ownership first (if you're authorised to)
sudo chown youruser:youruser file
chmod 644 file
Files in /var/www revert to wrong permissions after deployment
Cause: Your deployment script (rsync, git pull, scp) copies files with source permissions, overwriting your carefully set server permissions. Fix:
# Add this to the end of your deploy script
find /var/www/html -type d -exec chmod 755 {} +
find /var/www/html -type f -exec chmod 644 {} +
sudo chown -R www-data:www-data /var/www/html/
sudo: sorry, you must have a tty to run sudo
Cause: Not directly a permissions issue, but related — sudo configuration requires a TTY. Fix:
# Check /etc/sudoers
sudo grep -n "requiretty" /etc/sudoers
# If "Defaults requiretty" is present, add an exception:
echo "Defaults:youruser !requiretty" | sudo tee -a /etc/sudoers.d/youruser
umask changes don’t persist after logout
Cause: The umask was set in the current shell session only. Fix:
# Add to the correct startup file based on your shell
echo "umask 022" >> ~/.bashrc # for bash
echo "umask 022" >> ~/.zshrc # for zsh
# Source immediately
source ~/.bashrc
Conclusion
Linux file permissions are a three-layer system: the permission string (what operations are allowed), ownership (who the permissions apply to), and umask (what defaults new files get). chmod changes the first, chown changes the second, umask configures the third. The special bits — SUID, SGID, and sticky — extend the model for specific use cases like shared directories and world-writable upload folders. ACLs add fine-grained per-user and per-group control when the basic model isn’t sufficient. The sovereignty audit script confirms your most sensitive files are correctly locked down.
The natural next step from here is Linux Users & Groups Management — understanding how users and groups are created and managed is the foundation for making ownership changes with chown meaningful in production.
People Also Ask: Linux File Permissions FAQ
What is the difference between chmod 755 and chmod +x?
chmod 755 sets permissions to exactly rwxr-xr-x — owner gets read/write/execute, group and others get read/execute. It replaces whatever permissions existed before. chmod +x adds execute permission for all three groups (owner, group, others) without changing read or write permissions. If a file is rw-r--r-- (644) and you run chmod +x, it becomes rwxr-xr-x (755). If you want only the owner to execute, use chmod u+x — this adds execute for the owner only, producing rwxr--r-- (744) from 644. Use numeric mode when you know the exact permission set you want. Use symbolic mode when you want to add or remove a specific permission without knowing the current state.
Why does chmod 777 work but is bad for production?
chmod 777 (rwxrwxrwx) makes a file or directory readable, writable, and executable by every user on the system — including untrusted processes and compromised applications. On a web server, a world-writable file in the web root means any PHP, Python, or Node.js process (including exploited application code) can overwrite it. For directories, 777 means any process can create, modify, or delete files inside — a classic path to privilege escalation. The only legitimate use for 777 is /tmp-style shared upload directories, and even then it should always be combined with the sticky bit (1777) to prevent cross-user file deletion. For development, 755 for directories and 644 for files are safe defaults that work with all standard web server setups.
How do I fix permissions recursively for a web server deployment?
The correct approach uses find with two separate commands rather than a single chmod -R:
# Fix directories (need execute = traversal)
find /var/www/html -type d -exec chmod 755 {} +
# Fix files (should not have execute unless they're scripts)
find /var/www/html -type f -exec chmod 644 {} +
# Fix ownership (Nginx/Apache need to read as www-data)
sudo chown -R www-data:www-data /var/www/html/
The reason chmod -R 755 alone is wrong: it sets execute on every file, including HTML, CSS, PHP, and image files — files that never need execute permission. This inflates the attack surface unnecessarily. The find-based approach applies 755 only to directories and 644 only to files.
What permissions should SSH keys have?
SSH enforces strict permission requirements and silently refuses to use keys with permissions that are too loose. Required permissions on Ubuntu 24.04: private key file (id_ed25519, id_rsa) must be 600 (rw-------); public key file (id_ed25519.pub) can be 644; authorized_keys file must be 600; the .ssh directory must be 700. If any of these are wrong, SSH logs "WARNING: UNPROTECTED PRIVATE KEY FILE!" or silently falls back to password authentication. Fix all at once: chmod 700 ~/.ssh && chmod 600 ~/.ssh/id_ed25519 ~/.ssh/authorized_keys && chmod 644 ~/.ssh/id_ed25519.pub.
*Tested on: Ubuntu 24.04 LTS (Hetzner CX22 VPS), Ubuntu 24.04 LTS (Raspberry Pi 5, ARM64). All commands verified against coreutils 9.4. Last verified: April 15, 2026. *