Handbook sections

Hardening sudo

Last reviewed

sudo is the controlled bridge from a user account to root. Harden the policy so one stolen session can't quietly become unaccountable, unauthenticated root, and log every escalation.

The threat: a lax sudoers policy is passwordless root

sudo is how an ordinary account becomes root for a moment, which makes its policy a permanent target. The danger is rarely sudo itself: it is a lax sudoers policy. A blanket NOPASSWD: ALL rule turns any hijacked or simply unlocked session into instant root, with no re-authentication and no challenge to prove who is really at the keyboard. Even when a password is required, the way escalation happens can leak privilege: a command run through sudo can be coerced into spawning a sub-shell (vi, less, find -exec, the GTFOBins catalogue), injecting keystrokes into the controlling terminal via the TIOCSTI ioctl, inheriting hostile environment variables like LD_PRELOAD or PATH, or picking up a malicious binary from the current directory. An escalation that is not logged is an attacker's dream: root actions with no authentication trail.

Why harden sudo

sudo sits exactly on the boundary between a user and the whole machine. Get the policy wrong and you have handed out passwordless root, the single worst privilege-escalation outcome. Hardening keeps that bridge narrow, authenticated and observable: every crossing is challenged, attributable to a person, recorded, and stripped of the side-channels (sub-shells, terminal injection, environment carry-over) that let a foothold widen into full control. Two axes matter: reduce what is granted, and make everything traceable.

What Pavois audits: the effective, merged policy

Pavois audits the effective sudoers policy, not a single file. Because sudo merges /etc/sudoers with every drop-in under /etc/sudoers.d/, Pavois scans the whole tree, so an exemption hidden in any drop-in is caught exactly as sudo would apply it. Across its Sudo rules it checks: no uncommented !authenticate and no blanket NOPASSWD (sudo-require-authentication, sudo-remove-no-authenticate), Defaults use_pty and a real-tty requirement (sudo-use-pty), noexec so escalated commands cannot launch sub-shells (sudo-noexec), env_reset (sudo-env-reset), ignore_dot (sudo-ignore-dot), and a restrictive sudo umask. Each Defaults check greps the active sudoers tree for the uncommented directive, reflecting the merged, applied policy rather than one config file.

How do you verify and operate sudo?

  • Always edit with visudo (visudo -cf /etc/sudoers.d/10-hardening for a drop-in): it validates syntax before writing, so a typo cannot lock you out of sudo. Keep a second root session open while you change the policy.
  • See the effective rules for a user with sudo -lU alice: it lists exactly what the merged policy grants them; visudo -c validates the whole tree.
  • Baseline the Defaults: grep -hE '^Defaults' /etc/sudoers /etc/sudoers.d/*. Note that use_pty is the upstream default only since sudo 1.9.14; Debian and Ubuntu backport it, but an older RHEL/Rocky may not, so check sudo --version.

Limit the privileges (ANSSI BP-028 R38 to R44)

The Defaults reduce abuse, but the rules are what grant power. Grant only the strict minimum:

Practice Rule of thumb
Grant via a dedicated group (R38) %sudo (Debian) or %wheel (RHEL), never a user directly
Target a non-root account (R40) alice ALL=(www-data) ... beats a reflexive (root)
Allowlist, never negate (R42) ALL, !/bin/su is bypassable via alternate paths or copies
Pin the arguments (R43) systemctl restart nginx, not bare systemctl (which allows edit then a root shell). A * in a path is almost always a hole: cat /var/log/messages* can read /etc/shadow via ../
Block shell escapes (R41) NOEXEC: on commands that can spawn a shell (less, vi, find, awk)
Edit files without a root shell (R44) sudoedit (sudo -e) edits as the user then reinstalls; the editor never runs as root

Log every escalation

By default sudo only logs to syslog. Make each use replayable: Defaults logfile, Defaults log_input, log_output, Defaults iolog_dir. sudoreplay -l lists sessions by their six-character TSID, and sudoreplay <TSID> replays the keystrokes and output, decisive for incident response. Caveat: log_input captures everything typed, including passwords, so add Defaults !log_passwords and lock /var/log/sudo-io to 0700 root:root. Forward the logs off-host (an attacker with root erases local logs) and watch sudoers changes with auditd (key scope).

Pitfalls

  • Do not use requiretty. It is often presented as hardening, but it breaks every non-interactive sudo (cron, deploy scripts, runuser -u app -- sudo ...). The modern, safe equivalent is use_pty, already the default on recent distributions.
  • A * in a command path is a trap: path traversal can reach /etc/shadow. Pin exact arguments instead.
  • Negation rules (!command) are bypassable: allowlist, never denylist.
  • log_input records secrets unless !log_passwords is set and the I/O logs are locked down.

Patch sudo too

No Defaults protects against a flaw in sudo itself. Two reminders: CVE-2021-3156 (Baron Samedit) let any local user become root without being in sudoers at all (fixed in 1.9.5p2); CVE-2023-22809 bypassed sudoedit via an injected -- in $EDITOR to edit out-of-scope files like /etc/shadow (fixed in 1.9.12p2). Reducing privileges and applying patches are complementary, never one without the other.

FAQ

Why does Pavois scan all of /etc/sudoers.d/? Because sudo applies the merged policy; a NOPASSWD exemption hidden in any drop-in is live, and Pavois audits what sudo actually enforces.

requiretty or use_pty? use_pty. requiretty breaks non-interactive sudo; use_pty gives the same terminal-injection protection without that cost.

How do I let someone edit a config without a root shell? sudoedit (R44): it edits the file as the user and reinstalls it, so the editor never runs as root.