For five lessons we have been seeing the same thing in every ls -l and every stat without explaining it: -rw-r-----, Uid: (990/meteora), Access: (0640/-rw-r-----). We have protected 2026-08-31.dat from a power cut with the journal, from a disk failure with RAID 1 and from a flipped bit with a CRC per record. But we have not answered the most basic question of all: who can read that file, and who can delete it?

The answer is a twelve-bit model designed in 1971 that remains, fifty years later, the basis of access control on every UNIX system. Its virtue is simplicity; its danger, that this simplicity hides surprising behaviors. That a directory's permissions mean different things from a file's. That you can delete a file you cannot read. That permission is checked on every directory along the path, not just on the final file. And that a program run by an ordinary user can, through a single bit, modify /etc/shadow.

This lesson explains the twelve bits one by one, with the reason for each behavior; it adds POSIX ACLs for the cases the twelve bits do not cover; it presents file attributes, which protect even from root; it compares the model with Windows's; and it ends by setting and justifying the exact permissions of every one of Meteora's files, with the complete commands and an audit of what is usually wrong.

Contents

  1. The classic model: user, group and others
  2. The r, w and x bits on files and on directories
  3. Symbolic and octal notation: chmod, chown, chgrp
  4. umask: how the final permission is calculated
  5. The special bits: setuid, setgid and sticky
  6. How the kernel checks an access, step by step
  7. POSIX access control lists
  8. Extended attributes and file attributes
  9. Comparison with the Windows model
  10. Meteora's permissions, justified
  11. Serious mistakes and how to audit them
  12. Closing module 4

The classic model: user, group and others

Every file has in its inode (04-01) two numeric identifiers and twelve mode bits:

  • Owner's UID: who owns it. In Meteora, 990 (meteora).
  • Owning group's GID: which group has special access. Also 990.
  • Mode: twelve bits saying what each class of user can do.

The inode stores numbers, not names. The translation into meteora is done by ls and stat consulting /etc/passwd and /etc/group; if you delete the user's entry, ls -l will show a bare 990 and the file will still be theirs.

On any access, the kernel classifies the requester into exactly one of three classes:

Class Abbreviation Condition
User (owner) u Their effective UID is the file's UID
Group g Not the owner, but one of their groups is the file's GID
Others o Neither of the above

And there is the first subtlety, which produces a classic bewilderment:

The three classes are mutually exclusive and evaluated in that order. If you are the owner, only the user bits apply, even if the group bits are more permissive.

$ ls -l odd.txt
----r--r-- 1 meteora meteora 100 Sep  1 15:10 odd.txt
$ id
uid=990(meteora) gid=990(meteora)
$ cat odd.txt
cat: odd.txt: Permission denied

The owner cannot read their own file, while any other member of the meteora group can. It is not a bug: the class is decided first and the permissions are applied afterwards. The owner can, however, always run chmod on it and fix it.

Each class has three bits, r, w and x, giving the nine bits ls -l shows, plus three special bits we will see in section 5.

The r, w and x bits on files and on directories

Here is the heart of the lesson, and what causes the most mistakes: the three bits mean different things depending on the type.

Bit On a file On a directory
r Read the content List the names it contains
w Modify the content Create, delete and rename entries
x Execute it as a program Traverse it: use it in a path and reach what is inside

Remember 04-02: a directory is a file whose content is the list of (name, inode) pairs. With that, the three meanings stop being arbitrary and become inevitable:

  • r = read the directory's content = read the list of names. That is exactly what ls does.
  • w = write to the directory's content = add or remove entries. Creating a file is adding an entry; deleting it is removing one. Both are writes to the directory, not to the file.
  • x = use the directory to reach something = being able to resolve one path component (04-02) and look up the inode associated with a name you already know.

Out of that come the four consequences you need to internalize.

1. x without r: you can enter but not look. It is the "dark directory":

chmod 0711 /home/analyst          # rwx for the owner, only x for everybody else

Anybody can access /home/analyst/report.pdf if they know the exact name, but ls /home/analyst fails with "Permission denied". It is the classic configuration for home directories on shared servers and for a web server's root: what is asked for is served, the content is not enumerated.

2. r without x: you can see the names and nothing else. ls data/ lists a.txt, but ls -l data/ fails on every entry — because reading each file's inode requires traversing the directory — and shows a line of question marks, -????????? ? ? ? ? a.txt. Those question marks are the unmistakable signature of r without x, a useless combination that is almost always a mistake.

3. You can delete a file you can neither read nor write. This is the most surprising behavior, and it is now obvious:

$ ls -ld /tmp/tests ; ls -l /tmp/tests/secret.txt
drwxrwxr-x 2 joan  joan   4096 Sep  1 15:20 /tmp/tests
-r-------- 1 root  root    128 Sep  1 15:20 /tmp/tests/secret.txt
$ cat /tmp/tests/secret.txt
cat: ...: Permission denied                     ← I cannot read it
$ rm -f /tmp/tests/secret.txt                   ← but I CAN delete it!

Deleting does not touch the file. unlink (04-02) removes an entry from the directory and decrements the inode's link count. The operation is a write to the directory, and that is why the permission checked is w on the directory, not on the file. The file's permissions are completely irrelevant.

It is the reason the sticky bit exists, and why a world-writable directory without that bit is a bomb.

4. Changing a file's content does not require permission on the directory, and vice versa. The two permissions are independent and protect different things: the file's protects its content, the directory's protects its name.

Operation Permission needed on the file Permission needed on the directory
Read the content r x along the whole path
Modify the content w x along the whole path
Create a file w + x
Delete a file none w + x
Rename a file none w + x on source and destination
List the directory r
Enter it (cd) x
View metadata (stat) x along the whole path

Symbolic and octal notation: chmod, chown, chgrp

