Signal9Signal9 — home
online
all lessons

Linux File Permissions

Beginner50 min8 steps
What you'll learn
  • 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
  1. 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, and sshd that 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, developers or www-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.

  2. 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. (A d here would mean a directory, l a 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 — alice and devs — 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?

  3. Octal: the numeric shorthand everyone uses

    Convert rwx ↔ three-digit numbers in your head, both directions.

    Typing rwxr-xr-x gets 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
    stat is the fastest way to see both notations side by side: %A prints the symbolic string, %a the octal, %n the 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 like ssh will refuse to use a key that's looser than this.

    Check yourself

    What octal number represents rw-r--r-- ?

  4. 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, or all — 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.sh runnable by its owner and runnable/readable by everyone else, in one shot. chmod itself prints nothing on success (silence is good news) — so the habit to build is chaining && ls -l to see the result. Notice the string is now exactly rwxr-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. a is shorthand for ugo. 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/app
    drwxr-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. -R applies the mode to a directory and everything inside it. Here the whole /srv/app tree becomes owner-full, group read+enter, others locked out. ls -ld shows the directory's own line (the d confirms it's a directory). Recursion is powerful — and, as the next callout warns, easy to misuse.
  5. 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 rwx for 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 requires root/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 file does the exact same thing. After any chown, re-run ls -l and confirm both names are what you expect — a wrong group is a subtle bug that surfaces much later.
  6. 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 rwx bits sit three special bits, written as a fourth, leading octal digit — so you'll see modes like 4755, 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/passwd is 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 /tmp can 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 rws on passwd is SUID — it runs as its owner, root. • The `t` in rwt on /tmp is the sticky bit — world-writable, but only-owner-deletes. One subtlety: a lowercase s/t means the special bit and the underlying x are both set. A capital S/T means the special bit is set but x is 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 f lists 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 -4000 means "has at least the SUID bit set"; 2>/dev/null hides 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?

  7. 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_OVERRIDE capability, 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 -l and stat never show these flags, so a file can look like an ordinary -rw-r--r-- root root and still refuse root. You read them with `lsattr` and set them with `chattr`: +i sets a flag and -i clears it. They are not ACLs (getfacl) and not the user extended attributes that getfattr shows. 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
    lsattr prints one flags column, then the name. Like the rwx string, each position is one possible attribute, and a - means that one is off. The i in the fifth slot is the flag you just set. The e near 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 scan lsattr output, look for an i or an a.
    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 (even rm -f, whose -f usually 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, run lsattr next.

    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, an rm -rf in the wrong directory, and any root process that has dropped that capability. A hardened service whose systemd unit sets CapabilityBoundingSet= 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
    capsh removes one capability from the bounding set and runs a shell. id -u prints 0, so the shell is still root. chattr -i is refused anyway, because setting or clearing i and a requires CAP_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.log
    line1
    line2
    -----a--------e------- /root/app.log
    Now the append-only flag, the a in 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.log
    tee: /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 (tee without -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 +a on 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.log
    sed: cannot rename /root/sedE1hC1Q: Operation not permitted
    -i
    "in place": write a new file, then rename it over the original
    sed -i sounds 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 random sed… name, different every run), then renames that file over the original. Replacing the file would throw its history away, so +a forbids 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.log
    1901840 /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 -i lines 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 +a you see is only there because the command set it again. Leave out that last chattr +a and 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 the i/a flags. GNU tar and rsync don'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/shadow or an authorized_keys file mid-incident, and put +a on 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 +i makes your cleanup fail with an error that looks like a permissions problem, and the persistence survives. That is why lsattr belongs 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 -Ra walks the tree, dotfiles included. Leave out -a and it skips them exactly as ls does, and attackers like hidden files. The grep keeps only lines whose flags column (letters and dashes, no /) contains an i or an a. That drops the /dir: header lines -R prints, and the many files that show only e. On a real host, point it at the places persistence lives: /etc (cron, systemd, sudoers), /root, /home, /var/spool and /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 rm fails exactly as it did above. The final count of 0 confirms nothing in your home directory still carries i or a.

    Check yourself

    You are root. echo x > /var/log/secure.log fails with 'Operation not permitted', yet echo x >> /var/log/secure.log works, and ls -l shows an ordinary -rw------- root root. What is going on, and which command proves it?

  8. 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.sh
    Baseline. 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.sh
    Absolute set: owner rwx, group r-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.sh
    One surgical symbolic edit — add read for others — on top of the previous mode. %a should now show 754 and %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.
Recap
  • 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.
Modes you'll use constantly
OctalSymbolicTypical use
755rwxr-xr-xScripts, programs, directories — owner edits, all run/enter
644rw-r--r--Regular files & configs — owner edits, all read
600rw-------Secrets: SSH private keys, credentials (owner only)
700rwx------Private directories & personal scripts (owner only)
2775rwxrwsr-xSGID team collaboration directory (group inheritance)
1777rwxrwxrwtSticky world-writable scratch space (like /tmp)
4755rwsr-xr-xSUID program that runs as its owner (like passwd)