Permission denied. Three words that stop a production deployment. A deployment script fails because the process user cannot write to a log directory. A web server returns 403 because the document root is not readable. A CI/CD pipeline fails because the SSH key file has permissions that are too open and SSH refuses to use it.
Linux file permissions are not a niche topic for systems administrators. They are a daily reality for every DevOps engineer. Understanding them deeply means diagnosing and fixing permission issues in seconds instead of minutes — or causing them in the first place.
Why It Matters
Every file and directory on a Linux system has an owner, a group, and a set of permission bits. These three things together determine who can read, write, and execute the file. Get them wrong and applications fail to start. Get them too permissive and you create security vulnerabilities. The Unix permission model has been unchanged for fifty years because it is simple, powerful, and correct.
At Razorpay, every file in the payment service directory is owned by the payment-svc user and group. No other user can write to those files. Not the web server. Not the CI runner. Not a developer who has SSH access. This containment means that even if one component is compromised, it cannot modify the payment service code.
Core Principles
Reading the 10-character permission string:
- r w x r - x r - -| |-----| |-----| |-----|| owner group other|+-- File type: - (file), d (dir), l (symlink), c (char device), b (block device) r = read (4)w = write (2)x = execute (1)- = no permission (0)Calculating octal from the string:
-rwxr-xr-- owner: rwx = 4+2+1 = 7group: r-x = 4+0+1 = 5other: r-- = 4+0+0 = 4 Octal: 754Production permission reference:
644 -rw-r--r-- Config files, HTML, static assets Owner reads/writes, everyone reads 640 -rw-r----- Sensitive config files (DB passwords) Owner reads/writes, group reads, others nothing 600 -rw------- SSH private keys, secret files Owner reads/writes only, no one else 755 -rwxr-xr-x Scripts, binaries, web directories Owner full, everyone reads/executes 700 -rwx------ SSH directory (~/.ssh/) Owner full, no one else 750 -rwxr-x--- Restricted scripts, app directories Owner full, group reads/executes 777 -rwxrwxrwx NEVER use in production Everyone can do everythingExecute on directories means something different:
For directories, the execute bit means "can enter this directory" (traverse). Without execute on a directory, you cannot cd into it or access anything inside it, even if you have read permission on the files inside.
## This is why web server directories need 755 not 644## Without the x bit, nginx cannot traverse into the document rootchmod 755 /var/www/html/The special permission bits:
Setuid (s on user execute position):When an executable has setuid, it runs as the file ownerregardless of who executes it.Example: /usr/bin/passwd has setuid root -- lets regularusers change their own password by temporarily running as root.chmod u+s filename or chmod 4755 filename Setgid (s on group execute position):On files: runs with file's group permissions.On directories: new files inherit the directory's groupinstead of the creator's primary group.chmod g+s dirname or chmod 2755 dirname Sticky bit (t on other execute position):On directories: users can only delete files they own,even if they have write permission on the directory./tmp has sticky bit -- you cannot delete other users' temp files.chmod +t dirname or chmod 1777 dirnameStep-by-Step Lab
Milestone 1 — Read and understand permissions
## Create test directory and filesmkdir -p /tmp/permissions-labcd /tmp/permissions-labtouch file1.txt file2.shmkdir subdir/ ## View permissions in long formatls -la## -rw-r--r-- 1 ubuntu ubuntu 0 Jan 15 10:00 file1.txt## -rw-r--r-- 1 ubuntu ubuntu 0 Jan 15 10:00 file2.sh## drwxr-xr-x 2 ubuntu ubuntu 4096 Jan 15 10:00 subdir ## Get detailed info on one filestat file1.txt## Access: (0644/-rw-r--r--) Uid: (1000/ubuntu) Gid: (1000/ubuntu)Milestone 2 — Change permissions with chmod
## Symbolic mode: u=user, g=group, o=other, a=all## + adds, - removes, = sets exactly ## Make a script executable by owner onlychmod u+x file2.shls -la file2.sh## -rwxr--r-- 1 ubuntu ubuntu 0 Jan 15 10:00 file2.sh ## Remove write from group and otherchmod go-w file1.txt ## Set exact permissions with octalchmod 644 file1.txt ## -rw-r--r--chmod 755 file2.sh ## -rwxr-xr-xchmod 700 subdir/ ## drwx------ ## Apply recursively to a directorychmod -R 755 /var/www/html/ ## Verify with lsls -laMilestone 3 — Change ownership with chown
## Create a service account user for testingsudo useradd -r -s /sbin/nologin app-service ## Change ownersudo chown app-service file1.txt ## Change owner and group togethersudo chown app-service:app-service file2.sh ## Change only the groupsudo chown :app-service subdir/## Shorthand for: chgrp app-service subdir/ ## Change recursivelysudo chown -R app-service:app-service /opt/myapp/ ## Verifyls -la## -rw-r--r-- 1 app-service ubuntu 0 Jan 15 10:00 file1.txt## -rwxr-xr-x 1 app-service app-service 0 Jan 15 10:00 file2.shMilestone 4 — Understand and set umask
umask controls default permissions for newly created files and directories. It is a mask that removes bits from the maximum (666 for files, 777 for directories).
## Check current umaskumask## 0022 ## How umask 022 works:## New file: 666 - 022 = 644 (-rw-r--r--)## New directory: 777 - 022 = 755 (drwxr-xr-x) ## Create files and verifytouch /tmp/test-umask-filemkdir /tmp/test-umask-dirls -la /tmp/test-umask-file /tmp/test-umask-dir## -rw-r--r-- /tmp/test-umask-file## drwxr-xr-x /tmp/test-umask-dir ## Set umask for more restrictive defaultsumask 027## New file: 666 - 027 = 640 (group can read, others nothing)## New directory: 777 - 027 = 750 ## Set permanently for a service account in /etc/profile.d/echo 'umask 027' | sudo tee /etc/profile.d/service-umask.shMilestone 5 — Set ACLs for granular control
ACLs (Access Control Lists) extend the basic model to allow per-user and per-group permissions beyond the three-level system.
## Install ACL tools if neededsudo apt install acl # Debian/Ubuntu ## View current ACLsgetfacl /tmp/permissions-lab/file1.txt## # file: file1.txt## # owner: ubuntu## # group: ubuntu## user::rw-## group::r--## other::r-- ## Give a specific user read/write access without changing ownershipsudo setfacl -m u:deploy:rw file1.txtgetfacl file1.txt## user::rw-## user:deploy:rw- <- deploy user now has read/write## group::r--## other::r-- ## Give a specific group read accesssudo setfacl -m g:developers:r file1.txt ## Set a default ACL on a directory (new files inherit it)sudo setfacl -d -m u:deploy:rw /var/log/myapp/ ## Remove an ACL entrysudo setfacl -x u:deploy file1.txt ## Remove all ACLssudo setfacl -b file1.txtMilestone 6 — Apply production permission patterns
## Web server document rootsudo chown -R www-data:www-data /var/www/html/sudo find /var/www/html/ -type f -exec chmod 644 {} \;sudo find /var/www/html/ -type d -exec chmod 755 {} \; ## Application directorysudo chown -R app-service:app-service /opt/myapp/sudo chmod 750 /opt/myapp/sudo chmod 640 /opt/myapp/config/*.conf # readable by group only ## SSH directory (strict requirements - SSH will refuse keys with wrong permissions)mkdir -p ~/.sshchmod 700 ~/.sshtouch ~/.ssh/authorized_keyschmod 600 ~/.ssh/authorized_keyschmod 600 ~/.ssh/id_ed25519 # private keychmod 644 ~/.ssh/id_ed25519.pub # public key can be world-readable ## Log directorysudo chown app-service:syslog /var/log/myapp/sudo chmod 775 /var/log/myapp/## Owner and syslog group can write; verify readable by log shippersCommon Mistakes
| Mistake | Problem | Fix |
|---|---|---|
| chmod 777 to "fix" permission errors | Creates security vulnerability | Identify correct owner and set minimal permissions |
| SSH private key permissions too open | SSH refuses to use it (security check) | chmod 600 ~/.ssh/id_ed25519 |
| Forgetting -R on recursive chown | Only top-level directory changes | Always use -R for directory trees |
| Not accounting for execute on directories | Cannot cd into dir even with r permission | Directories need x bit for traversal |
| Using octal 0 for no permission on all | File becomes 000 — owner locked out too | Verify octal before applying |
Troubleshooting
| Symptom | First Command | What to Look For |
|---|---|---|
| Permission denied when writing | ls -la on target file/dir |
Write bit for current user/group |
| SSH ignores authorized_keys | ls -la ~/.ssh/ |
~/.ssh should be 700, authorized_keys 600 |
| Web server returns 403 Forbidden | ls -la /var/www/html/ |
www-data needs read + execute on path |
| Script runs fine but fails in cron | Check script file permissions | Script needs execute bit for cron user |
| App cannot read config file | ls -la configfile |
File owner/group matches app user |
PLACEMENT PRO TIP**Tip:** When diagnosing a permission issue, trace the full path. Every directory in the path needs execute (traverse) permission for the user. A file with 644 permissions inside a directory with 700 permissions is inaccessible to anyone except the directory owner.
REMEMBER THIS**Remember:** The numeric (octal) and symbolic (rwx) modes do the same thing. Octal is faster to type. Symbolic is clearer to read. Learn both — you will see both in production scripts and documentation.
COMMON MISTAKE / WARNING**Security:** Setuid binaries are a common privilege escalation vector. Audit setuid files regularly: `find / -perm -4000 -type f 2>/dev/null`. Any unexpected setuid binary is a potential security issue.