Guides12 min read

Unix File Permissions Explained: chmod, chown, and Beyond

toolsto.dev
Unix File Permissions Explained: chmod, chown, and Beyond

You SSH into a server, deploy your app, and nothing works. The logs say "permission denied." You run chmod 777 everything and it works — but now your senior engineer is asking why the entire production server is world-writable.

Unix file permissions are one of those fundamentals that every developer encounters but few truly understand. This guide covers the permission system from the ground up — the octal notation, the symbolic format, special bits, and the real-world scenarios where getting permissions wrong causes outages, security vulnerabilities, or both.

The Permission Model

Every file and directory on a Unix-like system (Linux, macOS, BSD) has three sets of permissions for three categories of users:

User categories:

  • Owner (u) — the user who owns the file
  • Group (g) — users in the file's group
  • Others (o) — everyone else

Permission types:

  • Read (r) — view file contents, list directory contents
  • Write (w) — modify file, create/delete files in directory
  • Execute (x) — run file as program, access directory contents

This gives you a 3×3 matrix — 9 permission bits total.

Reading ls -l Output

When you run ls -l, you see something like this:

-rwxr-xr-- 1 deploy www-data 4096 Jan 15 09:30 app.js

Let's break that first column apart:

-  rwx  r-x  r--
│  │    │    │
│  │    │    └── Others: read only
│  │    └─────── Group:  read + execute
│  └──────────── Owner:  read + write + execute
└─────────────── File type: - (regular file)

The file type character can be - (regular file), d (directory), l (symbolic link), b (block device), c (character device), p (pipe), or s (socket).

Octal (Numeric) Notation

Each permission has a numeric value:

PermissionValue
Read (r)4
Write (w)2
Execute (x)1
None (-)0

You add the values for each user category to get a three-digit number:

rwxr-xr-- = (4+2+1)(4+0+1)(4+0+0) = 754

The most common permission sets:

OctalSymbolicTypical Use
755rwxr-xr-xExecutable files, public directories
644rw-r--r--Regular files (HTML, CSS, config)
600rw-------Private files (SSH keys, .env)
700rwx------Private directories, scripts for one user
775rwxrwxr-xShared group directories
666rw-rw-rw-World-writable files (almost never correct)
777rwxrwxrwxWorld-writable + executable (security risk)

chmod: Changing Permissions

Two ways to use chmod:

Numeric Mode

chmod 755 deploy.sh    # Owner: rwx, Group: r-x, Others: r-x
chmod 600 .env         # Owner: rw-, Group: ---, Others: ---
chmod 644 index.html   # Owner: rw-, Group: r--, Others: r--

Symbolic Mode

chmod u+x script.sh        # Add execute for owner
chmod g-w config.yaml       # Remove write for group
chmod o=r public.html       # Set others to read only
chmod a+r README.md         # Add read for all (a = all)
chmod u=rwx,g=rx,o=r file   # Set all at once

The symbolic mode is more flexible for targeted changes. Instead of recalculating the entire octal number, you modify specific bits.

Recursive Changes

chmod -R 755 /var/www/html          # Everything: 755 (bad idea for files)
find /var/www -type d -exec chmod 755 {} \;  # Directories only: 755
find /var/www -type f -exec chmod 644 {} \;  # Files only: 644

The -R flag on its own sets the same permissions for both files and directories. Since files usually shouldn't be executable but directories need execute permission (to cd into them), you'll almost always want the find approach instead.

chown: Changing Ownership

chown deploy app.js           # Change owner
chown deploy:www-data app.js  # Change owner AND group
chown :www-data app.js        # Change group only
chown -R deploy:www-data /var/www/  # Recursive

Ownership matters as much as permissions. A file with 600 permissions is private — but private to the owner. If the wrong user owns it, the right user can't read it.

How Directory Permissions Work

Directory permissions are counterintuitive. They don't work like file permissions:

PermissionOn a FileOn a Directory
Read (r)View contentsList filenames with ls
Write (w)Modify contentsCreate, delete, rename files inside
Execute (x)Run as programcd into it, access files inside

The critical insight: you need execute permission on a directory to access anything inside it, even if you have read permission on the files within.

# This is a common trap:
chmod 744 /var/www          # Owner: rwx, Others: r--
# Others can list filenames but CANNOT read any files inside
# because they lack execute (x) on the directory

chmod 755 /var/www          # Now others can cd in and read files

A directory with r-- permission lets you see filenames but nothing else — no file sizes, no permissions, no access to the files themselves.

