HardStacked
Share:

Attack surface · sudo

sudo-rs: "wildcards are not allowed in command arguments" (Ubuntu 26.04 sudoers)

Published · HardStacked · 10 min

You upgraded a server from Ubuntu 24.04 to 26.04 LTS, and now every sudo prints wildcards are not allowed in command arguments, the faulty line of your sudoers file and a row of ^~~~ under it. A rule with a * in it, probably for a nightly script in NOPASSWD, no longer works. The question has been on askubuntu since 2026-08-07 (question 1568873, 935 views when we read it on 2026-10-26, one answer, none accepted), and the person who asks puts it in one sentence:

"Top of my list, at the moment, is the refusal of sudo-rs to allow wildcards inside a sudoers definition."

sudo-rs does not break your sudoers file. It throws away the faulty line, the whole line, and carries on with the rest, telling you again at every sudo. The * it refuses is the one that, with the old sudo, let a user read /etc/shadow. And Ubuntu 26.04 already accepts a lone * as the last argument: the rule from the question works as is, without the backslash. For the rest, a small script that validates its argument. The fix that looks easiest, removing the argument, opens everything. All measured on an Ubuntu 26.04.1 LTS virtual machine (arm64, official cloud image), sudo-rs 0.2.13-0ubuntu1.2, on 2026-10-26.

01. wildcards are not allowed in command arguments: sudo-rs drops the line and keeps going

We use a dedicated user demo (no password), a group demoadm, and one file, /etc/sudoers.d/50-demo, rewritten at each step. We never touch %sudo, and sudo-rs keeps working when a file is faulty, so a mistake here costs you the demo rules, not your own access. sudo -n means non-interactive: demo has no password, so without the right it is refused at once. The classic sudo (sudo.ws, 1.9.17p2) stays installed beside sudo-rs. The two do not report the same failure either: on a nosuid mount, sudo.ws names the mount, while sudo-rs claims it "must be owned by uid 0 and have the setuid bit set". We cut the debconf and needrestart lines of apt-get.

the lab
$ sudo --version
sudo-rs 0.2.13-0ubuntu1.2
$ sudo apt-get install -y -qq ipset >/dev/null
$ sudo groupadd demoadm
$ sudo useradd -m -G demoadm demo
$ echo 'secret log line' | sudo tee /var/log/demo-app.log
secret log line
$ sudo chmod 600 /var/log/demo-app.log
$ sudo ipset create demo-bl hash:ip

Now the trap: two rules, one with a * in the middle of the arguments, one harmless. The chmod after the tee already goes through sudo, so it shows the message in full. While the file stays faulty every sudo reprints it; we cut the repeats in what follows.

a * in the middle
$ printf '%s\n' '%demoadm ALL=(root) NOPASSWD: /usr/bin/cat /var/log/demo-*' '%demoadm ALL=(root) NOPASSWD: /usr/bin/id' | sudo tee /etc/sudoers.d/50-demo
%demoadm ALL=(root) NOPASSWD: /usr/bin/cat /var/log/demo-* %demoadm ALL=(root) NOPASSWD: /usr/bin/id
$ sudo chmod 440 /etc/sudoers.d/50-demo
/etc/sudoers.d/50-demo:1:31: wildcards are not allowed in command arguments %demoadm ALL=(root) NOPASSWD: /usr/bin/cat /var/log/demo-* ^~~~~~~~~~~~~~~~~~~~~~~~~~~~
$ sudo true; echo "exit $?"
/etc/sudoers.d/50-demo:1:31: wildcards are not allowed in command arguments %demoadm ALL=(root) NOPASSWD: /usr/bin/cat /var/log/demo-* ^~~~~~~~~~~~~~~~~~~~~~~~~~~~ exit 0
$ sudo -l -U demo
User demo may run the following commands on ubuntu: (root) NOPASSWD: /usr/bin/id
$ sudo -u demo sudo -n /usr/bin/cat /var/log/demo-app.log
sudo: I'm sorry demo. I'm afraid I can't do that
$ sudo visudo -c; echo "exit $?"
/etc/sudoers.d/50-demo:1:31: wildcards are not allowed in command arguments %demoadm ALL=(root) NOPASSWD: /usr/bin/cat /var/log/demo-* ^~~~~~~~~~~~~~~~~~~~~~~~~~~~ /etc/sudoers.d/50-demo:1:31: syntax error: wildcards are not allowed in command arguments %demoadm ALL=(root) NOPASSWD: /usr/bin/cat /var/log/demo-* ^~~~~~~~~~~~~~~~~~~~~~~~~~~~ visudo: invalid sudoers file exit 1

