Files
varlog/_cache/articles/bea327e2-9d1c-4ff6-a5a5-26748c80018b.json
T
2026-05-15 10:37:48 +02:00

1 line
15 KiB
JSON

{"uuid":"bea327e2-9d1c-4ff6-a5a5-26748c80018b","slug":"anatomie-d-un-script-d-auto-deploiement-bash-fetch-scripts-sh","title":"Script Bash d'auto-déploiement : `fetch_scripts.sh`","author":"cedric@abonnel.fr","published":true,"published_at":"2026-05-04 07:04","created_at":"2026-05-12 10:55:39","updated_at":"2026-05-12 11:10:51","revisions":[{"n":1,"date":"2026-05-12 11:08:57","comment":"Catégorie modifiée, contenu modifié, article publié, couverture modifiée","title":"Script Bash d'auto-déploiement : `fetch_scripts.sh`"},{"n":2,"date":"2026-05-12 11:10:51","comment":"Contenu modifié : mention autocritique","title":"Script Bash d'auto-déploiement : `fetch_scripts.sh`"}],"cover":"cover.svg","files_meta":{"d6ea4554c9fbfc14-23663.svg":{"author":"","source_url":""},"cover.svg":{"author":"","source_url":""}},"external_links":[{"url":"https://git.abonnel.fr/cedricAbonnel/notes-techniques/raw/commit/18c6dd9e45e57d272659da6e2c53b79048985400/scripts/fetch_scripts.sh","name":"Script bash fetch_scripts.sh dans sa version 18c6dd9e45e57d272659da6e2c53b79048985400","added_at":"2026-05-12 11:07:42","meta":{"mime":"text/plain","size":4334}}],"seo_title":"","seo_description":"Auto analyse pédagogique et critique du script Bash d'auto-mise à jour qui synchronise des scripts depuis un dépôt Forgejo. Sept failles identifiées, sept correctifs à effectuer.","og_image":"https://varlog.a5l.fr/file?uuid=bea327e2-9d1c-4ff6-a5a5-26748c80018b&name=cover.svg","category":"informatique","content":"# > Comment un simple script Bash peut télécharger, mettre à jour et synchroniser une bibliothèque de scripts distants — et pourquoi il faut le lire avec un œil critique.\r\n\r\n[fetch_scripts.sh](https://git.abonnel.fr/cedricAbonnel/notes-techniques/raw/commit/18c6dd9e45e57d272659da6e2c53b79048985400/scripts/fetch_scripts.sh)\r\n\r\n> 📝 **Note** — Cet article est une autocritique. Le script `fetch_scripts.sh` analysé ici est de ma propre fabrication, déployé sur mes propres machines. L'exercice consiste à le relire avec la distance d'un reviewer extérieur, pour identifier ce qui tient la route et ce qui mériterait d'être repris.\r\n\r\n## Le contexte\r\n\r\nL'idée derrière ce script est élégante : centraliser une collection de scripts utilitaires dans un dépôt Git public (ici, une instance Forgejo auto-hébergée), puis fournir un **unique point d'entrée** que l'on télécharge sur n'importe quelle machine. Ce point d'entrée se met à jour tout seul, propose à l'opérateur de choisir quels sous-ensembles de scripts récupérer, et maintient une synchronisation locale du dépôt distant.\r\n\r\nC'est typiquement le genre d'outil qui se déploie en une ligne :\r\n\r\n```bash\r\nwget https://git.example.fr/.../fetch_scripts.sh && bash fetch_scripts.sh\r\n```\r\n\r\nDécortiquons ce qu'il fait, étape par étape, puis voyons où il faudrait taper.\r\n\r\n---\r\n\r\n## Étape 1 — L'auto-mise à jour\r\n\r\n```bash\r\nSCRIPT_URL=\"https://git.abonnel.fr/.../fetch_scripts.sh\"\r\nSCRIPT_NAME=$(basename \"$0\")\r\nTMP_SCRIPT=\"/tmp/$SCRIPT_NAME\"\r\n\r\nwget -q -O \"$TMP_SCRIPT\" \"$SCRIPT_URL\"\r\n\r\nif ! cmp -s \"$TMP_SCRIPT\" \"$0\"; then\r\n echo \"🔄 Mise à jour du script...\"\r\n mv \"$TMP_SCRIPT\" \"$0\"\r\n chmod +x \"$0\"\r\n exec \"$0\" \"$@\"\r\nfi\r\n```\r\n\r\n**Ce qui se passe :** le script télécharge sa propre version distante dans `/tmp`, la compare octet-à-octet avec lui-même (`cmp -s`), et si elle diffère, il s'écrase, se rend exécutable, et **se relance** via `exec` (qui remplace le processus courant — pas d'empilement de shells).\r\n\r\n**Pourquoi c'est malin :** ça garantit qu'à chaque exécution, l'opérateur travaille avec la version canonique du dépôt. Pas besoin de mécanisme de versioning, pas de vérification de hash, pas de paquet à publier.\r\n\r\n**Pourquoi c'est risqué :** on y reviendra dans la critique, mais en résumé — l'auto-mise à jour silencieuse depuis une URL en HTTPS sans signature est une porte d'entrée pour la chaîne d'approvisionnement.\r\n\r\n---\r\n\r\n## Étape 2 — Récupération du catalogue de dossiers\r\n\r\n```bash\r\nREPO_URL=\"https://git.abonnel.fr/.../scripts\"\r\nDIR_LIST_FILE=\".directories.txt\"\r\nMANDATORY_DIR=\"common\"\r\n\r\nTMP_DIR=$(mktemp -d)\r\nwget -q -O \"$TMP_DIR/$DIR_LIST_FILE\" \"$REPO_URL/$DIR_LIST_FILE\"\r\nmapfile -t AVAILABLE_DIRS < \"$TMP_DIR/$DIR_LIST_FILE\"\r\n```\r\n\r\nLe dépôt distant contient un fichier `.directories.txt` qui liste les catégories de scripts disponibles (par exemple : `common`, `proxmox`, `php`, `monitoring`…). Ce fichier est la **source de vérité** : ajouter une catégorie côté serveur la rend immédiatement disponible côté client.\r\n\r\n`mapfile` (alias `readarray`) lit le fichier ligne à ligne dans un tableau Bash. Plus propre qu'une boucle `while read`.\r\n\r\nUn dossier `common` est marqué comme obligatoire — il sera toujours téléchargé, sans demander à l'utilisateur.\r\n\r\n---\r\n\r\n## Étape 3 — Mémoire de la sélection précédente\r\n\r\n```bash\r\nSELECTED_DIRS_FILE=\".selected_dirs.txt\"\r\n\r\nif [ -f \"$SELECTED_DIRS_FILE\" ]; then\r\n mapfile -t PREVIOUS_SELECTION < \"$SELECTED_DIRS_FILE\"\r\nelse\r\n PREVIOUS_SELECTION=()\r\nfi\r\n```\r\n\r\nÀ chaque exécution, le script relit la sélection de la fois précédente. C'est ce qui permet à l'interface graphique (étape suivante) de **pré-cocher** les bons dossiers : on n'a pas à refaire son choix à chaque mise à jour.\r\n\r\n---\r\n\r\n## Étape 4 — L'interface `whiptail`\r\n\r\n```bash\r\nCHOICES=()\r\nfor dir in \"${AVAILABLE_DIRS[@]}\"; do\r\n if [ \"$dir\" == \"$MANDATORY_DIR\" ]; then\r\n continue\r\n fi\r\n if [[ \" ${PREVIOUS_SELECTION[*]} \" =~ \" $dir \" ]]; then\r\n CHOICES+=(\"$dir\" \"\" ON)\r\n else\r\n CHOICES+=(\"$dir\" \"\" OFF)\r\n fi\r\ndone\r\n\r\nSELECTED_DIRS=$(whiptail --title \"Sélection des dossiers\" --checklist \\\r\n \"Sélectionnez les dossiers à télécharger :\" 20 60 10 \\\r\n \"${CHOICES[@]}\" 3>&1 1>&2 2>&3)\r\n```\r\n\r\n`whiptail` est l'outil de dialogue ncurses standard sur Debian/Ubuntu — il affiche cette boîte bleue familière avec des cases à cocher, navigable au clavier. Idéal en SSH.\r\n\r\nLa gymnastique `3>&1 1>&2 2>&3` est un classique : `whiptail` écrit son interface sur stdout et sa réponse sur stderr. Il faut donc **échanger les deux** pour capturer la sélection dans `$SELECTED_DIRS` tout en laissant l'interface s'afficher.\r\n\r\nL'expression `[[ \" ${ARRAY[*]} \" =~ \" $dir \" ]]` est une astuce courante pour tester l'appartenance à un tableau Bash — on entoure d'espaces pour éviter les correspondances partielles (`web` qui matcherait `web-server`).\r\n\r\n---\r\n\r\n## Étape 5 — Synchronisation : ajouts et suppressions\r\n\r\n```bash\r\nSELECTED_DIRS_ARRAY=(\"$MANDATORY_DIR\" $(echo \"$SELECTED_DIRS\" | tr -d '\"'))\r\necho \"${SELECTED_DIRS_ARRAY[@]}\" > \"$SELECTED_DIRS_FILE\"\r\n\r\nfor dir in \"${PREVIOUS_SELECTION[@]}\"; do\r\n if [[ ! \" ${SELECTED_DIRS_ARRAY[*]} \" =~ \" $dir \" ]]; then\r\n echo \"🗑 Suppression du dossier $dir...\"\r\n rm -rf \"$dir\"\r\n fi\r\ndone\r\n```\r\n\r\nLogique de diff : tout ce qui était sélectionné avant et ne l'est plus est **supprimé du disque**. Ça maintient le répertoire local propre — pas de scripts orphelins qui traînent.\r\n\r\n`whiptail` renvoie la sélection sous forme de chaîne entre guillemets (`\"dir1\" \"dir2\"`), d'où le `tr -d '\"'` pour les retirer avant de constituer le tableau.\r\n\r\n---\r\n\r\n## Étape 6 — Téléchargement des fichiers de chaque dossier\r\n\r\n```bash\r\nfor TARGET_DIR in \"${SELECTED_DIRS_ARRAY[@]}\"; do\r\n wget -q -O \"$LIST_PATH\" \"$REPO_URL/$TARGET_DIR/.list_files.txt\"\r\n mkdir -p \"$TARGET_DIR\"\r\n\r\n while read -r file; do\r\n wget -q -O \"$TARGET_DIR/$(basename \"$file\")\" \"$REPO_URL/$TARGET_DIR/$file\"\r\n done < \"$LIST_PATH\"\r\n\r\n for existing_file in \"$TARGET_DIR\"/*; do\r\n if [ -f \"$existing_file\" ] && ! grep -qx \"$(basename \"$existing_file\")\" \"$LIST_PATH\"; then\r\n rm \"$existing_file\"\r\n fi\r\n done\r\n\r\n chmod +x \"$TARGET_DIR\"/*.sh\r\ndone\r\n```\r\n\r\nMême logique récursive d'un niveau plus bas : chaque dossier contient son propre `.list_files.txt` listant ses fichiers. On télécharge ceux qui y figurent, on supprime ceux qui n'y figurent plus, et on rend tout exécutable.\r\n\r\nC'est une forme de `rsync` artisanal, basé sur des manifestes plats. Ça fonctionne sans avoir à installer `git` sur la machine cible — seuls `wget` et `whiptail` sont requis.\r\n\r\n---\r\n\r\n## Critique : ce qui marche, ce qui inquiète\r\n\r\n### Les bons côtés\r\n\r\n**La logique d'idempotence** est solide. Le script peut tourner cent fois de suite, il convergera toujours vers le même état : les dossiers sélectionnés contiendront exactement les fichiers du manifeste, ni plus, ni moins. C'est le bon réflexe DevOps.\r\n\r\n**L'auto-bootstrap** est ergonomique. Une seule URL à retenir, tout le reste se télécharge tout seul. Pour une bibliothèque personnelle de scripts d'admin, c'est imbattable en simplicité.\r\n\r\n**Pas de dépendances exotiques.** `wget`, `whiptail`, `mapfile` : tout est disponible nativement sur Debian. Le script tourne aussi bien sur un conteneur LXC fraîchement provisionné que sur une machine établie.\r\n\r\n**Le manifeste séparé** (`.directories.txt` et `.list_files.txt`) découple la liste des fichiers de leur contenu. C'est plus simple qu'un parsing HTML de l'index Git, et ça reste sous contrôle éditorial.\r\n\r\n### Les angles morts\r\n\r\n#### 1. Aucune vérification d'intégrité\r\n\r\nC'est **le** point critique. Le script télécharge du code exécutable en HTTPS, sans vérifier :\r\n- ni signature GPG,\r\n- ni hash SHA256,\r\n- ni même que le serveur a bien répondu correctement.\r\n\r\n`wget -q` en mode silencieux **n'échoue pas visiblement** : si la requête renvoie une page d'erreur 404 ou une page de connexion captive Wi-Fi en HTML, elle sera écrite dans le fichier de destination. La vérification suivante (`cmp -s`) considérera ce HTML comme « différent », fera le `mv`, et au prochain `exec` le shell essaiera d'exécuter du HTML. Au mieux ça crashe, au pire ça exécute des balises interprétables.\r\n\r\n**Pire encore pour l'auto-update :** si quelqu'un compromet l'instance Forgejo (ou interpose un proxy malveillant capable de servir un certificat valide pour `git.abonnel.fr`), le prochain `fetch_scripts.sh` télécharge et exécute du code arbitraire avec les privilèges de l'utilisateur courant — souvent root pour ce genre d'outils d'admin.\r\n\r\n**Correctif minimal :** publier un fichier `.sha256sums` signé GPG dans le dépôt, le télécharger, vérifier sa signature avec une clé connue localement, puis valider chaque fichier téléchargé contre ce manifeste.\r\n\r\n#### 2. `wget` sans gestion d'erreur\r\n\r\n```bash\r\nwget -q -O \"$TMP_SCRIPT\" \"$SCRIPT_URL\"\r\nif ! cmp -s \"$TMP_SCRIPT\" \"$0\"; then\r\n```\r\n\r\nSi `wget` échoue (réseau coupé, DNS HS, certificat expiré), `$TMP_SCRIPT` sera soit vide soit absent. `cmp -s` retournera « différent », et le script **écrasera la version locale par un fichier vide**. À la prochaine exécution, plus rien ne fonctionne.\r\n\r\n**Correctif :** vérifier le code de retour de `wget`, vérifier que le fichier téléchargé n'est pas vide, et vérifier qu'il commence bien par `#!/bin/bash` avant d'écraser quoi que ce soit.\r\n\r\n```bash\r\nif ! wget -q -O \"$TMP_SCRIPT\" \"$SCRIPT_URL\"; then\r\n echo \"❌ Téléchargement échoué, on garde la version actuelle\"\r\nelif [ ! -s \"$TMP_SCRIPT\" ] || ! head -n1 \"$TMP_SCRIPT\" | grep -q \"^#!\"; then\r\n echo \"❌ Fichier téléchargé invalide\"\r\n rm -f \"$TMP_SCRIPT\"\r\nelif ! cmp -s \"$TMP_SCRIPT\" \"$0\"; then\r\n # ...\r\nfi\r\n```\r\n\r\n#### 3. Le `exec \"$0\" \"$@\"` perd les modifications de l'environnement\r\n\r\nSi le script a été lancé par `bash fetch_scripts.sh` (donc sans le bit exécutable, sans shebang utilisé), `$0` vaut `fetch_scripts.sh`. Après `mv`, on `exec` un fichier qui pourrait ne pas être dans le `$PATH`. En pratique ça marche parce qu'on est dans le bon répertoire, mais c'est fragile — un `cd` quelque part dans le script suffirait à le casser.\r\n\r\n#### 4. Injection via les noms de fichiers du manifeste\r\n\r\n```bash\r\nwhile read -r file; do\r\n wget -q -O \"$TARGET_DIR/$(basename \"$file\")\" \"$REPO_URL/$TARGET_DIR/$file\"\r\ndone < \"$LIST_PATH\"\r\n```\r\n\r\nLe contenu de `.list_files.txt` est utilisé directement dans une URL et dans un chemin de fichier local. Si quelqu'un peut écrire dans ce fichier manifeste (ce qui revient à pouvoir pousser sur le dépôt Forgejo), il peut y mettre des chemins comme `../../../etc/cron.d/backdoor` et écrire en dehors du répertoire prévu.\r\n\r\n`basename` neutralise partiellement la chose côté nom local, mais l'URL côté distant accepte n'importe quoi. C'est moins critique que la première faille, mais ça mérite un filtre regex (`[a-zA-Z0-9._-]+` uniquement).\r\n\r\n#### 5. `whiptail` et la sélection vide\r\n\r\nSi l'utilisateur ne coche rien et valide, `$SELECTED_DIRS` est vide. Le script continue avec seulement `common`, ce qui est probablement le comportement attendu. Mais si `whiptail` n'est pas installé (rare mais possible, par exemple sur Alpine ou un Debian minimal sans `whiptail`), le script échoue avec une erreur peu explicite. Un test préalable `command -v whiptail` éviterait la déconvenue.\r\n\r\n#### 6. Pas de log, pas de mode dry-run\r\n\r\nPour un outil qui supprime des fichiers (`rm -rf \"$dir\"`), l'absence d'option `--dry-run` qui afficherait ce qui *serait* fait sans rien toucher est gênante. Une frappe distraite sur la checklist, et un dossier entier disparaît sans warning.\r\n\r\n#### 7. Le verrou manquant\r\n\r\nRien n'empêche deux instances de `fetch_scripts.sh` de tourner en parallèle (par exemple via `cron` et un opérateur en interactif). Un `flock` sur un fichier de lock éviterait des courses sur les opérations de download/delete.\r\n\r\n---\r\n\r\n## Verdict\r\n\r\nC'est un script **utile, lisible, et bien construit pour un usage personnel** sur des machines de confiance. La logique de synchronisation est saine, l'ergonomie `whiptail` est appréciable, l'auto-bootstrap est élégant.\r\n\r\nMais dès qu'on franchit la frontière du « j'utilise ça sur mes propres machines avec mon propre dépôt », les manques se font sentir : **pas de vérification d'intégrité, pas de gestion d'erreur réseau, pas d'option de récupération**. Dans un contexte d'équipe ou de production, ces points sont bloquants.\r\n\r\n### Pistes d'évolution prioritaires\r\n\r\n1. **Signature ou checksum** : publier un `MANIFEST.sha256` signé GPG, le vérifier avant tout `mv` ou exécution.\r\n2. **`set -euo pipefail`** en tête de script pour faire échouer proprement à la première erreur.\r\n3. **Vérifier `wget`** : code de retour, fichier non vide, shebang présent.\r\n4. **Backup avant écrasement** : conserver la version précédente (`fetch_scripts.sh.bak`) pour pouvoir revenir en arrière.\r\n5. **Option `--dry-run`** pour visualiser sans appliquer.\r\n6. **Filtre regex** sur les noms de fichiers du manifeste pour éviter les traversées de chemin.\r\n7. **Lock file** via `flock` pour éviter les exécutions concurrentes.\r\n\r\nAvec ces ajouts, on passe d'un script « pratique » à un outil de déploiement digne de ce nom — sans rien perdre de sa simplicité initiale."}