# > 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. [fetch_scripts.sh](https://git.abonnel.fr/cedricAbonnel/notes-techniques/raw/commit/18c6dd9e45e57d272659da6e2c53b79048985400/scripts/fetch_scripts.sh) > 📝 **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. ## Le contexte L'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. C'est typiquement le genre d'outil qui se déploie en une ligne : ```bash wget https://git.example.fr/.../fetch_scripts.sh && bash fetch_scripts.sh ``` Décortiquons ce qu'il fait, étape par étape, puis voyons où il faudrait taper. --- ## Étape 1 — L'auto-mise à jour ```bash SCRIPT_URL="https://git.abonnel.fr/.../fetch_scripts.sh" SCRIPT_NAME=$(basename "$0") TMP_SCRIPT="/tmp/$SCRIPT_NAME" wget -q -O "$TMP_SCRIPT" "$SCRIPT_URL" if ! cmp -s "$TMP_SCRIPT" "$0"; then echo "🔄 Mise à jour du script..." mv "$TMP_SCRIPT" "$0" chmod +x "$0" exec "$0" "$@" fi ``` **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). **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. **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. --- ## Étape 2 — Récupération du catalogue de dossiers ```bash REPO_URL="https://git.abonnel.fr/.../scripts" DIR_LIST_FILE=".directories.txt" MANDATORY_DIR="common" TMP_DIR=$(mktemp -d) wget -q -O "$TMP_DIR/$DIR_LIST_FILE" "$REPO_URL/$DIR_LIST_FILE" mapfile -t AVAILABLE_DIRS < "$TMP_DIR/$DIR_LIST_FILE" ``` Le 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. `mapfile` (alias `readarray`) lit le fichier ligne à ligne dans un tableau Bash. Plus propre qu'une boucle `while read`. Un dossier `common` est marqué comme obligatoire — il sera toujours téléchargé, sans demander à l'utilisateur. --- ## Étape 3 — Mémoire de la sélection précédente ```bash SELECTED_DIRS_FILE=".selected_dirs.txt" if [ -f "$SELECTED_DIRS_FILE" ]; then mapfile -t PREVIOUS_SELECTION < "$SELECTED_DIRS_FILE" else PREVIOUS_SELECTION=() fi ``` À 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. --- ## Étape 4 — L'interface `whiptail` ```bash CHOICES=() for dir in "${AVAILABLE_DIRS[@]}"; do if [ "$dir" == "$MANDATORY_DIR" ]; then continue fi if [[ " ${PREVIOUS_SELECTION[*]} " =~ " $dir " ]]; then CHOICES+=("$dir" "" ON) else CHOICES+=("$dir" "" OFF) fi done SELECTED_DIRS=$(whiptail --title "Sélection des dossiers" --checklist \ "Sélectionnez les dossiers à télécharger :" 20 60 10 \ "${CHOICES[@]}" 3>&1 1>&2 2>&3) ``` `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. La 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. L'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`). --- ## Étape 5 — Synchronisation : ajouts et suppressions ```bash SELECTED_DIRS_ARRAY=("$MANDATORY_DIR" $(echo "$SELECTED_DIRS" | tr -d '"')) echo "${SELECTED_DIRS_ARRAY[@]}" > "$SELECTED_DIRS_FILE" for dir in "${PREVIOUS_SELECTION[@]}"; do if [[ ! " ${SELECTED_DIRS_ARRAY[*]} " =~ " $dir " ]]; then echo "🗑 Suppression du dossier $dir..." rm -rf "$dir" fi done ``` Logique 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. `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. --- ## Étape 6 — Téléchargement des fichiers de chaque dossier ```bash for TARGET_DIR in "${SELECTED_DIRS_ARRAY[@]}"; do wget -q -O "$LIST_PATH" "$REPO_URL/$TARGET_DIR/.list_files.txt" mkdir -p "$TARGET_DIR" while read -r file; do wget -q -O "$TARGET_DIR/$(basename "$file")" "$REPO_URL/$TARGET_DIR/$file" done < "$LIST_PATH" for existing_file in "$TARGET_DIR"/*; do if [ -f "$existing_file" ] && ! grep -qx "$(basename "$existing_file")" "$LIST_PATH"; then rm "$existing_file" fi done chmod +x "$TARGET_DIR"/*.sh done ``` Mê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. C'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. --- ## Critique : ce qui marche, ce qui inquiète ### Les bons côtés **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. **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é. **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. **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. ### Les angles morts #### 1. Aucune vérification d'intégrité C'est **le** point critique. Le script télécharge du code exécutable en HTTPS, sans vérifier : - ni signature GPG, - ni hash SHA256, - ni même que le serveur a bien répondu correctement. `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. **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. **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. #### 2. `wget` sans gestion d'erreur ```bash wget -q -O "$TMP_SCRIPT" "$SCRIPT_URL" if ! cmp -s "$TMP_SCRIPT" "$0"; then ``` Si `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. **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. ```bash if ! wget -q -O "$TMP_SCRIPT" "$SCRIPT_URL"; then echo "❌ Téléchargement échoué, on garde la version actuelle" elif [ ! -s "$TMP_SCRIPT" ] || ! head -n1 "$TMP_SCRIPT" | grep -q "^#!"; then echo "❌ Fichier téléchargé invalide" rm -f "$TMP_SCRIPT" elif ! cmp -s "$TMP_SCRIPT" "$0"; then # ... fi ``` #### 3. Le `exec "$0" "$@"` perd les modifications de l'environnement Si 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. #### 4. Injection via les noms de fichiers du manifeste ```bash while read -r file; do wget -q -O "$TARGET_DIR/$(basename "$file")" "$REPO_URL/$TARGET_DIR/$file" done < "$LIST_PATH" ``` Le 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. `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). #### 5. `whiptail` et la sélection vide Si 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. #### 6. Pas de log, pas de mode dry-run Pour 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. #### 7. Le verrou manquant Rien 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. --- ## Verdict C'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. Mais 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. ### Pistes d'évolution prioritaires 1. **Signature ou checksum** : publier un `MANIFEST.sha256` signé GPG, le vérifier avant tout `mv` ou exécution. 2. **`set -euo pipefail`** en tête de script pour faire échouer proprement à la première erreur. 3. **Vérifier `wget`** : code de retour, fichier non vide, shebang présent. 4. **Backup avant écrasement** : conserver la version précédente (`fetch_scripts.sh.bak`) pour pouvoir revenir en arrière. 5. **Option `--dry-run`** pour visualiser sans appliquer. 6. **Filtre regex** sur les noms de fichiers du manifeste pour éviter les traversées de chemin. 7. **Lock file** via `flock` pour éviter les exécutions concurrentes. Avec ces ajouts, on passe d'un script « pratique » à un outil de déploiement digne de ce nom — sans rien perdre de sa simplicité initiale.