The Unix Permission Model: More Than Just rwxrwxrwx
Every file and directory on a Linux system has an owner, a group, and a set of permission bits. That's the foundation. But I've seen engineers who've been running Linux systems for years still get bitten by the subtleties — especially around what execute permission means on a directory, or why umask behaves the way it does when you're running a systemd service. Let's break it down properly.
When you run
ls -lon a file, you get something like this:
-rwxr-x--- 1 infrarunbook-admin devops 4096 Apr 9 10:22 deploy.sh
That first column is the permission string. The leading dash means it's a regular file. A
dmeans directory,
lfor symlink,
bfor block device. After that come three groups of three bits: owner permissions, group permissions, and "others" — everyone on the system who isn't the owner and isn't in the file's assigned group.
Read, Write, Execute — What They Actually Mean
On a file, the semantics are intuitive. Read (
r) means you can open and view the file's contents. Write (
w) means you can modify it. Execute (
x) means you can run it as a program. Simple enough.
Directories are where engineers get confused. On a directory, the bits mean something entirely different. Read permission lets you list its contents — essentially lets
lswork. Write permission lets you create, delete, or rename files inside it. Execute permission is the one that trips people up: it's what lets you traverse into the directory or access anything inside it by path at all.
Here's the implication. If you have a file at
/var/data/reports/q1.csvand the
reportsdirectory has permissions
r--for a given user, they can list the directory but can't open any file inside it. They need execute (traverse) permission on every directory component in the path. I've seen this exact misunderstanding break deployments where someone ran
chmod 444on a directory thinking they were making it read-only for content protection, but actually broke all read access to every file inside it. The permission on the directory gatekeeps entry — the permission on the file only applies once you're already inside.
Octal Notation and Symbolic Notation
There are two ways to express permissions with chmod: octal (numeric) and symbolic. You'll need both.
Octal treats each group of three bits as a three-bit binary number. Read is 4, write is 2, execute is 1. You add them together to get the digit for each class. So
rwxis 7,
r-xis 5,
r--is 4, and
---is 0. A permission string like
rwxr-x---becomes
750in octal — readable once you internalize it, and compact enough for scripts.
chmod 750 deploy.sh
chmod 644 /etc/app/solvethenetwork.conf
chmod 600 /home/infrarunbook-admin/.ssh/id_rsa
Symbolic notation is more readable when you want to make targeted changes without knowing the current full state. You specify who (
ufor user/owner,
gfor group,
ofor others,
afor all), the operation (
+to add,
-to remove,
=to set exactly), and which bits to affect.
chmod u+x script.sh # add execute for owner only
chmod go-w sensitive.conf # remove write from group and others
chmod a=r public.txt # set everyone to read-only, stripping everything else
chmod u=rwx,g=rx,o= app.sh # set all three classes explicitly in one pass
In my experience, octal is better for setting full permissions from scratch, and symbolic is better for incremental changes. Symbolic has one key operational advantage: it doesn't accidentally wipe permissions you didn't intend to touch, which matters when you're scripting changes across a heterogeneous file tree.
Recursive chmod and the File vs Directory Problem
The
-Rflag makes chmod recursive, but use it carefully. Running
chmod -R 755on a directory containing both regular files and subdirectories will stamp execute permission onto all files — including data files and configs that have no business being executable. The right approach for mixed content is to target files and directories separately with
find:
find /var/www/solvethenetwork -type d -exec chmod 755 {} \;
find /var/www/solvethenetwork -type f -exec chmod 644 {} \;
This is a pattern I reach for every time I'm setting up a web root. Directories need execute for traversal, regular content files don't need it, and keeping them separate prevents a whole category of security misconfigurations.
chown and chgrp: Changing Ownership
While chmod controls what permissions are set,
chowncontrols who owns the file. Ownership has two components: the user owner and the group owner. You can change both at once with the
user:groupsyntax, or just change the group with
chgrp.
chown infrarunbook-admin:devops /opt/app/config.yml
chown -R infrarunbook-admin:www-data /var/www/solvethenetwork/
chgrp wheel /usr/local/bin/admin-tool
Only root can transfer file ownership to a different user. A regular user can change a file's group, but only to a group they're already a member of. This becomes a real stumbling block in deployment scripts running under a service account — the script might have permission to write a file, but can't then reassign ownership to a different service user without escalation. Plan your deployment user's group memberships accordingly.
A pattern that works well in provisioning: create the directory structure as root, set ownership to the service account before the application ever runs, and let the application write files under its own identity from the start. That avoids a whole class of runtime permission failures.
umask: The Permission Mask You Probably Forget About
umask is the sleeper topic here. Most engineers know chmod and chown, but umask is what silently determines the default permissions on every new file and directory created on the system — and it's not always obvious why a file ends up with unexpected permissions.
The umask is applied against the maximum default permissions at creation time. For files, the theoretical maximum default is
666(rw-rw-rw-) — no execute by default, because executables should always be made executable deliberately. For directories, the maximum is
777. The umask value specifies which bits to mask off (remove) from those maximums.
A umask of
022means: remove write permission from group and others. So a new file gets
666 & ~022 = 644(rw-r--r--) and a new directory gets
777 & ~022 = 755(rwxr-xr-x). That's the standard for most Linux systems and it's a reasonable default for interactive user sessions.
# Check current umask
umask
# Set umask for current shell session
umask 027
# Verify a newly created file
touch /tmp/testfile && ls -l /tmp/testfile
A umask of
027is more appropriate for service accounts and security-conscious environments: new files get
640(rw-r-----) and directories get
750(rwxr-x---). Others get zero access. On a multi-tenant system — say, a shared application server at 10.10.5.20 running several services — you don't want application files world-readable by default. Setting a restrictive umask at the service level is far more reliable than trying to audit and lock down files after the fact.
Where umask Is Actually Set
umask can live in several places:
/etc/profile,
/etc/bashrc, user-level
~/.bashrcor
~/.bash_profile, and through PAM via
/etc/pam.d/using the
pam_umaskmodule. The PAM approach is the most reliable for service accounts because it applies regardless of how the session starts — login shell, SSH, su, sudo, it doesn't matter.
# /etc/pam.d/common-session (Debian/Ubuntu) or /etc/pam.d/system-auth (RHEL/Rocky)
session optional pam_umask.so umask=027
For services managed by systemd, set it directly in the unit file. This is the piece a lot of engineers don't know about:
[Service]
User=infrarunbook-admin
Group=devops
UMask=0027
That
UMaskdirective ensures files created by the service have the right permissions from the start, regardless of what the OS default umask is. If you're running a service that creates log files, config files, or data files, set UMask in the unit and you'll never need to chase down permission issues caused by mismatched defaults.
Special Permission Bits: Setuid, Setgid, and Sticky
Beyond the standard nine permission bits, there are three special bits that alter execution behavior or directory semantics in important ways. These are setuid, setgid, and the sticky bit — represented as a fourth octal digit when set numerically.
Setuid (SUID)
When the setuid bit is set on an executable, the program runs with the effective UID of the file's owner rather than the user who launched it. The textbook example is
/usr/bin/passwd. A regular user needs to modify
/etc/shadowto change their password, but that file is only writable by root. The passwd binary is owned by root with SUID set, so when any user runs it, the process temporarily holds root's effective UID for just that operation.
ls -l /usr/bin/passwd
-rwsr-xr-x 1 root root 68208 Mar 14 08:00 /usr/bin/passwd
Notice the
swhere the owner's execute bit would normally appear. That's SUID active with execute also set. An uppercase
Smeans SUID is set but execute is not — which is almost always a mistake and warrants investigation. SUID on shell scripts is ignored by the kernel entirely as a deliberate security measure. Don't try to make a SUID shell script; it won't work, and for good reason.
Setgid (SGID)
On executables, SGID works like SUID but for group identity — the program runs with the group of the file rather than the calling user's primary group. On directories, though, SGID serves a genuinely useful collaboration purpose.
When SGID is set on a directory, new files created inside inherit the directory's group instead of the creator's primary group. This means a shared project directory will have all files consistently owned by the
devopsgroup regardless of which team member creates them. No more chasing down files that ended up owned by
jsmith's personal group and inaccessible to teammates.
chmod g+s /opt/shared/project
ls -ld /opt/shared/project
drwxrwsr-x 2 infrarunbook-admin devops 4096 Apr 9 11:00 /opt/shared/project
The
sin the group execute position indicates SGID. This pattern is something I reach for on any shared project directory where multiple users collaborate. Combine it with a sticky bit if you also want to prevent users from deleting each other's files.
Sticky Bit
The sticky bit on a directory means only the file's owner (or root) can delete or rename files inside it, even if other users have write permission to the directory itself. The canonical example is
/tmp.
ls -ld /tmp
drwxrwxrwt 10 root root 4096 Apr 9 12:00 /tmp
The
tin the others execute position is the sticky bit. Without it, any user with write access to
/tmpcould delete anyone else's files, since deletion is a directory operation, not a file operation. Which brings us neatly to the most common misconception about Linux permissions.
Real-World Scenarios
A web application on sw-infrarunbook-01 was failing to write log files. The app ran as
www-data, and the log directory was owned by
infrarunbook-adminwith permissions
755. World-readable and world-traversable, but not world-writable. The fix was to align the group ownership and make it group-writable:
chown infrarunbook-admin:www-data /var/log/solvethenetwork-app/
chmod 775 /var/log/solvethenetwork-app/
Another classic: SSH refusing to use a private key at
/home/infrarunbook-admin/.ssh/id_rsawith permissions
644. SSH enforces strict key file permission requirements as a security control — it won't use a private key that's group- or world-readable, and it will tell you so. The fix is always the same:
chmod 600 /home/infrarunbook-admin/.ssh/id_rsa
chmod 700 /home/infrarunbook-admin/.ssh/
For a compliance audit finding on a server at 172.16.50.10 flagging world-writable files, here's the find pattern I use:
find / -xdev -type f -perm -o+w -not -path "/proc/*" 2>/dev/null
find / -xdev -type d -perm -o+w -not -path "/proc/*" -not -path "/tmp" -not -path "/var/tmp" 2>/dev/null
The
-xdevflag keeps the search within the current filesystem, avoiding traversal into mounts. Running this as a periodic check — or wiring it into auditd rules — catches unintended world-writable files before an auditor or an attacker does.
Common Misconceptions
The biggest one: people assume root is subject to standard permission checks. Root bypasses all permission checks for reads and writes. A file with permissions
000is fully accessible to root. The only exception is execute — root needs at least one execute bit set on a binary somewhere to run it. This matters when you're trying to "lock down" a file from root — you can't, not with standard permissions. That's what security modules like SELinux or AppArmor are for.
The deletion misconception is the one that surprises the most people. Deleting a file doesn't require any permission on the file itself. It's a directory operation — you're removing an entry from the parent directory's namespace. To delete a file you need write permission on the containing directory. The file's own mode is irrelevant. This is precisely why
/tmphas the sticky bit; without it,
won the directory would let anyone delete anyone else's files.
There's also persistent confusion about umask arithmetic. umask doesn't subtract bits numerically — it's a bitwise AND NOT operation. The umask bits represent permissions to strip. A umask of
022doesn't set permissions to
022; it removes group-write and other-write from whatever the default maximum would be. Thinking of it as a mask rather than a subtraction makes the behavior much clearer.
Finally, a misconception about the limits of standard permissions. Unix permissions allow exactly one owner user and one owner group per file. If you need three different users with three different access levels on the same file, standard permissions can't express that. You need POSIX ACLs via
setfacland
getfacl. The presence of a
+at the end of the permission string in
ls -loutput signals an ACL is present on that file or directory.
getfacl /opt/solvethenetwork/data/
setfacl -m u:infrarunbook-admin:rwx /opt/solvethenetwork/data/
setfacl -m u:deploy-agent:rx /opt/solvethenetwork/data/
Knowing where standard permissions end and ACLs begin prevents a lot of frustration — and prevents the wrong workaround (like making a file world-writable) when the real answer is a targeted ACL entry.
File permissions are foundational. Get them wrong and you're either breaking functionality or opening security holes — sometimes both at once, in ways that aren't immediately obvious. The details here — directory execute semantics, the correct interpretation of umask, SGID on shared directories, UMask in systemd units, the sticky bit — are the parts that matter in real operational work. When something breaks at 2 AM and the error message says "permission denied," you'll know exactly where to start.
