Linux File Permissions Explained: ls -l, chmod, chown, umask
Linux file permissions explained: read ls -l, r/w/x on files and folders, octal 600/644/755, chmod, chown, umask, and the uid a container writes as.
By Matthews WongReviewed on
Key takeaway
Linux file permissions decide which processes may read, write or execute a file. Every file stores an owner uid, a group gid and nine mode bits: read, write and execute for the owner, the group and everyone else. The kernel compares the process's numeric ids with the file's, picks exactly one of those three sets, and checks its bits. Root skips the check.
You will be able to
- Read an ls -l line from left to right, and turn its mode into octal and back.
- Explain what r, w and x mean on a directory, and why the owner's bits apply even when they grant less than the others'.
- Fix Permission denied between a container and a host file by matching the uid, not with chmod 777.
Step through the diagram
Press Play, or step with the arrows. Every step is written out below the diagram.
How the kernel decides who may write a file
One host file, mode 640, owned by uid 1000. Your shell writes it through the owner bits, a container running as uid 1001 falls through to the other bits and is refused, root skips the bits, and chown to 1001 lets the container in through the owner bits.
-rw-r----- 1 1000 1000 report.txtKeys: ← → step · Home / End · K play or pause
report.txt sits in a host folder a container mounts. Owner uid 1000, group 1000, mode 640. Your shell, the app container and root each try to write it.
-rw-r----- 1 1000 1000 report.txtYour shell runs as uid 1000, the file's owner. The kernel uses the owner bits, rw-, and nothing else. Write is set, so the write goes through.
echo ok >> report.txtPredict the next step Next, the app container writes as uid 1001, which is not in group 1000. Which bits decide? (The owner bits, rw- · The group bits, r-- · The other bits, ---)Inside the container the process is uid 1001. The kernel sees only that number: not 1000, not in group 1000. So the other bits, ---, decide: Permission denied.
touch: report.txt: Permission deniedRoot, uid 0, bypasses the read and write checks. That is why the same mount worked while the image ran as root, and broke once it switched to USER.
CAP_DAC_OVERRIDEPredict the next step What is the smallest change that lets uid 1001 write report.txt? (chmod 777 report.txt · chown 1001 report.txt · Run the container as root)chown 1001 makes uid 1001 the owner and keeps group 1000. The app now gets the owner bits, rw-, and your shell still reads through the group bits.
sudo chown 1001 report.txt
What are Linux file permissions?
Linux file permissions are the rules the kernel applies whenever a process opens, creates or runs a file. Each file's inode records two numbers, the owner's user ID and the group ID, plus a mode: nine permission bits and three special ones. The nine bits are read, write and execute, once for the owner, once for the group and once for everyone else.
Read more (2)Show less
The check itself is short. If the process's user ID equals the file's owner, the kernel uses the first three bits. Otherwise, if the file's group is the process's group or one of its supplementary groups, it uses the middle three. Otherwise it uses the last three. Exactly one set is used, so the owner of a ----r--r-- file cannot read it, even though everyone else can.
Root is the exception. The path_resolution man page says the superuser bypasses permission checks; on Linux that power is the CAP_DAC_OVERRIDE capability, which still grants execute only when at least one execute bit is set. Docker keeps DAC_OVERRIDE in its default capability set, and a container runs as root unless the image or docker run says otherwise. That is why so much works until an image switches to a normal user.
How do you read ls -l from left to right?
Take a line like -rw-r--r-- 1 alice staff 6 Oct 18 08:40 notes.txt. The first character is the file type: - for a regular file, d for a directory, l for a symbolic link. The next nine characters are the mode in three groups of three: rw- for the owner, r-- for the group, r-- for everyone else. A dash means that bit is off.
Read more (2)Show less
GNU ls may print one more character right after the mode: a dot for a file with an SELinux security context, a plus for another access method such as an ACL. Then come the number of hard links, the owner, the group, the size in bytes, the modification time and the name. In an execute position, s means set-user-ID or set-group-ID plus execute, t means the sticky bit plus execute, and a capital S or T means the special bit without execute.
Owner and group are shown as names, but the file stores numbers. ls maps user IDs to names through /etc/passwd, and when a number has no name, GNU ls prints the number instead. ls -n skips the names and shows the numeric user and group IDs. That is the view that matters when a container and a host share files.
What do r, w and x mean on a directory?
On a file, r reads the contents, w changes them and x runs the file as a program. On a directory the same bits mean something else. The GNU coreutils manual puts it plainly: read lists the contents, write creates and removes files in it, and execute lets you access files in it, which is why x on a directory is also called search.
Read more (2)Show less
The two halves come apart. With r but no x, you can read the names but not open anything inside, and GNU ls -l prints question marks where the details should be. With x but no r, you cannot list the directory, yet you can open a file whose name you already know. To reach a file at all, the process needs x on every directory along the path; one missing x ends the lookup with Permission denied.
Deleting is a change to the directory, not to the file. rm -f removes your own read-only file from a directory you can write, and fails on any file inside a directory with mode 555. The sticky bit, the t at the end of /tmp's drwxrwxrwt, narrows it again: there, only a file's owner, the directory's owner or a privileged process can delete or rename a file.
How do octal modes like 600, 644 and 755 work?
Each group of three bits fits in one octal digit: read is 4, write is 2, execute is 1, and you add them up. rw- is 6, r-x is 5, r-- is 4 and --- is 0. Three digits give the owner, the group and everyone else, in that order, so 644 is rw-r--r--, 600 is rw-------, 755 is rwxr-xr-x and 700 is rwx------.
Read more (1)Show less
An optional fourth digit in front holds the special bits: 4 for set-user-ID, 2 for set-group-ID, 1 for the sticky bit. That is why ssh shows a key's mode as 0640: the leading zero is the empty special digit. The common choices follow from who needs what: 644 for files anyone may read, 755 for directories and programs anyone may enter or run, 600 for secrets, 700 for private directories.
chmod: symbolic or octal?
chmod takes either form. An octal mode sets every bit at once and replaces what was there; the GNU manual notes it can only set bits absolutely. A symbolic mode changes only what you name: who (u for the owner, g for the group, o for others, a for all), an operator (+ adds, - removes, = sets exactly and clears the rest for that who), and the bits (r, w, x, plus X, s and t).
Read more (2)Show less
So chmod 640 notes.txt and chmod u=rw,g=r,o= notes.txt end in the same place, while chmod g+r only adds group read and keeps everything else. Leave out the who letter and chmod acts as if you wrote a, except that bits set in your umask are left alone: under umask 022, chmod +w adds write for the owner only, while a+w gives it to everyone. -R walks a whole directory tree, and chmod never changes a symbolic link's own permissions.
Only a file's owner or a privileged process may change its mode; anyone else gets EPERM. The rule looks at ownership, not at the mode, which is why you can lock yourself out of a file with chmod 044 in the try-it below and still get back in with chmod 644.
chown, and why owners are numbers
chown OWNER:GROUP file changes both, chown OWNER file changes only the owner, chown :GROUP file only the group, and OWNER: with a trailing colon also sets the group to that user's login group. OWNER and GROUP may be names or numbers. Changing the owner needs privilege, CAP_CHOWN on Linux, which is why you type sudo chown; a file's owner may change its group only to a group they belong to.
Read more (1)Show less
The kernel never sees names. A process's user and group IDs are integers, the inode stores the owner and group as integers, and /etc/passwd is only the table that turns a login name into a numeric user ID and primary group ID. Two systems that share files can give the same number different names, or no name at all, and the kernel will not notice.
Which uid does a container write as?
Whatever number its process runs as. Docker's default user inside a container is root, uid 0. USER in a Dockerfile sets the user, by name or UID, for the rest of the stage, including RUN and the container's command, and docker run --user picks one at start. COPY creates files owned by UID and GID 0 unless --chown says otherwise; names in --chown are looked up in the image's own /etc/passwd and /etc/group, while numeric IDs need no lookup.
Read more (2)Show less
On a bind mount there is no translation. Unless the daemon uses user namespace remapping, which you have to turn on, the uid a container process writes as is the uid the host stores. In a test on a Linux Docker host, a root container left files owned by 0, a container running as 1001 left files owned by 1001, and a host folder owned by 1000 with mode 755 refused uid 1001: not the owner, not in the group, so the other bits r-x applied.
Docker Desktop on a Mac shares host folders through its own file-sharing layer, and the same test there behaved differently: the write succeeded and the file showed up owned by the Mac user. So reproduce ownership problems on a Linux host, or with a named volume, where the same steps gave Permission denied on Docker Desktop too.
What does umask do?
umask decides which permissions a new file does not get. A program asks for a mode when it creates a file, and the kernel turns off every bit that is set in the process's umask. The typical default is 022, which removes write for the group and everyone else: a file requested as 0666 comes out 0644, rw-r--r--. A child process inherits its parent's umask, so the value a shell or a service starts with applies to everything it runs.
Read more (1)Show less
Run umask to see yours: macOS printed 022 and Alpine printed 0022 in the try-it, and new files came out 644 and new directories 755. The umask only acts at creation. A file that already exists keeps its mode when a program truncates and rewrites it, so tightening it later is chmod's job, and a script that writes secrets can run umask 077 first so the file is born 600.
The real config behind this lesson
Excerpts from the repository that builds and deploys this website, quoted as they are.
RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjsThree ownership decisions. public is copied without --chown, so it belongs to uid and gid 0 and the server reads it through the other bits. The server and static files get --chown=nextjs:nodejs, stored as 1001:1001, and USER nextjs makes the process uid 1001, their owner, so the kernel hands it the owner bits. Building these lines on node:20-alpine and running id prints uid=1001(nextjs) gid=65533(nogroup): adduser --system put nextjs in nogroup, so the process never matches the nodejs group on its own files.
cat > .env <<'ENVEOF'
GROQ_API_KEY=${{ secrets.GROQ_API_KEY }}
RESEND_API_KEY=${{ secrets.RESEND_API_KEY }}
CONTACT_FROM_EMAIL=${{ vars.CONTACT_FROM_EMAIL }}
CONTACT_TO_EMAIL=${{ vars.CONTACT_TO_EMAIL }}
IMAGE=${{ needs.build-and-push.outputs.image }}
APP_PORT=${{ vars.APP_PORT || 3002 }}
ENVEOF
chmod 600 .envcat > .env creates the file with whatever the server's umask leaves, 644 under the typical 022, which other accounts that can reach the folder could read. chmod 600 keeps read and write for the deploy user only, and docker compose runs as that same user later in the same SSH session, so the owner bits cover it. On later deploys the file already exists, so cat truncates it and the 600 stays: a mode is applied only when a file is created.
Mistakes that cost real time
Permission denied on a bind mount after adding USER
The container used to run as root, which skips the bits. Now it runs as uid 1001, and the host folder belongs to your uid, or to root if docker run -v created it. Compare ls -ln on the host with id in the container, then make the numbers meet: sudo chown 1001 on the folder, or run the container with --user "$(id -u):$(id -g)".
chmod 777 makes the error go away
It works because it sets rwx in the other bits, so every uid on the machine passes the check. On a directory that means any account or container can create, delete and rename files in it. The uid mismatch is still there; you have only stopped looking at it. Give the files to the uid that writes them, or share a group: chgrp the folder to a group the process is in, then chmod g+w.
WARNING: UNPROTECTED PRIVATE KEY FILE
OpenSSH prints Permissions 0640 for 'lab_key' are too open, then ignores the key. The ssh manual says a private key must not be accessible by others, and group read alone was enough to trigger it in the test. chmod 600 on the key fixes it, and 400 works too. This is the client checking your own file; lesson 2 covers what the server checks.
Permission denied running your own script
./deploy.sh needs the execute bit, and a new file made with echo, printf or touch starts at 644 under umask 022. The shell reports Permission denied with exit status 126. Run chmod u+x deploy.sh, or 755 if others should run it too. sh deploy.sh works without x, because then sh is the program and the script is only a file it reads.
| Aspect | Octal: chmod 640 | Symbolic: chmod g+r |
|---|---|---|
| What it changes | Every permission bit at once | Only the classes and bits you name |
| Bits you did not mention | Replaced | Kept, except for the who in an = operation |
| The umask | Ignored | Respected when you leave out the who letter |
| Reach for it when | You know the final mode: 600, 644, 755 | You add or remove one thing: u+x, o-r |
Do it on your own machine
Steps 1 to 5 need only a terminal on Linux, macOS or WSL; step 6 needs Docker. Run them as your normal user, not with sudo or as root, because root skips the checks. The outputs quoted were checked on macOS and in an Alpine Linux container.
Make a lab folder and read the long listing. With umask 022, expect -rw-r--r-- for the file and drwxr-xr-x for the folder, or drwxr-sr-x if the parent folder has the set-group-ID bit. ls -ln shows the same lines with numbers instead of names.
mkdir perm-lab && cd perm-lab echo "hello" > notes.txt mkdir box && echo "inside" > box/file.txt ls -l ls -ln umaskSet a mode in octal, then add bits symbolically. Then take your own read bit away: cat fails with Permission denied although group and others may read, and chmod still works because you own the file.
chmod 600 notes.txt && ls -l notes.txt chmod g+r,o+r notes.txt && ls -l notes.txt chmod 044 notes.txt cat notes.txt chmod 644 notes.txtSplit r from x on a directory. At 600, ls may print the name, an error or both, depending on your ls, but cat fails. At 100, ls fails but cat by name prints inside.
chmod 600 box ls box cat box/file.txt chmod 100 box ls box cat box/file.txt chmod 755 boxA new script cannot run until it has x. The first ./run.sh fails with Permission denied; after chmod u+x it prints ran.
printf '#!/bin/sh\necho ran\n' > run.sh ./run.sh chmod u+x run.sh && ./run.shWatch ssh refuse a group-readable key: Permissions 0640 for 'lab_key' are too open. After chmod 600 the same command prints the public key.
ssh-keygen -q -t ed25519 -N "" -f lab_key chmod 640 lab_key ssh-keygen -y -f lab_key chmod 600 lab_key && ssh-keygen -y -f lab_keyNow the container part, on a named volume so it behaves the same on every platform. Root makes a folder, uid 1001 is refused, and after chown to the number, uid 1001 writes and ls -ln shows 1001.
docker volume create perm-lab docker run --rm -v perm-lab:/data alpine:3 sh -c 'mkdir /data/out && ls -ln /data' docker run --rm --user 1001:1001 -v perm-lab:/data alpine:3 touch /data/out/report.txt docker run --rm -v perm-lab:/data alpine:3 chown 1001:1001 /data/out docker run --rm --user 1001:1001 -v perm-lab:/data alpine:3 sh -c 'touch /data/out/report.txt && ls -ln /data/out'Clean up the volume and the lab folder.
docker volume rm perm-lab cd .. && rm -rf perm-lab
Three questions before you move on
Pick an answer to see why it is right or wrong. Nothing is graded, so guess freely; a miss is kept in this browser so the course can ask it again later.
Two questions from earlier lessons
Recalling an older lesson after a gap is what makes it stick. Answer from memory before you look anything up.
Answer in your head, then check
1.A file you own has mode 604. Can you write to it? Can a member of its group, who is not you, read it?Show the answer
Answer:Yes, you get the owner bits, rw-. No, a group member gets the group bits, ---, even though everyone outside the group may read through the other bits, r--. The kernel uses exactly one set.
2.Why does ls -l inside a container sometimes print 1001 where you expected a name?Show the answer
Answer:The file stores only the number. ls looks it up in the container's own /etc/passwd, and when that image has no user with uid 1001, it prints the number instead.
3.Why does this site's deploy run chmod 600 right after writing .env?Show the answer
Answer:The file holds secrets, and a file created under the typical umask 022 is 644, readable by other accounts on the server. 600 leaves read and write to the deploy user, which is the user that runs docker compose.
Related deep dives on this site
Sources
Primary sources checked on the research date. Follow them for the current version.
- man page: path_resolution(7), permissions and the superuser
- man page: inode(7), owner, group and mode bits
- man page: chmod(1)
- man page: chmod(2), who may change a mode
- man page: chown(1)
- man page: chown(2), who may change an owner
- man page: umask(2)
- man page: open(2), the mode applies when a file is created
- man page: credentials(7), user and group IDs
- man page: passwd(5)
- GNU coreutils: Structure of file mode bits
- GNU coreutils: Numeric modes
- GNU coreutils: Setting permissions (symbolic modes)
- GNU coreutils: The umask and protection
- GNU coreutils: What ls -l lists
- OpenSSH manual: ssh(1), private key file permissions
- Docker docs: Dockerfile reference (USER, COPY --chown)
- Docker docs: Running containers (default user, capabilities)
- Docker docs: Bind mounts
- Docker docs: Isolate containers with a user namespace