Not every helper needs full root. A developer who only restarts their app, or a support account that reads logs, can get exactly those commands through sudo and nothing more. This guide shows how to write tight sudoers rules, which mistakes quietly hand out full root anyway, and how to test the result.
Before you start
- A Linux VPS with Ubuntu 22.04 or 24.04, Debian 12, or AlmaLinux or Rocky Linux 8 or 9.
- Root or full sudo access, and a second root shell open while you edit.
- The account you want to limit, for example
deploy. It must not also be in thesudoorwheelgroup, or it keeps full rights. - Familiarity with
visudo. If you are new to it, read Edit the sudoers file safely with visudo first.
Warning: A syntax error in sudoers can break sudo for every user. Always edit with visudo, and keep a root session open until a fresh login proves the rule works.
Remove full admin rights first
Check the user's groups:
id deployIf you see sudo (Ubuntu, Debian) or wheel (AlmaLinux, Rocky), remove it:
sudo gpasswd -d deploy sudoUse wheel instead of sudo on AlmaLinux and Rocky. The change applies at the user's next login.
Find the full path of each command
sudoers rules must use absolute paths. Look them up on your own server, because they can differ between distributions:
command -v systemctl journalctl/usr/bin/systemctl
/usr/bin/journalctlWrite the rule
Create a drop-in file:
sudo visudo -f /etc/sudoers.d/deployAllow restarting, reloading and checking one service, and reading its logs:
Cmnd_Alias MYAPP_CTL = /usr/bin/systemctl restart myapp.service, \
/usr/bin/systemctl reload myapp.service, \
/usr/bin/systemctl status myapp.service, \
/usr/bin/journalctl -u myapp.service *
deploy ALL=(root) MYAPP_CTLHow the matching works:
- A command with no arguments listed, such as
/usr/bin/systemctl, allows that program with any arguments. That would includesystemctl stop sshd. Always list the arguments. - A command with arguments listed must be typed exactly that way.
sudo systemctl restart myappdoes not matchmyapp.serviceabove. Pick one form and tell your users. - Adding
""as the only argument, like/usr/bin/df "", allows the command with no arguments at all.
Add NOPASSWD: before the alias only if automation needs it; see Allow sudo without a password (NOPASSWD).
Avoid the common traps
Wildcards match more than you think
A * in sudoers matches any characters, including spaces. So this rule:
deploy ALL=(root) /usr/bin/systemctl restart *lets the user restart any service, including SSH and the database. Path wildcards are just as loose. A rule for /usr/bin/cat /var/log/* also matches cat /var/log/../../etc/shadow, because * does not stop at ... In the journalctl example above the wildcard only adds options to a read-only command for one unit, which is an accepted trade-off. Never put a wildcard after a command that changes the system or reads arbitrary files.
Programs that can spawn a shell
Many ordinary tools can run other commands or edit files. Giving any of these via sudo is the same as giving full root:
- Editors and pagers:
vim,nano,less,more,man. - File tools with exec options:
find,tar,rsync,zip,awk,sed. - Interpreters:
bash,sh,python3,perl,php. cp,mv,tee,chmodorchownon arbitrary paths, which can overwrite/etc/sudoersor/etc/shadow.
Scripts the user can edit
If you allow /home/deploy/restart.sh, the user can change the script and run anything. Put scripts in a root-owned path such as /usr/local/sbin/, owned by root:root and not writable by others.
Let a user edit one file safely
To let someone change a single config file, use sudoedit instead of granting an editor:
deploy ALL=(root) sudoedit /etc/nginx/sites-available/example.comThe user runs sudoedit /etc/nginx/sites-available/example.com. Their own editor opens a temporary copy as their own user, and sudo writes it back. No root shell is ever exposed.
Test the rule
Check the syntax of all sudoers files:
sudo visudo -cThen, as root, list what the user may run without logging in as them:
sudo -l -U deployUser deploy may run the following commands on web01:
(root) /usr/bin/systemctl restart myapp.service, /usr/bin/systemctl reload myapp.service,
/usr/bin/systemctl status myapp.service, /usr/bin/journalctl -u myapp.service *Check it works
Log in as deploy in a new session and run one allowed and one blocked command:
sudo systemctl restart myapp.service
sudo systemctl stop sshdThe first runs after the password prompt. The second is refused with a message like Sorry, user deploy is not allowed to execute '/usr/bin/systemctl stop sshd' as root. Refusals are logged to the system journal, which you can read with journalctl -t sudo.
Troubleshooting
- Allowed command is refused: the user typed it differently from the rule, for example without
.service. Match the rule exactly or add both forms. - User can still run everything: they are still in
sudoorwheel, or another file grantsALL. Checkid deployandsudo -l -U deploy. - "command not allowed" for a command with a relative path: sudoers matches the resolved full path. Make sure
command -von the server matches what you wrote. visudo -creports an error in your alias: alias names must be uppercase, and a line continuation needs\as the very last character.
Next steps
- Lock down remote access too: Limit SSH logins with AllowUsers and AllowGroups.
- Record what admins run: Log root commands with auditd.
