File permissions are the access control layer every Linux admin interacts with—often daily when a web server cannot read a static file, a script "works for me" as root but fails under systemd, or SSH refuses a private key with lax modes. Unlike Windows ACLs, Unix permissions are compact: owner, group, and others each get read, write, and execute bits. Extended ACLs and SELinux contexts add depth, but the chmod/chown foundation solves most homelab and developer issues.

This guide explains permission notation, symbolic and octal chmod, ownership changes, setuid/setgid/sticky bits, umask defaults, and practical recipes for Docker bind mounts, web roots, and SSH keys.

Before you begin

Identify the acting user for a failing command:

whoami
id

Inspect target object:

ls -l /path/to/file
ls -ld /path/to/directory   # -d for directory itself, not contents
namei -l /long/nested/path   # traverse path permissions

Remember: directories need execute (x) permission to enter (cd) even if you can list (r) them. Files need x to execute as programs or scripts with shebangs.

Reading permission strings

Example: -rw-r----- 1 deploy www-data 4096 May 20 10:00 app.conf

Field Meaning
- File type (d directory, l symlink)
rw- Owner: read+write
r-- Group: read only
--- Others: no access
deploy Owner user
www-data Group

chmod: symbolic and octal

Symbolic (human-readable):

chmod u+x script.sh        # owner +execute
chmod g-w secret.txt       # group -write
chmod o-rwx private/       # others no access
chmod a+r public.txt       # all +read
chmod u=rw,go=r file       # set exactly

Octal (compact, common in docs):

Octal Bits Meaning
4 r read
2 w write
1 x execute
chmod 644 file.txt    # rw-r--r--
chmod 755 script.sh   # rwxr-xr-x
chmod 700 ~/.ssh      # rwx------ directory
chmod 600 ~/.ssh/id_ed25519

Recursive caution:

chmod -R 755 /var/www/site   # OK for static site dirs
chmod -R 777 /anything       # Never on servers

Recursive chmod on mixed file/dir trees may need find:

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

chown and chgrp

Change owner and group (requires root for other users' files):

sudo chown deploy:deploy app/
sudo chown root:www-data /var/www/html
sudo chgrp docker /var/run/docker.sock   # example; package sets this
sudo chown -R $USER:$USER ~/project      # fix home dir after sudo mistakes

chown accepts user:group or user.group syntax depending on shell escaping—prefer colon form.

umask: default permissions for new files

umask
umask 022   # common default → files 644, dirs 755
umask 077   # paranoid → files 600, dirs 700

Add to ~/.bashrc or /etc/profile for persistence. Developers creating shared group projects often use umask 002 with a common group.

Special bits: setuid, setgid, sticky

setuid (4xxx): Executed file runs as owner (e.g., passwd).

setgid (2xxx): On dirs, new files inherit group; on executables, run as group.

sticky (1xxx): On dirs like /tmp, only owner deletes own files.

chmod 4755 binary        # setuid
chmod 2775 shared_dir    # setgid dir for team collaboration
ls -ld /tmp              # drwxrwxrwt sticky bit 't'

Misapplied setuid binaries are security risks—avoid creating your own unless you understand implications.

ACLs when owner/group/others are not enough

Install ACL tools:

sudo apt install acl       # Debian/Ubuntu
sudo dnf install acl       # Fedora

Grant single user read on a file:

setfacl -m u:colleague:r-- report.pdf
getfacl report.pdf

Default ACLs on directories propagate to new files—useful for shared project folders.

SELinux and AppArmor contexts (awareness)

On Fedora/RHEL, wrong SELinux context breaks nginx reading files:

ls -Z /var/www/html
sudo restorecon -Rv /var/www/html

Ubuntu AppArmor profiles may restrict services similarly—check logs before chmod-spamming.

Practical recipes

SSH private key:

chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_ed25519
chmod 644 ~/.ssh/id_ed25519.pub

Web root (nginx/apache):

sudo chown -R root:www-data /var/www/mysite
sudo find /var/www/mysite -type d -exec chmod 755 {} +
sudo find /var/www/mysite -type f -exec chmod 644 {} +

Script executed by cron/systemd:

chmod 750 /usr/local/bin/backup.sh
chown root:backup /usr/local/bin/backup.sh

Docker bind mount "permission denied": Container UID/GID may not match host. Align with:

chown -R 1000:1000 ./data
# or run container with --user $(id -u):$(id -g)

Server hardening patterns

Web applications: Run services as dedicated users, not root. Keep code read-only except upload directories:

sudo useradd -r -s /usr/sbin/nologin myapp
sudo chown -R root:myapp /opt/myapp
sudo chmod -R 750 /opt/myapp
sudo chmod 770 /opt/myapp/uploads

Secrets: .env files should be 600 owned by the service user. Never commit secrets to git—permission bits are last-resort defense when repos leak.

SSH and keys: Combine chmod 600 on keys with AllowUsers restrictions on servers—permissions alone do not stop privilege escalation if attackers gain user shells.

Shared development directories: Use setgid directories so new files inherit project group membership:

sudo chgrp devteam /srv/project
sudo chmod 2775 /srv/project
sudo usermod -aG devteam alice

Auditing permissions at scale

Before chmod-spamming production:

find /var/www -type f ! -perm 644 -ls
find /var/www -type d ! -perm 755 -ls

Ansible file module enforces desired state reproducibly—better than manual fixes on multiple homelab nodes.

Numeric reference card

Octal File typical Directory typical
755 scripts (executable) public dirs
644 config, static HTML
600 secrets, keys
700 private scripts .ssh, admin dirs
750 group-readable app dirs

Container and NFS bind mount gotchas

When bind-mounting NFS or SMB shares into Docker, UID mismatches manifest as Permission denied inside containers despite correct host modes. Align container user: directives with NFS anonuid settings or use local volumes for database workloads.

For read-only config mounts:

docker run -v /etc/myapp/config.yaml:ro ...

Read-only binds reduce blast radius when containers are compromised.

Troubleshooting

"Permission denied" but chmod looks correct. Check parent directory execute bit with namei -l. Check SELinux (ausearch, restorecon). Verify mount options (noexec, nosuid).

Cannot write despite ownership. Disk full (df -h), immutable attribute (lsattr), or read-only mount.

Executable script fails. Shebang line (#!/bin/bash), Windows CRLF line endings (dos2unix), or missing interpreter.

Group changes not visible until re-login. newgrp groupname or logout/login refreshes group membership.

sudo works, systemd service fails. Services run as specific users—match file ownership to User= in unit files.

Key takeaways

  • Learn to read ls -l output fluently—owner, group, and triplet bits explain most access failures.
  • Prefer least privilege: 644 files, 755 public dirs, 600 secrets, 700 ~/.ssh.
  • Use find for recursive dir/file permission splits instead of blind chmod -R.
  • Combine chown with service users (www-data, container UIDs) for servers and compose stacks.
  • Check SELinux/AppArmor and parent path permissions before reaching for 777.

FAQ

What does chmod 777 do?
Grants read/write/execute to everyone—convenient, dangerous on multi-user or internet-facing systems.

Why can't others cd into my home?
Home dirs often drwxr-x--- (750)—others lack execute on directory.

Do I need ACLs at home?
Rarely. Standard owner/group/others plus one group per project suffices for most homelabs.