Part 1 covered SUID, SGID, the sticky bit, and umask. This part covers the mechanism that takes over when standard Unix permissions aren’t expressive enough: Access Control Lists (ACLs). Then we’ll look at how to diagnose permission failures in the real world — because permissions break in subtle ways that ls -l alone won’t reveal.


The Limit of Standard Unix Permissions

Standard Unix permissions give you three buckets: owner, group, and others. That’s it. The moment you need something more nuanced — say, one specific user needs read access to a file without being in the group — you’re stuck.

ACLs solve this. They let you attach arbitrary per-user and per-group permission entries to any file or directory, layered on top of (not replacing) the standard permission model.


ACL Basics

Checking if ACLs are supported

ACLs are a filesystem feature. On modern Ubuntu/Debian with ext4 or XFS, they’re enabled by default. Verify:

tune2fs -l /dev/sda1 | grep "Default mount options"
# Should include: acl

Or just try using them — if getfacl is installed and the filesystem supports it, you’re good.

# Install acl tools if not present
sudo apt install acl

Reading ACLs with getfacl

getfacl /srv/team-project/report.txt

Output:

# file: srv/team-project/report.txt
# owner: limon
# group: developers
user::rw-
user:alice:r--
group::r--
mask::r--
other::---

Breaking this down:

  • user::rw- — the file owner (limon) has read+write
  • user:alice:r-- — alice specifically has read access, independent of group membership
  • group::r-- — the developers group has read
  • mask::r-- — the effective permission ceiling for named users and groups (more on this below)
  • other::--- — everyone else has no access

The + at the end of a permission string in ls -l tells you an ACL is active:

ls -l /srv/team-project/report.txt
-rw-r-----+ 1 limon developers 1024 Apr 8 11:00 report.txt

That trailing + means: there is an ACL on this file — check getfacl for the full picture.


Setting ACLs with setfacl

Grant a specific user access:

# Give alice read permission on the file
setfacl -m u:alice:r-- /srv/team-project/report.txt

# Give bob read+write
setfacl -m u:bob:rw- /srv/team-project/report.txt

Grant a specific group access:

# Give the qa group read access
setfacl -m g:qa:r-- /srv/team-project/report.txt

Remove a specific ACL entry:

setfacl -x u:alice /srv/team-project/report.txt

Remove all ACLs (reset to standard permissions):

setfacl -b /srv/team-project/report.txt

Check the result:

getfacl /srv/team-project/report.txt

Default ACLs — The Inheritance Mechanism

Setting an ACL on an existing file is straightforward. But what about files created inside a directory in the future? That’s where default ACLs come in.

When you set a default ACL on a directory, any new file or subdirectory created inside it inherits those ACL entries automatically.

# Set a default ACL so alice always gets read on new files in this directory
setfacl -d -m u:alice:r-- /srv/team-project/

The -d flag sets it as a default. Verify:

getfacl /srv/team-project/
# file: srv/team-project/
# owner: limon
# group: developers
user::rwx
group::rwx
other::---
default:user::rwx
default:user:alice:r--
default:group::rwx
default:mask::rwx
default:other::---

Now create a file inside the directory and check it:

touch /srv/team-project/newfile.txt
getfacl /srv/team-project/newfile.txt

The file will have alice’s read entry already applied, inherited from the directory’s default ACL.


The Mask — The Most Misunderstood Part of ACLs

The mask entry in an ACL is what trips people up. It acts as an upper bound on the effective permissions for all named ACL entries (named users and named groups). It does not affect the file owner or the other entry.

user::rw-         ← owner, unaffected by mask
user:alice:rwx    ← alice wants rwx
mask::r--         ← mask limits effective to r-- only
other::---        ← unaffected by mask

Even though alice’s ACL entry says rwx, the mask reduces her effective permission to r--. getfacl shows this clearly:

user:alice:rwx          #effective:r--

When does the mask change?

Running chmod on a file that has ACLs modifies the mask, not the ACL entries themselves. This is a critical point — if you chmod 644 a file with ACLs, you’ve just set the mask to r--, which silently restricts what all named users and groups can do.

# After chmod 644:
getfacl file.txt
user::rw-
user:bob:rwx       #effective:r--   ← bob silently restricted
group::r--
mask::r--          ← chmod set this
other::---

To restore intended effective permissions, explicitly reset the mask:

setfacl -m mask::rw- file.txt

ACL Precedence Order

When Linux evaluates access for a process, it checks ACL entries in this order and stops at the first match:

  1. Is the process running as the file owner? → use user:: entry
  2. Does any named user: ACL entry match? → use it (subject to mask)
  3. Does the process’s group or any supplementary group match a named group: or the base group:: entry? → use it (subject to mask)
  4. Fall through to other:: entry

