Ansible empêchera-t-il l'exécution de 'rm -rf /' dans un script shell

23

Ceci est basé sur cette question de canular ici. Le problème décrit est d'avoir un script bash qui contient quelque chose à l'effet de:

rm -rf {pattern1}/{pattern2}

... qui, si les deux modèles incluent un ou plusieurs éléments vides, s'étendra à au moins une instance de rm -rf /, en supposant que la commande d'origine a été correctement transcrite et que l'OP effectuait une expansion d'accolade plutôt qu'une expansion de paramètre .

Dans l' explication du PO sur le canular , le PO déclare:

La commande [...] est inoffensive mais il semble que presque personne ne l'a remarqué.

L'outil Ansible empêche ces erreurs, mais [...] personne ne semblait le savoir, sinon ils sauraient que ce que j'ai décrit ne pourrait pas se produire.

Donc, en supposant que vous avez un script shell qui émet une rm -rf /commande via l'expansion d'accolade ou l'expansion de paramètres, est-il vrai que l'utilisation d' Ansible empêchera l'exécution de cette commande, et si oui, comment fait-elle cela?

L'exécution rm -rf /avec les privilèges root est-elle vraiment "inoffensive" tant que vous utilisez Ansible pour le faire?

aroth
la source
4
J'ai débattu de ce qu'il fallait faire de cette question, mais j'ai finalement décidé de voter et d'y répondre, afin d'avancer pour enfin mettre ce désordre ridicule désolé dans le passé où il appartient.
Michael Hampton
Je pense que la réponse réside vraiment dans la rmsource, que j'ai analysée ci-dessous.
Aaron Hall

Réponses:

54

J'ai des machines virtuelles, explosons-en! Pour la science.

[root@diaf ~]# ansible --version
ansible 2.0.1.0
  config file = /etc/ansible/ansible.cfg
  configured module search path = Default w/o overrides

Premier essai:

[root@diaf ~]# cat killme.yml 
---
- hosts: localhost
  gather_facts: False
  tasks:
    - name: Die in a fire
      command: "rm -rf {x}/{y}"
[root@diaf ~]# ansible-playbook -l localhost -vvv killme.yml
Using /etc/ansible/ansible.cfg as config file
1 plays in killme.yml

PLAY ***************************************************************************

TASK [Die in a fire] ***********************************************************
task path: /root/killme.yml:5
ESTABLISH LOCAL CONNECTION FOR USER: root
localhost EXEC /bin/sh -c '( umask 22 && mkdir -p "` echo $HOME/.ansible/tmp/ansible-tmp-1461128819.56-86533871334374 `" && echo "` echo $HOME/.ansible/tmp/ansible-tmp-1461128819.56-86533871334374 `" )'
localhost PUT /tmp/tmprogfhZ TO /root/.ansible/tmp/ansible-tmp-1461128819.56-86533871334374/command
localhost EXEC /bin/sh -c 'LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8 /usr/bin/python /root/.ansible/tmp/ansible-tmp-1461128819.56-86533871334374/command; rm -rf "/root/.ansible/tmp/ansible-tmp-1461128819.56-86533871334374/" > /dev/null 2>&1'
changed: [localhost] => {"changed": true, "cmd": ["rm", "-rf", "{x}/{y}"], "delta": "0:00:00.001844", "end": "2016-04-20 05:06:59.601868", "invocation": {"module_args": {"_raw_params": "rm -rf {x}/{y}", "_uses_shell": false, "chdir": null, "creates": null, "executable": null, "removes": null, "warn": true}, "module_name": "command"}, "rc": 0, "start": "2016-04-20 05:06:59.600024", "stderr": "", "stdout": "", "stdout_lines": [], "warnings": ["Consider using file module with state=absent rather than running rm"]}
 [WARNING]: Consider using file module with state=absent rather than running rm


PLAY RECAP *********************************************************************
localhost                  : ok=1    changed=1    unreachable=0    failed=0

OK, alors commandpassez les littéraux et rien ne se passe.

Que diriez - vous de notre by - pass de sécurité préférée, raw?

[root@diaf ~]# cat killme.yml
---
- hosts: localhost
  gather_facts: False
  tasks:
    - name: Die in a fire
      raw: "rm -rf {x}/{y}"
[root@diaf ~]# ansible-playbook -l localhost -vvv killme.yml
Using /etc/ansible/ansible.cfg as config file
1 plays in killme.yml

PLAY ***************************************************************************

TASK [Die in a fire] ***********************************************************
task path: /root/killme.yml:5
ESTABLISH LOCAL CONNECTION FOR USER: root
localhost EXEC rm -rf {x}/{y}
ok: [localhost] => {"changed": false, "invocation": {"module_args": {"_raw_params": "rm -rf {x}/{y}"}, "module_name": "raw"}, "rc": 0, "stderr": "", "stdout": "", "stdout_lines": []}

PLAY RECAP *********************************************************************
localhost                  : ok=1    changed=0    unreachable=0    failed=0

Non allez encore! Comment peut-il être difficile de supprimer tous vos fichiers?

