Denied for a reason this page does not cover?
Fix sudo-rs "I'm afraid I can't do that" works out which of the other causes it is: no rule at all, an argument that differs by one character, or a command outside sudo's secure_path.
The nightly job that has run sudo ipset add ... for years fails the morning after the upgrade, and the error it prints is a sudoers syntax error, not a denial. Ubuntu 26.04 replaced sudo with sudo-rs. sudo-rs reads the same /etc/sudoers, but it does not support every rule classic sudo accepted, and the rules it cannot read are dropped, not reported once at boot.
What Changed Between 24.04 and 26.04?
Reproduced on 2026-10-10 in two containers on this machine: ubuntu:24.04 (Ubuntu 24.04.5 LTS, sudo 1.9.15p5) and ubuntu:26.04 (Ubuntu 26.04.1 LTS, sudo-rs 0.2.13-0ubuntu1.2), each with a user deploy and an ipset called blacklist. The same rule on both, allowing addresses from one range:
Two things to notice. The parse error is printed by every sudo call, from any user, not only by visudo. And the final message is a denial, because the rule that allowed the command no longer exists as far as sudo-rs is concerned. Other lines in the same file keep working, which is why only some jobs break.
Which sudoers Rules Does sudo-rs Reject?
Each line below was written to /etc/sudoers.d/ in the 26.04 container and checked with visudo -c:
Rule (after deploy ALL=(root) NOPASSWD:) | sudo-rs 0.2.13 |
|---|---|
/usr/sbin/ipset add blacklist * | accepted: lone * last means any further arguments |
/usr/local/bin/* | accepted: wildcards in the path are fine |
/usr/bin/systemctl restart nginx, /usr/bin/systemctl reload nginx | accepted |
/usr/local/bin/x "a b" and /usr/local/bin/x a\,b | accepted |
/usr/sbin/ipset add blacklist 203.0.113.* | wildcards are not allowed in command arguments |
/usr/local/bin/x * foo (the * is not last) | wildcards are not allowed in command arguments |
/usr/local/bin/x a? and /usr/local/bin/x add *.txt | wildcards are not allowed in command arguments |
/usr/sbin/ipset add blacklist \* | illegal escape sequence |
/usr/local/bin/x ^a.*$ | regular expressions are not supported |
sha256:<hash> /usr/local/bin/x | digest specifications are not supported |
The \* row deserves a second look. In the 24.04 container, classic sudo read \* as a literal asterisk, so that rule only ever allowed the argument * itself:
The second call got past sudo and reached ipset. On 26.04 the same file fails to parse at all.
How Do I Fix the Rule?
Option 1: a lone * at the end. It is the only wildcard sudo-rs accepts in arguments, and it means "anything after this":
It parses and it works, and it also let one call add 256 addresses. "Any arguments" is wider than "any IP address" was meant to be.
Option 2: a wrapper that checks the arguments. sudoers allows one root-owned script with no argument pattern, and the script decides what is valid. Install it as /usr/local/sbin/blocklist-add, owned by root and not writable by anyone else (a wrapper the user can edit is a root shell):
This is stricter than the 24.04 rule ever was. In the 24.04 container, the 203.0.113.* rule also let sudo -n /usr/sbin/ipset add blacklist 203.0.113.7 timeout 0 through, because classic sudo's * matches spaces, so it matched extra arguments too.
Option 3: go back to classic sudo with sudo update-alternatives --set sudo /usr/bin/sudo.ws. It works the same day and leaves you on a configuration future upgrades do not assume. Use it to buy time, then fix the rules.
The Script: Find These Rules Before You Upgrade
Save as sudoers-rs-check.sh and run it with sudo on the 24.04 server before do-release-upgrade. It reads /etc/sudoers and /etc/sudoers.d/ the way sudo does (skipping file names with a dot or a trailing ~), follows backslash continuation lines, and reports every command rule sudo-rs will refuse. On a box that already runs sudo-rs, it also runs visudo -c as the final check.
Prerequisites
bash 4 or later and sed. It reads root-only files, so it needs sudo, but it changes nothing. It does not need sudo-rs installed: the checks are static, which is the point of running it before the upgrade.
How Does Each Check Work?
- The include rules match sudo's
@includedir: files whose names contain a dot or end in~are never read, sodeploy.bakordeploy~are not reported. - Continuation lines (a trailing
\) are joined first, and the finding is reported against the line the rule starts on.visudo -creports the physical line of the offending command, which can be a line or two lower. - Command splitting cuts the list on commas that are not escaped, then strips the
(root)run-as part and tags such asNOPASSWD:andSETENV:. Aliases (Cmnd_Alias) are checked the same way, because sudo-rs rejects a bad alias line just as it rejects a bad user line. - The wildcard test removes a final lone
*, then looks for*,?or[in what is left. That mirrors the 0.2.13 parser's behaviour in the table above. - The escape test allows
\,,\:,\=,\\and an escaped space, and flags any other backslash. visudo -cruns only whenvisudo --versionsays sudo-rs. Its output is printed as is, because sudo-rs's own parser decides what loads.
What Does the Script Print?
The same sudoers.d file in both containers: one escaped \* rule from an older setup, an app-* service rule, and a Cmnd_Alias split over two lines with a *.log path argument:
On 26.04 the same three findings print, followed by sudo-rs's own verdict, which flags the same three lines:
Note that sudo-rs rejected line 3 as a whole, so /usr/bin/systemctl status app-web, which is valid on its own, was lost with it. Split rules onto separate lines so one bad command cannot take the good ones down. The script exits 1 when it finds anything, so it can gate an upgrade playbook.
The rest of the upgrade's surprises, from Rust coreutils to changed exit codes, are collected in Ubuntu 26.04: What Breaks in Bash Scripts.
Frequently Asked Questions
Why does sudo-rs say wildcards are not allowed in command arguments?
sudo-rs, the Rust sudo that Ubuntu 26.04 installs as the default, does not implement glob matching on command arguments, so any *, ? or [ inside an argument in sudoers is a syntax error. The line is skipped, the error is printed on every sudo call, and the user gets I'm afraid I can't do that for the command it used to allow. Paths may still contain wildcards, and a lone * as the final argument is accepted.
How do I allow any arguments with sudo-rs?
End the rule with a lone * after the fixed part, without a backslash: deploy ALL=(root) NOPASSWD: /usr/sbin/ipset add blacklist *. sudo-rs treats that as any further arguments, including none and including several. That is broader than it looks: in the test on this page it let a whole /24 into an ipset in one call, so use a wrapper script when the value matters.
What does illegal escape sequence mean in sudoers on Ubuntu 26.04?
A backslash before a character sudo-rs does not expect, almost always \* copied from an older sudoers file. Classic sudo read \* as a literal asterisk. sudo-rs rejects it and drops the line. Remove the backslash: a lone * at the end means any arguments, and a literal asterisk argument is better handled by a wrapper.
Can I switch Ubuntu 26.04 back to the classic sudo?
Yes. The classic sudo is installed as /usr/bin/sudo.ws by the sudo package, and update-alternatives chooses which one /usr/bin/sudo is: sudo update-alternatives --set sudo /usr/bin/sudo.ws. That is a stopgap. Fixing the rules keeps you on the default, which is what future upgrades will assume.
How do I find sudoers rules that will break before upgrading to 26.04?
Run sudoers-rs-check.sh from this page on the 24.04 box as root. It reads /etc/sudoers and /etc/sudoers.d the way sudo does and lists every rule with an argument wildcard, an illegal escape, a regex or a digest, with the fix for each. On a box already running sudo-rs it also runs visudo -c, which is the final word.
Part of the bash snippets collection
Related Scripts
- Fix sudo-rs "I'm afraid I can't do that" — the other reasons sudo-rs denies a command, and a script that names the one you hit
- Ubuntu 26.04: What Breaks in Bash Scripts — sudo-rs, Rust coreutils and the exit codes that changed, with a pre-upgrade checklist
- Fix Bash "Permission denied" (Exit 126) — when the denial comes from the filesystem, not sudo
- File Permissions Security — keep wrapper scripts root-owned and unwritable