Skip to content

Fix sudo-rs "wildcards are not allowed in command arguments" on Ubuntu 26.04

sudoubuntusudoerssecuritytroubleshooting
7 min read

Quick Answer

Ubuntu 26.04 makes sudo-rs the default sudo, and sudo-rs refuses wildcards inside command arguments. A sudoers line such as deploy ALL=(root) NOPASSWD: /usr/sbin/ipset add blacklist 203.0.113.* worked with classic sudo on 24.04. On 26.04 it prints wildcards are not allowed in command arguments on every sudo call, the rule is dropped, and the command fails with I'm sorry deploy. I'm afraid I can't do that. The rest of the file still loads. sudo-rs accepts exactly one wildcard form: a lone * as the last argument, which means any further arguments. A backslash-escaped \* is a separate error, illegal escape sequence, and regex (^...$) and sha256: digest rules are rejected too. Run visudo -c on 26.04 to see every rejected line, or run sudoers-rs-check.sh on 24.04 before you upgrade. When the arguments must be checked, point the rule at a root-owned wrapper script that validates them.

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:

text
## Ubuntu 24.04.5 LTS / Sudo version 1.9.15p5 $ cat /etc/sudoers.d/blocklist deploy ALL=(root) NOPASSWD: /usr/sbin/ipset add blacklist 203.0.113.* $ sudo -n /usr/sbin/ipset add blacklist 203.0.113.7 exit=0
text
## Ubuntu 26.04.1 LTS / sudo-rs 0.2.13-0ubuntu1.2 $ sudo -n /usr/sbin/ipset add blacklist 203.0.113.7 /etc/sudoers.d/blocklist:1:29: wildcards are not allowed in command arguments deploy ALL=(root) NOPASSWD: /usr/sbin/ipset add blacklist 203.0.113.* ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ sudo: I'm sorry deploy. I'm afraid I can't do that exit=1

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 nginxaccepted
/usr/local/bin/x "a b" and /usr/local/bin/x a\,baccepted
/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 *.txtwildcards 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/xdigest 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:

text
## Ubuntu 24.04.5 LTS, rule: /usr/sbin/ipset add blacklist \* $ sudo -n /usr/sbin/ipset add blacklist 203.0.113.7 sudo: a password is required exit=1 $ sudo -n /usr/sbin/ipset add blacklist '*' ipset v7.19: Syntax error: cannot parse *: resolving to IPv4 address failed exit=1

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":

text
$ cat /etc/sudoers.d/blocklist deploy ALL=(root) NOPASSWD: /usr/sbin/ipset add blacklist * $ visudo -c /etc/sudoers: parsed OK exit=0 $ sudo -n /usr/sbin/ipset add blacklist 203.0.113.7 exit=0 $ sudo -n /usr/sbin/ipset add blacklist 198.51.100.0/24 exit=0 $ ipset list blacklist | grep -c "^198.51.100." 256

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):

bash
#!/bin/bash # Script: blocklist-add # Purpose: sudo-rs cannot match "any IPv4 address" with a wildcard, so this root-owned wrapper does the matching and sudoers allows only the wrapper. # Usage: sudo /usr/local/sbin/blocklist-add IPV4 set -euo pipefail CHECK="✓" CROSS="✗" SET_NAME="blacklist" OCTET='(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])' # Exactly one argument, and it must be a dotted quad: nothing else reaches ipset as root. if [[ $# -ne 1 || ! "$1" =~ ^$OCTET(\.$OCTET){3}$ ]]; then echo "$CROSS blocklist-add: refusing '$*' (expected one IPv4 address)" >&2 exit 2 fi /usr/sbin/ipset add "$SET_NAME" "$1" -exist echo "$CHECK $1 is in $SET_NAME"
text
$ cat /etc/sudoers.d/blocklist deploy ALL=(root) NOPASSWD: /usr/local/sbin/blocklist-add $ sudo -n /usr/local/sbin/blocklist-add 203.0.113.8 ✓ 203.0.113.8 is in blacklist exit=0 $ sudo -n /usr/local/sbin/blocklist-add 198.51.100.0/24 ✗ blocklist-add: refusing '198.51.100.0/24' (expected one IPv4 address) exit=2 $ sudo -n /usr/local/sbin/blocklist-add "203.0.113.8; id" ✗ blocklist-add: refusing '203.0.113.8; id' (expected one IPv4 address) exit=2

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.