Oh, mais que faire si c'étaient des variables non définies ou quelque chose?

[root@diaf ~]# cat killme.yml
---
- hosts: localhost
  gather_facts: False
  tasks:
    - name: Die in a fire
      command: "rm -rf {{x}}/{{y}}"
[root@diaf ~]# ansible-playbook -l localhost -vvv killme.yml
Using /etc/ansible/ansible.cfg as config file
1 plays in killme.yml

PLAY ***************************************************************************

TASK [Die in a fire] ***********************************************************
task path: /root/killme.yml:5
fatal: [localhost]: FAILED! => {"failed": true, "msg": "'x' is undefined"}

NO MORE HOSTS LEFT *************************************************************
        to retry, use: --limit @killme.retry

PLAY RECAP *********************************************************************
localhost                  : ok=0    changed=0    unreachable=0    failed=1

Eh bien, cela n'a pas fonctionné.

Mais que faire si les variables sont définies, mais vides?

[root@diaf ~]# cat killme.yml 
---
- hosts: localhost
  gather_facts: False
  tasks:
    - name: Die in a fire
      command: "rm -rf {{x}}/{{y}}"
  vars:
    x: ""
    y: ""
[root@diaf ~]# ansible-playbook -l localhost -vvv killme.yml
Using /etc/ansible/ansible.cfg as config file
1 plays in killme.yml

PLAY ***************************************************************************

TASK [Die in a fire] ***********************************************************
task path: /root/killme.yml:5
ESTABLISH LOCAL CONNECTION FOR USER: root
localhost EXEC /bin/sh -c '( umask 22 && mkdir -p "` echo $HOME/.ansible/tmp/ansible-tmp-1461129132.63-211170666238105 `" && echo "` echo $HOME/.ansible/tmp/ansible-tmp-1461129132.63-211170666238105 `" )'
localhost PUT /tmp/tmp78m3WM TO /root/.ansible/tmp/ansible-tmp-1461129132.63-211170666238105/command
localhost EXEC /bin/sh -c 'LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8 /usr/bin/python /root/.ansible/tmp/ansible-tmp-1461129132.63-211170666238105/command; rm -rf "/root/.ansible/tmp/ansible-tmp-1461129132.63-211170666238105/" > /dev/null 2>&1'
fatal: [localhost]: FAILED! => {"changed": true, "cmd": ["rm", "-rf", "/"], "delta": "0:00:00.001740", "end": "2016-04-20 05:12:12.668616", "failed": true, "invocation": {"module_args": {"_raw_params": "rm -rf /", "_uses_shell": false, "chdir": null, "creates": null, "executable": null, "removes": null, "warn": true}, "module_name": "command"}, "rc": 1, "start": "2016-04-20 05:12:12.666876", "stderr": "rm: it is dangerous to operate recursively on ‘/’\nrm: use --no-preserve-root to override this failsafe", "stdout": "", "stdout_lines": [], "warnings": ["Consider using file module with state=absent rather than running rm"]}

NO MORE HOSTS LEFT *************************************************************
        to retry, use: --limit @killme.retry

PLAY RECAP *********************************************************************
localhost                  : ok=0    changed=0    unreachable=0    failed=1

Enfin, quelques progrès! Mais il se plaint toujours que je ne l'ai pas utilisé --no-preserve-root.

Bien sûr, cela m'avertit également que je devrais essayer d'utiliser le filemodule et state=absent. Voyons voir si ça marche.

[root@diaf ~]# cat killme.yml 
---
- hosts: localhost
  gather_facts: False
  tasks:
    - name: Die in a fire
      file: path="{{x}}/{{y}}" state=absent
  vars:
    x: ""
    y: ""
[root@diaf ~]# ansible-playbook -l localhost -vvv killme.yml    
Using /etc/ansible/ansible.cfg as config file
1 plays in killme.yml

PLAY ***************************************************************************

TASK [Die in a fire] ***********************************************************
task path: /root/killme.yml:5
ESTABLISH LOCAL CONNECTION FOR USER: root
localhost EXEC /bin/sh -c '( umask 22 && mkdir -p "` echo $HOME/.ansible/tmp/ansible-tmp-1461129394.62-191828952911388 `" && echo "` echo $HOME/.ansible/tmp/ansible-tmp-1461129394.62-191828952911388 `" )'
localhost PUT /tmp/tmpUqLzyd TO /root/.ansible/tmp/ansible-tmp-1461129394.62-191828952911388/file
localhost EXEC /bin/sh -c 'LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8 /usr/bin/python /root/.ansible/tmp/ansible-tmp-1461129394.62-191828952911388/file; rm -rf "/root/.ansible/tmp/ansible-tmp-1461129394.62-191828952911388/" > /dev/null 2>&1'
fatal: [localhost]: FAILED! => {"changed": false, "failed": true, "invocation": {"module_args": {"backup": null, "content": null, "delimiter": null, "diff_peek": null, "directory_mode": null, "follow": false, "force": false, "group": null, "mode": null, "original_basename": null, "owner": null, "path": "/", "recurse": false, "regexp": null, "remote_src": null, "selevel": null, "serole": null, "setype": null, "seuser": null, "src": null, "state": "absent", "validate": null}, "module_name": "file"}, "msg": "rmtree failed: [Errno 16] Device or resource busy: '/boot'"}