The nine bits are expressed in two equivalent ways. In octal, each digit encodes three bits by adding 4 (read) + 2 (write) + 1 (execute/traverse): so 6 is rw-, 5 is r-x and 7 is rwx. The most common values, with their correct use:

Octal Symbolic What for
644 rw-r--r-- Public read-only file
640 rw-r----- A service's data file (Meteora)
600 rw------- Secrets: keys, passwords
755 rwxr-xr-x Executable program, public directory
750 rwxr-x--- A service's directory (Meteora)
700 rwx------ Private directory
2770 rwxrws--- Directory shared by a group (setgid)
1777 rwxrwxrwt /tmp: world-writable, with the sticky bit
777 rwxrwxrwx Never. Ever. See section 11

The commands:

chmod 640 /etc/meteora/meteora.conf     # octal: sets all NINE bits at once
chmod g+r,o-rwx file                    # symbolic: modifies ONLY what is stated
chmod -R u=rwX,g=rX,o= /var/lib/meteora # the capital X: see below
chown meteora:meteora file              # change owner AND group
chgrp analysts file                     # group only

Three clarifications that prevent disasters. Octal is absolute and symbolic is relative: chmod 640 sets exactly those bits, while chmod g+r adds one without touching the others; in a chmod -R over a tree, octal is dangerous because it applies the same thing to files and to directories. The capital X is the solution to that problem, because it means "execute/traverse only if it is a directory or if it already had some x": chmod -R u=rwX,g=rX,o= leaves directories navigable without turning data files into executables. And only the owner and root can run chmod, while only root can run chown to give a file away to another user: if an ordinary user could, they would dodge disk quotas and could plant compromising files in somebody else's account.

umask: how the final permission is calculated

When a program creates a file, it passes a mode to open() (04-04) — typically 0666 — or to mkdir — typically 0777. But the file does not end up with those permissions, because the kernel applies a per-process mask: the umask.

Final permissions = requested mode AND (NOT umask)

In practice, it is easier to see it as a subtraction of bits: the umask says which permissions to remove.

$ umask
0027

File:       requested mode 666   rw- rw- rw-
            umask          027   --- -w- rwx      (bits to remove)
            RESULT         640   rw- r-- ---      ✔

Directory:  requested mode 777   rwx rwx rwx
            umask          027   --- -w- rwx
            RESULT         750   rwx r-x ---      ✔

Note the important detail: the umask never adds the execute bit. Programs request 0666 for regular files precisely so that a data file is never born executable, however permissive the umask is. A program's x is set afterwards by the compiler or the installer with an explicit chmod.

The usual values and what they produce:

umask Files Directories Use
022 644 755 The default on most distributions. Everybody reads
002 664 775 Group work: the group writes too
027 640 750 Services: the group reads, nobody else anything
077 600 700 Maximum privacy: the owner only

Meteora uses umask 027, and that explains all the -rw-r----- and drwxr-x--- we have been seeing since 04-01. The justification is exact: the ingestor creates 2026-09-01.dat and needs the aggregator and meteo-api — members of the meteora group — to be able to read it, but no other user on the system to have access. 027 gives precisely that, without a single subsequent chmod.

An important warning: the default umask is 022, which leaves files readable by everybody; with it, 2026-09-01.dat would be born 644 and any user on the server could read the data. A service's umask is set with UMask=0027 in its systemd unit (module 7), not in .bashrc, because a daemon does not read shell profiles. And it is inherited across fork (02-01), so a script that sets it to 000 "so there are no problems" is creating 666 files and 777 directories without anybody noticing.

The special bits: setuid, setgid and sticky

The twelve mode bits are nine permission bits plus three special ones, which in octal form a fourth digit in front:

Bit Octal Where it appears in ls -l Value
setuid 4000 s in the user's x -rwsr-xr-x
setgid 2000 s in the group's x -rwxr-sr-x
sticky 1000 t in the others' x drwxrwxrwt

If the corresponding x bit is not set, the letter appears in uppercase (S, T), and that almost always indicates a mistake: a setuid without execute is useless.

setuid: how passwd writes to /etc/shadow

The problem is concrete. An ordinary user must be able to change their password, which lives in /etc/shadow:

$ ls -l /etc/shadow
-rw-r----- 1 root shadow 1847 Sep  1 09:12 /etc/shadow

Only root writes there. How, then, can any user modify it? The answer is the setuid bit:

$ ls -l /usr/bin/passwd
-rwsr-xr-x 1 root root 68208 Mar 14  2026 /usr/bin/passwd
   ↑
   the 's' is the setuid bit

The full walkthrough, step by step:

  1. User joan (UID 1000) runs /usr/bin/passwd; fork creates a child with real UID 1000 and effective UID 1000.
  2. execve loads the binary and sees the setuid bit. Then it does the decisive thing: it sets the effective UID to the file owner's, which is root (0).
  3. The process now runs with real UID 1000 (who launched it) and effective UID 0 (with which privileges it acts). The kernel checks permissions with the effective one.
  4. passwd can open /etc/shadow for writing. But first it carefully verifies identity: it consults the real UID to know who you really are, asks for the current password, and only allows changing that line.
  5. When it finishes, the process dies and the privilege dies with it.
Concept Value during passwd What it is for
Real UID 1000 (joan) Who you are: accounting, signals, auditing
Effective UID 0 (root) What you can do: it is what the kernel checks
Saved UID 0 Allows dropping and regaining the privilege

Why it is dangerous. A setuid root binary is a program anybody can run with root privileges, so any flaw in it is a complete privilege escalation: a buffer overflow, a command injection, a poorly validated environment variable, a temporary file with a TOCTOU (04-04), or simply an option that allows running an arbitrary program. The history of UNIX security is full of vulnerabilities in setuid binaries, and that is why the rule is blunt:

