Safe sudo: let a user manage one service without giving away root
Karim, from the web team, needs to restart Apache and read its logs. Give him the root password? No. Add him to the wheel group? He'd be root on the whole server. The right answer is a precise sudoers rule. You still have to write it without leaving a door open: here are the traps I show in my courses, and the rule I use in production.
Every example below was tested with sudo 1.9.15, the version shipped with RHEL 10.
The starting point
Create a dedicated file, always with visudo, so a typo can't break sudo:
sudo visudo -f /etc/sudoers.d/audit-web
# WHO WHERE AS WHOM WHAT
karim ALL=(root) /usr/bin/journalctl -u httpd
Karim can run sudo journalctl -u httpd. But as soon as he types sudo journalctl -u httpd --since today, sudo refuses. Why?
Rule #1: arguments in the rule mean an exact match
When a rule lists arguments, the command must repeat them exactly, no more, no less.
| Rule | What Karim can run |
|---|---|
/usr/bin/journalctl -u httpd | only journalctl -u httpd |
/usr/bin/journalctl "" | only journalctl, with no arguments at all |
/usr/bin/journalctl | any arguments: -u sshd, -u httpd -u sshd… |
/usr/bin/journalctl alone gives access to every log on the system.The * trap
The natural reflex to allow "more options" is to add a wildcard:
karim ALL=(root) /usr/bin/systemctl * httpd
It looks safe: only commands ending with httpd go through. Here is what sudo actually allows:
| Command | Result |
|---|---|
systemctl restart httpd | ✅ allowed |
systemctl edit httpd | ⚠️ allowed: opens an editor as root |
systemctl stop sshd httpd | ⚠️ allowed: also stops SSH |
systemctl status httpd | ⚠️ allowed: opens less as root |
In sudoers, * matches any sequence of characters, spaces included. It doesn't check the action, only the end of the line. Same for logs: with /usr/bin/journalctl -u httpd *, Karim can type -u httpd -u sshd and read the SSH logs. And the plain form, journalctl -u httpd, is no longer allowed: the wildcard expects something after it.
The right solution: a regular expression
Since sudo 1.9.10, arguments that start with ^ and end with $ are treated as a regular expression. You can finally say "exactly these actions, on exactly this service":
# /etc/sudoers.d/audit-web
Cmnd_Alias HTTPD_SVC = /usr/bin/systemctl ^(start|stop|restart|reload|enable|disable|is-active|is-enabled) httpd([.]service)?$, \
/usr/bin/systemctl ^status httpd([.]service)? --no-pager$
Cmnd_Alias HTTPD_LOGS = /usr/bin/journalctl ^-u httpd([.]service)?( --since (today|yesterday))?( --no-pager)?$
karim ALL=(root) NOEXEC: HTTPD_SVC, HTTPD_LOGS
Result, tested command by command:
| Command | Result |
|---|---|
systemctl restart httpd / stop httpd.service | ✅ allowed |
systemctl status httpd --no-pager | ✅ allowed |
journalctl -u httpd --since today | ✅ allowed |
systemctl edit httpd | ❌ denied |
systemctl stop sshd httpd | ❌ denied |
journalctl -u httpd -u sshd | ❌ denied |
*. It's longer, but just as safe.The hidden trap: the pager
journalctl and systemctl status show their output in less. Started by sudo, less runs as root. And less can run commands: type !bash and you get a shell… as root.
karim → sudo journalctl -u httpd → less (root) → !bash → root shell
Recent systemd versions start the pager in "secure" mode under sudo, which blocks this escape. But you don't build security on a default behaviour. Two protections, in the rule itself:
NOEXEC:stops the allowed program from launching another program (pager, shell, editor);--no-pagerrequired by the regex: output goes straight to the terminal, withoutless.
The same logic applies to vi, less, find, awk, tar and many others: they can all open a shell. The GTFOBins site lists them. To edit a system file, allow sudoedit rather than an editor.
Several rules? The last one wins
When several lines match the same command, sudo applies the last one. These two files give opposite results:
# Karim CANNOT stop httpd: the second line wins
karim ALL=(root) /usr/bin/systemctl ^stop httpd$
karim ALL=(root) !/usr/bin/systemctl
# Karim CAN stop httpd
karim ALL=(root) !/usr/bin/systemctl
karim ALL=(root) /usr/bin/systemctl ^stop httpd$
Files in /etc/sudoers.d/ are read in alphabetical order: a zz-… file can override an audit-… file.
Test before you ship
No need to log in as Karim to check a rule:
# 1. Syntax
sudo visudo -cf /etc/sudoers.d/audit-web
# 2. Everything Karim can do
sudo -l -U karim
# 3. One specific command: prints its path if allowed, nothing otherwise
sudo -l -U karim /usr/bin/systemctl edit httpd
# 4. Denied attempts
sudo journalctl _COMM=sudo --since today | grep karim
In short
- A command without arguments in the rule allows all arguments.
*only checks the overall shape of the line and matches spaces: avoid it.- Prefer a
^…$regex (sudo ≥ 1.9.10, RHEL 10) or an exact list (RHEL 9). - Add
NOEXEC:and--no-pagerto close the pager door. - Never allow
systemctl edit, an editor or a pager as root. - Test with
sudo -l -Ubefore you ship.
Hardening your RHEL servers?
I audit your sudo rules and access, or train your Linux teams on security best practices.
Need help with this?
One Cloud & DevOps tip a month
OpenShift, Kubernetes, Terraform, security: one practical tip from the field, straight to your inbox. No spam.


