Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

Chapitre 1318 minDébutant

Scripts Bash d'administration Linux : sauvegarde, disques et journaux

Par Josselyn C., prof de BTS SIO. Mis à jour le .

À la fin de ce chapitre, tu sauras

  • écrire un script de sauvegarde vérifié, verrouillé et copié ailleurs ;
  • contrôler l'espace disque avec les codes de sortie de la supervision ;
  • analyser les journaux avec journalctl et vérifier leur rotation ;
  • ranger et protéger un script d'administration sur un serveur.

3 exercices corrigés

Un script Bash d'administration automatise une tâche d'exploitation d'un serveur Linux : sauvegarder une configuration, vérifier l'espace disque, analyser les journaux, contrôler les services. Ce chapitre construit trois scripts complets pour srv-web, le serveur Debian 13 de la DMZ de Sorbier, en appliquant tout ce qui précède : mode strict, verrou contre les exécutions simultanées, codes de sortie, journal système. Ces scripts sont ceux qu'on montre, commentés, dans une fiche de réalisation professionnelle.

Où ranger un script d'administration

Sur Debian, un script d'administration se range dans /usr/local/sbin (réservé à root) ou /usr/local/bin (pour tous), appartient à root et n'est modifiable que par lui :

bash
sudo install -o root -g root -m 0750 sauvegarde-config.sh /usr/local/sbin/sauvegarde-config.sh

install copie le fichier en fixant d'un coup le propriétaire, le groupe et les droits. 0750 : lecture, écriture et exécution pour root, lecture et exécution pour le groupe, rien pour les autres. Un script lancé par root et modifiable par un autre compte est une élévation de privilèges toute prête : ce compte n'aurait qu'à y ajouter ses commandes.

Sauvegarder la configuration

Ce script archive la configuration de srv-web, vérifie l'archive, en calcule l'empreinte, supprime les archives de plus de 14 jours et envoie une copie sur srv-journaux, qui centralise les sauvegardes de configuration. Les données elles-mêmes (sites, bases) suivent le plan de sauvegarde de l'entreprise, décrit dans le cours Sauvegarde, PCA et PRA.

bash
#!/usr/bin/env bash
# /usr/local/sbin/sauvegarde-config.sh
# Archive la configuration de srv-web, la vérifie, garde 14 jours d'archives
# et envoie une copie sur srv-journaux. Code de sortie : 0 succès, 1 échec.
set -Eeuo pipefail

SOURCES=(/etc/nginx /etc/ssh /etc/nftables.conf /etc/fail2ban)
DEST_LOCALE=/srv/sauvegardes/config
DEST_DISTANTE="sauvegarde@srv-journaux:/srv/sauvegardes/srv-web/"
RETENTION_JOURS=14
ETIQUETTE=sauvegarde-config

journal() { logger -t "$ETIQUETTE" -- "$*"; printf '%s %s\n' "$(date '+%F %T')" "$*"; }
echec()   { logger -t "$ETIQUETTE" -p user.err -- "ÉCHEC : $*"; echo "ÉCHEC : $*" >&2; exit 1; }
trap 'echec "commande en erreur à la ligne $LINENO"' ERR

# Une seule exécution à la fois : le verrou est libéré automatiquement à la fin du script
exec 9> /run/lock/sauvegarde-config.lock
flock -n 9 || echec "une sauvegarde est déjà en cours"

mkdir -p "$DEST_LOCALE"
archive="$DEST_LOCALE/$(hostname -s)-config-$(date +%F_%H%M).tar.gz"

journal "Début, sources : ${SOURCES[*]}"
tar -czf "$archive" -C / "${SOURCES[@]#/}"           # chemins relatifs à / : etc/nginx…
tar -tzf "$archive" > /dev/null                       # l'archive se relit-elle ?
nom=$(basename "$archive")
(cd "$DEST_LOCALE" && sha256sum "$nom" > "$nom.sha256")   # nom relatif : vérifiable ailleurs
journal "Archive : $archive ($(du -h "$archive" | cut -f1))"

# Rotation : -mtime +14 = modifiée il y a au moins 15 jours entiers
find "$DEST_LOCALE" -type f -name '*-config-*.tar.gz*' -mtime +"$RETENTION_JOURS" -print -delete |
    while IFS= read -r f; do journal "Supprimé : $f"; done

rsync -a -e "ssh -o BatchMode=yes -o ConnectTimeout=10" "$archive" "$archive.sha256" "$DEST_DISTANTE"
journal "Copie envoyée sur srv-journaux"

