DEV Community

Cover image for The sudo Rule Ubuntu 26.04 Rejected Was Already Broken
Mustafa ERBAY
Mustafa ERBAY

Posted on Originally published at mustafaerbay.com.tr

The sudo Rule Ubuntu 26.04 Rejected Was Already Broken

Some day after upgrading to Ubuntu 26.04 LTS you will open visudo, look at a line that has been sitting there for years, and see a red arrow pointing at it. Same rule. Same file. But sudo is no longer the same sudo.

The first reflex is predictable: "They rewrote it in Rust and forgot half of it." I was ready to say exactly that. Then I looked at what the rejected rule actually did, and the story flipped. That rule had never done what I assumed it did. sudo-rs is not introducing a new restriction; it is pointing at a door that has been quietly open for a decade.

First, the inventory: what exactly is "sudo" on 26.04?

Since Ubuntu 25.10 the default sudo provider has been sudo-rs — a from-scratch Rust implementation of sudo and su, maintained by the Trifecta Tech Foundation. 26.04 LTS carries that into the LTS line. The original sudo that Todd C. Miller has maintained for thirty years was not deleted; it was renamed. It is now sudo.ws.

In a clean 26.04 container the picture looks like this:

$ dpkg-query -W -f='${Package} ${Version}\n' sudo sudo-rs
sudo 1.9.17p2-1ubuntu3
sudo-rs 0.2.13-0ubuntu1.2

$ update-alternatives --query sudo | grep -E '^(Value|Alternative|Priority):'
Value: /usr/lib/cargo/bin/sudo
Alternative: /usr/bin/sudo.ws
Priority: 40
Alternative: /usr/lib/cargo/bin/sudo
Priority: 50
Enter fullscreen mode Exit fullscreen mode

Both implementations are on disk, update-alternatives decides which one wins, and sudo-rs leads 50 to 40. The version number hides an important detail: the 26.04 package is based on 0.2.13, while upstream is at 0.2.15 as I write this. But the 0ubuntu1.2 suffix exists precisely to close that gap. The package changelog states plainly what you are getting:

$ zcat /usr/share/doc/sudo-rs/changelog.Debian.gz | head -6
rust-sudo-rs (0.2.13-0ubuntu1.2) resolute-security; urgency=medium

  * d/patches/fix-sudoedit.patch: Fix privilege escalation race in sudoedit
    (LP: #2165142)
Enter fullscreen mode Exit fullscreen mode

So the sudoedit race fix that upstream shipped in 0.2.15 already reached 26.04 through the security pocket. Do not look at the version number and conclude "outdated"; on Ubuntu it is the package changelog, not the upstream tag, that tells you which fixes arrived. That same changelog also shows the wildcard behaviour this article is about was a tracked change — the 0.2.13 package explicitly cites the "accept a 'final' wildcard" merge.

The rejected rule

Let us make it concrete. Almost every team has some variant of this — a permission handed to a deploy user so it can "clean up after itself":

deploy ALL=(root) NOPASSWD: /bin/rm *.txt
Enter fullscreen mode Exit fullscreen mode

On 26.04, visudo refuses that line:

# visudo -c -f /etc/sudoers.d/deploy
/etc/sudoers.d/deploy:1:29: syntax error: wildcards are not allowed in command arguments
deploy ALL=(root) NOPASSWD: /bin/rm *.txt
                            ^~~~~~~~~~~~~
visudo: invalid sudoers file
Enter fullscreen mode Exit fullscreen mode

At runtime the same error is printed and the command is denied. The wording of the refusal also tells you something about the sudo-rs authors' sense of humour:

$ sudo -n rm /tmp/README.txt
/etc/sudoers.d/deploy:1:29: wildcards are not allowed in command arguments
sudo: I'm sorry deploy. I'm afraid I can't do that
Enter fullscreen mode Exit fullscreen mode

So far this reads like a "the tool is missing a feature" story. Now for the real question.

What did that rule actually allow?

Hand the same sudoers file to the original sudo and try a rather less innocent command as the same user:

# update-alternatives --set sudo /usr/bin/sudo.ws
# sudo --version | head -1
Sudo version 1.9.17p2

# mkdir -p /tmp/victim && touch /tmp/victim/important.conf
# su - deploy -c 'sudo -n rm -rf /tmp/victim .txt'
# ls /tmp/victim
ls: cannot access '/tmp/victim': No such file or directory
Enter fullscreen mode Exit fullscreen mode

The directory is gone. The rule that said *.txt happily matched rm -rf /tmp/victim .txt, because original sudo applies the wildcard to the argument text and * swallows everything, spaces included. As long as the last argument ends in .txt, anything in between is fair game. In other words that line was equivalent to:

deploy ALL=(root) NOPASSWD: /bin/rm
Enter fullscreen mode Exit fullscreen mode

A pattern written to narrow a privilege was in fact widening it all the way to root. All sudo-rs does is refuse to parse it. Run the same command under sudo-rs and /tmp/victim is still there.

⚠️ This is not a sudo-rs quirk

sudoers rules with wildcards inside argument text are a long-known privilege escalation pattern. The only new thing is that an implementation now says so to your face instead of silently accepting it. 26.04 did not break the rule; 26.04 noticed the rule was already broken.

Where is the real boundary?

The sentence in the README ("wildcards are only supported in argument positions for a command as the final argument") is confusing on first read. Deriving the behaviour from examples is far clearer. The table below shows visudo -c results under sudo-rs 0.2.13 on 26.04:

Command spec Result
/usr/bin/systemctl restart nginx accepted
/sbin/fsck* accepted
/opt/scripts/*.sh accepted
/opt/scripts/* accepted
/usr/bin/systemctl * accepted
/usr/bin/systemctl restart * accepted
/bin/rm *.txt rejected
/usr/bin/tee /etc/nginx/sites-enabled/* rejected
/usr/bin/find /var/log -name *.gz -delete rejected
/usr/bin/docker compose -f /srv/*/compose.yml up -d rejected

The pattern emerges, and it is actually simple:

  • In the command path, wildcards are free. /opt/scripts/*.sh works, because what is being matched there is a filesystem path.
  • In arguments, wildcards are forbidden — with one exception: a bare * standing alone as the final argument. That is a special token introduced in 0.2.13 meaning "zero or more following arguments".
  • Any wildcard embedded inside an argument (*.txt, /srv/*/compose.yml, -name *.gz) is a parse error. The same ban covers ? — look for that too when scanning your inventory.