Special Permission Bits

Beyond the basic 9 bits, there are three special bits:

Setuid (4xxx)

When set on an executable, it runs as the file's owner, not the user who launched it.

chmod 4755 /usr/bin/passwd
# or
chmod u+s /usr/bin/passwd

This is why /usr/bin/passwd can modify /etc/shadow (owned by root) even when run by a regular user. The s in ls -l output replaces the owner's x:

-rwsr-xr-x 1 root root  passwd

Setgid (2xxx)

On an executable, it runs with the file's group. On a directory, new files created inside inherit the directory's group.

chmod 2775 /var/www/shared
# or
chmod g+s /var/www/shared

This is essential for shared project directories. Without setgid, every file a user creates gets their primary group, not the shared group.

Sticky Bit (1xxx)

On a directory, only a file's owner can delete or rename their files — even if others have write permission on the directory.

chmod 1777 /tmp
# or
chmod +t /tmp

This is why /tmp is drwxrwxrwt — everyone can write to it, but you can't delete someone else's files. The t replaces the others' x in ls -l output.

Real-World Permission Scenarios

Web Server Deployment

# Typical Nginx/Apache setup
chown -R deploy:www-data /var/www/myapp
find /var/www/myapp -type d -exec chmod 755 {} \;
find /var/www/myapp -type f -exec chmod 644 {} \;

# Upload directory (web server needs to write)
chmod 775 /var/www/myapp/uploads

The web server (Nginx/Apache) runs as www-data. Your deploy user owns the files. The group www-data lets the web server read everything and write to specific directories.

SSH Key Permissions

SSH is extremely strict about permissions. If they're wrong, SSH silently refuses to use the key:

chmod 700 ~/.ssh              # Directory: owner only
chmod 600 ~/.ssh/id_rsa       # Private key: owner read/write only
chmod 644 ~/.ssh/id_rsa.pub   # Public key: anyone can read
chmod 600 ~/.ssh/authorized_keys  # Auth keys: owner only
chmod 600 ~/.ssh/config       # Config: owner only

If ssh -i my_key server gives "Permissions 0644 for 'my_key' are too open," this is SSH protecting you from using a private key that others can read.

Docker and Containers

# Don't run as root in containers
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
COPY --chown=appuser:appgroup . /app
USER appuser

Container files are often owned by root. If your process runs as a non-root user (as it should), you need explicit ownership changes.

CI/CD Pipeline Scripts

# Make deploy scripts executable
chmod +x scripts/deploy.sh
chmod +x scripts/setup.sh

# Protect secrets
chmod 600 .env.production
chmod 600 secrets/*.key

A common CI failure: you write a shell script, commit it, and the pipeline fails because the file isn't executable. Git tracks the executable bit, so chmod +x followed by a commit fixes it permanently.

Common Permission Mistakes

chmod 777 on anything in production — This means any user on the system can read, write, and execute the file. It's a security audit failure. The correct fix is figuring out which user needs which permission and granting only that.

Forgetting directory execute bits — A 403 Forbidden from your web server usually means the web server user can't traverse the directory tree to reach the file. Every directory in the path needs execute permission for the web server's user or group.

Running everything as root — If your app runs as root, permissions don't matter (root bypasses them). This seems convenient until a vulnerability in your app gives an attacker root access to the entire system.

Ignoring umask — When a process creates a file, the actual permissions are requested_permissions & ~umask. With the default umask of 022, a file created with 666 gets 644. Change your umask if new files aren't getting the permissions you expect:

umask 022   # Default: new files 644, new dirs 755
umask 002   # Group-friendly: new files 664, new dirs 775
umask 077   # Private: new files 600, new dirs 700

Quick Reference

Check current permissions:

ls -la                     # List all files with permissions
stat -c '%a %U:%G %n' *   # Show octal, owner:group, name (Linux)
stat -f '%A %Su:%Sg %N' * # Same for macOS

Find permission problems:

# Find world-writable files
find / -type f -perm -002 2>/dev/null

# Find files with setuid bit
find / -type f -perm -4000 2>/dev/null

# Find files owned by nobody
find / -nouser -o -nogroup 2>/dev/null

Unix permissions are a small system with big consequences. Most deployment issues, security vulnerabilities, and mysterious "permission denied" errors come down to one of the patterns above. Understanding the model — rather than reaching for chmod 777 — is what separates a deploy that works from a deploy that works safely.

Related Tools