Quelques lignes méritent une explication.

  • "${SOURCES[@]#/}" retire le / initial de chaque élément du tableau, et -C / dit à tar de partir de la racine : l'archive contient etc/nginx/…, sans l'avertissement « Suppression de « / » au début des noms des membres ». À la restauration, tar -xzf archive -C /tmp/restau extrait dans un dossier de test, sans écraser la configuration en place.
  • tar -tzf relit l'archive entière : une archive corrompue (disque plein pendant l'écriture) fait échouer le script, au lieu d'être découverte le jour où l'on en a besoin. sha256sum en garde l'empreinte. Il est lancé depuis le dossier de l'archive pour que le fichier .sha256 contienne un nom relatif : sur srv-journaux, sha256sum -c archive.tar.gz.sha256, lancé dans le dossier de réception, vérifie que la copie est intacte, alors qu'avec un chemin absolu de srv-web la vérification chercherait un fichier qui n'existe pas.
  • flock empêche deux sauvegardes de tourner en même temps, par exemple si une exécution manuelle tombe pendant celle de la nuit. exec 9> ouvre le fichier de verrou sur le descripteur 9 pour toute la durée du script.
  • trap … ERR transforme toute commande en échec en message d'erreur dans le journal système, avec le numéro de ligne, puis en code de sortie 1 : la supervision, ou le timer systemd du chapitre 14, le verront.
  • -print -delete affiche chaque fichier supprimé avant de le supprimer, ce qui alimente le journal. Avant de mettre un -delete dans un script, lance la commande find sans lui et lis la liste.

Surveiller l'espace disque

Ce script contrôle tous les systèmes de fichiers et renvoie un code de sortie selon la convention des outils de supervision : 0 pour OK, 1 pour attention, 2 pour critique, 3 pour un état inconnu (ici, une mauvaise utilisation). Un agent de supervision peut le lancer et transformer ce code en alerte.

bash
#!/usr/bin/env bash
# /usr/local/bin/verif-disques.sh [-a seuil_attention] [-c seuil_critique]
# Codes de sortie : 0 OK, 1 ATTENTION, 2 CRITIQUE, 3 mauvais usage
set -euo pipefail

attention=80
critique=90
while getopts ":a:c:" opt; do
    case "$opt" in
        a) attention="$OPTARG" ;;
        c) critique="$OPTARG" ;;
        *) echo "usage : $0 [-a pourcentage] [-c pourcentage]" >&2; exit 3 ;;
    esac
done

etat=0
while read -r point pct; do
    pct="${pct%\%}"                                   # « 93% » devient « 93 »
    if (( pct >= critique )); then
        echo "CRITIQUE : $point plein à ${pct} %"
        etat=2
    elif (( pct >= attention )); then
        echo "ATTENTION : $point plein à ${pct} %"
        if (( etat < 1 )); then etat=1; fi
    fi
done < <(df --output=target,pcent -x tmpfs -x devtmpfs -x efivarfs | tail -n +2)

if (( etat == 0 )); then
    echo "OK : tous les systèmes de fichiers sont sous ${attention} %"
fi
exit "$etat"

df --output=target,pcent n'affiche que le point de montage et le pourcentage d'occupation, ce qui évite de découper les colonnes de df -h ; -x tmpfs écarte les systèmes de fichiers en mémoire. < <( … ) (une substitution de processus) donne la sortie de la commande à la boucle sans pipe, pour que la variable etat survive à la boucle.

Avec un serveur dont df affiche / à 47 %, /var à 83 %, /srv/sauvegardes à 93 % et /boot à 21 % :

text
$ verif-disques.sh
ATTENTION : /var plein à 83 %
CRITIQUE : /srv/sauvegardes plein à 93 %
$ echo $?
2

Chez Sorbier, la supervision Zabbix de srv-journaux surveille déjà l'espace disque de chaque serveur par son agent. Ce script sert là où il n'y a pas d'agent, ou pour une vérification à la demande avant une opération risquée. Les seuils, les alertes et les tableaux de bord sont le sujet du cours Supervision.

Analyser les journaux

Sur Debian 13, les journaux sont gardés par journald et se lisent avec journalctl ; le paquet rsyslog, qui écrivait /var/log/auth.log et /var/log/syslog, n'est plus installé par défaut depuis Debian 12.

CommandeCe qu'elle affiche
journalctl -u ssh --since "24 hours ago"Les messages du service SSH des dernières 24 heures
journalctl -p err -bLes messages de niveau erreur ou plus grave depuis le dernier démarrage
journalctl -t sauvegarde-config -n 20Les 20 derniers messages écrits par le script de sauvegarde
journalctl -u nginx -fSuivre en direct les messages de nginx
journalctl --disk-usageLa place occupée par les journaux
-o cat, --no-pagerN'afficher que le texte des messages ; ne pas ouvrir de pager (indispensable dans un script)

