from the rwx model to a secure Magento baseline
Incorrectly set file permissions are one of the most common causes of hacked web servers and broken deployments. This guide explains the read/write/execute model, symbolic and octal chmod notation, the difference between chmod and chown, and a battle-tested permission baseline for Magento web roots with www-data ownership, 755 for directories, and 644 for files.
Table of Contents
- 1. The permission model: read, write, execute
- 2. Reading permissions: ls -l and stat
- 3. Symbolic chmod: u, g, o, a and +/-/=
- 4. Octal chmod: numbers instead of letters
- 5. chown and chgrp: changing owner and group
- 6. Special bits: setuid, setgid, and the sticky bit
- 7. umask: controlling default permissions for new files
- 8. ACLs: permissions beyond rwx
- 9. The correct baseline for Magento web roots
- 10. Summary
- 11. FAQ
1. The permission model: read, write, execute
Every file and directory on Linux has three permission classes: User (owner), Group, and Other (everyone else). Each of these three classes independently holds the rights read (r), write (w), and execute (x). For a file, r means reading, w means modifying or deleting the content, and x means executing it. For a directory, r means listing its contents, w means creating or removing entries, and x means being able to change into the directory or access the files it contains.
That last point is frequently overlooked: without the execute bit on a directory, it does not matter whether a file inside is readable, because the access path itself is denied. A directory with rw- and no x can still be listed with ls, but no process can change into it or open a file it contains. The permission model is strictly hierarchical: the kernel checks the chain from the root down to the target file, and a missing execute bit anywhere along that path blocks the entire access, regardless of the permissions on the target file itself.
2. Reading permissions: ls -l and stat
The ls -l command shows permissions as a ten-character string, such as -rwxr-xr--. The first character indicates the file type (- for a regular file, d for a directory, l for a symlink), followed by three groups of three characters for User, Group, and Other. In this example the owner has rwx, the group has r-x, and everyone else has only r--. To interpret the bits quickly, add up read=4, write=2, execute=1 per class: rwx equals 7, r-x equals 5, r-- equals 4, together forming the octal notation 754.
For machine-readable or script-friendly output, stat is better suited than ls. With stat -c "%a %U:%G %n" file.txt, the command directly returns the octal permission, the owner, and the group in a single line, ideal for deployment scripts that need to check or log permissions before a rollout. In CI pipelines, stat is significantly more robust than parsing ls -l, since the output format of ls can vary slightly across distributions and locales.
3. Symbolic chmod: u, g, o, a and +/-/=
The symbolic notation for chmod addresses classes with u (user), g (group), o (other), or a (all), and changes rights with the operators + (add), - (remove), and = (set exactly, overwriting all other bits of that class). The big advantage over octal notation: symbolic changes are relative to the current state and modify only the bits mentioned, without needing to know or calculate the remaining permissions. chmod g+w script.sh adds write access for the group without touching the user or other bits.
Particularly useful is the capitalized X in chmod -R a+X directory/: it sets the execute bit only on directories and on files that are already executable for some class. Regular text files and images stay untouched, while directories become traversable. This is the correct way to restore recursive access to a directory tree after an extraction or migration without accidentally making every file executable, which plain chmod -R a+x would do.
#!/usr/bin/env bash
# Symbolic chmod notation: classes u/g/o/a, operators +/-/=
set -euo pipefail
# Add execute for the owner only
chmod u+x deploy.sh
# Remove write access for group and other
chmod go-w config/secrets.php
# Set exact permissions for other: read-only, overwrites existing bits
chmod o=r public/index.php
# Combine multiple classes and operators in one call
chmod u+rwx,g+rx,o-rwx private-script.sh
# Recursively grant execute only on directories and already-executable files
# Regular files (images, text) stay untouched
chmod -R a+X /var/www/html/pub/media
# Verify the result
stat -c "%a %n" deploy.sh config/secrets.php public/index.php
4. Octal chmod: numbers instead of letters
Octal notation describes all three classes at once as a three-digit number. Each digit is the sum of read=4, write=2, and execute=1 for User, Group, and Other in that order. 755 means: the owner has rwx (4+2+1=7), while group and other have r-x (4+0+1=5). 644 means: the owner has rw- (4+2=6), while group and other have only r-- (4). These two values are by far the most common baselines in web server administration: 755 for directories and executable scripts, 644 for regular files like PHP classes, configuration files, or assets.
Octal mode is absolute, not relative: chmod 644 file.php sets exactly that state, regardless of what permissions applied before. That makes it ideal for reproducible deployment scripts where a defined end state must be guaranteed, but unsuitable when only a single bit needs to change relative to the existing state. A common beginner mistake is chmod 777, which grants full rights to all three classes. On a production web server, that opens the door to arbitrary write access for every local process and, in the worst case, for any attacker with code execution.
#!/usr/bin/env bash
# Octal chmod: absolute mode, common values for web roots
set -euo pipefail
# 755 = rwxr-xr-x: directories and executable scripts
chmod 755 /var/www/html/bin/n98-magerun2.phar
# 644 = rw-r--r--: regular files, no execute bit
chmod 644 /var/www/html/app/etc/env.php
# 750 = rwxr-x---: directory readable/executable only by owner and group
chmod 750 /var/www/html/var/log
# 640 = rw-r-----: sensitive config, no world-readable access
chmod 640 /var/www/html/.env
# NEVER on a production web root: gives write access to everyone
# chmod -R 777 /var/www/html
# Bulk-fix a tree correctly: directories 755, files 644, separately
find /var/www/html -type d -exec chmod 755 {} \;
find /var/www/html -type f -exec chmod 644 {} \;
5. chown and chgrp: changing owner and group
While chmod determines what a class is allowed to do, chown determines who owns a file, and chgrp determines which group it belongs to. Both can be set together with chown: chown user:group file changes owner and group in a single call. Only root may transfer ownership of a file to a different user, while the current owner may change the group to any group they themselves belong to. This restriction prevents users from arbitrarily shifting disk quota usage onto other accounts simply by reassigning file ownership.
On a web server, correct ownership matters just as much as correct mode bits: the web server process typically runs under a dedicated system user such as www-data, and only that user or its group should have write access to the application. The command chown -R www-data:www-data /var/www/html recursively sets owner and group for the whole tree. For mixed deployment workflows, where a developer user uploads files but the web server later needs to write to them, a shared group with the correct group-write bit is often the cleaner solution than constantly reassigning ownership.
#!/usr/bin/env bash
# chown / chgrp: ownership vs. permission bits
set -euo pipefail
# Change owner and group in a single call
chown deploy:www-data /var/www/html/app/etc/env.php
# Change only the group, owner keeps deploy
chgrp www-data /var/www/html/var/cache
# Recursively hand the whole web root to the webserver user
chown -R www-data:www-data /var/www/html
# Copy ownership from a reference file to a target
chown --reference=/var/www/html/index.php /var/www/html/pub/index.php
# Common mixed-workflow pattern: deploy user owns files,
# webserver group can write via shared group + group-write bit
usermod -aG www-data deploy
chown -R deploy:www-data /var/www/html
find /var/www/html/var /var/www/html/pub/media -type d -exec chmod 2775 {} \;
6. Special bits: setuid, setgid, and the sticky bit
Beyond the nine classic rwx bits, Linux knows three special bits, prepended as a fourth octal digit. setuid (value 4, symbolically s in place of the execute bit for user) makes an executable program run with the permissions of the file owner, regardless of who starts it. The classic example is /usr/bin/passwd, which briefly needs root privileges to write to /etc/shadow. setgid (value 2) has an analogous effect for the group on programs, and on directories it causes newly created files to automatically inherit the directory's group instead of the primary group of the creating user.
The sticky bit (value 1, shown as t at the end of the other class) is used almost exclusively on directories and prevents users from deleting or renaming files they do not own, even if they have write access to the directory. /tmp carries 1777 by default, meaning full permissions for everyone plus the sticky bit, so anyone can create files there but only delete their own. For shared upload or cache directories in web applications, the combination chmod 2775 (setgid plus group write) is a proven pattern that lets every member of a deploy group write consistently.
7. umask: controlling default permissions for new files
umask determines which permissions are withheld from newly created files and directories by default, not which ones are granted. The value is subtracted from the theoretical maximum permissions: directories start at 777, files start at 666 for security reasons (no execute bit by default). A umask of 022 withholds write access from group and other, so new directories end up at 755 and new files at 644, exactly the baseline most web servers want. The current value can be shown with the umask command without arguments.
For systems with multiple developers sharing a deploy group, umask 002 is often more appropriate: it withholds write access only from other and leaves the group full rights, so every newly created file remains writable by all group members. The default umask for a login user is usually configured in /etc/login.defs or /etc/profile, and for individual system services it can additionally be overridden in the respective systemd unit via the UMask= directive, independent of the executing user's own setting.
# /etc/login.defs: default umask applied at login for all users
# 022 removes write access for group and other:
# new directories become 755, new files become 644
UMASK 022
# For a shared deploy group where group members must write
# to freshly created files (e.g. shared upload directories):
# UMASK 002
# Per-user override in ~/.bashrc or ~/.profile takes precedence:
# umask 027 # stricter: group gets read-only, other gets nothing
8. ACLs: permissions beyond rwx
The classic Unix model knows only a single group per file. As soon as multiple user groups need different permissions on the same path, for example a deploy team and a separate monitoring team, the classic model is no longer sufficient. Access Control Lists (ACLs) extend the model with an arbitrary number of additional user and group entries per file. With setfacl -m u:monitoring:r-x /var/www/html/var/log, the user monitoring gains read and traverse rights on a directory without changing the classic rwx bits or the primary group at all.
Set ACL entries become visible in the output of ls -l through an appended + after the regular permission bits, and getfacl file shows the details. Especially valuable are default ACLs on directories (setfacl -d): they are automatically inherited by every newly created file and subdirectory, comparable to the setgid bit, but with full control over arbitrary users instead of just the group. This requires a file system with ACL support enabled; under ext4 and XFS this is now the default, but on older mount configurations it must be explicitly enabled with the acl option.
9. The correct baseline for Magento web roots
A Magento web root needs a clear, repeatable permission scheme, because PHP-FPM runs under www-data while deploy processes often run under a separate user. The proven baseline: all directories get 755, all files get 644, and the entire tree is owned by www-data:www-data. The exceptions are the directories that PHP fills at runtime and that must stay writable for the web server process throughout: var/, generated/, pub/media/, and pub/static/. Without write access there, cache generation, layout compilation, and image uploads fail. app/etc/env.php, which holds the database credentials, should be locked down tighter than the rest, typically 640 instead of 644.
After every deploy via Git or Composer, permissions should be corrected automatically, not adjusted manually. A fix script that filters directories and files separately and then opens up the runtime directories specifically prevents the slow drift where individual files accumulate wrong owners or overly broad permissions over time, usually because someone under time pressure used chmod 777 as a quick fix once.
#!/usr/bin/env bash
# fix-permissions.sh: correct baseline for a Magento web root
set -euo pipefail
readonly WEBROOT="/var/www/html"
readonly WEB_USER="www-data"
readonly WEB_GROUP="www-data"
# Ownership: the entire tree belongs to the webserver user
chown -R "${WEB_USER}:${WEB_GROUP}" "$WEBROOT"
# Baseline: 755 for directories, 644 for files
find "$WEBROOT" -type d -exec chmod 755 {} \;
find "$WEBROOT" -type f -exec chmod 644 {} \;
# Runtime-writable directories PHP fills at request time
for dir in var generated pub/media pub/static; do
chmod -R u+w,g+w "${WEBROOT}/${dir}"
done
# Tighten the credentials file beyond the baseline
chmod 640 "${WEBROOT}/app/etc/env.php"
echo "[OK] Permission baseline restored for ${WEBROOT}"
| Situation | Wrong / risky | Correct baseline | Why |
|---|---|---|---|
| Entire web root | chmod -R 777 |
755 dirs, 644 files |
777 grants write access to every process |
| Ownership after deploy | chown -R $USER:$USER |
chown -R www-data:www-data |
The PHP-FPM process must own the tree |
| app/etc/env.php | 644 (world-readable secrets) |
640, group www-data |
DB credentials not readable by everyone |
| Bulk correction | find . -exec chmod 755 {} \; |
Filter dirs and files separately | Otherwise files become wrongly executable |
| var/, pub/media | Read-only like regular files | Additional g+w for www-data |
Otherwise cache and uploads fail |
Mironsoft
Server hardening, deployment automation, and Magento hosting operations
Insecure permissions on your web server?
We audit existing Magento web roots for incorrect ownership and overly broad chmod bits, set up a clean permission baseline, and automate the correction directly in your deploy pipeline.
Permission audit
Full review of ownership, mode bits, and ACLs across the entire web root
Baseline rollout
Setting up www-data ownership, the 755/644 scheme, and protected config files
Deploy automation
Integrating fix scripts into CI/CD pipelines so permissions never drift
10. Summary
File permissions with chmod and chown follow a clear, recurring pattern: the rwx model for User, Group, and Other governs read, write, and execute rights, with the execute bit on directories deciding the access path itself. Symbolic notation with u/g/o/a and +/-/= suits targeted, relative changes, while octal notation like 755 and 644 suits reproducible, absolute baselines. chown and chgrp handle ownership separately from mode bits, and special bits like setgid and the sticky bit solve edge cases for shared directories.
umask determines the default permissions of new files, and ACLs extend the model for cases with more than one relevant group. For Magento web roots a fixed baseline has proven itself: 755 for directories, 644 for files, www-data ownership across the entire tree, with runtime directories such as var/, generated/, and pub/media/ specifically opened up, plus a tighter env.php. Automating this baseline after every deploy prevents the slow drift that otherwise leads to uncontrolled access rights over months.
File Permissions with chmod and chown: The Essentials at a Glance
Permission model
read, write, execute for User, Group, Other. Execute on directories is required for any access to files they contain.
chmod
Symbolic (u+x, a+X) for relative changes, octal (755, 644) for absolute, reproducible states.
chown & chgrp
chown -R www-data:www-data sets owner and group recursively, independent of the mode bits.
Magento baseline
755 dirs, 644 files, www-data ownership, var/generated/pub/media writable, env.php at 640.