The sudoers file is the single most important text file on a Linux server. A misconfigured rule is the most common path from "user account compromised" to "full root access," and almost every penetration test report includes at least one sudo finding. This guide explains the rule patterns that lead to privilege escalation, the audit commands that reveal them, and how to write sudo rules that are both useful and safe.
Where sudo rules live
Two locations:
/etc/sudoersβ the historic main file, edited only viavisudo./etc/sudoers.d/β drop-in directory loaded in lexical order. Add rules here, never in the main file.
Always edit with visudo -f /etc/sudoers.d/myapp. visudo locks the file and runs syntax validation before saving β it is the difference between "rule applied" and "every sudo invocation now fails for everyone."
The five most dangerous patterns
1. NOPASSWD on broad commands
deploy ALL=(ALL) NOPASSWD: ALL # full root, no auth
deploy ALL=(ALL) NOPASSWD: /bin/bash # equivalent
deploy ALL=(ALL) NOPASSWD: /usr/bin/su # one step away
Any one of these turns a compromised user account into root. Audit:
sudo grep -r NOPASSWD /etc/sudoers /etc/sudoers.d/
2. Wildcards that match shell escape
deploy ALL=(ALL) NOPASSWD: /usr/bin/vim * # exploit: sudo vim file ; :!sh
Most editors, pagers, and compilers can spawn shells. vim, less, more, vi, nano, man, find with -exec, awk, perl, python, ruby, node, tar with --checkpoint-action=exec, zip with --unzip-command β all are escape vectors. The community list at GTFOBins is the canonical reference.
3. Configuration files that the user can modify
deploy ALL=(ALL) NOPASSWD: /usr/local/bin/backup.sh
If backup.sh is writable by deploy, the privilege escalation is trivial: append cp /bin/bash /tmp/rs; chmod 4755 /tmp/rs to the script. Audit ownership and writability:
find /usr/local/bin -perm -o+w -ls
find /usr/local/bin -not -user root -ls
4. Commands without absolute paths
deploy ALL=(ALL) NOPASSWD: tail -f /var/log/syslog
Without an absolute path, the user controls which binary runs. Always anchor: /usr/bin/tail.
5. !authenticate or runas mistakes
deploy ALL=(www-data) NOPASSWD: /usr/sbin/service nginx * # ok if service trusted
The runas spec defaults to root; explicitly restricting it to a less-privileged user is good practice for service-management commands.
The audit script every server needs
#!/bin/bash
echo '== NOPASSWD entries =='
sudo grep -rH NOPASSWD /etc/sudoers /etc/sudoers.d/ 2>/dev/null
echo
echo '== Wildcards =='
sudo grep -rHE 'NOPASSWD.*\*' /etc/sudoers /etc/sudoers.d/ 2>/dev/null
echo
echo '== !authenticate =='
sudo grep -rH '!authenticate' /etc/sudoers /etc/sudoers.d/ 2>/dev/null
echo
echo '== Group memberships of high-privilege groups =='
for g in wheel sudo admin; do
getent group "$g" 2>/dev/null
done
echo
echo '== Per-user effective sudo rules =='
for u in $(awk -F: '$3>=1000 {print $1}' /etc/passwd); do
printf 'User %s\n' "$u"
sudo -lU "$u" 2>/dev/null | sed 's/^/ /'
done
Run weekly. The sudo -lU username output is the single most useful line β it shows exactly what each user is allowed to run as root, regardless of which file the rule lives in.
Writing rules that survive review
Three principles:
- Specific is safer than generic. Allow
/usr/sbin/service nginx reload, not/usr/sbin/service ALL. - Use Cmnd_Alias for groups of related commands. Easier to review and harder to mistype:
Cmnd_Alias NGINX_OPS = /usr/sbin/service nginx reload, \ /usr/sbin/service nginx restart, \ /usr/sbin/service nginx status deploy ALL = (root) NOPASSWD: NGINX_OPS - Default to requiring a password. Use NOPASSWD only for jobs that must run unattended (CI/CD, monitoring agents).
Logging and detection
Sudo logs to syslog by default. Capture:
sudo journalctl _COMM=sudo --since today
sudo grep 'COMMAND=' /var/log/auth.log | awk -F'COMMAND=' '{print $2}' | sort | uniq -c | sort -rn
Add I/O logging for forensics on high-value commands:
Defaults!NGINX_OPS log_input, log_output
Defaults iolog_dir=/var/log/sudo-io
Replay later with sudoreplay -d /var/log/sudo-io 0.
Removing sudo access cleanly
When an employee leaves or a role rotates, two steps:
sudo deluser bob sudo # drop from group
sudo rm /etc/sudoers.d/bob 2>/dev/null # drop direct file
sudo -lU bob # confirm: nothing
Pair with revoking SSH keys and disabling the OS account.
Common pitfalls
- Editing
/etc/sudoerswithnanodirectly; a syntax error breaks all sudo invocations until you boot single-user. - Granting
NOPASSWD: /usr/bin/aptβ the user can install any package, including a malicious one from a private repo they control. - Using
!authenticateon automation users without then restricting allowed commands tightly. - Forgetting that
%groupnamein sudoers means group, not user β typo turns a single-user grant into a group-wide grant.
Sudo is one of the systems where 80% of the security work is auditing what already exists rather than designing new rules. Run the audit script monthly, treat every NOPASSWD line as a potential incident, and your sudo configuration becomes a controlled, reviewable surface rather than a tangle of historical exceptions.