HardStacked
Partager :

Surface d'attaque · sudo

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

Publié le · HardStacked · 10 min

Tu as passé un serveur d'Ubuntu 24.04 à 26.04 LTS, et maintenant chaque sudo imprime wildcards are not allowed in command arguments, la ligne fautive de ton fichier sudoers et une rangée de ^~~~ dessous. Une règle avec un *, probablement pour un script de nuit en NOPASSWD, ne marche plus. La question traîne sur askubuntu depuis le 2026-08-07 (question 1568873, 935 vues quand on l'a lue le 2026-10-26, une réponse, aucune acceptée), et la personne qui la pose le résume en une phrase :

« Top of my list, at the moment, is the refusal of sudo-rs to allow wildcards inside a sudoers definition. » (« En tête de ma liste, en ce moment, il y a le refus de sudo-rs d'accepter des jokers dans une définition sudoers. »)

sudo-rs ne casse pas ton fichier sudoers. Il jette la ligne fautive, la ligne entière, et continue avec le reste, en te le redisant à chaque sudo. Le * qu'il refuse est celui qui, avec l'ancien sudo, laissait lire /etc/shadow. Et Ubuntu 26.04 accepte déjà un * seul en dernier argument : la règle de la question marche telle quelle, sans le backslash. Pour le reste, un petit script qui valide son argument. Le correctif qui paraît le plus facile, retirer l'argument, ouvre tout. Tout est mesuré sur une machine virtuelle Ubuntu 26.04.1 LTS (arm64, image cloud officielle), sudo-rs 0.2.13-0ubuntu1.2, le 2026-10-26.

01. wildcards are not allowed in command arguments : sudo-rs jette la ligne et continue

On utilise un utilisateur demo (sans mot de passe), un groupe demoadm et un seul fichier, /etc/sudoers.d/50-demo, réécrit à chaque étape. On ne touche jamais à %sudo, et sudo-rs continue de fonctionner quand un fichier est fautif : une erreur ici te coûte les règles de demo, pas ton accès. sudo -n veut dire non interactif : demo n'a pas de mot de passe, donc sans le droit il est refusé tout de suite. Le sudo classique (sudo.ws, 1.9.17p2) reste installé à côté de sudo-rs. Les deux ne rapportent pas non plus la même panne : sur un montage nosuid, sudo.ws nomme le montage, alors que sudo-rs affirme qu'il « must be owned by uid 0 and have the setuid bit set ». On a coupé les lignes debconf et needrestart de l'apt-get.

le labo
$ 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

Maintenant le piège : deux règles, une avec un * au milieu des arguments, une inoffensive. Le chmod après le tee passe déjà par sudo, donc il montre le message en entier. Tant que le fichier reste fautif, chaque sudo le réimprime ; on coupe les répétitions dans la suite.

un * au milieu
$ 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 sort quand même avec 0 : sudo-rs s'est plaint, il ne s'est pas arrêté. La règle avec le * a disparu, demo est refusé pour cat. L'autre règle a survécu, et sudo -l ne dit rien de celle qui a été jetée ; la seule trace est le message, qui disparaît le jour où un script l'envoie dans /dev/null. visudo -c dit invalid sudoers file avec le code 1 pendant que sudo continue : mets-le dans ce qui déploie tes fichiers sudoers.

02. Un seul * emporte la ligne entière

Les deux mêmes commandes sur une seule ligne, séparées par une virgule :

une ligne, deux commandes
$ 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 est parti avec. L'unité que sudo-rs garde ou jette, c'est la ligne, pas la commande. C'est le piège silencieux. (Le tee qui réécrit le fichier passe aussi par sudo et imprime l'erreur du fichier précédent ; on l'a coupée.)

Au moins 22 projets GitHub ont un ticket ou un correctif qui cite ce message mot pour mot, ouverts entre le 2026-04-25 et le 2026-10-23 (dokku, magebox, xymon, lerd et d'autres). Le pire qu'on a lu est le ticket 24 de gha-runnerd, 2026-10-15 : trois chown sur une ligne avec *, la ligne entière ignorée, et un script qui appelait sudo avec 2>/dev/null || true. Plus de 100 000 fichiers accumulés avant que quelqu'un s'en aperçoive.

03. Pourquoi sudo-rs refuse : l'ancien * lit /etc/shadow

Même règle, lancée par le sudo classique, sudo.ws ; demo demande le journal et un deuxième fichier. Le tail -1 garde la dernière ligne, l'entrée de demo dans /etc/shadow.

le sudo classique, 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:::

Le * a avalé l'espace et un deuxième fichier, et cat a imprimé /etc/shadow en root. Un commentaire du ticket 101 de dokku-datastore (2026-08-06) le dit : « as * can match spaces thus allowing additional parameters ». C'est ça que sudo-rs a fermé.

04. Un * seul à la fin marche déjà sur Ubuntu 26.04

Aucun des fils qu'on a lus ne le dit. Le changelog de sudo-rs 0.2.13, daté du 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` » (point 1455). Launchpad publie 0.2.13-0ubuntu1 pour Ubuntu 26.04 le 2026-03-28, puis .2 le 2026-09-01.

Le cas d'askubuntu est exactement celui-là. La personne lance sudo ipset add blacklist {nasty-IP-address} chaque nuit et écrit « I could write a line for each possible IP address but that's 4,294,967,296 of them! » (« je pourrais écrire une ligne par adresse IP possible, mais ça en fait 4 294 967 296 »). Elle a mis NOPASSWD: ALL en attendant et dit que c'est moins sûr. Pas besoin. Les adresses ci-dessous sont réservées à la documentation (RFC 5737).

un * seul à la fin
$ 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, l'adresse entre, destroy est refusé. Deux limites. D'abord, la ligne de la question a un backslash devant l'étoile. Cette forme donne un autre message :

avec un 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

On ne peut pas savoir ce que contient vraiment le fichier de la personne, seulement que cette forme échoue et que celle sans backslash passe. Sur un deuxième essai, hors capture, la ligne de la question sans le backslash, avec /sbin/ipset comme elle l'écrit, passe aussi.

Deuxième limite : un * final veut dire zéro argument ou plus, de n'importe quel genre. Le -exist est passé, et au deuxième essai -file /etc/shadow après l'adresse a aussi franchi sudo (code 0, aucun effet visible ; on n'a pas étudié chaque option d'ipset). Ce n'est pas de la validation.

05. Pour le reste : un script qui vérifie son argument

La seule réponse sous la question (score 12) dit de ne pas utiliser sudo dans un script : le faire tourner en root depuis cron ou un service systemd et un timer. Pour un vrai job de nuit, c'est défendable, et plus propre. Mais ça ne répond pas à la question sudoers, et ça donne root à tout le script, plus large qu'une seule règle ipset.

Si tu veux une règle, mets la vérification dans un petit script et ne donne à sudo aucun motif d'argument. Trois conditions : le script est à root, aucun utilisateur ne peut le modifier, la règle utilise son chemin absolu. Le nôtre prend exactement une adresse IPv4 ; la regex est lâche exprès, c'est ipset qui juge la valeur (sur un troisième essai, ipset a refusé 999.1.1.1 : cannot parse, code 1). Le fichier contient encore la ligne fautive avec \* jusqu'au dernier tee, donc les premières commandes ont imprimé son erreur. On l'a coupée.

le 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

L'adresse entre, et 198.51.100.5 -exist, qu'un * seul aurait laissé passer, est refusé par le script. La règle n'a pas d'argument, donc c'est le script qui décide, et ça ne tient que parce qu'il est à root. Le ticket 34621 d'aidevops (2026-10-14) arrive à la même forme.

06. Les correctifs qui ouvrent tout

Le correctif tentant, c'est de retirer l'argument :

sans 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, le message a disparu, et la règle autorise cat sur n'importe quel fichier : demo lit /etc/shadow. Le source de sudo-rs le dit : « if no arguments are mentioned, anything is allowed ». visudo -c est vert parce que la règle est valide, juste très large.

NOPASSWD: ALL, ce que la personne a mis en attendant, c'est le même geste à pleine échelle : ALL, c'est toute commande, comme cat sans argument, c'est tout fichier. Le contournement du ticket dokku (ouvert le 2026-05-03), sudo update-alternatives --set sudo /usr/bin/sudo.ws, revient au sudo classique : on ne l'a pas lancé, c'est une forme, mais il ramène l'étoile de la section 03. Une expression régulière au style de sudo 1.9.10 (ipset ^add demo-bl [0-9.]+$) a donné regular expressions are not supported sur un deuxième essai.

07. Tout retirer, et ce qu'on n'a pas testé

Le démontage retire tout ce qu'on a créé ; l'avertissement de userdel ne parle que d'une boîte courriel qui n'a jamais existé. Saute la ligne apt-get remove si ipset était déjà installé avant l'essai. La dernière commande compte ce qu'il reste avec demo dans son nom.

démontage
$ 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

Mesuré : une machine virtuelle Ubuntu 26.04.1 LTS (arm64), sudo-rs 0.2.13-0ubuntu1.2, sudo.ws 1.9.17p2, le 2026-10-26. Non mesuré : Ubuntu 25.10, qui publie sudo-rs 0.2.8 selon Launchpad, antérieur à 0.2.13 ; on déduit du changelog que même le * final y est refusé, on ne l'a pas lancé. Non mesurés non plus : Ubuntu 26.10 (0.2.14) et Debian.

Partager :