The less setuid, the better. Every setuid root binary on the system is an attack surface, and you must be able to justify each one.

Two points worth knowing. setuid is ignored on scripts: Linux does not honor it on files with #!, because the window between opening the script and running the interpreter allowed it to be swapped — a TOCTOU. And nosuid at mount time (04-03) makes the kernel ignore the bit across a whole volume, which is why data volumes are mounted that way.

setgid: on files and, above all, on directories

On an executable file, setgid is the group analogue of setuid: the effective GID becomes the file's. It is used to give access to a group resource without giving root; /usr/bin/wall, for example, is setgid tty.

On a directory it does something completely different and very useful:

A directory with setgid makes everything created inside inherit its group, instead of the primary group of whoever creates it. And subdirectories also inherit the setgid bit itself, so the property propagates across the whole tree.

Without setgid, if user analyst (primary group analyst) creates a file in /var/lib/meteora/, that file will belong to group analyst and the aggregator — which is in group meteorawill not be able to read it. With setgid:

sudo chgrp meteora /var/lib/meteora/readings
sudo chmod 2750    /var/lib/meteora/readings     # the 2 is setgid

$ ls -ld /var/lib/meteora/readings
drwxr-s--- 3 meteora meteora 4096 Sep  1 15:40 /var/lib/meteora/readings
      ↑ the 's' in the group

Now any file created in there will belong to group meteora, whoever creates it. It is the standard mechanism for directories shared by a team, and it guarantees group consistency without depending on each user's discipline.

The sticky bit and the case of /tmp

Remember consequence 3 of section 2: w on a directory allows deleting any entry, whatever the file's permissions are. Now apply that to /tmp, which by definition must be world-writable:

$ ls -ld /tmp
drwxrwxrwt 18 root root 4096 Sep  1 15:42 /tmp
         ↑ the 't' is the sticky bit

Without the t, any user could delete anyone else's temporary files, or worse: delete a service's session file and replace it with their own, which is a direct route to impersonation.

The sticky bit adds a restriction to directories:

In a directory with sticky, only these can delete or rename an entry: the file's owner, the directory's owner, or root. The directory's w permission stops being enough.

sudo chmod 1777 /tmp        # the 1 is the sticky
sudo chmod +t /run/meteora  # symbolic form

It applies to every directory writable by several users: /tmp, /var/tmp, /dev/shm and any shared exchange area. And there is one more thing to know: the sticky bit on files does nothing on modern Linux. It used to mean "keep this executable in swap"; today it is ignored.

How the kernel checks an access, step by step

When meteo-api runs open("/var/lib/meteora/readings/2026-08-31.dat", O_RDONLY), the kernel does this:

graph TB
    A["open() of the full path"] --> B["For EACH directory along the path:<br/>/, var, lib, meteora, readings<br/><b>do I have x permission?</b>"]
    B -->|"No on one of them"| Z["EACCES: Permission denied"]
    B -->|"Yes on all"| C{"Is the effective UID == 0?"}
    C -->|Yes| Y["Granted (almost always)"]
    C -->|No| D{"Is the effective UID ==<br/>the file's UID?"}
    D -->|Yes| E["Use ONLY the USER bits<br/>and decide"]
    D -->|No| F{"Is the effective GID or any<br/>supplementary group == the file's GID?"}
    F -->|Yes| G["Use ONLY the GROUP bits<br/>and decide"]
    F -->|No| H["Use the OTHERS bits<br/>and decide"]

The five points to retain from this diagram:

1. The x permission is checked on EVERY directory along the path. It is what the path resolution of 04-02 does, component by component. If /var/lib/meteora lacks the x for your class, it does not matter that the final file is rw-rw-rw-: you do not get there. It is the number one source of incomprehensible "Permission denied" errors, and it is diagnosed in a second with namei -l, which shows the permissions of each component:

$ namei -l /var/lib/meteora/readings/2026-08-31.dat
 drwxr-xr-x root    root    /
 drwxr-xr-x root    root    var
 drwxr-xr-x root    root    lib
 drwxr-x--- meteora meteora meteora           ← everything is decided here
 drwxr-s--- meteora meteora readings
 -rw-r----- meteora meteora 2026-08-31.dat

When somebody cannot access a file, this is the first command to run.

2. The classes are exclusive and the order is fixed. Owner, then group, then others: the first match decides and nothing else is looked at. Hence the ----r--r-- file its owner cannot read.

3. All supplementary groups count. A user belongs to a primary group and to several secondary ones (id shows them all), and any of them will put you in the group class. Important warning: groups are read at login time, so a usermod -aG does not affect open sessions or services that are already running.

4. Root bypasses almost everything. Effective UID 0 grants almost any access, with two notable exceptions: it cannot execute a file with no x bit at all, and it cannot write to an immutable file (section 8).

5. It is checked at open(), not at read(). Permissions are verified only once, on opening. If somebody afterwards runs chmod 000, the process that already has it open goes on reading without a problem, because its descriptor points to the entry of the open-file table (04-04) and not to the name or the mode. To cut off access you have to make it close the descriptor.

POSIX access control lists

The three-class model has a structural limit: it can only express permissions for one user, one group and everybody else. A real Meteora case exceeds it immediately: you have to give read-only access to /var/lib/meteora/readings to the user nuria, from the analysis team, without putting her in the meteora group — which would give her access to everything else in the service, including the configuration — and without opening up the "others" permissions.

With the nine bits it cannot be done. With POSIX ACLs, it can:

# Give nuria read and traverse on the directory, and read on the files
sudo setfacl -m u:nuria:rx  /var/lib/meteora/readings
sudo setfacl -R -m u:nuria:r /var/lib/meteora/readings/*.dat

# And have her inherit the permission on FUTURE files ("default" ACL)
sudo setfacl -d -m u:nuria:r /var/lib/meteora/readings

The result:

$ ls -ld /var/lib/meteora/readings
drwxr-s---+ 3 meteora meteora 4096 Sep  1 15:40 /var/lib/meteora/readings
          ↑ THIS '+' means "there is an extended ACL"

$ getfacl /var/lib/meteora/readings
# file: var/lib/meteora/readings
# owner: meteora
# group: meteora
# flags: -s-
user::rwx
user:nuria:r-x                ← the new entry
group::r-x
mask::r-x                     ← the CEILING of the named entries
other::---
default:user:nuria:r--        ← inheritance for what is created afterwards

The elements: user::rwx and group::r-x are the owner and the owning group, equivalent to the usual u and g bits; user:nuria:r-x and group:analysts:r-- are named entries, giving permissions to a specific user or group; other::--- is everybody else; default:... is what files created here will inherit; and mask::r-x is the mask, the effective maximum of all the named entries and of the group.

The mask is the part that confuses everybody, and it deserves a clear explanation. The effective permissions of any named entry are the intersection of that entry with the mask. If the mask is r-- and nuria's entry is rwx, nuria gets only r--, and getfacl says so explicitly:

user:nuria:rwx           #effective:r--

It exists because ls -l has only nine bits to display. When there is an ACL, the group bits ls -l shows are actually the mask, not the owning group's permissions. It is a deliberate compromise so that old tools see something reasonable, and out of it comes the practical trap:

A chmod g-w on a file with an ACL modifies the MASK, and therefore cuts back in one stroke the effective permissions of all the named entries. It is the most common way of breaking an ACL by accident.

The rule is: when a file shows + in ls -l, manage its permissions with setfacl, not with chmod.

Essential commands:

getfacl file                           # view the full ACL
setfacl -m u:nuria:r file              # add or modify an entry
setfacl -x u:nuria file                # remove an entry
setfacl -b file                        # remove the WHOLE ACL
setfacl -d -m u:nuria:r directory      # default ACL (inheritance)
setfacl -R -m u:nuria:rX directory     # recursive, with the capital X

Two operational warnings: ACLs require the file system to support them — ext4 and XFS do, and on current distributions they are enabled by default; and you have to verify that your copying tools preserve them, because cp -a, rsync -A and tar --acls do, but a plain cp loses them silently, which produces mysterious permission failures after a restore.

Extended attributes and file attributes

Besides permissions, an inode can carry two more things.

Extended attributes (xattr): arbitrary key-value pairs organized into four namespaces — system.* for ACLs and the kernel's internal use, security.* for SELinux labels and capabilities, trusted.* for privileged use, and user.* for application metadata, writable by anyone with w permission. They are manipulated with setfattr -n user.origin -v "station-42" file and getfattr -d file. The ACLs of the previous section are extended attributes (system.posix_acl_access), just like the SELinux and AppArmor labels of module 5.

File attributes (chattr): flags in the inode that modify the file system's behavior. They are far more powerful than they look, because some of them bind even root:

Attribute Effect
i (immutable) Nobody, not even root, can modify, delete, rename or link the file
a (append-only) You can only append at the end: it cannot be modified or truncated
A (no atime) Do not update this file's atime (04-01)
C (no CoW) Disables copy-on-write on Btrfs (04-05)
j (data journalling) This file uses data=journal even if the volume does not (04-05)

The first two are the interesting ones:

# The configuration with secrets: let nobody touch it by accident
sudo chattr +i /etc/meteora/meteora.conf
sudo lsattr    /etc/meteora/meteora.conf
----i---------e------- /etc/meteora/meteora.conf

$ sudo rm /etc/meteora/meteora.conf
rm: cannot remove '...': Operation not permitted         ← not even root!

# Tamper-proof log: append only
sudo chattr +a /var/log/meteora/meteo-api.log
$ sudo truncate -s 0 /var/log/meteora/meteo-api.log
truncate: cannot open ... : Operation not permitted
$ echo "line" | sudo tee -a /var/log/meteora/meteo-api.log   ← appending DOES work

+a is especially valuable for security logs, because an attacker who gains root cannot erase their traces from the log: they can only keep appending. It is a natural complement to the O_APPEND of 04-04, with the difference that the latter is an agreement between programs and this one is imposed by the file system.

Now the small print, so as not to create false confidence. Removing the attribute requires the CAP_LINUX_IMMUTABLE capability, which root has: chattr -i disables it in a second, and an attacker with root can do it. Its real value is twofold: it prevents your own mistakes — a badly typed rm -rf, a faulty deployment script — and it forces a deliberate, auditable step before touching something critical. It is not a barrier against a determined attacker; it is insurance against accidents and a trace in the logs, because strong isolation from root is a matter of capabilities, SELinux and AppArmor, which come in module 5. And a practical consequence that bites everybody: a file with +i breaks the package manager's automatic updates, confusingly. Always document what you have marked.

Comparison with the Windows model

It is worth knowing the other model, because the concepts cross constantly in mixed environments:

UNIX / Linux Windows (NTFS)
Unit of permission 9 bits + 3 special A list of ACEs (access control entries)
Identity Numeric UID and GID SID (security identifier)
Recipients One user, one group, everybody else Any number of users and groups
Granularity 3 permissions (r, w, x) 13 permissions (read data, write attributes, delete, take ownership...)
Explicit deny Does not exist Yes, and it takes priority over any permission
Inheritance Only ACL default and setgid Native and automatic, with propagation
Deleting a file w on the directory Delete permission on the file or Delete child on the folder
Effective permissions Worked out by eye A specific tool to compute them
The administrator bypasses everything Yes (root) Not entirely: they can take ownership and then grant themselves permissions
Complexity Low High

The most important difference for somebody coming from Windows is the semantics of deletion: there it is controlled by a permission on the file, here by the w permission on the directory. It is the cause of most surprises when migrating, and it explains why the sticky bit is necessary in UNIX and has no direct equivalent in Windows. The other is the explicit deny: in Windows you can say "everybody in the Sales group can read, except John", while in UNIX only permissions are granted and the only way to exclude somebody is not to include them in any class that has them.

The balanced judgement: the Windows model is more expressive and the UNIX one more comprehensible. And in security, being comprehensible has a value of its own: a Windows ACL with twenty inherited entries, denials and nested groups can be so hard to audit that nobody really knows who has access. UNIX's nine bits are read at a glance.

Meteora's permissions, justified

Now, the complete application. These are the exact permissions of everything that makes up the service, with the reason for each decision.

Path Owner:group Mode Justification
/var/lib/meteora/ meteora:meteora 2750 The service controls it; the group enters but does not write; setgid to inherit the group; nobody else gets in
/var/lib/meteora/readings/ meteora:meteora 2750 Same: the ingestor (owner) writes, the aggregator (group) reads
.../readings/*.dat meteora:meteora 0640 The ingestor writes, the group reads, nobody else. Created by umask 027
/etc/meteora/ root:meteora 0750 Owned by root: the service must not be able to modify its own configuration
/etc/meteora/meteora.conf root:meteora 0640 Root edits it; the service reads it via the group. No secrets inside
/etc/meteora/secrets.conf root:meteora 0640 API and database keys, in a separate file
/var/log/meteora/ meteora:adm 2750 The service writes; the adm group reads the logs without being part of the service
/var/log/meteora/*.log meteora:adm 0640 The same, and +a on the audit ones
/usr/bin/meteo-api root:root 0755 Owned by root, not by the service: the service cannot modify its binary
/run/meteora/ meteora:meteora 0750 Sockets and FIFOs. On tmpfs, recreated at boot (04-02)
/run/meteora/api.sock meteora:www-data 0660 The web server connects via the group

The complete commands:

# --- Data ---
sudo chown -R meteora:meteora /var/lib/meteora
sudo find /var/lib/meteora -type d -exec chmod 2750 {} +   # setgid on directories
sudo find /var/lib/meteora -type f -exec chmod 0640 {} +   # 640 on files

# --- Configuration and binary: owned by ROOT, not by the service ---
sudo chown root:meteora /etc/meteora /etc/meteora/*.conf
sudo chmod 0750 /etc/meteora
sudo chmod 0640 /etc/meteora/meteora.conf /etc/meteora/secrets.conf
sudo chattr +i  /etc/meteora/secrets.conf      # immutable: deliberate change
sudo chown root:root /usr/bin/meteo-api && sudo chmod 0755 /usr/bin/meteo-api

# --- Logs: adm group, and append-only auditing ---
sudo chown -R meteora:adm /var/log/meteora
sudo chmod 2750 /var/log/meteora && sudo chmod 0640 /var/log/meteora/*.log
sudo chattr +a  /var/log/meteora/audit.log

# --- Read-only access for the analysis team ---
sudo setfacl -m  u:nuria:rx /var/lib/meteora /var/lib/meteora/readings
sudo setfacl -R -m u:nuria:r /var/lib/meteora/readings
sudo setfacl -d -m u:nuria:r /var/lib/meteora/readings    # inheritance

The four decisions that really matter here, because they are the ones people get wrong:

1. The configuration belongs to root, not to the service. It is the direct application of the principle of least privilege: if meteora owned meteora.conf, an attacker who compromised meteo-api could rewrite the configuration — change paths, disable validation, point at another server — and wait for a restart. Owned by root with group meteora and mode 640, the service reads but does not write. The same with the binary: meteora cannot replace /usr/bin/meteo-api with a trojan.

2. Secrets go in a separate file. Not because 640 is not enough, but because it separates two life cycles: meteora.conf can be tracked in Git, shared in an incident report and copied between environments; secrets.conf cannot. Mixing them guarantees the keys will end up in a repository or a ticket. Mode 0640 with group meteora is used — and not 0600 — because the service needs to read it at startup.

3. The logs have group adm, not meteora. That way administrators and monitoring read the logs without belonging to the service's group, which would also give them access to the data and the configuration. It is separation of privilege, not bureaucracy.

4. No setuid binaries. meteo-api listens on port 8080 and not on 80, precisely so as not to need privileges. If it had to use a port below 1024, the correct solution is not setuid root, but AmbientCapabilities=CAP_NET_BIND_SERVICE in the systemd unit, or a reverse proxy in front. We will come back to capabilities in module 5.

Serious mistakes and how to audit them

The four most damaging mistakes, and the command that finds them.

chmod 777. It is the emblematic mistake. It means "any user on the system can read, modify and delete this", and it also marks files as executable. It is almost always applied to get past a "permission denied" that was not understood, and it leaves the hole open forever.

# Files and directories writable by ANYBODY
sudo find / -xdev \( -type f -o -type d \) -perm -0002 \
     ! -path '/proc/*' ! -path '/sys/*' -ls 2>/dev/null

-perm -0002 looks for the write bit for "others". The only legitimate results are directories with the sticky bit (/tmp, /var/tmp, /dev/shm); everything else needs reviewing. Faced with a "permission denied", the correct reflex is namei -l, not chmod 777.

World-readable secrets. A private key with read permission for "others" is a compromised key; in fact, ssh refuses to use a key that is too open, precisely because of this.

sudo find /etc -type f \( -name '*secret*' -o -name '*.key' -o -name '*.pem' \
     -o -name '*credential*' -o -name '*password*' \) -perm -0004 -ls

chmod -R over a mixed tree. This one deserves an explanation because it looks harmless:

chmod -R 777 /var/lib/meteora     # ⚠ CATASTROPHE
chmod -R 644 /var/lib/meteora     # ⚠ ALSO BROKEN

The second is the instructive case. It applies 644 to everything, including the directories, which are left without the x bit: nobody can traverse them, and the whole tree becomes inaccessible — the "question marks" effect of section 2. The correct way uses the capital X or separates by type:

chmod -R u=rwX,g=rX,o= /var/lib/meteora          # option 1: capital X
find /var/lib/meteora -type d -exec chmod 2750 {} +   # option 2: by type
find /var/lib/meteora -type f -exec chmod 0640 {} +

Unnecessary setuid binaries. The inventory has to be reviewed and justified entry by entry. A clean list has between 10 and 20: passwd, su, sudo, mount, umount, ping, chsh, newgrp... Any setuid binary in /home, /tmp, /var or in an application directory is a red alert, and probably a back door. Keeping a reference list and comparing against it is a cheap and effective detection:

sudo find / -xdev \( -perm -4000 -o -perm -2000 \) -type f 2>/dev/null | sort \
     > /root/setuid.current
diff /root/setuid.reference /root/setuid.current   # has a new one appeared?

Two other checks worth automating are find / -xdev \\( -nouser -o -nogroup \\) and find / -xdev -type f -perm -0002 -perm -0111. Orphan files — whose UID matches no user — appear after deleting an account and are dangerous because the next user who receives that UID will inherit their ownership. And a file that is at once world-writable and executable is a textbook attack vector: anybody replaces its content and waits for somebody to run it.

Common Mistakes and Tips

Answering a "permission denied" with chmod 777. The problem is almost always a missing x on a directory along the path. Run namei -l first: it will tell you at which exact component it breaks.

Applying chmod -R with an octal value. It treats files and directories alike, and leaves directories without x or files with x. Use u=rwX,g=rX,o= or separate with find -type d and find -type f.

Believing that removing r and w from a file prevents deleting it. Deleting depends on the w permission on the directory. If you want to protect a file for real, protect the directory or use chattr +i.

Forgetting that groups are read at login. After a usermod -aG, neither your open session nor the running services see the new group. Restart the session or the service.

Using chmod on a file with an ACL. It modifies the mask and cuts back the effective permissions of every named entry: if you see a + in ls -l, use setfacl. And when copying, preserve the ACLs with cp -a, rsync -A or tar --acls, because a plain cp loses them silently.

Letting a service own its configuration and its binary. If it is compromised, it can rewrite both and persist. Configuration and binary belong to root; the service only reads.

Setting setuid on your own program "so it works". A setuid root binary is a privilege escalation waiting for a bug. There is almost always an alternative: a specific capability, a group, a socket with permissions, or a separate service.

Tip: namei -l and getfacl are your two diagnostic tools — the first walks the path component by component, the second reveals the ACLs that ls -l only hints at with a + — and audit periodically against a baseline: the setuid list, the world-writable files and the orphans detect both your own mistakes and intrusions.

Exercises

Exercise 1: directory permissions, demonstrated

Create a test structure and demonstrate empirically, showing the output of each command: (a) that with x but without r on a directory you can read a file whose name you know but you cannot list it; (b) that with r but without x you get the question marks from ls -l; (c) that you can delete another user's file, with no read permission on it, if you have w on the directory; (d) that once the sticky bit is enabled you no longer can. Explain in each case which permission is checked and on which object.

Exercise 2: read-only access without touching the group

The user nuria needs to read every file in /var/lib/meteora/readings, including those created in the future, without belonging to the meteora group and without the "others" permissions changing. Implement the complete solution with ACLs, verify that it works, check what happens afterwards with a chmod g-w on the directory and explain why. Compare with the two bad alternatives — putting her in the meteora group, or setting o+r — stating exactly what extra access she would get in each case.

Exercise 3: auditing a server's permissions

Write an audit script that reviews: setuid and setgid binaries compared against a reference list; world-writable files and directories without the sticky bit; secrets in /etc readable by others; orphan files; and files that are simultaneously world-writable and executable. For each finding it must state the concrete risk and the proposed fix. Run it on your machine and interpret the results: say which ones are legitimate and why.

Solutions

Solution 1

mkdir -p /tmp/lab && cd /tmp/lab
mkdir test && echo "secret content" > test/data.txt
chmod 0644 test/data.txt

(a) x without r — the dark directory:

chmod 0711 test                      # rwx owner, --x others
sudo -u nobody ls test               # ls: cannot open directory 'test': Permission denied
sudo -u nobody cat test/data.txt     # secret content        ← it DOES work!

ls needs r to read the directory's list of names, and it does not have it. cat only needs x to traverse it down to the inode of a name it already knows. In the first case r is checked on the directory, and in the second x on the directory plus r on the file.

(b) r without x:

chmod 0644 test
sudo -u nobody ls    test      # data.txt          ← it sees the name
sudo -u nobody ls -l test      # -????????? ? ? ?  ← it cannot read the inode
sudo -u nobody cat test/data.txt     # Permission denied

ls reads the list and works. ls -l needs to traverse the directory to read each entry's inode, and without x it cannot: hence the question marks. cat fails for the same reason.

(c) Deleting without being able to read, and (d) with the sticky bit:

chmod 0777 test                                    # w for everybody, NO sticky
sudo chown root:root test/data.txt && sudo chmod 0600 test/data.txt
sudo -u nobody cat test/data.txt                   # Permission denied
sudo -u nobody rm -f test/data.txt                 # NO ERROR!  (c)

echo x | sudo tee test/data.txt >/dev/null && sudo chmod 0600 test/data.txt
chmod 1777 test                                    # ← the 1 is the sticky
sudo -u nobody rm -f test/data.txt                 # Operation not permitted  (d)

In (c), nobody cannot read a root-owned file with mode 600 but can delete it, because unlink is a write to the directory and it has w there: the file's permissions and owner play no part whatsoever.

In (d), with sticky, only the file's owner (root), the directory's owner or root can delete. Note the detail: the error is no longer EACCES ("Permission denied") but EPERM ("Operation not permitted"), because the check that fails is not the w bit but the sticky's additional rule. It is exactly the mechanism that protects /tmp.

Solution 2

# 1. Access to the directory (r to list + x to traverse)
sudo setfacl -m u:nuria:rx /var/lib/meteora
sudo setfacl -m u:nuria:rx /var/lib/meteora/readings

# 2. Read access to the existing files
sudo setfacl -R -m u:nuria:r /var/lib/meteora/readings

# 3. Inheritance for the FUTURE files the ingestor creates
sudo setfacl -d -m u:nuria:r /var/lib/meteora/readings

# 4. Verification
sudo -u nuria cat /var/lib/meteora/readings/2026-08-31.dat > /dev/null && echo OK
sudo -u nuria ls  /var/lib/meteora/readings
sudo -u nuria cat /etc/meteora/secrets.conf     # must still fail ✔
getfacl /var/lib/meteora/readings

Step 3 is the one most often forgotten: without the default ACL, nuria would read today's files but not tomorrow's, and the failure would show up a week later with no apparent cause.

What happens with chmod g-w:

sudo chmod g-w /var/lib/meteora/readings
getfacl /var/lib/meteora/readings
# user:nuria:r-x            #effective:r-x     ← if the mask keeps r and x

chmod on a file with an ACL modifies the mask. If the chmod cut back the r or x bits of the group class, the mask would drop and nuria's effective permissions would be cut back with it, even though her entry still says r-x. It is restored with setfacl -m m::rx. The operational rule: when ls -l shows a +, manage the permissions with setfacl.

The two bad alternatives:

Alternative What extra access it would grant
usermod -aG meteora nuria Everything belonging to the meteora group: /etc/meteora/meteora.conf, /etc/meteora/secrets.conf with the API keys, /run/meteora/api.sock and any future file of the service. And permanently and transitively: anything created with group meteora
chmod o+r on the .dat files Every user on the system, including the service accounts of other applications and any compromised process. It turns a named, auditable access into a universal one

The ACL gives exactly the access requested, to the person requested, on the files requested, and it is documented in getfacl: it is the principle of least privilege applied with the right tool.

Solution 3

#!/bin/bash
# permissions-audit.sh — run as root
REF=/root/setuid.reference
echo "=== PERMISSIONS AUDIT — $(hostname) — $(date +%F) ==="

echo -e "\n[1] setuid/setgid binaries new relative to the reference"
find / -xdev \( -perm -4000 -o -perm -2000 \) -type f 2>/dev/null | sort > /tmp/su.now
if [ -f "$REF" ]; then
    diff "$REF" /tmp/su.now | grep '^>' && \
      echo "  RISK: NEW privileged binary. Verify its origin; if unjustified," \
           "chmod u-s and analyze the system for compromise."
else
    cp /tmp/su.now "$REF"; echo "  Reference created with $(wc -l < "$REF") entries."
fi

echo -e "\n[2] World-writable without the sticky bit"
find / -xdev \( -type f -o \( -type d ! -perm -1000 \) \) -perm -0002 \
     ! -path '/proc/*' ! -path '/sys/*' -ls 2>/dev/null
echo "  RISK: any user can modify or delete. Fix with chmod o-w."

echo -e "\n[3] Possible secrets readable by others in /etc"
find /etc -type f \( -name '*.key' -o -name '*.pem' -o -name '*secret*' \
     -o -name '*credential*' \) -perm -0004 -ls 2>/dev/null
echo "  RISK: exposed credential. Fix with chmod 600 and ROTATE the key."

echo -e "\n[4] Orphan files (UID/GID with no user)"
find / -xdev \( -nouser -o -nogroup \) ! -path '/proc/*' -ls 2>/dev/null
echo "  RISK: the next user to receive that UID will inherit ownership."

echo -e "\n[5] World-writable AND executable"
find / -xdev -type f -perm -0002 -perm -0111 ! -path '/proc/*' -ls 2>/dev/null
echo "  CRITICAL RISK: anybody replaces the content and waits for it to be run."

Interpreting the results on a healthy machine. In [1] between 10 and 20 binaries appear, all in /usr/bin, /usr/sbin, /usr/lib or /binpasswd, su, sudo, mount, umount, chsh, newgrp, pkexec, ping, and setgid crontab, wall, write — and all are legitimate because they need privileges an ordinary user does not have: modifying /etc/shadow, mounting file systems, opening ICMP sockets. A setuid outside those directories — in /home, /tmp, /opt or /var — is a red alert.

In [2] the expected outcome is no results, because /tmp, /var/tmp and /dev/shm are excluded by the ! -perm -1000 filter; if something shows up, it is almost always a historical chmod 777. In [3] it must be empty, and any result demands two actions and not one: fix the permission and rotate the credential, because you have to assume it has been read. In [4] there is usually something after uninstalling software or deleting accounts, and it is fixed with chown or by deleting the file. And [5] must be empty always: it is the most dangerous combination of all.

What makes this a useful tool is running it periodically and comparing with the previous run: the new findings, not the full list, are what you should look at. Automating these checks and integrating them with system auditing is the subject of Auditing, Logging and Incident Response.

Conclusion

The UNIX permission model is twelve bits and two numbers in the inode, with three classes — user, group and others — that are mutually exclusive and evaluated in that order, which is why a ----r--r-- file cannot be read by its own owner. The r, w and x bits mean different things on files and on directories, and that difference stops being arbitrary as soon as you remember that a directory is a file with a list of (name, inode) pairs: r is reading that list, w is adding or removing entries and x is traversing. Hence the four consequences: the 0711 dark directory, the question marks of r without x, and above all that you can delete a file you cannot read, because unlink writes to the directory and does not touch the file. That last one is why the sticky bit exists, without which /tmp would be unusable.

Permissions are expressed in octal or symbolic form, and the difference matters: octal is absolute and symbolic relative, so a chmod -R 644 leaves directories without x and breaks the whole tree. The correct way to walk a mixed tree is the capital X or separating with find -type d and -type f. The umask decides what permissions files are born with by subtracting bits from the requested mode — 666 for files, 777 for directories — and never adds the x: Meteora's 027 is exactly what produces the 0640 and 0750 we have been seeing all module long, as against the default 022 that would leave them readable by the whole system.

The three special bits solve three concrete problems. setuid explains how passwd writes to /etc/shadow: execve sets the effective UID to the binary owner's, leaving the real UID to tell who you really are; it is extremely powerful and therefore dangerous, and nosuid at mount time (04-03) disables it per volume. setgid on a directory makes everything created inside inherit its group and propagates the bit itself to subdirectories, which is what guarantees group consistency in /var/lib/meteora. And the sticky bit restricts deletion to the file's owner.

The kernel's check has five properties to remember: x is required on every directory along the pathnamei -l is the diagnosis — the classes are exclusive, all supplementary groups count but only those you had at login, root bypasses almost everything except executing with no x at all and the immutable attribute, and it is checked at open(), so a chmod 000 does not cut off whoever already has the file open. When the three classes are not enough, POSIX ACLs give permissions to specific users and groups, with default ACLs for inheritance and a mask acting as a ceiling — which a careless chmod cuts back, breaking the ACL without warning. The attributes chattr +i and +a add a layer that binds even root: the first prevents accidents on critical files and the second makes a log you can only append to. Against the Windows model, more expressive with its ACEs, its native inheritance and its explicit denials, UNIX's wins on something that counts for a lot in security: it can be read at a glance.

Applied to Meteora, it all comes down to four decisions: data at 2750/0640 with meteora:meteora; configuration and binary owned by root, so that a compromised service cannot rewrite itself; secrets in a separate file with +i; logs with group adm to separate who administers from who runs; and no setuid binaries. And the audits with find — setuid against a reference list, world-writable without sticky, readable secrets, orphans and writable-and-executable — turn those decisions into something verifiable rather than an intention.

Closing module 4

It is worth looking at the whole journey, because the module has had a very clear thread.

We started with the abstraction (04-01): a file is a named sequence of bytes with a size and an owner that the system places wherever it likes, and that idea solves in one stroke the seven problems any application would face over module 2's raw block vector, with the same strategy as virtual memory. We saw UNIX's seven types, dissected the inode — and its most important absence, the name — walked through the physical layout with the superblock, bitmaps and inode table, calculated the block size trade-off and chose ext4 for /var/lib/meteora with arguments rather than out of habit.

The name, which was missing, arrived in 04-02: a directory is a file with (name, inode) pairs, the acyclic graph organization explains why hard links to directories are not allowed, path resolution costs eleven accesses cold and almost none with the dentry cache, and unlink removes a name instead of deleting a file, which is where hard links, the deleted file still taking up 17 GB and the truncate -s 0 /proc/<pid>/fd/N that cures it all come from.

Then we assembled the tree (04-03): GPT partitions, LVM with its snapshots for live backups, and mounting step by step, with UUID= in a validated fstab and the nosuid,nodev,noexec options that close three vectors for the price of three words. And underneath everything, the VFS with its four objects, which is what allows the same open() to work on ext4, on tmpfs, on /proc — which invents its files as you read them — and on NFS.

With the map complete we moved on to using it (04-04): five system calls, the three tables that explain fork, dup and 2>&1, the two buffer layers you must tell apart so as not to lose data, and the two patterns you will write most — atomic publication with a temporary file and rename(), and O_APPEND for logs — plus flock so there are never two aggregator instances. Then we went down to the disk (04-05): the four allocation strategies up to extents, the three mechanisms that make a file written 24 bytes every 125 ms end up contiguous, the journal with its atomic commit block, ext4's three modes with ordered as the reasoned choice, and the distinction that settles the matter: metadata consistency is not data integrity, RAID 1 detects discrepancies but does not know which copy is good, and sometimes the guarantee has to come from the application.

And this last lesson has answered who can do what.

If module 2 answered how a scarce resource is shared out and module 3 how several flows coordinate over shared data, module 4 has answered how something is stored so that it is still there tomorrow. And the answer has always had the same shape: a structure on the disk, a layer of indirection that makes it manageable, and a discipline — of format, of synchronization or of permissions — that the programmer must respect.

But notice where the last section leaves us. We have locked down access to /var/lib/meteora with nine bits and an ACL, and we have assumed all along that UID 990 is meteora, that whoever logs in is who they say they are, and that a process running as meteora does what meteora would do. None of those three assumptions is free. How does somebody prove they are who they claim to be? Why can root bypass every permission, and is there any way to stop it? What stops a compromised meteo-api, with its permissions perfectly configured, from doing things nobody foresaw? And how do you detect that something like that has happened, when the attacker controls the system that writes the logs?

File permissions are only one piece of a much broader protection model, covering subjects, objects, protection domains, authentication, least privilege, mandatory access control and auditing. That is Module 5: System Protection and Security, and it starts in Protection Principles and Access Control.

Operating Systems Fundamentals

Module 1: Introduction to Operating Systems

Module 2: Resource Management

Module 3: Concurrency

Module 4: File Structures

Module 5: System Protection and Security

Module 6: Virtualization and Containers

Module 7: Administration and Troubleshooting in Practice

© Copyright 2026. All rights reserved