Diagram

Boiled down to one sentence: sudo-rs lets you describe which program you may run with a wildcard, and refuses to let you describe what you may type into it with a wildcard. The second one was never safely expressible anyway.

But the top half of that table is not innocent either. sudo-rs accepts /opt/scripts/* without complaint — yet anyone who can write a file into that directory can become root. The danger is the same as with the *.txt rule; the difference is that the parser stays quiet this time. Writing /opt/scripts/ instead (trailing slash, no wildcard) is the more honest expression: it grants execution of the files in that directory and does not descend into subdirectories. It still equates write access on that directory with root. sudo-rs closed the argument wildcard, not the path wildcard — that door is still yours to shut.

Does one broken rule take down the whole file?

That is the question that keeps you up on upgrade night. The answer is reassuring: no. Test with a file whose first line is broken and second line is sound — sudo-rs prints the error but still runs the sound rule:

# cat /etc/sudoers.d/deploy
deploy ALL=(root) NOPASSWD: /bin/rm *.txt
deploy ALL=(root) NOPASSWD: /usr/bin/id

# su - deploy -c 'sudo -n id'
/etc/sudoers.d/deploy:1:29: wildcards are not allowed in command arguments
uid=0(root) gid=0(root) groups=0(root)
Enter fullscreen mode Exit fullscreen mode

So a broken line does not leave your server unprivileged; only that line goes dark. Good news. The bad news: this warning is printed to stderr on every single sudo call. If you have an Ansible task or a cron script parsing that output, it is now meeting lines it did not expect.

What you genuinely lose beyond wildcards

These are the items that carry the actual risk in an upgrade plan. None of them is an oversight; they are deliberate design decisions.

Session recording is absent from sudo-rs. sudo-rs does not recognise settings like log_output or iolog_dir:

# cat /etc/sudoers.d/io
Defaults log_output
Defaults iolog_dir=/var/log/sudo-io

# visudo -c -f /etc/sudoers.d/io
/etc/sudoers.d/io:1:10: syntax error: unknown setting: 'log_output'
/etc/sudoers.d/io:2:10: syntax error: unknown setting: 'iolog_dir'
Enter fullscreen mode Exit fullscreen mode

The same file passes cleanly under visudo.ws -c. There is a nuance here that is easy to miss: the recording tools were not deleted from 26.04, they simply do not come with sudo-rs. Install the sudo package and they reappear under a .ws suffix — but sudo-rs neither depends on nor recommends that package, so on a machine carrying only sudo-rs those commands do not exist at all:

# only sudo-rs installed
$ command -v sudoreplay sudoreplay.ws cvtsudoers.ws sudo_logsrvd
(no output)

# after installing the "sudo" package as well
$ command -v sudoreplay.ws cvtsudoers.ws sudo_logsrvd
/usr/bin/sudoreplay.ws
/usr/bin/cvtsudoers.ws
/usr/sbin/sudo_logsrvd
Enter fullscreen mode Exit fullscreen mode

If your audit requirement rests on sudo's I/O logging, this is a fork in the road: either those hosts stay on sudo.ws, or you move recording to another layer. I have previously written up session recording on a bastion with tlog and sudo I/O; on 26.04 the sudo leg of that chain does not stand under sudo-rs, while the tlog leg does. My own view is that the right move is to take recording off sudo entirely from the start — because burying your audit trail inside your privilege tool means that the day you change the tool, you change the audit too.

sudoers in LDAP is gone. sudo-rs does not support sudoers.ldap and does not implement cvtsudoers; and the sudo-ldap package is gone from the archive — on 26.04 apt-cache policy sudo-ldap does not even report a candidate. You can still authenticate against LDAP through PAM or SSSD; what you need to find is a distribution mechanism for the policy you used to store centrally.

The password prompt changed. For automation this is the sneakiest item on the list:

sudo.ws : [sudo] password for deploy:
sudo-rs : [sudo: authenticate] Password:
Enter fullscreen mode Exit fullscreen mode

sudo-rs prints transparently whatever PAM says; it may be Password: or PIN:. An expect script or a collector matching on the literal [sudo] password for will time out here. The escape hatch the documentation suggests is to remove the prompt entirely: --prompt "".

Password feedback is now on. As of 0.2.13, pwfeedback is enabled by default, so you see asterisks as you type. If shoulder surfing is in your threat model, add Defaults !pwfeedback via visudo, or press TAB at the prompt.

use_pty is on by default. sudo runs the command in its own pseudo-terminal; a CI step that expects no TTY can behave unexpectedly here. It can be turned off, but think before you do: it stands against session hijacking.

The rest. INTERCEPT, mail via sendmail, and the file-based logfile are absent; logging always goes to syslog and denied invocations are always logged. sudo -E is gone too — but its selective counterpart --preserve-env=list remains, so carrying an environment variable is no reason to fall back to sudo.ws. Resource limits and umask come from PAM rather than sudoers. On the other hand NOEXEC: is supported via seccomp, so your tool for blocking shell escapes is still there.

So what did this change buy?

The rationale for the Rust rewrite is memory safety, and that rationale is not abstract. Two serious vulnerabilities were disclosed in original sudo in 2025: CVE-2025-32463 — local root access by way of an nsswitch.conf read from a user-controlled directory with the --chroot option, scored 7.8 by NVD — and CVE-2025-32462, where a sudoers file naming a host that is neither the current host nor ALL let commands run on unintended machines, scored 8.8 by NVD. Both affect versions before 1.9.17p1, and neither affected sudo-rs. The interesting part is the reason: not memory safety, but the simple fact that those features do not exist in sudo-rs at all. Smaller surface, fewer doors.

This does not prove sudo-rs has fewer bugs — the project says so honestly in its own FAQ; new code brings new bugs. What it does prove is more modest and more useful: not carrying most of the features a long-lived C program accumulated also means not carrying the risk those features brought.

Before you upgrade: the short list

If it were my fleet, I would clear these five steps before the 26.04 wave.

  1. Feed your inventory to the 26.04 parser. Collect /etc/sudoers and /etc/sudoers.d/* from every server, then scan them with visudo-rs rather than with your own eyes:
   $ docker run --rm -v "$PWD/sudoers.d:/in:ro" ubuntu:26.04 bash -c \
       'apt-get update -qq && apt-get install -y -qq sudo-rs >/dev/null
        for f in /in/*; do visudo-rs -c -f "$f"; done'
Enter fullscreen mode Exit fullscreen mode

One trap: do not run this check on 24.04. The sudo-rs in the noble archive is 0.2.2 and the wildcard check arrived in 0.2.13; it answers parsed OK to /bin/rm *.txt. A false all-clear is worse than no report.

  1. Treat every rule you find as a security finding. Not "let us make it fit 26.04", but "what does this rule permit today under original sudo". The answer will usually be uncomfortable.
  2. Reach for a wrapper when rewriting. Instead of /usr/bin/tee /etc/nginx/sites-enabled/*, grant the privilege to a script that does one obvious thing and validates its own arguments. You can make sudoers enforce the argument-free part for you: the rule /opt/bin/rotate-logs "" permits the script only with no arguments at all, so nothing starting with - gets through. An elaborate sudoers pattern is nearly always an authorization problem solved in the wrong place.
  3. Build automation around exit codes, not prompt text. Find everything that parses the password prompt and replace it with --prompt "" or a passwordless design.
  4. Verify the audit chain separately. If you depend on I/O recording, decide before the upgrade; noticing afterwards means a window during which you kept no records.

ℹ️ The way back is still there

If you need to buy time on a host, update-alternatives --set sudo /usr/bin/sudo.ws returns you to the original sudo, and update-alternatives --set sudo /usr/lib/cargo/bin/sudo brings you back. Use it as a temporary bridge. Choose it as the permanent answer and all you own is deferred work and a sudoers file that deserves to be rejected.

Closing

I started this piece assuming sudo-rs was a regression. It is not. The most expensive feature of a thirty-year-old tool was its willingness to silently accept a badly written rule; writing *.txt and handing out rm -rf rights can sit unnoticed for years. A parser telling you "no" is uncomfortable, but it is cheaper than one silently saying "yes".

Preferring a loud error to quiet permissiveness is a general rule in infrastructure. Black boxes spend their worst nights without telling anyone. The sudo in 26.04 chooses to speak up before one of those nights arrives — and to my mind that belongs in the upgrade notes as a gain, not as a warning line.

Official Sources

Top comments (0)