bash
#!/bin/bash # Script: sudoers-rs-check.sh # Purpose: sudo-rs (default sudo on Ubuntu 26.04) silently drops sudoers rules it cannot parse, so a nightly job that ran under sudo for years fails after the upgrade; this finds those rules before you upgrade. # Usage: sudo ./sudoers-rs-check.sh [SUDOERS_FILE ...] (default: /etc/sudoers and /etc/sudoers.d/*) set -euo pipefail CHECK="✓" CROSS="✗" DEFAULT_FILES=(/etc/sudoers) SUDOERS_DIR="/etc/sudoers.d" FINDINGS=0 # sudo's includedir skips names containing a dot or ending in ~, so editor backups are never read; mirror that. if [[ $# -eq 0 && -d "$SUDOERS_DIR" ]]; then for f in "$SUDOERS_DIR"/*; do name=${f##*/} [[ -f "$f" && "$name" != *.* && "$name" != *~ ]] && DEFAULT_FILES+=("$f") done fi FILES=("$@") [[ $# -gt 0 ]] || FILES=("${DEFAULT_FILES[@]}") report() { # file line command reason fix echo "$CROSS $1:$2" echo " rule: $3" echo " why: $4" echo " fix: $5" FINDINGS=$((FINDINGS + 1)) } check_command() { # file line command local file=$1 line=$2 cmd=$3 args last # A digest prefix (sha256:...) pins the binary's hash; sudo-rs rejects the whole rule. if [[ "$cmd" =~ ^sha(224|256|384|512): ]]; then report "$file" "$line" "$cmd" "digest specifications are not supported by sudo-rs" "drop the sha256:... prefix and make the binary root-owned and not writable by anyone else" return fi read -r _ args <<< "$cmd" [[ -n "${args:-}" ]] || return 0 # ^...$ turns the argument list into a regular expression in sudo 1.9.10+; sudo-rs has no regex support. if [[ "$args" == ^*\$ ]]; then report "$file" "$line" "$cmd" "regular expressions are not supported by sudo-rs" "list the exact argument lists, or call a root-owned wrapper script that validates its arguments" return fi # sudo-rs accepts \, \: \= \\ and an escaped space; any other backslash (classically \*) is a syntax error. if [[ "$args" =~ \\[^,:=\\\ ] ]]; then report "$file" "$line" "$cmd" "illegal escape sequence (sudo-rs only accepts \\, \\: \\= \\\\ and an escaped space)" "a literal * argument needs no backslash in sudo-rs; to allow any arguments end the rule with a lone *" return fi # A lone * as the LAST argument means "any further arguments" and is allowed; every other glob character is not. last=${args##* } local rest=$args [[ "$last" == "*" ]] && rest=${args% \*} [[ "$rest" == "*" ]] && rest="" if [[ "$rest" == *[\*\?\[]* ]]; then report "$file" "$line" "$cmd" "wildcards are not allowed in command arguments (only a lone trailing * is)" "end the rule at the fixed arguments with a lone * (e.g. ... add blocklist *) and validate the value inside a wrapper script" fi } check_line() { # line text (file is $CURRENT_FILE) local file=$CURRENT_FILE line=$1 text=$2 cmds seg # Only user specs and Cmnd_Alias lines carry commands; Defaults and the other aliases cannot trigger these errors. [[ "$text" =~ ^[[:space:]]*(Defaults|User_Alias|Runas_Alias|Host_Alias|@include|#include) ]] && return 0 [[ "$text" == *=* ]] || return 0 cmds=${text#*=} # Split on commas that are not backslash-escaped, then strip the (runas) and TAG: prefixes from each command. while IFS= read -r seg; do seg=${seg#"${seg%%[![:space:]]*}"} seg=$(sed -E 's/^\([^)]*\)[[:space:]]*//; s/^([A-Z_]+:[[:space:]]*)+//' <<< "$seg") [[ -z "$seg" || "$seg" == "ALL" || "$seg" =~ ^[A-Z][A-Z0-9_]*$ ]] && continue check_command "$file" "$line" "$seg" done < <(sed -E 's/\\,/\x01/g; s/,/\n/g; s/\x01/\\,/g' <<< "$cmds") } for file in "${FILES[@]}"; do if [[ ! -r "$file" ]]; then echo "$CROSS cannot read $file (sudoers files are root-only: run this with sudo)" >&2 exit 2 fi CURRENT_FILE=$file start=0 buf="" lineno=0 while IFS= read -r raw || [[ -n "$raw" ]]; do lineno=$((lineno + 1)) [[ "$raw" =~ ^[[:space:]]*# ]] && continue # A trailing backslash continues the rule on the next line, and the error is reported against the first one. if [[ -z "$buf" ]]; then start=$lineno; fi if [[ "$raw" == *\\ ]]; then buf+="${raw%\\} "; continue; fi buf+="$raw" check_line "$start" "$buf" buf="" done < "$file" done # On a box that already runs sudo-rs, its own visudo is the final authority on what it will load. if command -v visudo >/dev/null && visudo --version 2>/dev/null | grep -q 'sudo-rs'; then if visudo -c >/dev/null 2>&1; then echo "$CHECK sudo-rs visudo -c accepts every file" else echo "$CROSS sudo-rs visudo -c reports:" visudo -c 2>&1 | sed 's/^/ /' || true FINDINGS=$((FINDINGS + 1)) fi fi if (( FINDINGS == 0 )); then echo "$CHECK ${#FILES[@]} file(s) checked: nothing sudo-rs would reject" exit 0 fi echo "$CROSS $FINDINGS problem(s): sudo-rs ignores each rule above, and every sudo call prints the parse error" exit 1

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, so deploy.bak or deploy~ are not reported.
  • Continuation lines (a trailing \) are joined first, and the finding is reported against the line the rule starts on. visudo -c reports 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 as NOPASSWD: and SETENV:. 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 -c runs only when visudo --version says 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:

