32blogby StudioMitsu

chmod & chown Practical Guide: Fix Permission Errors Fast

Master chmod and chown to control Linux file permissions and ownership. Covers octal and symbolic modes, recursive changes, setuid/setgid, sticky bit, umask, and real-world patterns.

15 min read
On this page

chmod sets what actions users can perform on a file. chown sets who owns it. Together they form Linux's two-layer permission model — ownership determines identity, permissions determine capability.

In short: chmod 755 script.sh gives the owner full access and everyone else read+execute. chown deploy:www-data /var/www changes who owns a file and which group it belongs to. Add -R for recursive changes. Use umask to control defaults for new files.

How Linux Permissions Actually Work

Every file and directory on Linux has three metadata components:

  1. Owner — the user who owns the file
  2. Group — the group associated with the file
  3. Permissions — what each category (owner, group, others) can do

Run ls -l and you'll see all three:

bash
ls -l deploy.sh
# -rwxr-xr-- 1 furuya developers 2048 Mar 23 10:00 deploy.sh

Breaking down -rwxr-xr--:

PositionCharactersMeaning
1-File type (- = file, d = directory, l = symlink)
2-4rwxOwner permissions (read, write, execute)
5-7r-xGroup permissions (read, execute)
8-10r--Others permissions (read only)

The furuya is the owner, developers is the group. Anyone in the developers group gets group permissions. Everyone else gets "others" permissions.

Why permission errors happen

The most common permission error I see on servers:

bash
bash: ./deploy.sh: Permission denied

This usually means one of three things:

  1. The execute bit isn't set (chmod +x deploy.sh)
  2. You're not the owner and group/others don't have the right permissions
  3. A parent directory blocks traversal (missing x on a directory in the path)

On a 32blog staging server, I spent 20 minutes debugging why Nginx couldn't serve static files. The files were 644 (correct), but the /var/www/32blog directory was 700 — Nginx's www-data user couldn't even enter the directory. Changed it to 755 and everything worked.

chmod: Setting Permissions with Numbers and Symbols

chmod has two syntax styles. Pick whichever clicks for you — they do exactly the same thing.

Octal (numeric) mode

Each permission maps to a number: read = 4, write = 2, execute = 1. Add them up per category.

OctalBinaryPermissionsMeaning
7111rwxFull access
6110rw-Read + write
5101r-xRead + execute
4100r--Read only
0000---No access
bash
# Owner: rwx, Group: r-x, Others: r-x
chmod 755 deploy.sh

# Owner: rw-, Group: r--, Others: r--
chmod 644 config.yaml

# Owner: rwx, Group: ---, Others: ---
chmod 700 ~/.ssh

# Owner: rw-, Group: rw-, Others: ---
chmod 660 shared-log.txt

Symbolic mode

Instead of numbers, use letters. The format is [who][operator][permissions]:

  • Who: u (user/owner), g (group), o (others), a (all)
  • Operator: + (add), - (remove), = (set exactly)
  • Permissions: r (read), w (write), x (execute)
bash
# Add execute permission for everyone
chmod a+x deploy.sh

# Remove write permission for group and others
chmod go-w config.yaml

# Set exact permissions for owner, remove all for others
chmod u=rwx,go=rx deploy.sh    # Same as chmod 755

# Add write permission for the group only
chmod g+w shared-project/

Symbolic mode shines when you want to change one thing without affecting the rest. chmod g+w file adds group write without touching anything else. With octal mode you'd need to know the current permissions first.

Recursive changes with -R

bash
# Set directories to 755 and files to 644 — the web server standard
chmod -R 755 /var/www/32blog/

# But wait — that makes files executable too. Better approach:
find /var/www/32blog -type d -exec chmod 755 {} +
find /var/www/32blog -type f -exec chmod 644 {} +

Useful chmod patterns

bash
# Make a script executable
chmod +x scripts/deploy.sh

# Protect your SSH keys (SSH refuses to use keys with loose permissions)
chmod 600 ~/.ssh/id_ed25519
chmod 644 ~/.ssh/id_ed25519.pub
chmod 700 ~/.ssh

# Web application files
find /var/www/app -type d -exec chmod 755 {} +
find /var/www/app -type f -exec chmod 644 {} +

# Shared directory where group members can write
chmod 775 /opt/shared-project/

# Remove all permissions for others
chmod o= sensitive-data/

chown: Changing File Ownership

chown changes who owns a file and which group it belongs to. Unlike chmod, you typically need sudo to run chown — you can't give away your files to another user without root privileges.

Basic syntax

bash
# Change owner only
sudo chown deploy deploy.sh

# Change owner and group
sudo chown deploy:www-data /var/www/32blog/

# Change group only (colon before group name, no owner)
sudo chown :www-data /var/www/32blog/index.html

# Same as above but using chgrp
sudo chgrp www-data /var/www/32blog/index.html

The user:group syntax handles the most common case in one command. When I set up a new 32blog deploy, the first thing I run is:

bash
sudo chown -R deploy:www-data /var/www/32blog

This gives the deploy user ownership (so deployment scripts can write files) and the www-data group access (so Nginx can read them).

Recursive ownership changes

bash
# Change owner and group for an entire directory tree
sudo chown -R deploy:www-data /var/www/32blog/

# Only change the group recursively
sudo chown -R :developers /opt/project/

# Verbose mode — see what's changing
sudo chown -Rv deploy:www-data /var/www/32blog/

Copying ownership from another file

bash
# Make target.conf match the ownership of reference.conf
sudo chown --reference=reference.conf target.conf

This is handy when you're creating config files that need to match existing ones.

By default, chown follows symbolic links and changes the target file. Use -h to change the link itself:

bash
# Changes the symlink target's ownership
sudo chown deploy:www-data /var/www/current

# Changes the symlink itself
sudo chown -h deploy:www-data /var/www/current

Special Permissions: setuid, setgid, and Sticky Bit

Beyond the basic rwx, Linux has three special permission bits. You'll see them in the field and need to understand them for security audits.

setuid (Set User ID)

When set on an executable, it runs as the file owner instead of the user who launched it. The classic example:

bash
ls -l /usr/bin/passwd
# -rwsr-xr-x 1 root root 68208 Mar 23 10:00 /usr/bin/passwd

That s in the owner's execute position means setuid is active. passwd runs as root even when a normal user executes it — because it needs to write to /etc/shadow. The Linux man page for chmod documents these special bits in detail.

bash
# Set the setuid bit
chmod u+s /usr/local/bin/my-tool

# Or with octal (4 prefix)
chmod 4755 /usr/local/bin/my-tool

# Remove setuid
chmod u-s /usr/local/bin/my-tool

setgid (Set Group ID)

On executables, it works like setuid but for the group. More commonly used on directories — files created inside inherit the directory's group:

bash
# Create a shared project directory
sudo mkdir /opt/team-project
sudo chown :developers /opt/team-project
sudo chmod 2775 /opt/team-project

# Now any file created inside gets the "developers" group
touch /opt/team-project/notes.txt
ls -l /opt/team-project/notes.txt
# -rw-rw-r-- 1 furuya developers 0 Mar 23 10:00 notes.txt

Without setgid, the file would get the creating user's primary group, breaking shared access.

bash
# Verify setgid on a directory
ls -ld /opt/team-project
# drwxrwsr-x 2 root developers 4096 Mar 23 10:00 /opt/team-project
#      ^ the 's' means setgid is active

Sticky bit

Prevents users from deleting files they don't own, even if they have write permission on the directory. /tmp is the classic example:

bash
ls -ld /tmp
# drwxrwxrwt 15 root root 4096 Mar 23 10:00 /tmp
#          ^ the 't' means sticky bit is active

Everyone can write to /tmp, but you can only delete your own files.

bash
# Set sticky bit
chmod +t /opt/shared-uploads/

# Or with octal (1 prefix)
chmod 1777 /tmp

