The arithmetic, once
7 is 4+2+1, so the owner gets read, write and execute. 5 is 4+1, so the group and everyone else get read and execute but not write. Written out:
| Class | Digit | Bits | Sum |
|---|---|---|---|
| Owner | 7 | rwx | 4 + 2 + 1 |
| Group | 5 | r-x | 4 + 1 |
| Others | 5 | r-x | 4 + 1 |
The full ls -l field for a directory at this mode is
drwxr-xr-x, and the absolute symbolic argument that
produces it is u=rwx,g=rx,o=rx.
What the last digit really buys
755's third digit is 5, and the interesting question is what the alternatives would have
done. Below is every value that digit could take on a directory whose first two digits
stay at 75, and what an account that is neither the owner nor in the group
can then do. The four capability columns are computed straight from the bits, with nothing
described by hand, which is why the row for 6 disagrees with intuition:
| Mode | Others | List names | Enter | Read a known file inside | Create / delete |
|---|---|---|---|---|---|
| 750 | --- | no | no | no | no |
| 751 | --x | no | yes | yes | no |
| 752 | -w- | no | no | no | no |
| 753 | -wx | no | yes | yes | yes |
| 754 | r-- | yes | no | no | no |
| 755 | r-x | yes | yes | yes | no |
| 756 | rw- | yes | no | no | no |
| 757 | rwx | yes | yes | yes | yes |
Read the 754 row and the 756 row together. 754 lets an outsider run
ls and get a list of names, then fails on every single one of them, because
reading a directory and reaching into it are separate rights. 756 adds
the write bit and changes nothing at all: creating an entry requires resolving the
directory first, and that needs the search bit which 6 does not have. Only the odd values
— 1, 3, 5, 7 — do anything useful for a directory, which is why
64 of the 512 three-digit modes leave a directory enterable by all
three classes.
755 on a file is a different decision
On a regular file the execute bit means "run this as a program", and it is worth being
deliberate about handing that to everyone. Three cases where 755 is right on a file: a
binary installed into /usr/local/bin, a wrapper script that any user is meant
to invoke, and a CGI script the web server executes as an unprivileged user. One case
where it is wrong: a deployment script that only your CI user should run, which wants
700 instead.
A static file served by nginx or Apache never needs an execute bit. The server opens and
reads it; nothing is executed. Adding x to your HTML does not make it load
faster or work better — it just widens what an attacker who finds a file-upload bug can
do. That is the argument for 644 on content.
Applying 755 to a whole tree without wrecking it
chmod -R 755 /var/www/html applies one mode to files and directories alike.
Directories come out correct. Files come out executable, which is not what you asked for
and not what any static site needs. Split the tree by type instead:
find /var/www/html -type d -exec chmod 755 {} +
find /var/www/html -type f -exec chmod 644 {} +
The + at the end batches paths into as few chmod invocations as
possible instead of one per file, which on a large tree is the difference between a second
and a minute. If you would rather have one command, capital X does the same
job: chmod -R u=rwX,g=rX,o=rX /var/www/html sets the execute bit on
directories and on files that already had one, and leaves plain files alone.
The trap to avoid entirely is the inverse. chmod -R 644 /var/www/html takes
the search bit off every directory, and once a directory has no execute bit you cannot
reach anything inside it — including to fix it, unless you are root or the owner and can
chmod the directory itself back. Recovering from that means walking the tree from the top
with find, which is exactly the tool you should have used first.
When 755 does not fix your error
755 solves "the server cannot enter this directory". It does not solve an ownership problem, and it does not solve a configuration problem. These two rows from the error index are worth reading before you escalate to 777:
| What you see | Where it comes from | Fires on | Fix |
|---|---|---|---|
access to /index.html denied (filesystem path '/var/www/html/index.html') because search permissions are missing on a component of the path Apache HTTP Server · error_log, tagged AH00035 shown for mode 750 on a directory on the path to the document root verbatim format string: | Apache HTTP Server — server/request.c · ap_directory_walk(), APLOGNO(00035) | APR_STATUS_IS_EACCES(rv) inside ap_directory_walk() — a branch on the errno the kernel returned while resolving a component, not an expression over st_mode described, not quoted — the message is not guarded by one expression 2048 of the 4096 modes fire it. Assumes that the Apache worker user is neither the owner nor in the owning group, so it is judged by the "others" digit. If the worker is in the group, read the group digit instead. | chmod 755 /var/www/html least-privilege mode 755 |
client denied by server configuration: /var/www/html/members/report.pdf Apache HTTP Server · error_log, tagged AH01630 shown for mode 644 on not a file mode at all verbatim format string: | Apache HTTP Server — modules/aaa/mod_authz_core.c · authorize_user_core(), APLOGNO(01630) | the AUTHZ_DENIED / AUTHZ_NEUTRAL branch of authorize_user_core() — a control-flow branch on the authorisation result, with no mode test anywhere in it described, not quoted — the message is not guarded by one expression No mode fires this — chmod cannot change the outcome. | edit the Require directive — no chmod will change this |
httpd-search-permissions — Apache names the failing URI and the filesystem path separately, which is the fastest way to see that a parent directory is at fault when you had been staring at the file you were trying to serve.
httpd-authz-denied — If your error line says "client denied by server configuration", stop changing modes and read the <Directory> block.