text
## Ubuntu 24.04.5 LTS (before the upgrade) $ ./sudoers-rs-check.sh ✗ /etc/sudoers.d/deploy:2 rule: /usr/sbin/ipset add blacklist \* why: illegal escape sequence (sudo-rs only accepts \, \: \= \\ and an escaped space) fix: a literal * argument needs no backslash in sudo-rs; to allow any arguments end the rule with a lone * ✗ /etc/sudoers.d/deploy:3 rule: /usr/bin/systemctl restart app-* why: wildcards are not allowed in command arguments (only a lone trailing * is) fix: end the rule at the fixed arguments with a lone * (e.g. ... add blocklist *) and validate the value inside a wrapper script ✗ /etc/sudoers.d/deploy:4 rule: /usr/bin/tail -n 100 /var/log/app/*.log why: wildcards are not allowed in command arguments (only a lone trailing * is) fix: end the rule at the fixed arguments with a lone * (e.g. ... add blocklist *) and validate the value inside a wrapper script ✗ 3 problem(s): sudo-rs ignores each rule above, and every sudo call prints the parse error exit=1

On 26.04 the same three findings print, followed by sudo-rs's own verdict, which flags the same three lines:

text
✗ sudo-rs visudo -c reports: /etc/sudoers.d/deploy:2:60: syntax error: illegal escape sequence deploy ALL=(root) NOPASSWD: /usr/sbin/ipset add blacklist \* ^ /etc/sudoers.d/deploy:3:29: syntax error: wildcards are not allowed in command arguments deploy ALL=(root) NOPASSWD: /usr/bin/systemctl restart app-*, /usr/bin/systemctl status app-web ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /etc/sudoers.d/deploy:5:19: syntax error: wildcards are not allowed in command arguments /usr/bin/tail -n 100 /var/log/app/*.log ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ visudo: invalid sudoers file

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

Raw script, MIT licensed: scripts/sudo-rs-wildcards-not-allowed.sh on GitHub

PAID RESOURCE — $9

The Production Bash Toolkit

An operational script system + a 30-function shared library + a 52-page field guide. The production layer the free snippets don't cover.

Get the Toolkit →
curl -O bashlib-starter.sh

Get the bashlib starter

Ten functions I source into every script on my own boxes — strict-mode setup, an ERR trap that names the failing line, lock and timeout wrappers, and cleanup that runs on every exit path. One email, no sequence.

BashSnippets logo

Written by Travis

Creator of BashSnippets.xyz

bashsnippets.xyz/about

Related Snippets

Frequently Asked Questions

faq — snippet

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.

faq — snippet

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.

faq — snippet

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.

faq — snippet

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.

faq — snippet

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.