Le rapport quotidien des tentatives SSH échouées, avec une alerte au-delà d'un seuil par adresse :

bash
#!/usr/bin/env bash
# /usr/local/bin/rapport-ssh.sh [seuil] : échecs SSH des dernières 24 heures, par adresse source
set -euo pipefail
seuil="${1:-10}"

journalctl -u ssh --since "24 hours ago" --no-pager -o cat |
    grep -oE 'Failed password for .* from ([0-9]{1,3}\.){3}[0-9]{1,3}' |
    awk '{ print $NF }' | sort | uniq -c | sort -rn |
    while read -r nb ip; do
        if (( nb >= seuil )); then
            echo "ALERTE $ip : $nb échecs"
        else
            echo "$ip : $nb échec(s)"
        fi
    done || true

awk '{ print $NF }' affiche le dernier champ de chaque ligne, l'adresse. || true évite que le script se termine en échec les jours sans aucune tentative (le grep ne trouve rien). Avec un seuil de 3 et les journaux de la nuit du chapitre 9, le rapport afficherait ALERTE 203.0.113.50 : 3 échecs, puis 198.51.100.23 : 2 échec(s) et 192.168.10.209 : 1 échec(s). Pour réagir automatiquement à ces attaques, c'est Fail2ban qui bannit les adresses, et un SIEM comme Wazuh qui corrèle les événements de tous les serveurs.

Vérifier la rotation des journaux

Les fichiers journaux que les applications écrivent encore dans /var/log (nginx, par exemple) sont tournés par logrotate : chaque jour ou chaque semaine, le fichier est renommé, compressé, et les plus anciens sont supprimés. Sur Debian 13, logrotate est lancé chaque jour par un timer systemd, logrotate.timer. Si la rotation s'arrête, un fichier grossit jusqu'à remplir /var. Un contrôle simple :

bash
#!/usr/bin/env bash
# verif-rotation.sh : la rotation des journaux fonctionne-t-elle ?
set -euo pipefail
probleme=0

if [[ $(systemctl is-active logrotate.timer) != active ]]; then
    echo "logrotate.timer n'est pas actif"; probleme=1
fi
resultat=$(systemctl show logrotate.service -p Result --value)
if [[ $resultat != success ]]; then
    echo "dernière exécution de logrotate en échec ($resultat) : journalctl -u logrotate"; probleme=1
fi
gros=$(find /var/log -type f -size +500M)
if [[ -n $gros ]]; then
    echo "fichiers de plus de 500 Mo :"; echo "$gros"; probleme=1
fi
(( probleme == 0 )) && echo "rotation des journaux : OK"
exit "$probleme"

Pour tester la configuration d'une application sans rien modifier, logrotate -d /etc/logrotate.d/nginx affiche ce qui serait fait (mode débogage, à sec). systemctl list-timers logrotate.timer donne l'heure de la dernière et de la prochaine exécution.

Créer des comptes locaux

Sur un serveur Linux sans annuaire, les comptes des administrateurs se créent avec useradd. À partir d'un fichier comptes.txt qui contient login;Prénom Nom sur chaque ligne :

bash
while IFS=';' read -r login nom; do
    if id "$login" > /dev/null 2>&1; then
        echo "$login existe déjà"; continue
    fi
    useradd -m -s /bin/bash -c "$nom" -G sudo "$login"
    passwd -e "$login" > /dev/null          # le mot de passe devra être changé à la première connexion
    echo "$login créé : déposer sa clé SSH publique dans /home/$login/.ssh/authorized_keys"
done < comptes.txt

IFS=';' découpe chaque ligne au point-virgule. Pour un administrateur, l'accès se fait par clé SSH : le mot de passe ne sert qu'à sudo. Dès que les serveurs se multiplient, on centralise ces comptes (annuaire, ou outil de déploiement comme Ansible) plutôt que de les créer serveur par serveur.

Exercices

Exercice 1 sur 3 La rotation des archives Réussi À revoir

Le dossier /srv/sauvegardes/config contient six archives, modifiées il y a 1, 5, 14, 15, 16 et 30 jours. Le script les nettoie avec find … -mtime +14 -delete.

  1. Lesquelles sont supprimées ?
  2. Un collègue propose de garder « les 3 plus récentes » plutôt que « 14 jours ». Écris la commande qui affiche les archives à supprimer dans ce cas, et dis lesquelles elle affiche.
  3. Quel avantage a sa méthode quand la sauvegarde ne tourne plus depuis trois semaines ?