NO MORE HOSTS LEFT *************************************************************
        to retry, use: --limit @killme.retry

PLAY RECAP *********************************************************************
localhost                  : ok=0    changed=0    unreachable=0    failed=1

Bonnes nouvelles tout le monde! Il a commencé à essayer de supprimer tous mes fichiers! Mais malheureusement, il a rencontré une erreur. Je vais laisser la correction et obtenir le playbook pour tout détruire en utilisant le filemodule comme un exercice pour le lecteur.


NE PAS exécuter de playbooks que vous voyez au-delà de ce point! Vous verrez pourquoi dans un instant.

Enfin, pour le coup de grâce ...

[root@diaf ~]# cat killme.yml
---
- hosts: localhost
  gather_facts: False
  tasks:
    - name: Die in a fire
      raw: "rm -rf {{x}}/{{y}}"
  vars:
    x: ""
    y: "*"
[root@diaf ~]# ansible-playbook -l localhost -vvv killme.yml
Using /etc/ansible/ansible.cfg as config file
1 plays in killme.yml

PLAY ***************************************************************************

TASK [Die in a fire] ***********************************************************
task path: /root/killme.yml:5
ESTABLISH LOCAL CONNECTION FOR USER: root
localhost EXEC rm -rf /*
Traceback (most recent call last):
  File "/usr/lib/python2.7/site-packages/ansible/executor/process/result.py", line 102, in run
  File "/usr/lib/python2.7/site-packages/ansible/executor/process/result.py", line 76, in _read_worker_result
  File "/usr/lib64/python2.7/multiprocessing/queues.py", line 117, in get
ImportError: No module named task_result

Cette VM est un ex-perroquet !

Fait intéressant, ce qui précède n'a rien fait avec commandau lieu de raw. Il vient d'imprimer le même avertissement concernant l'utilisation de fileavec state=absent.

Je vais dire qu'il semble que si vous n'utilisez pas rawcela, il y a une certaine protection contre la rmfolie. Mais vous ne devriez pas vous fier à cela. J'ai jeté un coup d'œil dans le code d'Ansible, et bien que j'ai trouvé l'avertissement, je n'ai rien trouvé qui supprimerait réellement l'exécution de la rmcommande.

Michael Hampton
la source
10
+1 pour la science. J'aurais +1 de plus pour le nom d'hôte, mais ce serait de la fraude; p /
Journeyman Geek
On dirait que vous pourriez avoir un système de fichiers monté sur /boot.
84104
1
@ 84104 C'est drôle, ça. Par pure coïncidence, bootc'est la première entrée de répertoire dans /. Aucun fichier n'a donc été perdu.
Michael Hampton
5
@aroth Exactement! Mais, pour la science, essayez rm -rf {{x}}/{{y}}quand yest réglé sur "*". Le --no-preserve-rootchèque est utile pour ce qu'il est, mais il ne vous sortira pas de toutes les situations possibles; c'est assez facile à contourner. C'est pourquoi cette question n'a pas été immédiatement considérée comme un canular: compte tenu du mauvais anglais et des erreurs de syntaxe apparentes, elle est plausible .
Michael Hampton
1
De plus raw, un mauvais cronpourrait être une autre façon de détruire un système.
84104
3

Ansible empêchera-t-il l'exécution d' rm -rf /un script shell?

J'ai inspecté la source coreutils rm , qui présente les éléments suivants:

  if (x.recursive && preserve_root)
    {
      static struct dev_ino dev_ino_buf;
      x.root_dev_ino = get_root_dev_ino (&dev_ino_buf);
      if (x.root_dev_ino == NULL)
        error (EXIT_FAILURE, errno, _("failed to get attributes of %s"),
               quoteaf ("/"));
    }

La seule façon d'effacer de la racine est de dépasser ce bloc de code. De cette source :

struct dev_ino *
get_root_dev_ino (struct dev_ino *root_d_i)
{
  struct stat statbuf;
  if (lstat ("/", &statbuf))
    return NULL;
  root_d_i->st_ino = statbuf.st_ino;
  root_d_i->st_dev = statbuf.st_dev;
  return root_d_i;
}

J'interprète cela comme signifiant que la fonction get_root_dev_inorenvoie null on /, et donc rm échoue.

La seule façon de contourner le premier bloc de code (avec récursivité) est d'avoir --no-preserve-rootet il n'utilise pas de variable d'environnement pour surcharger, donc il devrait être passé explicitement à rm.

Je crois que cela prouve qu'à moins qu'Ansible ne passe explicitement --no-preserve-rootà rm, il ne le fera pas.

Conclusion

Je ne crois pas qu'Ansible empêche explicitement rm -rf /parce que rmlui - même l'empêche.

Aaron Hall
la source