Linux File Permissions: chmod and chown Explained in Practice

Understand rwx permissions, ownership, and how to use chmod and chown on Linux to fix Permission denied on VPS servers the right way.

You deployed your application to a VPS, ran the command, and the terminal returned Permission denied. Or nginx returned 403 even though the file is right there, visible in ls. This error is the gateway every developer hits when they leave the desktop environment and meet a multi-user system for the first time. The good news: the Linux permission model is simple and predictable once you understand three things — who owns the file, who belongs to the group, and what each bit means.

This guide is for anyone who has seen chmod 777 in some tutorial without understanding what it actually does, or who needs to fix file access without throwing the whole server open. We will cover how to read ls -l output, what rwx means for files and directories, and how to use chmod (change permission) and chown (change owner) in real web server scenarios.

Estimated time: 20 to 30 minutes to read, run the commands, and lock in the mental model. You walk away able to diagnose any permission error on your own.

Prerequisites

Prerequisites

You need a Linux VPS (Ubuntu 22.04+, Debian 12+, or any equivalent distribution) with SSH access and sudo privileges. The examples were tested on Ubuntu 24.04 LTS but work on any modern distribution. Recommended: a non-root user to practice the commands without risk.

Tested system Ubuntu 24.04 LTS
Suggested user non-root + sudo
Practice directory ~/perms-test

How Linux represents permissions

Every file on Linux has three associated entities: the owner (user/owner), the group, and the rest of the world (others). For each of those three entities, there are three permission bits: r (read), w (write), and x (execute). Nine bits in total — three for owner, three for group, three for world.

Run ls -l in any directory to see this rendered:

ls -l /etc/hosts

The output looks like:

-rw-r--r-- 1 root root 220 May 14 10:22 /etc/hosts

Decoding from left to right:

  • - in the first character indicates a regular file. A directory would show d, a symbolic link l.
  • rw- (characters 2-4): owner permissions. Reads and writes, does not execute.
  • r-- (characters 5-7): group permissions. Read only.
  • r-- (characters 8-10): world permissions. Read only.
  • 1: number of hard links.
  • root root: owner and group, respectively.
  • 220: size in bytes.
  • May 14 10:22: last modification.
  • /etc/hosts: file name.

Octal notation: the shortcut everyone uses

Each set of three bits (rwx) can be represented by a digit from 0 to 7. Each bit has a weight:

  • r = 4
  • w = 2
  • x = 1

Sum the weights of the enabled bits to get the digit. Practical examples:

OctalSymbolMeaning
7rwxReads, writes, executes
6rw-Reads and writes
5r-xReads and executes
4r—Read only
0---No permission

Since each file has three sets (owner, group, world), the complete permission becomes a three-digit number. 644 is rw-r--r--. 755 is rwxr-xr-x. 600 is rw-------. This is the notation you will use 95% of the time with chmod.

The magic meaning of x on directories

On a file, x means execute as a program. On a directory, x means enter the directory (use cd) and access files by name. Without x on a directory, you cannot list its contents even with r, and you cannot open files inside even if you know the name.

That is why the classic default is 755 for directories (everyone can enter and list) and 644 for files (everyone reads, only the owner writes, no one executes). Inverting this breaks access in strange ways — files that exist but “do not exist” for another user.

chmod: changing permissions in practice

chmod accepts two notations: octal (the one shown above) and symbolic (u+x, g-w, o=r). Octal is more common in production because it is deterministic — you define the final state, not the transition.

01

Create a practice directory and a test file:

mkdir ~/perms-test
cd ~/perms-test
echo "test content" > file.txt
ls -l file.txt

The output should be something like -rw-rw-r-- (644 or 664 depending on your shell’s umask). We are going to manipulate this.

02

Apply a restrictive permission in octal — only the owner reads and writes:

chmod 600 file.txt
ls -l file.txt

The output now shows -rw-------. Nobody besides the owner can even read it. This is the standard for an SSH private key (~/.ssh/id_rsa) and any file with credentials.

03

Go back to public read but keep writes restricted to the owner:

chmod 644 file.txt
ls -l file.txt

-rw-r--r-- is the default state for a regular file on a web server: owner writes, everyone reads.

04

Use symbolic notation to add execute only for the owner without touching the rest:

chmod u+x file.txt
ls -l file.txt

Now it shows -rwxr--r--. Symbolic notation is useful when you want to modify a specific bit without having to remember the complete state. Syntax: [ugoa][+-=][rwx]. u=user, g=group, o=others, a=all.

Never use chmod 777 in production

777 gives rwx to everyone — including any compromised process on the machine. If a web application gets hit by an RCE and the attacker can run commands as www-data, 777 files let them overwrite your application, plant a backdoor, or wipe everything. The “it worked with 777” symptom almost always means ownership is wrong — fix it with chown, do not loosen the permission.

Recursive chmod: -R with care

The -R flag applies the change to everything inside a directory. Useful in specific situations, dangerous in others:

chmod -R 755 ~/perms-test

The problem: this adds x on every file, including ones that should not be executable. On /var/www/site, you want 644 for .html, .css, .php files and 755 only for directories. The correct form separates by type:

find /var/www/site -type f -exec chmod 644 {} \;
find /var/www/site -type d -exec chmod 755 {} \;

Or, more efficient on large directories:

find /var/www/site -type f -print0 | xargs -0 chmod 644
find /var/www/site -type d -print0 | xargs -0 chmod 755

chown: changing owner and group

chown changes the file’s owner. Basic syntax:

chown new_owner file
chown new_owner:new_group file
chown :new_group file

Only root (or a user with sudo) can change ownership to another user. A regular user cannot give their file to someone else — this prevents attacks where someone dumps files into another user’s quota.

05

Look at the most common production scenario: a web application running as www-data needs to read/write in /var/www/site. Assign owner and group at once:

sudo chown -R www-data:www-data /var/www/site

Here -R is appropriate because you want everything inside to belong to the web service user. This is the step that resolves 80% of Permission denied errors during deploy.

06

Confirm the change:

ls -l /var/www/site | head

The owner and group columns should show www-data www-data on every line.

Combine chown and chmod in your deploy

In a typical deploy script, the order is: copy files, adjust ownership to the service user, adjust permissions with find by type. Hard-code these commands in your provisioning script or Ansible playbook — do not rely on “I’ll remember to run it manually”. A forgotten permission is a common source of intermittent failures in staging environments.

Verification: confirm it is correct

After messing with permissions, validate three things: the current state of the bits, ownership, and that the application can actually read.

ls -ld /var/www/site
ls -l /var/www/site | head
sudo -u www-data cat /var/www/site/index.html

ls -ld shows the permission of the directory itself (without -d, it lists contents). sudo -u www-data runs the command as if it were the web service user — if this works without error, your application will too. This test is more reliable than just looking at ls -l because it covers SELinux/AppArmor if they are active.

To verify the selective recursive chmod worked, count files by permission:

find /var/www/site -type f -perm 644 | wc -l
find /var/www/site -type f ! -perm 644

The second line should return nothing. If it does, those are files with a permission other than 644 — investigate case by case before applying a mass fix.

Troubleshooting

”Permission denied” even after chmod 755

Check ownership with ls -l. If the file belongs to root:root and the process runs as www-data, 755 only helps the other bit (last digit). But on some systems with SELinux active, extra security contexts block access regardless of Unix permission. Check with ls -Z and use restorecon or chcon if needed.

”Operation not permitted” when running chown

You are not running as root. Even if you own the file, transferring it to another user requires sudo. Add sudo before the command or run as root.

Parent directory without execute permission blocks access

Classic scenario: /home/user/public_html/site/index.html has 644, but nginx returns 403. The problem might be in /home/user — if that folder does not have x for group/world, nginx (running as www-data) cannot traverse the path to the file. Solution: ensure x on every directory in the path, or use a /var/www structure where permissions are designed for web access from the start.

Next steps

Basic permissions cover most scenarios, but there are advanced layers worth exploring as your setup gets more complex:

  • ACLs (Access Control Lists): allow per-user permissions beyond the owner/group/world trio. Commands getfacl and setfacl. Useful when two services need different access to the same file.
  • Special bits: setuid (4000), setgid (2000), and sticky bit (1000). The sticky bit on /tmp is the classic example — it prevents user A from deleting user B’s files.
  • umask: the value that defines the default permission for new files. Configure it in /etc/profile or the user’s shell rc so files are created with the right permission from the start.
  • Capabilities (setcap): a modern alternative to setuid for granting specific privileges (e.g. binding to ports < 1024) without giving full root.

If you are taking an application to production and need real isolation, root access, and dedicated SSH to practice these commands without affecting production, a Hostini VPS delivers that with Ubuntu 24.04 LTS pre-installed and snapshots so you can revert any experiment.

Frequently asked questions

What is the difference between chmod 755 and chmod 775?

755 gives rwx to the owner, r-x to the group and r-x to the world (5 = 4+1, read and execute). 775 gives rwx to the group as well (7 = 4+2+1). Use 755 for application code where only the owner writes; 775 for directories shared between users in the same group, like uploads managed by both an owner and a web process.

When should you use chmod -R and when should you avoid it?

Use -R (recursive) only when you know every file in the directory must share the same permission — usually log or cache folders. Avoid it on /var/www as a whole: files need 644 and directories need 755, and -R 755 leaves files executable for no reason. Use find with -type f and -type d separately.

Why does my web application show Permission denied even with chmod 777?

777 solves the permission bit but not ownership. If the file belongs to root and the web process runs as www-data, reading works (other bit), but some systems block access via SELinux/AppArmor or PHP's open_basedir. Check ownership with ls -l and the service logs — 777 is almost always the wrong fix.

What is the setuid bit (4000) and when does it make sense to use it?

Setuid makes a binary run with the owner's privileges instead of the user who executed it. It is used in /usr/bin/sudo and /usr/bin/passwd. In your own code it is a privilege escalation vector — never use setuid on shell scripts and almost never on custom binaries; prefer sudoers or capabilities (setcap).

What is the difference between chown and chgrp?

chown changes the owner (and optionally the group, with the user:group syntax). chgrp only changes the group. chown www-data:www-data file is equivalent to chown www-data file followed by chgrp www-data file. Use chown with the combined syntax — fewer commands and fewer mistakes.

Do different permissions for files and directories make sense?

Yes. On a directory, the x bit means enter (cd), not execute. Without x on a directory you cannot read its contents even with r. Sensible default: 644 for files (rw-r--r--) and 755 for directories (rwxr-xr-x). Executable files get 755 explicitly; never 777.

Topics:
Next steps Ryzen cloud with NVMe storage and always-on DDoS protection.Go live on a Hostini VPS →
Was this tutorial helpful?
Chat on WhatsApp