Get the math right: rwx means different things for files vs directories
Most misunderstandings come from treating a directory's rwx as a file's rwx. For a file, r reads content, w writes content, x executes. For a directory, r lists the entries, w creates/deletes/renames entries inside it, and x traverses that directory to reach a target below.
That is why a directory showing only -r-------- looks readable yet cannot be entered—it lacks x; while one with x but no r lets you reach the files inside without being able to list names. These edges are the source of endless mysterious Permission denied errors, so set the model straight first.
ll -d /srv/data
# A directory needs x to be traversable
chmod a+x /srv/data
# The clearest check is to look and compare
ls -l /srv/data/secret.txt /srv/data
# Decimal cheat sheet: r=4 w=2 x=1
umaskEnter ACLs: when bit permissions cannot express one-user-one-right
A common business need is "dev can write, ops can write, everyone else read-only", which the classic nine bits cannot express since they give group only one bucket. So use setfacl/getfacl to grant write to specific users rather than stuffing everyone into one group.
ACLs change how permissions display: ls -l shows a + after the permission bits, and from then on the rwx bits represent the mask, not the real rights. A classic trap is running chmod over a + directory which silently wipes the ACL; a demo and a fix follow.
setfacl -m u:devuser:rwx -R /srv/app # grant one user recursive write
setfacl -m u:opsuser:rwx /srv/app/deploy.sh
setfacl -x u:devuser /srv/app/secret # revoke a user
# Bad example: chmod overwrites the ACL
chmod 644 /srv/app/deploy.sh # the + disappears, ACL is reset
# Fix comparison: adjust the mask while keeping the ACL
setfacl -m m::rwx /srv/app/deploy.sh
# Inspect
getfacl /srv/app/deploy.shsetuid/SGID/sticky: the three special bits are a slippery board by the pool
setuid lets a binary run as its owner—/usr/bin/passwd runs as root. It is genuinely useful, but adding setuid to any script you have written is an incident waiting to happen: the shebang runs it through bash in a root context, multiplying the blast radius. SGID makes new files in a directory inherit the directory 's group, handy for shared collaboration dirs.
The sticky bit (the trailing 1 in 1777) lets only the owner delete their own files; /tmp is the live example. When judging special bits, do not trust only the s/t letter—uppercase S/T means the bit is set but x is not, so it does nothing and confuses everyone.
ls -l /usr/bin/passwd # -rwsr-xr-x the s is expected
find / -perm -4000 2>/dev/null # audit every setuid file
chmod 4755 /opt/app/bin/app # setuid (usually not for scripts)
chmod 2755 -R /srv/shared # SGID shared dir, group inheritance
# Uppercase S/T means the bit is set but x may be missing
ls -l /tmp # drwxrwxrwtsudo: stop handing out ALL, minimal grants let you sleep
Dropping a user into the wheel/sudo group hands them the whole box, but the operational reality is often just 'that person only needs to restart Nginx'. Use visudo to grant single commands, reserve NOPASSWD for low-frequency side-effect-free actions, and keep dangerous system-admin commands behind the password prompt.
Command paths must be absolute, and beware alias and wildcard traps: NOPASSWD: ALL versus NOPASSWD: /usr/sbin/reboot are worlds apart. sudo -l shows what the current user may execute—the very first tool for verifying a grant.
sudo -l
# A snippet of visudo contents
# Bad example: grant everything on the whole host
# dev ALL=(ALL) NOPASSWD: ALL
# Fix comparison: grant only a single command
dev ALL=(root) NOPASSWD: /usr/bin/systemctl restart nginx
dev ALL=(root) /usr/sbin/reboot, /usr/bin/systemctl stop firewalld
# Validate the syntax
visudo -c
sudo -l -U devVerify with two accounts, not by trusting the bit combo
Permission configs done on paper leak easily: the mask blocks someone already authorized, find cannot see setuid that root can, or a rule gets swallowed by a wildcard earlier in the chain. The most persuasive verification is switching to the affected account and actually trying ls/write/execute, then switching back to confirm root is unaffected.
For setuid and ACL the checks differ: setuid looks at the runtime euid (see with id -u or a whoami line), ACL checks that the target user can read the rwx matching mask. Bake the verification into a rerunnable script so rollback is one re-run.
# Bad example: assume 777 is always right
# chmod 777 /srv/app
# Fix comparison: test per account for real
sudo -u devuser ls -l /srv/app/deploy.sh
sudo -u devuser touch /srv/app/probe.txt
sudo -u devuser id -u # check euid when testing setuid
# Script the verification
for u in devuser opsuser; do echo -n "$u: "; sudo -u $u test -r /srv/app/secret && echo "can read" || echo "denied"; done