# Remove sticky bit
chmod -t /opt/shared-uploads/

Quick reference table

BitOctal prefixSymbolicOn filesOn directories
setuid4u+sRuns as file owner(no effect)
setgid2g+sRuns as file groupNew files inherit directory group
sticky1+t(no effect)Only owners can delete their files

Finding special permission files

Security audits often require finding setuid/setgid binaries:

bash
# Find all setuid files
find / -perm -4000 -type f 2>/dev/null

# Find all setgid files
find / -perm -2000 -type f 2>/dev/null

# Find both
find / -perm /6000 -type f 2>/dev/null

I run these on every server I set up for 32blog infrastructure. Unexpected setuid binaries are a red flag.

umask: Controlling Default Permissions

Every time you create a file or directory, the system applies a umask — a mask that removes permissions from the defaults. The defaults are 666 for files and 777 for directories.

bash
# Check your current umask
umask
# 0022

# With symbolic output
umask -S
# u=rwx,g=rx,o=rx

How umask calculation works

The umask subtracts permissions from the default:

DefaultUmask 022Result
Files666 (rw-rw-rw-)022644 (rw-r--r--)
Directories777 (rwxrwxrwx)022755 (rwxr-xr-x)

Common umask values:

UmaskFile resultDir resultUse case
022644755Default for most systems
002664775Shared group development
077600700Maximum privacy
027640750Balanced security
bash
# Set umask for current session
umask 027

# Test it
touch test-file
mkdir test-dir
ls -l test-file    # -rw-r----- (640)
ls -ld test-dir    # drwxr-x--- (750)

Making umask persistent

The umask resets with each new shell session. To make it permanent, add it to your shell profile:

bash
# For bash
echo "umask 027" >> ~/.bashrc

# For zsh
echo "umask 027" >> ~/.zshrc

# System-wide (all users)
echo "umask 027" >> /etc/profile

For system services, the shell profile umask doesn't apply. Set it in the systemd unit file with UMask=027 in the [Service] section instead.

Real-World Permission Patterns

Here are the patterns I actually use on production servers and development machines.

Web server setup (Nginx + deployment user)

bash
# Create the web root
sudo mkdir -p /var/www/32blog
sudo chown -R deploy:www-data /var/www/32blog

# Set permissions
find /var/www/32blog -type d -exec chmod 2755 {} +
find /var/www/32blog -type f -exec chmod 644 {} +

# Directories where the app needs to write (uploads, cache)
chmod 2775 /var/www/32blog/.next/cache

The 2755 on directories sets the setgid bit — new files created by the deploy process will inherit the www-data group, so Nginx can always read them.

SSH key permissions

SSH is strict about permissions and will refuse to use keys if they're too open:

bash
chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_ed25519         # Private key
chmod 644 ~/.ssh/id_ed25519.pub     # Public key
chmod 600 ~/.ssh/authorized_keys
chmod 600 ~/.ssh/config

If you ever see WARNING: UNPROTECTED PRIVATE KEY FILE!, this is why.

Shared development directory

bash
# Create shared project space
sudo mkdir -p /opt/projects/32blog
sudo chown -R :developers /opt/projects/32blog
sudo chmod -R 2775 /opt/projects/32blog

# Ensure all team members are in the group
sudo usermod -aG developers furuya
sudo usermod -aG developers colleague

The 2775 + setgid combo means: everyone in developers can read and write, and new files automatically belong to the developers group.

Docker and container permissions

bash
# Fix common Docker volume permission issues
# When container runs as UID 1000 but host files are owned by root
sudo chown -R 1000:1000 ./docker-volumes/

# Dockerfile pattern for non-root user
# RUN useradd -r -u 1000 appuser && \
#     chown -R appuser:appuser /app

Quick fix script for a Next.js deployment

bash
#!/bin/bash
# fix-permissions.sh — Run after deploying 32blog
set -euo pipefail

WEBROOT="/var/www/32blog"
OWNER="deploy"
GROUP="www-data"

