Vucense

Linux File Permissions Explained: chmod, chown & umask 2026 Guide

🟢Beginner

Master Linux file permissions with practical examples. chmod numeric & symbolic, chown, umask, SUID/SGID/sticky bit, ACLs, and real-world scenarios every developer hits. Ubuntu 24.04.

Noah Choi

Author

Noah Choi

Linux & Cloud Native Infrastructure Engineer

Published

Duration

Reading

19 min

Build

20 min

Linux File Permissions Explained: chmod, chown & umask 2026 Guide
Article Roadmap

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 file sets owner=rwx (7), group=r-x (5), others=r-x (5). chmod +x script.sh adds execute for everyone. chmod -R 644 /var/www sets 644 recursively.
  • chown in 60 seconds: sudo chown www-data:www-data /var/www/html gives the Nginx user ownership of the web root. sudo chown -R youruser:youruser /opt/myapp claims ownership of a whole directory.
  • umask explained: Your umask (usually 0022) is subtracted from 666 for files and 777 for directories. Result: new files get 644, new directories get 755. Change it to 077 for 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:

CharacterMeaningExample
-Regular fileconfig.txt, script.sh
dDirectory/var/www/html
lSymbolic link/usr/bin/python → python3.14
bBlock device/dev/sda
cCharacter 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:

PermissionSymbolValue
Readr4
Writew2
Executex1
No permission-0

Add the values together for each group:

CombinationCalculationResult
rwx4+2+17
rw-4+2+06
r-x4+0+15
r—4+0+04
-wx0+2+13
-w-0+2+02
—x0+0+11
---0+0+00

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

WhoMeaning
uUser (owner)
gGroup
oOthers
aAll (u+g+o)
OperatorMeaning
+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:

PrefixBitApplies toEffect
4xxxSUIDExecutablesRun as file owner
2xxxSGIDFiles / DirectoriesRun as file group / inherit group
1xxxStickyDirectoriesOnly owner can delete
6xxxSUID+SGIDExecutablesBoth effects
7xxxAll threeAll 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. *


Further Reading

All Dev Corner

Comments