How sudo Resolves Privileges
When a user runs sudo, the binary reads /etc/sudoers and any files in /etc/sudoers.d/ in lexicographic order. Rules are evaluated top to bottom, and the last matching rule wins - not the first. This trips up engineers who add a restrictive rule at the top expecting it to block access, only to find a broader rule lower in the file overrides it.
The sudoers file must never be edited directly with a text editor. Use visudo, which validates syntax before writing the file. A syntax error in /etc/sudoers locks every user out of sudo immediately.
Rule syntax follows this pattern:
user host=(runas_user:runas_group) command
Each field accepts either explicit values or aliases. The special value ALL matches everything in that position. On a single-machine deployment you will almost always see ALL in the host field, but on shared sudoers files distributed across a fleet, the host field lets you scope rules to specific hostnames or netgroups.
# Check which sudo rules apply to the current user
sudo -l
# Check rules for a specific user (run as root)
sudo -l -U deploy
# Validate sudoers syntax without opening visudo
visudo -c
# Edit the main sudoers file safely
visudo
# Edit a drop-in file in sudoers.d
visudo -f /etc/sudoers.d/deploy
sudoers Syntax: Users, Groups, Aliases, and Runas
The four alias types in sudoers - User_Alias, Runas_Alias, Host_Alias, and Cmnd_Alias - let you define reusable groups and reference them across multiple rules. Without aliases, a sudoers file for a medium-sized team becomes an unmaintainable list of per-user lines.
Command aliases are the most operationally useful. You can group related binaries under a single name and apply that name to multiple user rules. This is how you give a DBA group access to pg_dump, pg_restore, and psql without granting them systemctl or useradd.
The runas constraint controls which target user and group the command runs as. Most configurations leave this as root, but service account delegation is a legitimate pattern. A deploy user running Ansible can be permitted to sudo as the app service account - not root - to restart a specific service. This limits blast radius significantly.
# /etc/sudoers.d/team-dba
# Define the group of DBA users
User_Alias DBA_TEAM = alice, bob, carol
# Define allowed PostgreSQL commands
Cmnd_Alias PG_CMDS = /usr/bin/pg_dump, /usr/bin/pg_restore, /usr/bin/psql, \
/usr/bin/pg_basebackup
# Define allowed service restart commands (specific service only)
Cmnd_Alias PG_SERVICE = /usr/bin/systemctl restart postgresql, \
/usr/bin/systemctl reload postgresql, \
/usr/bin/systemctl status postgresql
# Grant DBA_TEAM the ability to run PG commands as postgres user
DBA_TEAM ALL=(postgres:postgres) NOPASSWD: PG_CMDS
# Grant PG service control as root with password required
DBA_TEAM ALL=(root) PG_SERVICE
NOPASSWD and When to Use It
NOPASSWD is the most misused tag in sudoers. The correct application is automation accounts - service users running scripts, CI runners, Ansible playbooks, and monitoring agents that have no interactive session to prompt. Granting NOPASSWD to human admin accounts removes the only friction point that prevents accidental destructive commands.
When you do use NOPASSWD for automation, scope it to exactly the commands the automation needs. A deployment pipeline that restarts an application server does not need NOPASSWD on /bin/bash or ALL. If your CI system needs broader access, that is a workflow design problem, not a sudoers problem.
The opposite tag is PASSWD, which forces password authentication even if a previous rule granted NOPASSWD. You can combine them in sequence to grant NOPASSWD on safe read-only commands while requiring PASSWD on anything destructive.
For teams using DevOps automation platforms like taskbotshub.ai to orchestrate multi-system workflows, scoping NOPASSWD tightly per command is critical - an automation token with broad sudo privileges is a significant lateral movement risk if the orchestration layer is compromised.
# /etc/sudoers.d/ci-deploy
# CI runner account gets NOPASSWD only for specific deploy operations
deploy ALL=(root) NOPASSWD: /usr/bin/systemctl restart myapp, \
/usr/bin/systemctl reload nginx, \
/usr/bin/rsync
# Same user must authenticate for anything touching user management
deploy ALL=(root) PASSWD: /usr/sbin/useradd, /usr/sbin/userdel
# Ansible service account - runas app user only, not root
ansible ALL=(appuser:appgroup) NOPASSWD: /usr/local/bin/deploy.sh
Timestamp and Session Timeout Configuration
By default, sudo caches credentials for 15 minutes after a successful authentication. During that window, any sudo command runs without re-prompting. On a shared terminal session or a machine with an unlocked screen, that 15-minute window is a real attack surface.
The timestamp_timeout Defaults entry accepts minutes as an integer or a float. Setting it to 0 requires password authentication for every single sudo invocation. Setting it to -1 keeps the credential cached indefinitely until explicitly invalidated with sudo -k. For interactive admin work, 5 minutes is a reasonable compromise. For NOPASSWD automation accounts, timestamp_timeout is irrelevant since no credential is cached.
The timestamp_type option controls the scope of the credential cache. The default global type shares the cache across all terminals. Setting it to tty creates a separate cache per terminal, so an authenticated session in one terminal does not grant access in another. Setting it to ppid creates a cache per parent process, which tightens scope further for script-driven invocations.
Sudo 1.9.0 introduced timestamp_file and timestamp_dir controls that let you relocate the credential cache files, useful on systems with noexec /var/run mounts.
# /etc/sudoers - Defaults section
# Require password every time for interactive users
Defaults:alice,bob timestamp_timeout=0
# 5-minute cache per terminal for general admin group
Defaults:%sudo timestamp_timeout=5,timestamp_type=tty
# Invalidate your own sudo timestamp immediately
sudo -k
# Invalidate another user's timestamp (run as root)
sudo -K -u alice
# Check current timestamp status
sudo -v
Environment Variable Control
sudo strips nearly all environment variables before executing the target command. This is deliberate - environment variables like LD_PRELOAD, LD_LIBRARY_PATH, and PYTHONPATH are common privilege escalation vectors. The env_reset Defaults option is enabled by default on all major distributions and should never be disabled system-wide.
The env_keep list defines variables that survive the privilege transition. The default list on most systems includes HOME, LOGNAME, USER, USERNAME, and SHELL. On RHEL and Fedora, sudo also keeps COLORS, DISPLAY, HOSTNAME, HISTSIZE, INPUTRC, KDEDIR, MAIL, PS1, PS2, QTDIR, USERNAME, LANG, and LC_* variables by default.
If an automation script or admin workflow needs a specific variable passed through, add it to env_keep or use env_check instead of disabling env_reset. The env_check list passes a variable only if its value does not contain % or / characters - useful for simple flag variables.
The secure_path Defaults option sets the PATH for sudo-invoked commands regardless of the user's PATH. Always verify this includes the paths your commands actually live in. A common production issue is a command that works interactively failing under sudo because secure_path does not include /usr/local/bin.
# /etc/sudoers - environment configuration
# Always reset environment (this is the default, make it explicit)
Defaults env_reset
# Secure path - verify this matches where your binaries live
Defaults secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
# Preserve specific variables for admin tooling
Defaults env_keep += "EDITOR VISUAL PAGER"
Defaults env_keep += "SSH_AUTH_SOCK SSH_AGENT_PID"
# Pass through AWS credentials for admin scripts (use with care)
Defaults:deploy env_keep += "AWS_PROFILE AWS_DEFAULT_REGION"
# Check what environment sudo actually provides
sudo env | sort
Logging and Audit Trail Configuration
The default sudo logging writes a one-line entry to syslog or journald for each invocation: who ran what, as whom, from which terminal, and whether it succeeded or failed. That is the minimum. For any environment subject to compliance requirements or post-incident forensics, you need I/O logging - a full record of keystrokes and output for each sudo session.
I/O logging is enabled with log_input and log_output Defaults options, or the NOEXEC tag on specific commands. Logs are written to /var/log/sudo-io/ by default, with each session stored in a directory named by sequence number. The sudoreplay utility plays back these sessions, making forensic analysis straightforward.
Sudo 1.9.4 added support for sending logs to a remote sudo_logsrvd daemon over TLS, decoupling log storage from the machine where commands run. This is the right architecture for a fleet - local disk logs can be deleted by a compromised root account, but logs already shipped to a remote syslog or logsrvd instance are harder to tamper with.
For JSON-structured log output compatible with SIEM ingestion, set log_format=json in /etc/sudo_logsrvd.conf. The structured output includes timestamps, uid, gid, command, arguments, exit status, and I/O data.
# /etc/sudoers - enable I/O logging for all sessions
Defaults log_input, log_output
Defaults iolog_dir=/var/log/sudo-io/%{user}
Defaults iolog_file=%{seq}
# Log to syslog with high priority for failed attempts
Defaults syslog=auth
Defaults syslog_badpri=alert
Defaults syslog_goodpri=notice
# Replay a recorded session (get sequence ID from /var/log/sudo-io)
sudoreplay /var/log/sudo-io/alice/000001
# List recorded sessions
sudoreplay -l
# List sessions for a specific user
sudoreplay -l user alice
# /etc/sudo_logsrvd.conf - remote logging
[server]
listen_address = 0.0.0.0(30344)
tls = true
tls_cert = /etc/sudo_logsrvd/server.crt
tls_key = /etc/sudo_logsrvd/server.key
[iolog]
log_format = json
iolog_dir = /var/log/sudo-io
Restricting Commands with NOEXEC
NOEXEC prevents a command permitted via sudo from spawning child processes using exec(). This closes the most common privilege escalation technique: getting sudo access to an editor like vi, less, or find, then using their built-in shell escape mechanisms to drop into a root shell.
NOEXEC works by preloading a shared library that intercepts exec() and execve() calls and returns EACCES. It is not foolproof - statically linked binaries, interpreted languages with their own exec implementations, and some syscall interfaces can bypass it. Do not use NOEXEC as your only control; use it in combination with tightly scoped command aliases.
For commands that need to call external tools as part of their normal operation, NOEXEC will break functionality. Test thoroughly before applying it to production tool access. The EXEC tag explicitly re-enables exec() for a command after a broader NOEXEC rule.
# /etc/sudoers - prevent shell escapes from allowed commands
# Allow alice to view log files but prevent shell escape
Defaults:alice NOEXEC
alice ALL=(root) NOEXEC: /usr/bin/less /var/log/*, /usr/bin/tail /var/log/*
# Block specific known-dangerous binaries from getting exec()
Cmnd_Alias NOEXEC_CMDS = /usr/bin/vi, /usr/bin/vim, /usr/bin/less, \
/usr/bin/more, /usr/bin/nano, /usr/bin/find
%sysadmin ALL=(root) NOEXEC: NOEXEC_CMDS
# Verify NOEXEC is working - this should fail with EACCES
# sudo less /var/log/syslog
# Then inside less, type: !bash
# Should return: sh: bash: Operation not permitted
sudoers.d Drop-in Files and Fleet Distribution
Managing a single /etc/sudoers file across a fleet is operationally painful. The includedir directive - present in the default sudoers on Ubuntu, Debian, RHEL, and most major distributions - reads all files in /etc/sudoers.d/ that do not contain a dot or end with a tilde. Files are read in lexicographic order.
The practical pattern is to create one file per team or service role. A file named 10-dba-team applies DBA rules, 20-platform-team applies platform team rules, and 90-emergency-access contains break-glass rules. Lexicographic ordering lets you control precedence explicitly by naming convention.
For fleet distribution, avoid templating the entire sudoers file. Distribute only the drop-in files that change, and use configuration management to enforce the base sudoers file is unmodified. With Ansible, the template module can write each drop-in file, followed by a validate step running visudo -c -f %s on the result before replacing the live file.
File permissions on /etc/sudoers and files in /etc/sudoers.d/ must be 0440 (or 0640 on some distributions). sudo refuses to read any sudoers file with world-write permissions.
# Check that includedir is present in your base sudoers
grep -n includedir /etc/sudoers
# Should output something like:
# @includedir /etc/sudoers.d
# List current drop-in files with permissions
ls -la /etc/sudoers.d/
# Deploy a new drop-in with correct permissions
install -m 0440 -o root -g root 30-devops-team /etc/sudoers.d/30-devops-team
# Validate the new file before deploying (CI/CD pre-check)
visudo -c -f /etc/sudoers.d/30-devops-team
# Ansible task snippet for safe drop-in deployment
# - name: Deploy sudo rules for devops team
# template:
# src: sudoers-devops.j2
# dest: /etc/sudoers.d/30-devops-team
# owner: root
# group: root
# mode: '0440'
# validate: 'visudo -c -f %s'
Detecting Privilege Escalation Attempts
sudo logs every failed authentication attempt and every attempt to run a command not permitted by sudoers. These entries go to syslog with the facility and priority set by the syslog and syslog_badpri Defaults. Monitoring for these entries is the minimum viable detection baseline.
Three log patterns are worth alerting on immediately: repeated authentication failures from a single user (brute-force on cached credentials), sudo attempts for commands not in the user's permitted list (reconnaissance or escalation attempt), and successful sudo to a shell binary or interpreter that was not explicitly permitted.
The mail_badpass and mail_no_user Defaults options send email on failed attempts. For modern environments, ship sudo logs to your SIEM directly from syslog or journald rather than relying on email. On systemd systems, journalctl filters sudo logs efficiently.
# Monitor sudo failures in real time
journalctl -f _COMM=sudo | grep -E 'NOT in sudoers|incorrect password|command not allowed'
# Count sudo failures per user in the last 24 hours
journalctl --since '24 hours ago' _COMM=sudo | \
grep 'NOT in sudoers\|incorrect password' | \
awk '{print $6}' | sort | uniq -c | sort -rn
# Find all successful sudo commands run as root today
journalctl --since today _COMM=sudo | grep 'COMMAND' | grep 'USER=root'
# Alert on shell access via sudo (adapt for your SIEM)
journalctl -f _COMM=sudo | grep -E 'COMMAND=.*(bash|sh|zsh|python|perl|ruby)'
# Check if anyone has modified sudoers files recently
find /etc/sudoers /etc/sudoers.d -newer /etc/passwd -ls
Hardening Defaults for Production Systems
The shipped defaults on most distributions are not production-ready from a security standpoint. Several Defaults options should be set explicitly on any system that matters.
requiretty forces sudo to be run from an actual terminal. This blocks non-interactive sudo from scripts and automation - which is why you should set it globally, then override it per account for automation users with !requiretty. logfile writes sudo events to a dedicated file separate from syslog, giving you a local record that is harder to suppress than syslog rotation.
use_pty forces sudo to allocate a pseudo-terminal for the command it runs, which prevents background processes from retaining elevated privileges after the sudo session ends. This is enabled by default in sudo 1.9.13 and later. If you are running an older version, set it explicitly.
insults is not a security control but it is worth mentioning: it responds to failed password attempts with insults from the original Unix tradition. Disable it on production systems with !insults to keep log output clean.
# /etc/sudoers - production hardening Defaults
# Require real terminal for interactive users, override per automation account
Defaults requiretty
Defaults:deploy !requiretty
Defaults:ansible !requiretty
# Force PTY allocation to contain privilege scope
Defaults use_pty
# Write to dedicated log file in addition to syslog
Defaults logfile=/var/log/sudo.log
Defaults log_year, log_host
# Limit password attempts before lockout
Defaults passwd_tries=3
Defaults passwd_timeout=1
# Alert on failed attempts
Defaults mail_badpass
Defaults mailto=security-alerts@example.com
# Disable insults in production
Defaults !insults
# Protect against PATH-based attacks
Defaults secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
# Show last login info on sudo authentication
Defaults lecture=once
Defaults lecture_file=/etc/sudo.lecture