Linux Users and Permissions — Footguns & Pitfalls¶
These are the mistakes that cause outages, security incidents, and career-defining bad days. Every one of these has happened in production. Learn from other people's pain.
chmod 777 — The Universal Wrong Answer¶
The footgun: When something doesn't work due to permissions, setting chmod 777 (or worse, chmod -R 777) "fixes" it by removing all access control.
Why it's terrible: - Every user on the system can read, write, and execute the file - Any compromised service can access everything - SUID/SGID bits are preserved, creating privilege escalation paths - Security audits will flag every instance - It masks the real problem instead of fixing it
# What people do:
$ chmod 777 /var/www/html/upload/ # "It works now!"
# What they should do:
$ chown www-data:www-data /var/www/html/upload/
$ chmod 755 /var/www/html/upload/
# Or if a specific user needs write access:
$ setfacl -m u:deploy:rwx /var/www/html/upload/
The real fix is always: figure out which user/group needs which permission, and grant exactly that.
Recursive chown on the Wrong Directory¶
The footgun: Running chown -R on a system directory.
# Intended:
$ sudo chown -R myapp:myapp /opt/myapp/
# Actual (typo or wrong variable expansion):
$ sudo chown -R myapp:myapp /
$ sudo chown -R myapp:myapp /etc/
$ sudo chown -R myapp:myapp /var/
What breaks:
- chown -R user / — the system is now owned by a regular user. sudo breaks, SSH breaks, most services break, PAM breaks. The system is effectively destroyed.
- chown -R user /etc/ — similar destruction: shadow, passwd, sudoers, SSH host keys all get wrong ownership.
- chown -R user /var/ — every service's data directory now has wrong ownership.
Prevention:
- Always double-check the path before running recursive chown
- Use variables carefully: chown -R user "$APP_DIR" — if $APP_DIR is empty, this becomes chown -R user (which does nothing) rather than chown -R user /
- Set APP_DIR with a default: ${APP_DIR:?Variable not set} — this will error if unset instead of expanding to empty string
- For critical systems, test with echo first: echo chown -R myapp:myapp /opt/myapp/
Recovery: See the Street Ops section on mass permission fix. Short version: reinstall packages to restore correct ownership.
SUID on Shell Scripts¶
The footgun: Setting the SUID bit on a shell script, expecting it to run as root.
Why it fails (and is dangerous): - Modern Linux kernels ignore SUID on scripts (the shebang line creates a race condition called the "SUID script race") - Even if it worked, shell scripts are trivially exploitable via environment variable manipulation (PATH, IFS, LD_PRELOAD, etc.) - An attacker could modify the script between the kernel opening it and the interpreter reading it
The right approaches:
# Option 1: sudo with specific command (preferred)
$ cat /etc/sudoers.d/myapp
deploy ALL=(root) NOPASSWD: /usr/local/bin/my-admin-script.sh
# Option 2: Write a compiled wrapper that drops privileges properly
# Option 3: Use capabilities instead of SUID
$ sudo setcap cap_net_bind_service=+ep /usr/local/bin/myapp
# Option 4: Use systemd to run the privileged part as a service
sudoers Syntax Error — Locked Out¶
The footgun: Editing /etc/sudoers with a regular editor (vim, nano) instead of visudo, introducing a syntax error.
# WRONG:
$ sudo vim /etc/sudoers
# Add a line with a typo:
# deploy ALL=(ALL) AL # <-- typo, should be ALL
# Now sudo is broken for EVERYONE:
$ sudo anything
>>> /etc/sudoers: syntax error near line 25 <<<
sudo: parse error in /etc/sudoers near line 25
sudo: no valid sudoers sources found, quitting
Why visudo exists: visudo checks syntax BEFORE saving. If there's an error, it tells you and gives you the option to re-edit, discard, or save anyway (don't save anyway).
Recovery options (in order of preference):
1. If you have another open root shell — use it to run visudo
2. pkexec visudo — PolicyKit may still work
3. Boot into single-user mode (see Street Ops)
4. Boot from live USB, mount filesystem, fix the file
Prevention:
# ALWAYS use visudo
$ sudo visudo
# For drop-in files, also use visudo
$ sudo visudo -f /etc/sudoers.d/deploy
# Validate before deploying via automation
$ visudo -cf /path/to/sudoers-fragment
/path/to/sudoers-fragment: parsed OK
World-Readable /etc/shadow¶
The footgun: Accidentally making /etc/shadow readable by all users.
# This happens with bad chmod commands:
$ sudo chmod -R 644 /etc/ # "Fix" permissions on /etc
# Now /etc/shadow is readable by everyone
Impact: - Every user can read password hashes - Hashes can be cracked offline with tools like hashcat or John the Ripper - Modern SHA-512 hashes resist brute force, but weak passwords fall quickly - This is a critical security vulnerability
Detection:
# Check shadow permissions
$ ls -l /etc/shadow
-rw-r----- 1 root shadow 1234 Mar 19 10:00 /etc/shadow
# Should be 640 (root:shadow) or 000 (root:root) depending on distro
# Audit for wrong permissions on sensitive files
$ stat -c "%a %U:%G %n" /etc/shadow /etc/gshadow /etc/sudoers
640 root:shadow /etc/shadow
640 root:shadow /etc/gshadow
440 root:root /etc/sudoers
Fix:
$ sudo chmod 640 /etc/shadow
$ sudo chown root:shadow /etc/shadow
$ sudo chmod 640 /etc/gshadow
$ sudo chown root:shadow /etc/gshadow
After exposure: Assume all passwords are compromised. Force password resets for all users:
$ sudo chage -d 0 -M 1 $(awk -F: '$2 ~ /^\$/ {print $1}' /etc/shadow)
# Forces immediate password change for all users with password hashes
umask 000 — Creating World-Accessible Files¶
The footgun: Setting umask to 000 in a script or shell profile.
# Someone adds this to a deployment script for "compatibility":
umask 000
# Now every file created by this process is world-readable and writable:
# Files: 666 (rw-rw-rw-)
# Directories: 777 (rwxrwxrwx)
What goes wrong: - Config files with credentials become readable by all users - Log files become writable by anyone (log injection) - Uploaded files become executable (code execution) - Temporary files in shared directories become modifiable
Detection:
# Find files with overly permissive permissions created recently
$ find /opt/myapp -newer /opt/myapp/deploy-timestamp -perm -006 -ls
# Check what umask a running process is using
$ cat /proc/$(pgrep myapp)/status | grep Umask
Umask: 0000 # BAD
Prevention:
# Set appropriate umask in systemd units
[Service]
UMask=0027 # Files: 640, Directories: 750
# In scripts, always set umask early
#!/bin/bash
umask 027
Forgetting Home Directory Permissions on User Creation¶
The footgun: Creating a user without setting appropriate home directory permissions.
$ sudo useradd -m newuser
$ ls -ld /home/newuser/
drwxr-xr-x 2 newuser newuser 4096 Mar 19 10:00 /home/newuser/
# Other users can READ this home directory!
Impact:
- Other users can see file listings in the home directory
- If .ssh/authorized_keys or other sensitive files have loose permissions, they're exposed
- Private configuration files, browser profiles, etc. are browsable
The fix:
# Set restrictive permissions on creation
$ sudo useradd -m -K UMASK=077 newuser
# Or fix after creation:
$ sudo chmod 700 /home/newuser
# Set default for all new users in /etc/login.defs:
UMASK 077
# Or HOME_MODE (newer systems):
HOME_MODE 0700
# Fix all existing home directories:
$ for dir in /home/*/; do
user=$(basename "$dir")
chmod 700 "$dir"
echo "Fixed: $dir"
done
usermod -G Without -a (Group Replacement)¶
The footgun: Using usermod -G instead of usermod -aG to add a user to a group.
# Intended: ADD deploy to the docker group
$ sudo usermod -G docker deploy
# What actually happened: deploy's supplementary groups are now ONLY docker
# All other groups (sudo, www-data, etc.) are REMOVED
# Before:
$ id deploy
uid=1000(deploy) gid=1000(deploy) groups=1000(deploy),27(sudo),33(www-data),999(docker)
# After the bad command:
$ id deploy
uid=1000(deploy) gid=1000(deploy) groups=1000(deploy),999(docker)
# sudo group is GONE — deploy can no longer sudo!
Recovery:
# If you still have a root shell:
$ sudo usermod -aG sudo,www-data,docker deploy
# If you've lost sudo access, see the emergency root recovery in Street Ops
Prevention: Always use -aG (append to groups). Make it muscle memory. Consider aliasing:
# In your bashrc (as a safety measure):
alias usermod='echo "Did you mean usermod -aG? Use /usr/sbin/usermod for raw command." #'
Password-Less Service Accounts With Login Shells¶
The footgun: Creating service accounts with valid login shells.
$ sudo useradd -m myservice
# Default shell is /bin/bash, and if a password gets set...
# ...this account can be used to log in via SSH
The risk: - Service accounts often have sudo privileges or access to sensitive data - If the account has a weak password or gets one during troubleshooting, it's an attack vector - Service accounts don't need interactive logins
The fix:
# Create service accounts properly
$ sudo useradd -r -s /sbin/nologin -d /opt/myservice -M myservice
# -r = system account (low UID, no aging)
# -s /sbin/nologin = no interactive login
# -M = no home directory (or -d + -m for a specific directory)
# Fix existing service accounts
$ sudo usermod -s /sbin/nologin myservice
# Lock the password too (belt and suspenders)
$ sudo passwd -l myservice
ACL Mask Silently Reducing Permissions¶
The footgun: Setting ACLs and then using chmod, which changes the ACL mask and silently reduces effective permissions.
# Set up ACLs
$ setfacl -m u:deploy:rwx /opt/project/data/
$ setfacl -m g:developers:rwx /opt/project/data/
# Later, someone "fixes" permissions:
$ chmod 750 /opt/project/data/
# The chmod changed the ACL mask to r-x
$ getfacl /opt/project/data/
user:deploy:rwx # effective:r-x <-- WRITE IS GONE
group:developers:rwx # effective:r-x <-- WRITE IS GONE
mask::r-x
# deploy and developers can no longer write, despite ACL saying rwx
The fix:
# Restore the mask
$ setfacl -m m::rwx /opt/project/data/
# Or re-apply the ACLs (they also set the mask)
$ setfacl -m u:deploy:rwx /opt/project/data/
Prevention: When using ACLs, be aware that chmod affects the mask. Document when directories use ACLs so future admins don't unknowingly break them with chmod.
Running passwd Instead of passwd username¶
The footgun: Forgetting to specify the username when resetting someone else's password.
# Intended: reset the deploy user's password
$ sudo passwd
# Oops — no username specified — this changes ROOT's password!
Changing password for root.
New password: [you type a random temp password]
Retype new password:
passwd: password updated successfully
# You just changed root's password and might not remember what you typed
Prevention: Always specify the username explicitly:
Recovery: If you just changed root's password and don't remember what you typed, change it again immediately while you still have sudo access: