Scoped sudo rules, the secure way
Someone asked for "just the right to read the app logs", you wrote one friendly line with a star in it, and now that person can read /etc/shadow. Nobody did anything clever. The star did exactly what stars do.
The short answer
Give each role exact commands with exact arguments in a file under /etc/sudoers.d/, checked with visudo -c. Use "" to forbid arguments, sudoedit instead of an editor or cat as root, and NOEXEC for programs that can start other programs. Never allow find, vi, less, bash or a wildcard path as root.
On this page
What goes wrong
A sudo rule is a promise: "this user may run this command as root". Most leaks come from rules that promise far more than the writer meant.
Two patterns cause most of them. The first is a wildcard in an argument, such
as /usr/bin/cat /var/log/app/*. In sudoers, * matches any characters,
including ../ and spaces, so the path can climb out of the directory.
The second is a program that can start other programs: find -exec, vi
with :!sh, less with !sh, awk, tar, git, env, python. If the
program runs as root, the program it starts runs as root too. The rule said
"find"; the user got a root shell.
ALL=(ALL) ALL for a whole team is the third, simpler case. It is full root
with an extra password prompt.
What the docs say
Wildcards in command line arguments should be used with care. Wildcards can match any character, including white space.
Source: sudoers manual, Wildcards
the NOEXEC tag can be used to prevent a dynamically-linked executable from running further commands itself.
Source: sudoers manual, EXEC and NOEXEC
If the empty string "" is the only command line argument in the sudoers file entry it means that command is not allowed to be run with any arguments.
Source: sudoers manual, Exceptions to wildcard rules
The manual warns about wildcards, but it does not show the ../ climb that
turns a log reader into a shadow-file reader. NOEXEC only works for
dynamically linked programs, so treat it as a backstop, not as the scope.
The secure configuration
One file per role, under /etc/sudoers.d/, edited with visudo -f so a
syntax error never locks out sudo for everyone.
# /etc/sudoers.d/deploy (mode 0440, owner root; edit with: visudo -f /etc/sudoers.d/deploy)
# deploy may restart the app, read its logs, and edit its config. Nothing else.
# Record what the user typed and saw, so a review is possible later.
Defaults:deploy log_output
# "" means: no arguments allowed at all.
Cmnd_Alias APP_RESTART = /usr/local/sbin/app-restart ""
# Exact arguments, no wildcards; the path cannot be changed.
Cmnd_Alias APP_LOGS = /usr/bin/tail -n 200 /var/log/app/app.log
# sudoedit copies the file, runs the editor as the user, then writes it back.
# The editor never runs as root, so :!sh gives the user their own shell.
Cmnd_Alias APP_CONFIG = sudoedit /etc/app/app.conf
deploy ALL=(root) NOPASSWD: APP_RESTART, APP_LOGS, APP_CONFIGFor a program that must be allowed and could start other programs, add
NOEXEC: as a second layer:
# Backstop only: NOEXEC stops exec() from dynamically linked programs.
auditor ALL=(root) NOEXEC: /usr/bin/less /var/log/app/app.logchmod 0440 /etc/sudoers.d/deploy
visudo -c # parse every sudoers file before you log out
sudo -l -U deploy # show exactly what the user can runKeep Defaults use_pty (Debian 12 sets it by default) so a command cannot
push keystrokes into your terminal after sudo exits.
Prove it
Broad rule 1, a wildcard in the path:
echo "deploy ALL=(root) NOPASSWD: /usr/bin/cat /var/log/app/*" > /etc/sudoers.d/deploy
su deploy -c "sudo cat /var/log/app/../../../etc/shadow | head -1"root:*:20714:0:99999:7:::Broad rule 2, a program that runs programs:
echo "deploy ALL=(root) NOPASSWD: /usr/bin/find" > /etc/sudoers.d/deploy
su deploy -c "sudo find /etc/hostname -exec id \;"uid=0(root) gid=0(root) groups=0(root)The scoped file from above, checked and listed:
visudo -c
sudo -l -U deploy/etc/sudoers: parsed OK
/etc/sudoers.d/README: parsed OK
/etc/sudoers.d/deploy: parsed OK
Matching Defaults entries for deploy on app-1:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin, use_pty, log_output
User deploy may run the following commands on app-1:
(root) NOPASSWD: /usr/local/sbin/app-restart "", /usr/bin/tail -n 200 /var/log/app/app.log, sudoedit /etc/app/app.confThe allowed commands work. Anything else, including the same program with
other arguments, finds no rule; sudo falls back to asking for a password,
which deploy does not have:
su deploy -c "sudo /usr/local/sbin/app-restart"
su deploy -c "sudo /usr/bin/tail -n 200 /var/log/app/app.log"
su deploy -c "sudo -n /usr/local/sbin/app-restart --now"
su deploy -c "sudo -n tail -n 200 /var/log/app/../../../etc/shadow"
su deploy -c "sudo -n find /etc/hostname -exec id \;"restarting app
app started
sudo: a password is required
sudo: a password is required
sudo: a password is requiredNOEXEC on its own, with find allowed:
echo "deploy ALL=(root) NOPASSWD: NOEXEC: /usr/bin/find" > /etc/sudoers.d/zz-noexec-demo
su deploy -c "sudo -n find /etc/hostname -exec id \;"find: 'id': Permission deniedThe test script is secure-tests/scoped-sudo-rules/run.sh.
Mistakes people make
The friendly star
/var/log/app/* reads as "files in that folder". sudo reads it as "any
characters", and ../ is characters. Name the exact file, or use a small
root-owned script that takes no arguments.
Allowing an editor or a pager as root
sudo vi file, sudo less file and sudo nano file all offer a way to run a
command from inside. Use sudoedit for editing. For reading, allow
tail or cat with a fixed path.
Editing /etc/sudoers directly
One typo and sudo refuses to run for anyone. Use visudo or visudo -f,
which checks the syntax before saving, and run visudo -c after any change.
Wrapper scripts that trust their input
A root-owned script is only as safe as its arguments. If a script takes a service name, check it against a fixed list. Keep it root-owned and not writable by the user, or the user edits the script and runs anything.
NOPASSWD: ALL for automation
A deploy bot with NOPASSWD: ALL is a root account whose key sits in CI.
Give it the exact commands it runs, the same as a person.
Checklist
- No rule grants
ALLcommands except to a small admin group. - No rule has a wildcard in a command argument.
- No rule allows an editor, pager, shell, interpreter,
find,tar,envorgitas root. - File edits go through
sudoeditwith an exact path. - Commands that must take no arguments end with
"". - Every file in
/etc/sudoers.d/is mode 0440 and owned by root. visudo -creportsparsed OKfor every file.sudo -l -U <user>matches what the role needs, and nothing more.- Scripts named in sudo rules are owned by root and not writable by others.
A good sudo rule reads like a receipt, not a wish. If you cannot say in one line what it lets someone do, it lets them do too much.
FND
Learn it on a live range
Linux 1: the command line to a working, secure host, in Foundation: a real host in your browser, and every objective checked on the machine.
Start freeThe Secure Way
More on linux hosts
SSH, sudo, firewalls, mount options, updates and the host basics every engineer should get right.
All linux hosts guides