The system stops at the first matching category, not the most permissive one. This means if you’re the file owner, the group ACL entry is irrelevant to you — you only get the owner’s permissions, even if the group ACL would give you more.


Real-World Troubleshooting

The symptom: “Permission denied” when ls -l looks fine

Step 1 — check if there’s an ACL overriding things:

getfacl /path/to/file

Step 2 — check if the mask is restricting effective permissions. Look for #effective: annotations in getfacl output.

Step 3 — verify the actual user and group of the running process, not just the logged-in user:

ps aux | grep nginx
# nginx worker processes often run as www-data, not root

Step 4 — trace the full directory chain. Permission denied on /var/www/html/app/data/cache.db might be caused by /var/www/html/app/data/ being 700 owned by root, not by the file itself.

namei -l /var/www/html/app/data/cache.db

namei -l walks every component of the path and shows permissions at each level. This is the fastest way to find which directory in a long path is blocking access.


The symptom: A service fails to write after an upgrade

Common cause: package upgrade recreated a directory with wrong ownership.

ls -ld /var/run/myapp/
# drwxr-xr-x 2 root root 60 Apr 8 12:00 /var/run/myapp/

# myapp runs as user 'myapp' — it can't write here
systemctl status myapp
# ...permission denied writing to /var/run/myapp/myapp.pid

Fix:

chown myapp:myapp /var/run/myapp/
# or add the app user to a group that owns it

For tmpfs-backed /var/run, the fix goes in the systemd service or /etc/tmpfiles.d/:

# /etc/tmpfiles.d/myapp.conf
d /var/run/myapp 0755 myapp myapp -

The symptom: chmod doesn’t seem to take effect

The file is on a network filesystem (NFS, CIFS/SMB) that doesn’t support or maps permissions differently — or the filesystem was mounted with noexec, nosuid, or a forced user/group mapping.

Check mount options:

findmnt /path
# or
mount | grep /path

If the filesystem is NFS with root_squash, root on the client maps to nobody. Any chmod by root on the client may silently fail or apply as nobody.


The symptom: SUID binary stops working after copying

Copy operations don’t preserve SUID by default — and for good reason. cp strips SUID on copy unless you explicitly pass --preserve=all. rsync has similar behavior with --perms.

cp --preserve=all /usr/bin/somebin /backup/somebin

Even then: mounting the destination with nosuid means the bit is stored but ignored at execution time.


The symptom: New files in a shared dir have wrong group

SGID is not set on the directory. Users are creating files and each file gets their personal primary group.

ls -ld /srv/shared/
drwxrwxr-x 2 root developers 4096 Apr 8 ...
# No 's' in group execute slot

Fix:

chmod g+s /srv/shared/

Then confirm with ls -ld — you should now see rwxrwsr-x.

Existing files won’t be retroactively changed — only new files will inherit the group. Fix existing files with:

chgrp -R developers /srv/shared/

The setuid/setgid Audit Habit

Any time you’re hardening a server or reviewing a new system, run this:

# All SUID files
find / -perm -4000 -type f 2>/dev/null

# All SGID files  
find / -perm -2000 -type f 2>/dev/null

# Both at once
find / \( -perm -4000 -o -perm -2000 \) -type f 2>/dev/null

Cross-reference against a known-good baseline. Any unexpected SUID binary — especially in home directories or /tmp — is a red flag.


Permission Decision Tree

When something fails with a permission error, work through this in order:

1. What user/group is the process actually running as?
   (ps aux, id, whoami — don't assume)

2. Walk the full path with namei -l
   (which directory in the chain is blocking?)

3. Is there an ACL? (getfacl — look for the + in ls -l)

4. Is the mask restricting effective permissions?
   (look for #effective: annotations in getfacl)

5. Is the filesystem mounted with restrictive options?
   (findmnt, mount | grep path)

6. Is SELinux or AppArmor enforcing?
   (audit.log, aa-status, sestatus)

Summary

ToolPurpose
getfaclRead all ACL entries on a file/directory
setfacl -mAdd or modify an ACL entry
setfacl -d -mSet default ACLs on a directory (inheritance)
setfacl -xRemove a specific ACL entry
setfacl -bStrip all ACLs
namei -lWalk a path and show permissions at each level
find -perm -4000Find SUID binaries
findmntCheck filesystem mount options

The standard rwx model handles 80% of cases cleanly. ACLs handle the remaining cases where you need surgical precision — one specific user, one specific group, without changing ownership or group membership. Know when to reach for each.


Next up: Domain 4 — Process & Resource Management, Round 2.

Leave a Reply

Your email address will not be published. Required fields are marked *