echo "Fixing ownership..."
sudo chown -R "${OWNER}:${GROUP}" "${WEBROOT}"

echo "Setting directory permissions..."
find "${WEBROOT}" -type d -exec chmod 2755 {} +

echo "Setting file permissions..."
find "${WEBROOT}" -type f -exec chmod 644 {} +

echo "Making scripts executable..."
find "${WEBROOT}/scripts" -name "*.sh" -exec chmod 755 {} +

echo "Done. Permissions fixed for ${WEBROOT}"

Auditing permissions

bash
# Find world-writable files (security risk)
find /var/www -perm -o=w -type f 2>/dev/null

# Find files without an owner (orphaned — usually from deleted users)
find / -nouser -o -nogroup 2>/dev/null

# Find files changed in the last 24 hours (incident response)
find /var/www -mtime -1 -type f -ls

# List setuid/setgid binaries
find / -perm /6000 -type f -ls 2>/dev/null

FAQ

What does chmod 777 actually do, and why should I avoid it?

chmod 777 gives every user on the system full read, write, and execute access. It completely bypasses Linux's permission model. On a web server, any compromised process could modify your application code. The only legitimate use is /tmp (protected by the sticky bit). Instead of 777, figure out which user or group needs access and set permissions accordingly — chmod 775 with proper group ownership solves most "permission denied" problems.

What's the difference between chmod and chown?

chmod controls what can be done (read, write, execute). chown controls who owns the file (user and group). They work together: first set ownership with chown to establish identity, then set permissions with chmod to define capabilities. You can use chmod as the file owner, but chown typically requires root.

How do I fix "Permission denied" when running a script?

First check if the execute bit is set: ls -l script.sh. If you see -rw-r--r-- (no x), add it with chmod +x script.sh. If the script still fails, check the parent directories — you need execute permission on every directory in the path from / to the script.

Should I use octal (755) or symbolic (u=rwx,go=rx) mode?

Both produce identical results. Octal is faster to type and more common in documentation. Symbolic is more readable and safer for selective changes — chmod g+w file adds group write without affecting other permissions. Most sysadmins use octal for setting full permissions and symbolic for tweaking individual bits.

How do I check what permissions a file currently has?

ls -l file shows permissions in symbolic format. For octal, use stat -c "%a %n" file on Linux or stat -f "%Lp %N" file on macOS. For ACLs (access control lists beyond basic permissions), use getfacl file.

What does the 's' in permission output mean?

It indicates setuid (in owner position) or setgid (in group position). rwsr-xr-x means the file runs as the owner regardless of who executes it — used by system commands like passwd that need root access. On directories, setgid (rws in group position) makes new files inherit the directory's group.

Why does SSH complain about my key permissions?

SSH enforces strict permissions for security. Private keys must be 600 (owner read/write only), the .ssh directory must be 700, and authorized_keys must be 600. If any of these are too open, SSH refuses to use them. Run chmod 600 ~/.ssh/id_* and chmod 700 ~/.ssh to fix it.

When should I use ACLs instead of chmod?

When you need more than three permission categories (owner, group, others). Standard permissions can only assign one group to a file. ACLs (setfacl/getfacl) let you set different permissions for multiple specific users and groups. Common use case: a file needs read access for group A and read/write for group B. Standard permissions can't do this — ACLs can. Red Hat has a solid introduction to Linux ACLs if you want to dig deeper.

Wrapping Up

Linux permissions come down to two questions: "who owns this?" (chown) and "what can they do?" (chmod). The patterns you'll use 90% of the time:

  • Web servers: chown deploy:www-data, directories 2755, files 644
  • SSH keys: chmod 600 private, 644 public, 700 directory
  • Shared directories: chown :group, chmod 2775 for group write with setgid
  • Scripts: chmod +x to make executable

Start with the principle of least privilege — give the minimum permissions needed. When something breaks, ls -l and stat are your diagnostic tools. And when in doubt, find with -perm can hunt down permission problems across your entire filesystem.