Voir la réponseMasquer la réponse de l'exercice 1
  1. Celles de 15, 16 et 30 jours. -mtime +14 signifie « strictement plus de 14 jours entiers » : find compte l'âge en jours entiers en arrondissant vers le bas, donc il faut au moins 15 jours. L'archive de 14 jours est gardée.
  2. ls -1t /srv/sauvegardes/config/*-config-*.tar.gz | tail -n +4 : ls -1t liste du plus récent au plus ancien, tail -n +4 affiche à partir de la quatrième ligne. Ici : les archives de 15, 16 et 30 jours, les mêmes par coïncidence (les trois plus récentes ont 1, 5 et 14 jours).
  3. Avec « 14 jours », si la sauvegarde échoue depuis trois semaines sans que personne ne le remarque, find finit par supprimer toutes les archives : il n'en reste aucune. Avec « les 3 plus récentes », il reste toujours trois archives, même anciennes. Le mieux est de combiner les deux règles, et surtout de superviser le code de sortie de la sauvegarde.

Tu avais trouvé ?

Exercice 2 sur 3 Prédire la vérification des disques Réussi À revoir

Sur srv-journaux, df --output=target,pcent -x tmpfs -x devtmpfs -x efivarfs affiche :

text
Monté sur        Uti%
/                 47%
/var              83%
/srv/sauvegardes  93%
/boot             21%

Donne ce qu'affiche verif-disques.sh et son code de sortie avec : (a) aucune option ; (b) -a 85 ; (c) -a 95 -c 99 ; (d) -x.

Voir la réponseMasquer la réponse de l'exercice 2
LancementAffichageCode
(a) sans optionATTENTION : /var plein à 83 % puis CRITIQUE : /srv/sauvegardes plein à 93 %2
(b) -a 85CRITIQUE : /srv/sauvegardes plein à 93 % (83 est sous 85)2
(c) -a 95 -c 99OK : tous les systèmes de fichiers sont sous 95 %0
(d) -xusage : /usr/local/bin/verif-disques.sh [-a pourcentage] [-c pourcentage] sur la sortie d'erreur3

La première ligne de df (l'en-tête, « Monté sur Uti% » en français) est retirée par tail -n +2. En (a), l'état passe à 1 sur /var, puis à 2 sur /srv/sauvegardes ; un ATTENTION après un CRITIQUE ne le ferait pas redescendre, grâce au test etat < 1. En (d), $0 vaut le chemin par lequel le script a été lancé : trouvé dans le PATH, c'est son chemin complet.

Tu avais trouvé ?

Exercice 3 sur 3 Réparer un script de sauvegarde Réussi À revoir

Ce script tourne chaque nuit sur srv-web depuis deux mois. Relève au moins cinq défauts et réécris-le.

bash
#!/bin/bash
DEST=/srv/sauvegardes
cd $DEST
tar -czf config-$(date +%F).tar.gz /etc/nginx /etc/ssh
find . -mtime +7 -exec rm {} \;
echo "Sauvegarde terminée"
Voir la réponseMasquer la réponse de l'exercice 3

Les défauts :

  1. Pas de mode strict : si tar échoue, le script affiche quand même « Sauvegarde terminée » et se termine avec le code 0.
  2. cd $DEST sans vérification : si le dossier n'existe pas, cd échoue, et find . -mtime +7 -exec rm supprime les vieux fichiers du dossier courant, c'est-à-dire le dossier personnel de l'utilisateur qui lance le script, ou / pour une tâche de cron lancée par root.
  3. find supprime tout ce qui a plus de 7 jours dans le dossier, quel que soit le fichier, et sans le journaliser.
  4. L'archive n'est ni vérifiée ni copiée ailleurs : elle disparaît avec le serveur.
  5. Variables sans guillemets, chemins absolus dans l'archive (avertissement de tar à chaque exécution), aucun verrou, aucune trace dans le journal système.

La version du chapitre (sauvegarde-config.sh) corrige tout cela. En version minimale :

bash
#!/usr/bin/env bash
set -Eeuo pipefail
DEST=/srv/sauvegardes/config
mkdir -p "$DEST"
archive="$DEST/$(hostname -s)-config-$(date +%F_%H%M).tar.gz"
tar -czf "$archive" -C / etc/nginx etc/ssh
tar -tzf "$archive" > /dev/null
find "$DEST" -type f -name '*-config-*.tar.gz' -mtime +14 -print -delete
rsync -a -e "ssh -o BatchMode=yes -o ConnectTimeout=10" "$archive" sauvegarde@srv-journaux:/srv/sauvegardes/srv-web/
logger -t sauvegarde-config "Archive $archive créée et copiée"

Tu avais trouvé ?

Chapitre terminé !

Fais les 3 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Chapitre suivant : Planifier un script : cron, timers systemd et Planificateur de tâchesRevoir le sommaire du cours