Linux File Permissions
- Read and decode the 10-character permission string from ls -l without hesitating
- Translate between symbolic (rwx) and octal (755) notation in both directions, fluently
- Set permissions with chmod two ways — absolute octal and surgical symbolic — and know when each fits
- Reassign owner and group with chown, and diagnose the 'permission denied' the wrong owner causes
- Explain what SUID, SGID, and the sticky bit do, recognise them in ls -l, and set them safely
- Read and set the immutable and append-only attributes with lsattr and chattr, and explain why they refuse even root
Why permissions exist (the mental model first)
Get the model — who can do what, to what — before a single command.
Before you learn a single command, let's get the idea straight — because once the model clicks, the syntax is just shorthand for something you already understand.
Linux is a multi-user system. Even on your own laptop there are dozens of "users" — you, plus system accounts like
root,www-data, andsshdthat run services. If every account could read and overwrite every file, one compromised web server could rewrite your login program, and any user could read everyone else's private keys. That's not a system anyone would trust.So Linux attaches a small, fixed rule to every single file and directory answering one question: who is allowed to touch this, and how?
Here's the picture to keep in your head. Think of each file as a room with three sets of keys:
- one key for the owner — usually the person who created the file;
- one key for a single group — a team of users (say,
developersorwww-data); - one key for others — everyone else on the system.
And for each key, the system records what that key is allowed to do: read the room's contents, write (change) them, or execute (run the file, or enter the directory).
That's the entire model: three classes (owner, group, others) × three actions (read, write, execute) = nine bits on every file. Master those nine bits and everything else in this lesson is just two different ways of writing them down.
Read a permission string
Decode the 10 characters at the front of every ls -l line, in chunks.
The place you see permissions is
ls -l. Every line begins with a 10-character string that looks cryptic until you learn to read it in chunks — and there are only ever four chunks.The first character is the file type. The next nine are three groups of
rwx, in a fixed order: owner, then group, then others. Read left to right, you're reading "what kind of thing is this, and what may each of the three classes do to it."Example — not runnable on your lab box
ls -l file.sh-rwxr-xr-- 1 alice devs 4096 Jan 15 10:22 file.sh
- r
- read — view a file's contents, or list a directory's entries
- w
- write — change a file's contents, or add/remove files in a directory
- x
- execute — run a file as a program, or cd INTO / pass THROUGH a directory
Split -rwxr-xr--into its four chunks and it reads like a sentence: •-→ the type: a plain regular file. (Adhere would mean a directory,la symlink.) •rwx→ the owner (alice) may read, write, and execute. •r-x→ the group (devs) may read and execute, but NOT write. •r--→ others may only read. The two names further along the line —aliceanddevs— are the file's owner and group. So this line says: alice owns a script she can fully control, her devs teammates can read and run it, and everyone else can only look.Check yourself
For the string -rw-r----- , what exactly can members of the file's GROUP do?
Octal: the numeric shorthand everyone uses
Convert rwx ↔ three-digit numbers in your head, both directions.
Typing
rwxr-xr-xgets tedious fast, so almost everyone writes permissions as a three-digit number instead — one digit per class. The trick is that each permission has a value, and you simply add them up within each class:- r = 4
- w = 2
- x = 1
Why those numbers? They're the three bits of binary — 4, 2, 1 — so every combination of rwx produces a unique total from 0 to 7. Think of it as a little panel of three light switches per class, where the switches are worth 4, 2, and 1.
Work an example the slow way once and you'll never need to again. Take
rwxr-xr-x:- owner
rwx= 4 + 2 + 1 = 7 - group
r-x= 4 + 0 + 1 = 5 - others
r-x= 4 + 0 + 1 = 5
…which gives 755. Going the other way is just as easy: to expand
644, split the digits (6, 4, 4) and turn each back into switches — 6 = 4+2 =rw-, 4 =r--, 4 =r--→rw-r--r--.Example — not runnable on your lab box
stat -c '%A %a %n' file.sh config.txt id_rsa-rwxr-xr-x 755 file.sh -rw-r--r-- 644 config.txt -rw------- 600 id_rsa
statis the fastest way to see both notations side by side:%Aprints the symbolic string,%athe octal,%nthe name. Three modes are worth committing to memory right now because you'll type them constantly: • 755 — programs, scripts, and directories: the owner can change them, everyone can run/enter them. • 644 — normal files and configs: the owner can edit, everyone can read. • 600 — private files: owner-only. This is the correct mode for secrets like SSH private keys, and tools likesshwill refuse to use a key that's looser than this.Check yourself
What octal number represents rw-r--r-- ?
chmod: change permissions two ways
Set the mode with absolute octal and with surgical symbolic edits — and know which to reach for.
chmod— short for change mode — is how you set those nine bits. It has two styles, and fluent admins use both depending on the job:- Octal (absolute): you state the entire final mode as a number, e.g.
chmod 755 file. It overwrites all nine bits at once, regardless of what was there before. Use it when you know exactly what the mode should end up as. - Symbolic (relative): you adjust specific bits, e.g.
chmod u+x file. You name a class —user,group,others, orall — an operator —+add,-remove,=set-exactly — and the permissions. Use it when you want to flip one thing without recomputing the whole number.
Let's build a worked-example ladder, from the everyday to the advanced.
Example — not runnable on your lab box
chmod 755 script.sh && ls -l script.sh-rwxr-xr-x 1 alice devs 128 Jan 15 10:40 script.sh
- 755
- absolute mode: owner rwx, group r-x, others r-x — replaces whatever was there
Example 1 — the everyday octal set. This makes script.shrunnable by its owner and runnable/readable by everyone else, in one shot.chmoditself prints nothing on success (silence is good news) — so the habit to build is chaining&& ls -lto see the result. Notice the string is now exactlyrwxr-xr-x, matching the 755 you asked for.Example — not runnable on your lab box
chmod u+x deploy.sh && chmod go-w shared.conf && chmod a+r public.txt- u / g / o / a
- which class: user(owner) / group / others / all
- + / - / =
- add the bits / remove the bits / set exactly these bits
Example 2 — three surgical symbolic edits. Each one touches only the bits you name and leaves the rest untouched: • u+x— give the user (owner) execute. Nothing else changes — perfect for "I just need to make my script runnable." •go-w— remove write from group and others at once (you can combine classes). A quick way to lock a file down to owner-writable. •a+r— add read for all three classes.ais shorthand forugo. Reach for symbolic when the sentence in your head is "add/remove this one thing," not "set the whole mode."Runnable in the fund-perms-ownership room — that room’s setup plants the files it reads
chmod -R 750 /srv/app && ls -ld /srv/appdrwxr-x--- 12 alice devs 4096 Jan 15 10:41 /srv/app
- -R
- recursive — apply to the directory and every file/subdirectory within
Example 3 — recursive, for a whole tree. -Rapplies the mode to a directory and everything inside it. Here the whole/srv/apptree becomes owner-full, group read+enter, others locked out.ls -ldshows the directory's own line (thedconfirms it's a directory). Recursion is powerful — and, as the next callout warns, easy to misuse.- Octal (absolute): you state the entire final mode as a number, e.g.
chown: who OWNS the file
Reassign owner and group — and see why the wrong owner causes 'permission denied'.
Permissions decide what each class may do. Ownership decides who the owner actually is, and which group applies. They work together: a
rwxfor the owner is worthless to you if you're not the owner.chown(change owner) sets both — and because it can hand your files to another user, changing an owner generally requiresroot/sudo.Example — not runnable on your lab box
sudo chown alice:devs report.txt && ls -l report.txt-rw-r--r-- 1 alice devs 2048 Jan 15 10:45 report.txt
- owner:group
- set both at once
- :group
- group only (colon, no owner) — same as chgrp
- -R
- recursive through a directory tree
The common form is chown owner:group file. There are three shapes worth knowing: •alice:devs— set owner and group in one step (shown here). •alice— owner only, group unchanged. •:devs— group only (the colon with nothing before it).chgrp devs filedoes the exact same thing. After anychown, re-runls -land confirm both names are what you expect — a wrong group is a subtle bug that surfaces much later.The special bits: SUID, SGID, sticky
Understand the fourth octal digit and the three behaviours it unlocks.
There's one more layer, and it explains some things you've probably already seen and wondered about (like how a normal user can change their own password, which lives in a root-only file). Beyond the nine
rwxbits sit three special bits, written as a fourth, leading octal digit — so you'll see modes like4755,2775,1777. Each one solves a specific real-world problem:- SUID (value 4) on an executable — the program runs as its owner, not as whoever launched it. Think of it as a borrowed badge:
/usr/bin/passwdis owned by root and SUID, so when you run it, it briefly acts with root's authority — just enough to update your entry in the root-only/etc/shadow— and nothing more. One tightly-controlled door instead of giving you the master key.
- SGID (value 2) on a directory — new files created inside inherit the directory's group instead of your default group. This is the magic that makes a team's shared folder stay shared: nobody has to remember to fix the group on every new file.
- Sticky bit (value 1) on a directory — even when everyone can write in it, only the owner of a file may delete that file. This is exactly how
/tmpcan be world-writable yet safe: you can create files there, but you can't delete mine.
ls -ld /usr/bin/passwd /tmp-rwsr-xr-x 1 root root 59976 /usr/bin/passwd drwxrwxrwt 10 root root 4096 /tmp
The special bits reuse the execute position in the string, so learn to spot them there: • The `s` in rwsonpasswdis SUID — it runs as its owner, root. • The `t` inrwton/tmpis the sticky bit — world-writable, but only-owner-deletes. One subtlety: a lowercases/tmeans the special bit and the underlyingxare both set. A capitalS/Tmeans the special bit is set butxis not — usually a mistake worth a second look.Example — not runnable on your lab box
chmod 2775 /srv/shared && chmod +t /srv/scratch && sudo chmod u+s /usr/local/bin/tool- leading 4 / 2 / 1
- octal: SUID / SGID / sticky
- u+s / g+s / +t
- symbolic: SUID / SGID / sticky
Either notation sets them. Octal: add the special digit in front — 2775= SGID + rwxrwxr-x. Symbolic:g+s(SGID),u+s(SUID),+t(sticky). The rule of thumb: for a team collaboration directory you want SGID (2/g+s); for a shared scratch directory you want the sticky bit (1/+t). SUID you'll set rarely and audit often (next callout).find / -perm -4000 -type f 2>/dev/null/usr/lib/openssh/ssh-keysign /usr/lib/dbus-1.0/dbus-daemon-launch-helper /usr/bin/su /usr/bin/sudo /usr/bin/chfn /usr/bin/chsh /usr/bin/umount /usr/bin/passwd /usr/bin/mount /usr/bin/at /usr/bin/newgrp /usr/bin/gpasswd /usr/bin/fusermount3
- -perm -4000
- match files that have AT LEAST the SUID bit (the leading '-' means 'at least these bits')
- -type f
- regular files only
- 2>/dev/null
- discard the permission-denied noise from unreadable directories
The audit you'll actually run. find / -perm -4000 -type flists every SUID file on the system — each one is a program that runs as its owner, so each is a potential privilege-escalation path worth accounting for.-perm -4000means "has at least the SUID bit set";2>/dev/nullhides the harmless "permission denied" errors from directories you can't enter. This exact command shows up on the security exams and in real hardening checklists. The list above is what a stock Ubuntu lab box returns: thirteen files, every one shipped by a package. The skill is knowing which belong there, so the fourteenth stands out.Check yourself
Which special bit makes new files in a shared directory automatically belong to that directory's group?
- SUID (value 4) on an executable — the program runs as its owner, not as whoever launched it. Think of it as a borrowed badge:
Below the permissions: immutable and append-only attributes
Read and set the inode flags that refuse even root, and know where they bite you later.
Everything so far has been the permission check: the kernel compares who you are with the file's owner, group and nine bits. Root mostly walks straight past that check (strictly, root holds the
CAP_DAC_OVERRIDEcapability, which overrides it). But the mode is not the last word on a file. Every inode on ext2/3/4, XFS and Btrfs (and tmpfs on kernels from 6.0) also carries a small set of attribute flags, and the kernel enforces two of them separately from the permission bits, with no root override:- `i`, immutable. Nobody may write to the file, delete it, rename it, hard-link to it, or change its metadata (mode, owner, timestamps). That includes root.
- `a`, append-only. The file can only be opened for appending.
>>works.>, truncation, rename and delete don't.
ls -landstatnever show these flags, so a file can look like an ordinary-rw-r--r-- root rootand still refuse root. You read them with `lsattr` and set them with `chattr`:+isets a flag and-iclears it. They are not ACLs (getfacl) and not the user extended attributes thatgetfattrshows. Those are different mechanisms with confusingly similar names.printf 'nameserver 10.0.0.53\n' > ~/pinned.conf && chattr +i ~/pinned.conf && lsattr ~/pinned.conf----i---------e------- /root/pinned.conf
- chattr +i / -i
- set / clear the immutable attribute
- chattr +a / -a
- set / clear the append-only attribute
- lsattr -d
- show a directory's own flags instead of listing its contents
- lsattr -R
- recurse into subdirectories
- lsattr -a
- include dotfiles, which -R alone skips, exactly like ls
lsattrprints one flags column, then the name. Like the rwx string, each position is one possible attribute, and a-means that one is off. Theiin the fifth slot is the flag you just set. Theenear the end is not something anyone set. ext4 marks every file whose data is stored in extents, so treat it as background. On a filesystem other than ext4 you may not see it at all. When you scanlsattroutput, look for anior ana.ls -l ~/pinned.conf | cut -c1-10; echo 'nameserver 6.6.6.6' | tee -a ~/pinned.conf >/dev/null; rm -f ~/pinned.conf; mv ~/pinned.conf ~/moved.conf; chmod 600 ~/pinned.conf; ln ~/pinned.conf ~/hard.conf-rw-r--r-- tee: /root/pinned.conf: Operation not permitted rm: cannot remove '/root/pinned.conf': Operation not permitted mv: cannot move '/root/pinned.conf' to '/root/moved.conf': Operation not permitted chmod: changing permissions of '/root/pinned.conf': Operation not permitted ln: failed to create hard link '/root/hard.conf' => '/root/pinned.conf': Operation not permitted
You are root, and the mode is an ordinary -rw-r--r--that gives the owner (root) write access. Every change is refused anyway: the append, the delete (evenrm -f, whose-fusually hides errors, reports this one), the rename, the mode change and the hard link. Read the wording. The permission check says Permission denied. This says Operation not permitted, the kernel refusing for a reason other than the permission bits. Earlier in this lesson that message meant "you don't own the file". When root gets it, on a file root owns, runlsattrnext.So who can clear the flag? A process that holds the `CAP_LINUX_IMMUTABLE` capability. An ordinary root shell does. Root is not exempt from the flag, but root can take it off, so the flag never stops an admin who means it. What it stops is everything else: a script's stray
>, a config-management run, anrm -rfin the wrong directory, and any root process that has dropped that capability. A hardened service whose systemd unit setsCapabilityBoundingSet=without it is one example. Once a capability is gone from a process's bounding set, neither it nor its children can get it back, even as uid 0. (The access-control lesson covers capabilities: root's power split into about forty separate pieces.) Watch it happen:capsh --drop=cap_linux_immutable -- -c 'id -u; chattr -i ~/pinned.conf'0 chattr: Operation not permitted while setting flags on /root/pinned.conf
- --drop=cap_linux_immutable
- remove this capability from the bounding set before running the command
- -- -c '…'
- everything after -- goes to the shell capsh starts
capshremoves one capability from the bounding set and runs a shell.id -uprints0, so the shell is still root.chattr -iis refused anyway, because setting or clearingiandarequiresCAP_LINUX_IMMUTABLE, and uid 0 alone is not enough. The file is still pinned.printf 'line1\n' > ~/app.log && chattr +a ~/app.log && echo line2 >> ~/app.log && cat ~/app.log && lsattr ~/app.logline1 line2 -----a--------e------- /root/app.log
Now the append-only flag, the ain the sixth slot. The>>append went through, so a log with this flag keeps growing normally. Next, watch what it refuses.echo clobber | tee ~/app.log >/dev/null; truncate -s 0 ~/app.log; rm -f ~/app.logtee: /root/app.log: Operation not permitted truncate: cannot open '/root/app.log' for writing: Operation not permitted rm: cannot remove '/root/app.log': Operation not permitted
Overwriting ( teewithout-a, the same as>), truncating and deleting are all refused. Nothing already in the file can be rewritten, emptied or removed, which is the point of+aon a log you want to be tamper-evident: an intruder (or a careless cleanup) can add lines, but cannot quietly remove the ones that record what happened.sed -i 's/line1/LINE1/' ~/app.logsed: cannot rename /root/sedE1hC1Q: Operation not permitted
- -i
- "in place": write a new file, then rename it over the original
sed -isounds like an in-place edit, but it doesn't edit in place. It writes the edited text to a new temporary file in the same directory (the randomsed…name, different every run), then renames that file over the original. Replacing the file would throw its history away, so+aforbids the rename, and sed reports it.perl -i, and editors that save by writing a new copy, fail the same way. sed removed its temporary file, so nothing is left behind.ls -i ~/app.log; chattr -a ~/app.log && sed -i 's/line1/LINE1/' ~/app.log && chattr +a ~/app.log; ls -i ~/app.log; lsattr ~/app.log; cat ~/app.log1901840 /root/app.log 1901854 /root/app.log -----a--------e------- /root/app.log LINE1 line2
The correct way to edit a protected file: clear the flag, edit, set the flag again. The two ls -ilines show the inode number before and after (your numbers will differ), and they are different. sed really did make a new file. Attributes belong to the inode, so the new file started with no flags, and the+ayou see is only there because the command set it again. Leave out that lastchattr +aand the log silently loses its protection, with nothing to tell you so.cp -a ~/app.log ~/app.copy && lsattr ~/app.log ~/app.copy-----a--------e------- /root/app.log --------------e------- /root/app.copy
- -a
- archive: preserve mode, ownership, timestamps, links and xattrs (but not chattr flags)
cp -a("archive", preserve everything) copies the mode, owner, timestamps and extended attributes, but not thei/aflags. GNUtarandrsyncdon't carry them either. So a file restored from a backup comes back unprotected, and nothing says so.These flags show up on both sides of an incident.
- Hardening and containment: pin what must not change while you work, such as
/etc/passwd,/etc/shadowor anauthorized_keysfile mid-incident, and put+aon a log you need to be tamper-evident (clearing it first for rotation, as above). - Detection: attackers know this trick too. A dropped cron job, systemd unit or SSH key pinned with
+imakes your cleanup fail with an error that looks like a permissions problem, and the persistence survives. That is whylsattrbelongs in a triage pass, run before you start removing things, and not only in a hardening checklist. The sweep looks like this:
lsattr -Ra ~ 2>/dev/null | grep -E '^[-a-zA-Z]*[ia][-a-zA-Z]* '----i---------e------- /root/pinned.conf -----a--------e------- /root/app.log
- -Ra
- recurse, including dotfiles
- 2>/dev/null
- hide errors from special files (sockets, devices) that have no flags to read
lsattr -Rawalks the tree, dotfiles included. Leave out-aand it skips them exactly aslsdoes, and attackers like hidden files. Thegrepkeeps only lines whose flags column (letters and dashes, no/) contains anior ana. That drops the/dir:header lines-Rprints, and the many files that show onlye. On a real host, point it at the places persistence lives:/etc(cron, systemd, sudoers),/root,/home,/var/spooland/usr/local/bin. The two lines may come out in either order.chattr -i ~/pinned.conf && chattr -a ~/app.log && rm -f ~/pinned.conf ~/app.log ~/app.copy && lsattr -Ra ~ 2>/dev/null | grep -cE '^[-a-zA-Z]*[ia][-a-zA-Z]* '0
Clean up, and note the order. The flags are stored on disk, so they outlive your shell, your session and a reboot. Clear them before deleting test files, or the rmfails exactly as it did above. The final count of0confirms nothing in your home directory still carriesiora.Check yourself
You are root.
echo x > /var/log/secure.logfails with 'Operation not permitted', yetecho x >> /var/log/secure.logworks, andls -lshows an ordinary -rw------- root root. What is going on, and which command proves it?Now do it — in your live terminal
Read it, then run it. These execute on the real sandbox to your right.
Reading permissions is one thing; the knowledge sticks when your fingers do it. You have a real shell to the right — run this exact loop (create → inspect → change → re-inspect) and watch the string change each time. This is precisely the loop a "set the correct permissions" lab grades, so you're rehearsing the real thing.
touch demo.sh && ls -l demo.shBaseline. A brand-new file is typically 644(-rw-r--r--): readable by all, writable by you, executable by nobody — so it won't run as a script yet. Confirm you see that before you change anything.chmod 750 demo.sh && ls -l demo.shAbsolute set: owner rwx, groupr-x, others nothing. The string should now read-rwxr-x---. You just made it runnable by you and your group, and invisible-to-run for everyone else.chmod o+r demo.sh && stat -c '%A %a %n' demo.shOne surgical symbolic edit — add read for others — on top of the previous mode. %ashould now show754and%A-rwxr-xr--. Seeing the symbolic and octal forms agree is the moment the two notations finally feel like one thing.Runnable in the immutable-attributes room — that room’s setup plants the files it reads
lsattr -Ra /root/case/srv 2>/dev/null | grep -E '^[-a-zA-Z]*[ia][-a-zA-Z]* 'In the Immutable and Append-Only lab this is the triage sweep from the attributes step, pointed at the case tree. It lists the files someone pinned or made append-only before you arrived (different files every session). Run it before you change anything, because the lab's later tasks add and remove flags.
- Every file carries nine permission bits: three classes (owner / group / others) × three actions (read / write / execute). On a directory, x means 'enter/traverse', not 'run'.
- Octal adds r=4, w=2, x=1 per class: memorise 755 (programs & dirs), 644 (readable files), 600 (secrets). Expand a digit by splitting it back into 4/2/1 switches.
- chmod octal (e.g. 755) sets the whole mode absolutely; chmod symbolic (u/g/o/a with +/-/=) flips specific bits; -R recurses. Never reach for 777.
- chown owner:group sets ownership — the wrong owner causes 'permission denied' even when the rwx bits look fine. www-data ownership, not 777, is the web-server fix.
- Special bits are a leading octal digit: SUID (4, run-as-owner, audit with find / -perm -4000), SGID (2, directory group inheritance), sticky (1, only-owner-deletes, e.g. /tmp).
- Below the mode sit inode attributes that ls -l never shows: chattr +i (immutable) and +a (append-only) refuse even root, because changing them needs CAP_LINUX_IMMUTABLE, not just uid 0. Read them with lsattr -Ra; sed -i, cp -a, tar and rsync all leave them behind.
| Octal | Symbolic | Typical use |
|---|---|---|
| 755 | rwxr-xr-x | Scripts, programs, directories — owner edits, all run/enter |
| 644 | rw-r--r-- | Regular files & configs — owner edits, all read |
| 600 | rw------- | Secrets: SSH private keys, credentials (owner only) |
| 700 | rwx------ | Private directories & personal scripts (owner only) |
| 2775 | rwxrwsr-x | SGID team collaboration directory (group inheritance) |
| 1777 | rwxrwxrwt | Sticky world-writable scratch space (like /tmp) |
| 4755 | rwsr-xr-x | SUID program that runs as its owner (like passwd) |