sudo true still exits 0: sudo-rs complained, it did not stop. The rule with the * is gone, demo is refused for cat. The other rule survived, and sudo -l says nothing about the dropped one; the only trace is the message, which disappears the day a script sends it to /dev/null. visudo -c says invalid sudoers file with exit 1 while sudo carries on, so put it in whatever deploys your sudoers files.

02. One star takes the whole line with it

Same two commands on one line, separated by a comma:

one line, two commands
$ echo '%demoadm ALL=(root) NOPASSWD: /usr/bin/id, /usr/bin/cat /var/log/demo-*' | sudo tee /etc/sudoers.d/50-demo
%demoadm ALL=(root) NOPASSWD: /usr/bin/id, /usr/bin/cat /var/log/demo-*
$ sudo -l -U demo
/etc/sudoers.d/50-demo:1:44: wildcards are not allowed in command arguments %demoadm ALL=(root) NOPASSWD: /usr/bin/id, /usr/bin/cat /var/log/demo-* ^~~~~~~~~~~~~~~~~~~~~~~~~~~~ User demo is not allowed to run sudo on ubuntu.

id left with it. sudo-rs keeps or throws the line, not the command. That is the silent trap. (The tee that rewrites the file also runs through sudo and prints the error of the previous file; we cut it.)

At least 22 GitHub projects have an issue or a fix quoting this exact message, opened between 2026-04-25 and 2026-10-23 (dokku, magebox, xymon, lerd and others). The worst we read is gha-runnerd issue 24, 2026-10-15: three chown on one line with *, the whole line ignored, and a script calling sudo with 2>/dev/null || true. More than 100,000 files accumulated before anyone noticed.

03. Why sudo-rs refuses: the old star reads /etc/shadow

Same rule, run by the classic sudo.ws; demo asks for the log and a second file. The tail -1 keeps the last line, demo's own entry in /etc/shadow.

the classic sudo.ws
$ echo '%demoadm ALL=(root) NOPASSWD: /usr/bin/cat /var/log/demo-*' | sudo tee /etc/sudoers.d/50-demo
%demoadm ALL=(root) NOPASSWD: /usr/bin/cat /var/log/demo-*
$ sudo -u demo sudo.ws -n /usr/bin/cat /var/log/demo-app.log /etc/shadow | tail -1
demo:!:20752:0:99999:7:::

The * swallowed the space and a second file, and cat printed /etc/shadow as root. A comment on dokku-datastore issue 101 (2026-08-06) says it: "as * can match spaces thus allowing additional parameters". That is what sudo-rs closed.

04. A lone * at the end already works on Ubuntu 26.04

None of the threads we read says it. The sudo-rs 0.2.13 changelog, dated 2026-03-10: "a standalone `*` as the last argument can now be used to denote "zero or more following arguments". Attempts to use wildcards in other positions will now result in an explicit parse error with `visudo`" (item 1455). Launchpad publishes 0.2.13-0ubuntu1 for Ubuntu 26.04 on 2026-03-28, then .2 on 2026-09-01.

The askubuntu case is exactly that. The person runs sudo ipset add blacklist {nasty-IP-address} every night and writes "I could write a line for each possible IP address but that's 4,294,967,296 of them!". They put NOPASSWD: ALL meanwhile and say it is less secure. No need. The addresses below are reserved for documentation (RFC 5737).

a lone * at the end
$ echo '%demoadm ALL=(root) NOPASSWD: /usr/sbin/ipset add demo-bl *' | sudo tee /etc/sudoers.d/50-demo
%demoadm ALL=(root) NOPASSWD: /usr/sbin/ipset add demo-bl *
$ sudo visudo -c
/etc/sudoers: parsed OK
$ sudo -u demo sudo -n /usr/sbin/ipset add demo-bl 203.0.113.7 && echo added
added
$ sudo -u demo sudo -n /usr/sbin/ipset destroy demo-bl
sudo: I'm sorry demo. I'm afraid I can't do that
$ sudo -u demo sudo -n /usr/sbin/ipset add demo-bl 203.0.113.8 -exist && echo added
added

parsed OK, the address goes in, destroy is refused. Two limits. First, the line in the question has a backslash before the star. That shape gives another message:

with a backslash
$ echo '%demoadm ALL=(root) NOPASSWD: /usr/sbin/ipset add demo-bl \*' | sudo tee /etc/sudoers.d/50-demo
%demoadm ALL=(root) NOPASSWD: /usr/sbin/ipset add demo-bl \*
$ sudo visudo -c
/etc/sudoers.d/50-demo:1:60: illegal escape sequence %demoadm ALL=(root) NOPASSWD: /usr/sbin/ipset add demo-bl \* ^ visudo: invalid sudoers file

We cannot know what the asker's file contains, only that this shape fails and the one without the backslash passes. On a second run, not in the capture, the question's line without the backslash, with /sbin/ipset as written there, passed too.

Second limit: a final * means zero or more arguments of any kind. The -exist went through, and on the second run -file /etc/shadow after the address got past sudo too (exit 0, no visible effect; we did not study each ipset option). It is not validation.

05. For the rest: a script that checks its argument

The only answer under the question (score 12) says not to use sudo in a script: run it as root from cron or a systemd service and a timer. For a real nightly job that is defensible, and cleaner. It does not answer the sudoers question, and it gives root to the whole script, wider than one ipset rule.

If you want a rule, put the check in a small script and give sudo no argument pattern. Three conditions: the script belongs to root, no user can modify it, the rule uses its absolute path. Ours takes exactly one IPv4 address; the regex is loose on purpose, ipset judges the value (on a third run, ipset refused 999.1.1.1: cannot parse, exit 1). The file still holds the faulty \* line until the last tee rewrites it, so the first commands printed its error. We cut it.

the script
$ printf '%s\n' '#!/bin/bash' '[[ $# -eq 1 && $1 =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] || { echo "usage: demo-bl-add <IPv4>" >&2; exit 2; }' 'exec /usr/sbin/ipset add -exist demo-bl "$1"' | sudo tee /usr/local/sbin/demo-bl-add
#!/bin/bash [[ $# -eq 1 && $1 =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] || { echo "usage: demo-bl-add <IPv4>" >&2; exit 2; } exec /usr/sbin/ipset add -exist demo-bl "$1"
$ sudo chmod 755 /usr/local/sbin/demo-bl-add
$ echo '%demoadm ALL=(root) NOPASSWD: /usr/local/sbin/demo-bl-add' | sudo tee /etc/sudoers.d/50-demo
%demoadm ALL=(root) NOPASSWD: /usr/local/sbin/demo-bl-add
$ sudo -u demo sudo -n /usr/local/sbin/demo-bl-add 198.51.100.4 && echo added
added
$ sudo -u demo sudo -n /usr/local/sbin/demo-bl-add 198.51.100.5 -exist
usage: demo-bl-add <IPv4>
$ sudo ipset list demo-bl | sed -n '/Members/,$p'
Members: 203.0.113.7 203.0.113.8 198.51.100.4

The address goes in, and 198.51.100.5 -exist, which a lone * would have let through, is refused by the script. The rule has no argument, so the script decides, and that holds only because it is root's. aidevops issue 34621 (2026-10-14) lands on the same shape.

06. The fixes that open everything

The tempting fix is to remove the argument:

no argument
$ echo '%demoadm ALL=(root) NOPASSWD: /usr/bin/cat' | sudo tee /etc/sudoers.d/50-demo
%demoadm ALL=(root) NOPASSWD: /usr/bin/cat
$ sudo visudo -c
/etc/sudoers: parsed OK
$ sudo -u demo sudo -n /usr/bin/cat /etc/shadow | tail -1
demo:!:20752:0:99999:7:::

parsed OK, the message is gone, and the rule allows cat on any file: demo reads /etc/shadow. The sudo-rs source says it: "if no arguments are mentioned, anything is allowed". visudo -c is green because the rule is valid, just very wide.

NOPASSWD: ALL, what the asker put in meanwhile, is the same move at full scale: ALL is every command, the way cat with no argument is every file. The workaround that circulates in the dokku ticket (opened 2026-05-03), sudo update-alternatives --set sudo /usr/bin/sudo.ws, is back to the classic sudo: we did not run it, it is a form, but it brings back the star of section 03. A regular expression in the sudo 1.9.10 style (ipset ^add demo-bl [0-9.]+$) gave regular expressions are not supported on a second run.

07. Clean up, and what we did not test

The teardown removes everything we created; the userdel warning is only about a mail spool that never existed. Skip the apt-get remove line if ipset was already installed before you started. The last command counts what is left with demo in its name.

teardown
$ sudo rm /etc/sudoers.d/50-demo /usr/local/sbin/demo-bl-add /var/log/demo-app.log
$ sudo ipset destroy demo-bl
$ sudo userdel -r demo
userdel: demo mail spool (/var/mail/demo) not found
$ sudo groupdel demoadm
$ id demo
id: 'demo': no such user
$ sudo apt-get remove -y -qq ipset >/dev/null
$ sudo ls /etc/sudoers.d/ | grep -c demo
0

Measured: an Ubuntu 26.04.1 LTS virtual machine (arm64), sudo-rs 0.2.13-0ubuntu1.2, sudo.ws 1.9.17p2, on 2026-10-26. Not measured: Ubuntu 25.10, which publishes sudo-rs 0.2.8 per Launchpad, older than 0.2.13; we infer from the changelog that even the final * is refused there, we did not run it. Not measured either: Ubuntu 26.10 (0.2.14) and Debian.

Share: