Stop Using chmod 777: What to Do Instead When You Get Permission Denied
It is 1am, a deploy is stuck, and the log says Permission denied. You search it, and the top answer on three different forums is the same: chmod 777. You run it. The error goes away. The deploy finishes. You go to bed.
You also just made that file writable by every single user and process on the machine. On a shared host, that is every other tenant. On a box running a web app, that is any script an attacker can reach. chmod 777 does not fix a permission problem. It removes permissions as a concept and hopes nothing bad walks through the door you just took off its hinges.
The honest version of what happened is this: a specific user needed access to a specific file, and instead of granting that, you granted access to everyone. Here is how to grant the access you actually meant.
do not chmod 777. Find out which user the failing process runs as, then fix ownership and use the minimum mode:
# who runs the process that is being denied?
ps -eo user,comm | grep -E 'nginx|php|node|gunicorn'
# give that user ownership, then sane modes
sudo chown -R www-data:www-data /var/www/app/storage
sudo find /var/www/app/storage -type d -exec chmod 755 {} +
sudo find /var/www/app/storage -type f -exec chmod 644 {} +
755 for directories, 644 for files, execute only where a file is genuinely a program. If something truly needs 777, that is a sign the ownership is wrong, not that the permissions are too tight.
The error you are staring at
It comes in a few shapes depending on what tripped over it, but the words are always the same:
Permission denied
-bash: ./deploy.sh: Permission denied
PermissionError: [Errno 13] Permission denied: '/var/www/app/storage/logs/app.log'
open() "/var/www/app/public/upload.tmp" failed (13: Permission denied)
Errno 13 is the kernel telling you a specific user was refused a specific access to a specific path. Every word of that sentence matters, and the fix depends on which word is wrong.
Why this happens: three numbers, one owner
Every file has an owner, a group, and a mode. The mode is three digits, and each digit is a sum of read (4), write (2) and execute (1). The three digits apply, in order, to the owner, the group, and everyone else ("other").
So 644 means the owner can read and write (6), the group can read (4), and everyone else can read (4). 755 means owner has everything (7), group and other can read and execute (5). And 777 means owner, group, and every other account on the system can read, write, and execute. That third 7 is the entire problem. It is not "make it work". It is "let anyone change this".
A Permission denied almost never means the mode is too restrictive. It usually means the file is owned by the wrong user. Your web server runs as www-data or nginx, your app runs as a service account, and the file was created by root during a deploy. The process is not the owner and not in the group, so it falls through to the "other" bits, which correctly do not allow writing. The right move is to change who owns the file, not to open it to the world.
The fix, step by step
Step 1: Look at what is actually there
ls -la /var/www/app/storage
Read the first column and the owner columns. A line like -rw-r--r-- 1 root root app.log tells you the file is owned by root and mode 644. If the process trying to write is not root, that is your answer already: wrong owner.
Step 2: Find out which user the process runs as
ps -eo user,comm,args | grep -E 'nginx|php-fpm|node|gunicorn|python' | grep -v grep
Note the value in the first column. For Nginx and PHP-FPM it is commonly www-data (Debian/Ubuntu) or nginx (RHEL family). For a Node or Python app under systemd it is whatever User= is set to in the unit. That user is the one that needs access. If you are not sure how your service defines its user, my write-up on a systemd unit file that actually keeps it running covers where User= lives.
Step 3: Give ownership to that user, not the world
sudo chown -R www-data:www-data /var/www/app/storage
Now the process is the owner. The owner bits (the first digit of the mode) apply to it, and those already allow writing at 6 or 7. You have granted exactly the access that was missing and nothing more.
Step 4: Set the minimum mode, separately for files and directories
sudo find /var/www/app/storage -type d -exec chmod 755 {} +
sudo find /var/www/app/storage -type f -exec chmod 644 {} +
Directories need the execute bit to be traversable, so 755. Regular files do not, so 644. Splitting by type with find is why you never want a blanket chmod -R 755, which would mark every data file executable. If a specific file is a script that must run, add execute to just that one: chmod u+x deploy.sh.
Verification: prove the right user can write and others cannot
Test as the actual service user, not as root, because root ignores permission bits entirely and will mislead you:
sudo -u www-data touch /var/www/app/storage/logs/test.write && echo "app user OK"
sudo -u nobody touch /var/www/app/storage/logs/test.deny 2>&1 || echo "other user correctly denied"
The first line should print app user OK. The second should fail and print other user correctly denied. That second line is the test chmod 777 can never pass, because under 777 the nobody user would succeed too. Then restart the service and confirm the original error is gone.
What people get wrong
"777 is fine, it is only temporary." Temporary permissions are the ones that live longest, because once the error is gone nobody revisits it. And "temporary" does not reduce the risk while it is set: a scanner hitting your box during that window is all it takes on an uploads path.
Running chmod -R 777 on a whole app directory. This is the truly dangerous one. It makes every config file, every .env with your database password, and every uploaded file world-writable at once. On shared hosting it exposes your app to every other account on the server. It is not a bigger version of the fix, it is a bigger version of the vulnerability.
Thinking the uploads case is theoretical. It is the single most common real-world path from "permissions convenience" to "server compromised". A world-writable directory the web server also executes lets an attacker upload shell.php and run it by visiting the URL. Serve user uploads from a directory the web server treats as static data, owned by the app user, mode 644 on the files.
When it is still broken after chown and 644
If the correct user owns the file with sane modes and you still get Permission denied, the block is one layer up:
- A parent directory lacks execute. To reach
/a/b/cthe user needs execute (the traversal bit) on/aand/a/b, not just onc. Check the whole path withnamei -l /var/www/app/storage/logs/app.log, which prints the permissions of every component. - SELinux or AppArmor is denying it. On RHEL/Fedora,
ls -Zshows the security context andausearch -m avc -ts recentshows denials that never appear as a normal permission problem. The file bits can be perfect and SELinux still says no. - The mount is read-only or has
noexec. Runmount | grep /var/www. A filesystem mountedroornoexecoverrides any chmod you apply. - An immutable flag is set.
lsattr filewill show aniif someone ranchattr +i. Even root cannot write it until you clear it.
None of those are solved by opening the world bit. If you find yourself reaching for 777 to get past them, stop: the permission bits were never the thing saying no. For locking down the rest of a fresh server the same way, my guide to hardening a fresh VPS in 20 minutes covers the ownership and access defaults worth setting before anything ships.
Frequently asked questions
- Is chmod 777 ever safe?
- Almost never on anything that survives a reboot. It is briefly acceptable inside a throwaway container or a scratch directory you delete minutes later. On any file served by a web server, any uploads directory, or anything on a shared or production host, 777 is a genuine security hole because every user and every process on the box can rewrite it. If something only works at 777, the real problem is ownership, not permission bits.
- What should I use instead of chmod 777?
- First find out which user the failing process runs as, then give that user ownership: chown user:group path. Then apply the minimum mode, which is 755 for directories and 644 for regular files. Add execute only on files that are actually programs. For a folder several accounts must share, use a shared group with setgid or POSIX ACLs rather than opening the world bit.
- Why is a world-writable uploads directory dangerous?
- If your web server can write to a directory and so can everyone else, an attacker who finds any upload path can drop a script such as a PHP web shell into it and then request that file over HTTP. The web server executes it, and a simple permission convenience becomes remote code execution. The fix is to let only the app user write, and to serve uploads from a location the web server will not execute as code.
- I ran chmod -R 777 already. How do I undo it?
- Reset directories to 755 and files to 644 with find: find /path -type d -exec chmod 755 {} + and find /path -type f -exec chmod 644 {} +, then chown the tree back to the correct owner. Re-add execute only on the specific scripts or binaries that need it. Do not blanket 755 everything, because that would leave regular files needlessly executable.