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+writeuser:alice:r--— alice specifically has read access, independent of group membershipgroup::r--— the developers group has readmask::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:
- Is the process running as the file owner? → use
user::entry - Does any named
user:ACL entry match? → use it (subject to mask) - Does the process’s group or any supplementary group match a named
group:or the basegroup::entry? → use it (subject to mask) - 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
| Tool | Purpose |
|---|---|
getfacl | Read all ACL entries on a file/directory |
setfacl -m | Add or modify an ACL entry |
setfacl -d -m | Set default ACLs on a directory (inheritance) |
setfacl -x | Remove a specific ACL entry |
setfacl -b | Strip all ACLs |
namei -l | Walk a path and show permissions at each level |
find -perm -4000 | Find SUID binaries |
findmnt | Check 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