sudo and least privilege
sudo-rs on Ubuntu, sudo on RHEL, and the logs.
sudo runs one command with another user's privileges, usually root's, after checking a rule that says you are allowed. It is how you stay logged in as your own limited account and reach for root only for the task that needs it, with every elevation recorded against your name. This lesson shows which sudo Ubuntu 26.04 ships (it is not the one most guides describe), how to read your own rights, how to write and validate a narrow rule, what the logs record, and where Ubuntu and RHEL differ. The goal is least privilege: grant the exact command needed, and no path from it to a root shell.
Which sudo is this?
Ubuntu 26.04 replaced the original sudo with sudo-rs, a reimplementation in Rust that reads the same /etc/sudoers rules but supports a deliberately smaller set of features. It matters because its output and its error messages differ from the classic tool, and because it rejects some classic syntax. Check what you have:
sudo --version reports sudo-rs 0.2.13, and /usr/bin/sudo resolves through the alternatives system to /usr/lib/cargo/bin/sudo. The original sudo is still installed as sudo.ws for when you need a feature sudo-rs does not have. RHEL 10 ships the classic sudo (version 1.9.17) at /usr/bin/sudo, so the two families behave differently in the details below. sudo-rs refuses some classic settings outright rather than ignoring them, for example wildcards inside command arguments and session recording (log_output), so a sudoers file copied from an older guide can fail validation. Where a classic-only feature is a hard requirement, update-alternatives can point sudo at sudo.ws; the hardening course covers that choice.
A narrow rule, validated before it is installed
To follow along you need something to grant. The setup below creates a small lab service, ess-sudo-app (a unit that only runs sleep; the systemd lesson explains unit files), an operator account ess-sudo-ops, and a service account ess-sudo-svc with a configuration file only its group may read:
Rules live in /etc/sudoers or, better, in separate files under /etc/sudoers.d/ so each grant stays small and easy to review. Here is a rule that lets the operator restart that one unit, and read its status, and nothing else. Save it in your home directory first (with any editor) as ~/50-ess-sudo-ops:
# ess-sudo-ops may bounce one service on this host, and nothing else.# who host=(run-as) TAG: exact command, arguments includedess-sudo-ops ALL=(root) NOPASSWD: /usr/bin/systemctl restart ess-sudo-app.service, \/usr/bin/systemctl --no-pager status ess-sudo-app.service
Read a rule from the left. The user comes first (a name starting with % is a group, as in Ubuntu's own %sudo ALL=(ALL:ALL) ALL, which lets members of sudo run anything as anyone). ALL in the host field means on any host, which only matters when one sudoers file is shared by many machines. (root) is the account the command may run as, NOPASSWD: skips the password prompt, and the rest is the exact command. The command and its arguments are part of the rule, so any other form is refused. --no-pager is part of it on purpose: systemctl status normally shows long output in less, and systemctl(1) recommends disabling the pager when untrusted users run it with elevated privileges, because a pager can start other programs.
A broken sudoers file can lock everyone out of sudo, so never edit it with a plain editor. visudo -cf FILE checks a candidate file's syntax without installing it, which is what to run before the file goes anywhere. Only after it passes do you install it and re-check the whole configuration.
visudo -cf reported parsed OK, then install placed the file with mode 0440 owned by root, and visudo -c confirmed the entire sudoers set still parses. On a fleet, run visudo -c in a check before the file ships. The same check refuses a bad rule. A common mistake is naming a command without its full path; here a one-line file with that mistake is checked, and sudo-rs requires an absolute path and says so:
It names the file, line and column, explains that a fully qualified path is needed, and exits non-zero, so a check step can fail the change before it reaches a machine.
vim can run :!sh), a pager (less can run !sh), an interpreter (python, awk, perl), or tools like find -exec and tar --checkpoint-action. Granting one of those with NOPASSWD is granting a root shell, not one command. When a user must edit one file as root, grant sudoedit /path (supported by sudo-rs) instead of an editor, so the editor runs as the user on a copy. The hardening and detection courses go deeper into these escapes.What the operator may run, and what happens when they try
sudo -l lists the rules that apply to you. The next terminals run as the operator: to do the same, switch to that account with sudo -iu ess-sudo-ops (-i starts a login shell, -u names the account; the operator needs no password of their own for this) and type exit to come back. sudo-rs prints the rules plainly, with no "Matching Defaults" block:
Now the operator reads the status, typed exactly as the rule names it, runs the allowed restart, then tries a command just outside the rule:
restart runs. stop is refused with sudo-rs's denial message, and the command never runs. This is least privilege on one screen: the operator can bounce the service, which is the job, and cannot stop it, wipe it or open a shell. Note that the rule named restart ess-sudo-app.service exactly, so even systemctl restart ess-sudo-app without the .service suffix would be refused, because sudoers matches the whole command line.
Every allowed use is written down
On Ubuntu the records go to /var/log/auth.log, which the adm group may read, so an admin account reads it without sudo. These are the last two sudo lines for the operator:
Each line names the user, the working directory, the target user and the exact command: the status and the restart the operator ran. Notice what is missing: the refused stop came after the restart, yet it has no line. This is specific to the version tested here. sudoers-rs(5), the manual page installed with sudo-rs 0.2.13, says that by default it logs both successful and unsuccessful attempts, but in this lab the refusal was shown to the operator on screen and written nowhere, in auth.log or the journal. Classic sudo on RHEL logs both, as the callout below shows. So on Ubuntu 26.04 do not build an alert on refused-sudo lines without testing that they appear, and re-test after sudo-rs updates. To answer "what could this account do?", read its rules with sudo -l -U USER rather than waiting for the log.
Become another user, not only root
Sometimes you do not need root, you need to be a service account for one command. sudo -u USER runs a command as that account, which is how you read a file or open a shell as www-data or a database user without their password.
Here the command ran as ess-sudo-svc and read the file that account's group owns, then handed the identity straight back: a smaller blast radius than a root shell.
Three related habits are worth knowing. After you type your password, sudo remembers it per terminal for a while (15 minutes on Ubuntu, 5 on RHEL; timestamp_timeout in sudoers sets it), and sudo -k forgets it at once, which is a good reflex before you walk away from a session. sudo -i starts root's login shell (reading root's startup files) and sudo -s starts a root shell without reading them; both are full root sessions, so prefer single commands, which each leave their own log line. su - switches user by asking for the target account's password, which on Ubuntu root does not have, so sudo is the normal route. Ubuntu's /etc/sudoers also sets Defaults use_pty, which runs each command in its own pseudo-terminal so a program run through sudo cannot push keystrokes back into your shell after it exits.
The RHEL callout: classic sudo
On Rocky Linux 10 the same rule, installed the same way, is reported differently. sudo -l prints a "Matching Defaults entries" block (with env_reset, secure_path and more), the admin group is wheel rather than sudo, and refused commands are logged to /var/log/secure.
The command not allowed line is exactly what a defender wants: on classic sudo a burst of them across many commands is someone probing what they can run, and it belongs in your alerts. secure_path in the Defaults block is a quiet win: it forces sudo to search a known, trusted list of directories, so a command named in a rule cannot be swapped for a look-alike planted in the caller's own PATH.
Try this
On a lab machine, create a throwaway user with sudo useradd -m -s /bin/bash ops1. Write ~/50-ops1 containing the single line ops1 ALL=(root) NOPASSWD: /usr/bin/systemctl --no-pager status cron.service, check it with sudo visudo -cf ~/50-ops1, install it with sudo install -m 0440 -o root -g root ~/50-ops1 /etc/sudoers.d/50-ops1 and run sudo visudo -c. Become the user with sudo -iu ops1, run sudo -l to read the grant and sudo systemctl --no-pager status cron.service (allowed), then sudo systemctl restart cron.service and read the refusal. Type exit, and on Ubuntu check with grep ops1 /var/log/auth.log which of the two was logged. Remove the drop-in with sudo rm /etc/sudoers.d/50-ops1, confirm sudo visudo -c still parses, and remove the user with sudo userdel -r ops1.
Takeaway
Grant the narrowest exact command, by absolute path, with no wildcard and no program that can spawn a shell, and validate every sudoers file with visudo -cf before it is installed. Read sudo --version so you know whether you are on sudo-rs or classic sudo, and remember that on Ubuntu's sudo-rs 0.2.13 only allowed commands reached the log in testing, so audit rights with sudo -l -U rather than trusting the log to show refusals.