Après chaque réinstallation d’Ubuntu, je lance un seul script et je reviens devant une machine entièrement configurée : /home remonté sur son LVM, NAS monté, Docker/KVM, supervision, relais mail, VPN, VeraCrypt, tâches planifiées — et une validation automatique qui m’envoie son rapport par mail après le reboot. Ce script, je le fais évoluer depuis des années ; le voici publié en intégralité (version 8.9.21 pour Ubuntu 26.04 LTS), avec son fichier de configuration et son script de validation.
C’est un script personnel : il installe mes outils et reflète mon infrastructure. Il n’est pas fait pour être exécuté tel quel, mais pour être adapté — et toute la conception va dans ce sens : ce qui est propre à la machine vit dans un fichier .conf séparé, et le script est découpé en 41 sections qu’on peut lister, rejouer une par une ou tout simplement supprimer. Ce guide d’adaptation précède les trois fichiers complets.
Les trois fichiers
| Fichier | Rôle |
|---|---|
postinstall.26.04_v2.sh |
Le script principal (41 sections, options --list / --from / --only) |
postinstall.26.04_v2.conf |
Votre configuration réelle, secrets inclus — à créer depuis le modèle ci-dessous, jamais versionné |
validate_postinstall.sh |
Validation post-reboot (lancée automatiquement par un timer systemd, rapport par mail) |
Mise en route :
# 1. Copier les trois fichiers EN LOCAL sur la machine (pas depuis un montage réseau : # le script verrouille son .conf en root:600, impossible sur du CIFS → refus de démarrer) # 2. Créer la configuration depuis le modèle cp postinstall.26.04_v2.conf.example postinstall.26.04_v2.conf vi postinstall.26.04_v2.conf # renseigner tous les champs ⚠️ # 3. Découvrir les sections bash postinstall.26.04_v2.sh --list # 4. Lancer (avec journal complet) sudo bash postinstall.26.04_v2.sh 2>&1 | tee postinstall.log # Reprendre après interruption / rejouer une section sudo bash postinstall.26.04_v2.sh --from 22 sudo bash postinstall.26.04_v2.sh --only 28
Ce que vous devez modifier — 1. le fichier .conf
Tout ce qui est propre à votre machine et vos comptes est dans le .conf. Le modèle publié plus bas contient des marqueurs ⚠️ À REMPLACER partout où une valeur est attendue :
| Variables | À mettre |
|---|---|
UTILISATEUR, HOSTNAME_MACHINE |
Votre compte principal et le nom que prendra la machine (hostnamectl, Postfix, supervision) |
REBOOT_AUTO |
false pour un premier essai sans redémarrage automatique |
NAS_IP, NAS_USER, NAS_PASSWORD, NAS_PARTAGES |
Votre NAS et la liste de vos partages CIFS (chacun devient un montage fstab + un signet Nautilus). Pas de NAS ? Voir la table des sections ci-dessous. |
LVM_HOME_DEVICE |
Le volume logique de votre /home (vérifier avec sudo lvdisplay). C’est le cœur de la section 1 : un /home sur son propre LVM survit aux réinstallations. |
GMAIL_USER, GMAIL_APP_PASSWORD |
Votre adresse et un App Password Gmail (myaccount.google.com) — utilisés par Postfix pour que la machine sache envoyer des mails (alertes SMART, rapport de validation…) |
VERACRYPT_* |
Chemin de votre conteneur chiffré et son mot de passe (alias de montage/démontage 1984/1984q — renommez-les à votre goût dans la section 8b) |
CMA_TOKEN, CMA_ENDPOINT, CMA_HOST |
Token et endpoint de votre serveur Centreon — uniquement si vous supervisez avec Centreon |
PRO_TOKEN |
Token Ubuntu Pro (ubuntu.com/pro/dashboard) — laisser vide pour sauter l’attachement, la section 2g s’ignore proprement |
VPN_*_USER/PASS/CERT/KEY |
Identifiants, certificats et clés de vos connexions OpenVPN (chez moi : CyberGhost, 4 pays). Les blocs heredoc du modèle montrent où coller chaque certificat. |
RDP_USER, RDP_PASS |
Identifiants GNOME Remote Desktop (section 26b) |
Le script applique lui-même root:600 sur ce fichier au démarrage et refuse de s’exécuter s’il n’y parvient pas (cas typique : lancement depuis un montage CIFS où chmod est inopérant).
Ce que vous devez modifier — 2. les sections
Chaque section est une fonction section_<id>() déclarée dans le tableau SECTIONS en tête de script. Pour désactiver une section : supprimez sa ligne du tableau, c’est tout — le dispatcher, --list et la numérotation d’affichage suivent automatiquement. Les sections à examiner en priorité selon votre contexte :
| Section | Contenu | À adapter si… |
|---|---|---|
1 |
Montage /home sur LVM via UUID |
Votre /home n’est pas sur un volume logique dédié → supprimez-la |
2 |
Dépôts et paquets (navigateurs, VirtualBox Oracle, outils…) | La liste des paquets est personnelle — faites votre marché |
2a, 23 |
Outils et installation Claude Code | Vous n’utilisez pas Claude Code |
2d, 2e |
NRPE + SNMP + agent Centreon, sudoers de supervision | Pas de Centreon → supprimez ; autre supervision → remplacez |
2g |
Ubuntu Pro (ESM + Livepatch) | Se saute tout seul si PRO_TOKEN est vide |
4 |
IP statique (fige le bail DHCP courant) + DNS | Vous préférez rester en DHCP pur |
5, 19 |
Montages NAS CIFS + signets Nautilus | Pas de NAS → supprimez les deux |
8 |
.bashrc + aliases |
La plus personnelle du lot : les alias (mise à jour d’une pile Docker distante, aide-mémoire, raccourcis) référencent mon environnement — réécrivez-les |
8b, 24 |
Scripts VeraCrypt + installation | Pas de conteneur chiffré |
22 |
Postfix en relais Gmail | Autre fournisseur mail → adaptez hôte SMTP et authentification |
26, 26b |
Agent DWService, GNOME Remote Desktop | Pas de prise en main à distance |
27 |
Crontab : rappels quotidiens + scan ClamAV planifié | Les rappels sont personnels |
28 |
OpenVPN multi-pays via NetworkManager | Autre fournisseur VPN → adaptez les serveurs remote et le bloc OVPN commun du .conf |
29b |
Timer systemd de validation post-reboot | À garder ! Ajustez plutôt validate_postinstall.sh aux sections que vous conservez |
Note sur le caviardage : les valeurs visibles dans les fichiers publiés sont des exemples — utilisateur utilisateur, machine ma-machine, NAS 192.168.1.100, serveur de supervision 192.168.1.50, e-mail prenom.nom@gmail.com. Remplacez-les par les vôtres via le .conf ; aucun secret réel ne figure dans ces fichiers.
Après le reboot
Quatre minutes après le redémarrage, le timer systemd lance validate-postinstall.sh (en utilisateur, pas root) : il recontrôle chaque section — montages, services, groupes, versions — et envoie son rapport à GMAIL_USER, puis désactive son propre timer. Les quelques points non automatisables (authentification Claude Code, code DWService…) sont listés en fin de rapport comme actions manuelles.
Le script principal — postinstall.26.04_v2.sh (complet)
3 842 lignes, 41 sections. L’historique des versions en tête de fichier raconte aussi les pièges rencontrés au fil des années — il fait partie de la documentation.
#!/bin/bash
################################################################################
# Script de post-installation Ubuntu 26.04 LTS "Resolute Raccoon"
#
# Description : Configure automatiquement un système Ubuntu 26.04 fraîchement installé
# avec montage /home sur LVM, montage NAS, optimisations système,
# automatisations, Claude Code, DWService + VeraCrypt
# + Fortune français + rappels quotidiens + Postfix Gmail + ClamAV planifié
# + VPN OpenVPN (Suisse + USA + France + Russie)
# + yt-dlp (dernière version GitHub) + alias mp3dl
# + smartmontools + supervision Centreon (groupe monitoring, sudoers)
#
# Auteur : Romain J.
# Fait avec amour par Romain J.
# Version : 8.9.21
# Date : 18 juillet 2026
#
# Usage : sudo bash postinstall.26.04_v2.sh [options]
# (sans option) exécution complète de toutes les sections + reboot
# (nombre exact : --list, source unique = tableau SECTIONS)
# -l, --list liste les sections disponibles (id + titre)
# -f, --from <id> reprend l'exécution À PARTIR de la section <id>
# -o, --only <id> exécute UNIQUEMENT la section <id> (pas de reboot
# sauf --only 31)
# -h, --help affiche l'aide
#
# Checklist finale avant exécution
# 1. ⚠️ MODIFIER LE MOT DE PASSE NAS → postinstall.26.04_v2.conf → NAS_PASSWORD
# 2. ⚠️ VÉRIFIER LE MOT DE PASSE APP GMAIL → postinstall.26.04_v2.conf → GMAIL_APP_PASSWORD
# 3. ⚠️ VÉRIFIER LE MOT DE PASSE VERACRYPT → postinstall.26.04_v2.conf → VERACRYPT_PASS
# 4. ⚠️ VÉRIFIER LES IDENTIFIANTS VPN → postinstall.26.04_v2.conf → VPN_*_USER / VPN_*_PASS
# 5. ⚠️ VÉRIFIER LE TOKEN CMA → postinstall.26.04_v2.conf → CMA_TOKEN
# 5b. ⚠️ VÉRIFIER LES IDENTIFIANTS RDP → postinstall.26.04_v2.conf → RDP_USER / RDP_PASS
# 5c. ⚠️ VÉRIFIER LE TOKEN UBUNTU PRO → postinstall.26.04_v2.conf → PRO_TOKEN
# 6. Vérifier le device LVM existe : sudo lvdisplay /dev/home-vg/home_lv
# 7. Vérifier connexion réseau filaire : nmcli connection show --active
# 8. Copier les 3 FICHIERS en LOCAL sur la machine à installer :
# postinstall.26.04_v2.sh + postinstall.26.04_v2.conf + validate_postinstall.sh
# ⚠️ TOUS les trois, même si une vieille copie traîne dans /home (le /home
# LVM survit aux réinstallations — un validate périmé fausse la validation).
# Le script applique LUI-MÊME root:600 sur le .conf au démarrage (v8.9.5).
# 9. Lancer avec logs : sudo bash postinstall.26.04_v2.sh 2>&1 | tee postinstall.log
#
# Sécurité bash : -u = erreur si variable indéfinie, -o pipefail = erreur si échec dans un pipeline
# -e volontairement omis : on veut que le script continue même si une étape échoue,
# les erreurs sont capturées via log_warn/log_error et résumées en fin d'exécution.
set -uo pipefail
# IFS restreint aux séparateurs tab+newline (protège contre les espaces dans les noms de fichiers/chemins)
IFS=$'\n\t'
# Version du script — source unique (évite la dérive entre l'en-tête et les messages)
readonly VERSION="8.9.21"
################################################################################
# TABLE DES SECTIONS + OPTIONS DE LIGNE DE COMMANDE
################################################################################
# Tableau ordonné "id|titre" — chaque id correspond à une fonction section_<id>()
# définie plus bas. Le dispatcher (fin de script) les exécute dans cet ordre.
readonly -a SECTIONS=(
"1|Configuration du montage de /home sur LVM (via UUID) + optimisation SSD"
"2|Mise à jour système, dépôts et paquets"
"2a|Outils Claude Code (npm-global, ccstatusline)"
"2b|Configuration Firefox Snap — restauration profil"
"2c|Configuration plocate (updatedb.conf)"
"2d|Monitoring — NRPE + SNMP + Centreon Monitoring Agent"
"2e|Configuration supervision Centreon (groupe monitoring, sudoers)"
"2f|Alertes SMART par mail (smartd)"
"2g|Attachement Ubuntu Pro (ESM + Livepatch)"
"3|Configuration Docker, KVM et groupes utilisateur"
"4|Configuration réseau : IP statique (DHCP figé) + DNS"
"5|Configuration des montages NAS"
"6|Configuration GRUB"
"7|Éditeur par défaut"
"8|Configuration .bashrc + aliases (backup + écrasement)"
"8b|Scripts VeraCrypt sécurisés"
"9|Sudo sans mot de passe"
"10|Flatpak et applications"
"11|Mises à jour automatiques"
"12|Nettoyage paquets"
"13|Limitation journal systemd"
"14|Configuration Nautilus/GNOME + extensions + thème terminal Ptyxis"
"15|Désactivation veille/éteint écran"
"16|Désactivation UFW"
"17|Création script nettoyage automatique"
"18|Création script mise à jour automatique (système)"
"19|Dossiers XDG locaux + signets NAS Nautilus"
"20|Vérification services"
"21|Nettoyage intermédiaire"
"22|Configuration Postfix relay Gmail"
"23|Installation Claude Code"
"24|Installation VeraCrypt"
"25|Installation fortune-mod, cowsay et citations françaises"
"26|Installation de l'agent DWService"
"26b|GNOME Remote Desktop (RDP headless + partage de session)"
"27|Configuration crontab rappels + scan ClamAV"
"28|Configuration OpenVPN (Suisse + USA + France + Russie)"
"29|Nettoyage final avant reboot"
"29b|Validation post-reboot automatique (timer + mail)"
"30|RÉSUMÉ FINAL"
"31|Redémarrage du système"
)
# Affiche l'aide et quitte (utilisable sans sudo)
afficher_aide() {
cat << AIDE
Usage : sudo bash $(basename "$0") [options]
Post-installation Ubuntu 26.04 LTS — v${VERSION}
Options :
(aucune) exécution complète des ${#SECTIONS[@]} sections + reboot final
-l, --list liste les sections disponibles (id + titre)
-f, --from <id> reprend l'exécution À PARTIR de la section <id>
(utile après une interruption — inclut résumé et reboot)
-o, --only <id> exécute UNIQUEMENT la section <id>
(pas de reboot sauf --only 31)
-h, --help affiche cette aide
Exemples :
sudo bash $(basename "$0") --list
sudo bash $(basename "$0") --from 22 # reprend à Postfix
sudo bash $(basename "$0") --only 28 # rejoue uniquement les VPN
AIDE
exit 0
}
# Liste les sections (id + titre) et quitte (utilisable sans sudo)
lister_sections() {
echo "Sections disponibles (${#SECTIONS[@]}) — postinstall v${VERSION} :"
local entree
for entree in "${SECTIONS[@]}"; do
printf ' %-4s %s\n' "${entree%%|*}" "${entree#*|}"
done
exit 0
}
# Vérifie qu'un id de section existe dans la table
section_existe() {
local id="$1" entree
for entree in "${SECTIONS[@]}"; do
[[ "${entree%%|*}" == "$id" ]] && return 0
done
return 1
}
# ---- Parsing des options (AVANT la vérification root : -h/-l sans sudo) ----
FROM_ID=""
ONLY_ID=""
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help) afficher_aide ;;
-l|--list) lister_sections ;;
-f|--from)
FROM_ID="${2:-}"
[[ -z "$FROM_ID" ]] && { echo "[ERREUR] --from requiert un id de section (--list)" >&2; exit 1; }
shift ;;
-o|--only)
ONLY_ID="${2:-}"
[[ -z "$ONLY_ID" ]] && { echo "[ERREUR] --only requiert un id de section (--list)" >&2; exit 1; }
shift ;;
*)
echo "[ERREUR] Option inconnue : $1 (voir --help)" >&2
exit 1 ;;
esac
shift
done
if [[ -n "$FROM_ID" && -n "$ONLY_ID" ]]; then
echo "[ERREUR] --from et --only sont mutuellement exclusives" >&2
exit 1
fi
if [[ -n "$FROM_ID" ]] && ! section_existe "$FROM_ID"; then
echo "[ERREUR] Section inconnue : '$FROM_ID' — voir la liste avec --list" >&2
exit 1
fi
if [[ -n "$ONLY_ID" ]] && ! section_existe "$ONLY_ID"; then
echo "[ERREUR] Section inconnue : '$ONLY_ID' — voir la liste avec --list" >&2
exit 1
fi
# Vérification root AVANT toute écriture dans /var/log
# (sinon mkdir/tee échouent silencieusement en non-root)
if [ "${EUID:-$(id -u)}" -ne 0 ]; then
echo "[ERREUR] Ce script doit être exécuté en root : sudo bash $0" >&2
exit 1
fi
################################################################################
# SYSTÈME DE LOG + SUIVI DES ANOMALIES
################################################################################
LOG_DIR="/var/log/postinstall"
mkdir -p "$LOG_DIR"
LOG_FILE="$LOG_DIR/postinstall-$(date +%Y%m%d_%H%M%S).log"
exec > >(tee -a "$LOG_FILE") 2>&1
# Tableau pour collecter les warnings et erreurs — affiché en résumé final
declare -a ANOMALIES=()
log_info() { echo "[$(date '+%F %T')] [INFO] $*"; }
log_ok() { echo "[$(date '+%F %T')] [OK] $*"; }
log_warn() {
echo "[$(date '+%F %T')] [WARN] $*"
ANOMALIES+=("[ATTENTION] $*")
}
log_error() {
echo "[$(date '+%F %T')] [ERREUR] $*"
ANOMALIES+=("[ERREUR] $*")
}
log_section() { echo ""; echo "[$(date '+%F %T')] ======== $* ========"; }
# Récapitulatif des anomalies — appelé par la section 30 (résumé final)
# et par le trap INT/TERM (interruption en cours d'exécution)
afficher_anomalies() {
if [ ${#ANOMALIES[@]} -eq 0 ]; then
echo "✅ AUCUNE ANOMALIE DÉTECTÉE DURANT L'EXÉCUTION"
else
echo "❌ ${#ANOMALIES[@]} ANOMALIE(S) DÉTECTÉE(S) — À VÉRIFIER :"
local item
for item in "${ANOMALIES[@]}"; do
echo " • $item"
done
fi
}
log_info "Démarrage post-installation v${VERSION} — log : $LOG_FILE"
################################################################################
# MODE NON-INTERACTIF APT/DEBCONF (v8.9.1)
################################################################################
# Le script tourne via « sudo bash … | tee » : stdin est indisponible, tout
# écran debconf (postfix mailer_type, etc.) bloquerait indéfiniment.
# Exporté AVANT toute commande apt — hérité par tous les apt install du script.
export DEBIAN_FRONTEND=noninteractive
################################################################################
# CHARGEMENT DE LA CONFIGURATION EXTERNE
################################################################################
# Le fichier postinstall.conf doit se trouver dans le même répertoire que ce script,
# ou son chemin peut être passé via la variable d'environnement POSTINSTALL_CONF.
CONF_FILE="${POSTINSTALL_CONF:-$(dirname "$(realpath "$0")")/postinstall.26.04_v2.conf}"
if [ ! -f "$CONF_FILE" ]; then
echo "[ERREUR] Fichier de configuration introuvable : $CONF_FILE"
echo " Le script attend postinstall.26.04_v2.conf dans son répertoire"
echo " (ou via la variable POSTINSTALL_CONF=/chemin/vers/mon.conf)"
exit 1
fi
# Sécurisation du .conf : le fichier est SOURCÉ en root et contient des
# secrets. Le script tourne en root (sudo) → il applique LUI-MÊME root:600
# au lieu d'exiger une préparation manuelle, source d'erreurs (v8.9.5 —
# au test réel, chown/chmod avaient été appliqués au .sh au lieu du .conf).
# On préserve 400 si déjà en place (lecture seule volontaire).
CONF_PERMS=$(stat -c "%a" "$CONF_FILE")
chown root:root "$CONF_FILE" 2>/dev/null || true
[ "$CONF_PERMS" != "400" ] && { chmod 600 "$CONF_FILE" 2>/dev/null || true; }
# Filet de sécurité : en usage normal (script + .conf copiés en local sur la
# machine à installer), l'application ci-dessus réussit toujours et ce
# contrôle est silencieux. Il ne se déclenche que si le chown/chmod n'a pas
# pris (ex. lancement par mégarde depuis le NAS CIFS, où ils sont inopérants).
CONF_OWNER=$(stat -c "%U" "$CONF_FILE")
CONF_PERMS=$(stat -c "%a" "$CONF_FILE")
if [ "$CONF_OWNER" != "root" ] || { [ "$CONF_PERMS" != "600" ] && [ "$CONF_PERMS" != "400" ]; }; then
echo "[ERREUR] Impossible de sécuriser $CONF_FILE (propriétaire : $CONF_OWNER, permissions : $CONF_PERMS — attendu root:600)" >&2
echo " Le .conf contient des secrets sourcés en root — lancement depuis le NAS CIFS ?" >&2
echo " Copiez le répertoire en LOCAL sur la machine à installer puis relancez." >&2
exit 1
fi
# Sourcing du fichier de configuration
# shellcheck source=postinstall.26.04_v2.conf
source "$CONF_FILE"
log_info "Configuration chargée depuis : $CONF_FILE"
# ---- Variables globales calculées une seule fois (C2 + U1 v8.8.24) ----
TS=$(date +%Y%m%d_%H%M%S) # Timestamp partagé par tous les backups
START=$(date +%s) # Pour calcul de la durée totale au résumé final
# ---- Vérification des variables critiques du .conf (R1 v8.8.24) ----
# Détection early-fail : crash explicite si une variable critique est manquante
# (sinon `set -u` ferait crasher quelque part au milieu du script)
VARS_CRITIQUES=(
UTILISATEUR HOSTNAME_MACHINE
NAS_IP NAS_USER NAS_PASSWORD NAS_CREDENTIALS
LVM_HOME_DEVICE
GMAIL_USER GMAIL_APP_PASSWORD
VERACRYPT_PASS_FILE VERACRYPT_PASS VERACRYPT_CONTAINER VERACRYPT_MOUNTPOINT
VPN_SUISSE_USER VPN_SUISSE_PASS VPN_USA_USER VPN_USA_PASS
VPN_FRANCE_USER VPN_FRANCE_PASS VPN_RUSSIE_USER VPN_RUSSIE_PASS
VPN_COMMON_OVPN VPN_COMMON_CA
VPN_SUISSE_CERT VPN_SUISSE_KEY VPN_USA_CERT VPN_USA_KEY
VPN_FRANCE_CERT VPN_FRANCE_KEY VPN_RUSSIE_CERT VPN_RUSSIE_KEY
CMA_TOKEN CMA_ENDPOINT CMA_HOST
)
MISSING=()
for v in "${VARS_CRITIQUES[@]}"; do
[ -z "${!v:-}" ] && MISSING+=("$v")
done
# Vérification spécifique pour le tableau NAS_PARTAGES — declare -p d'abord :
# sous set -u, ${#NAS_PARTAGES[@]} sur un tableau absent ferait crasher le
# script avec un message brut "variable sans liaison" avant l'early-fail
if ! declare -p NAS_PARTAGES &>/dev/null || [[ ${#NAS_PARTAGES[@]} -eq 0 ]]; then
MISSING+=("NAS_PARTAGES")
fi
if [ ${#MISSING[@]} -gt 0 ]; then
log_error "Variables manquantes dans $CONF_FILE : ${MISSING[*]}"
log_error "Compléter le .conf avant de relancer le script."
exit 1
fi
log_ok "Variables critiques du .conf : ${#VARS_CRITIQUES[@]} validées"
# Dériver BASHRC_USER depuis UTILISATEUR (variable calculée, pas configurable)
# Placé APRÈS la vérification : si UTILISATEUR manquait, le message explicite
# ci-dessus s'affiche au lieu d'un crash brut "variable sans liaison" (set -u)
BASHRC_USER="/home/$UTILISATEUR/.bashrc"
# PATH étendu pour propager les binaires npm-global via sudo -u (sections 2a et 23)
# Défini ici (et non en section 2a) : avec --only 23, une définition locale à la
# section 2a laisserait la variable indéfinie → crash sous set -u
NPM_PATH="/home/${UTILISATEUR}/.npm-global/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
# ---- Wrapper apt update standardisé (C1 v8.8.24, fix v8.8.25) ----
# Réduit le bruit sans masquer les vraies erreurs/warnings.
# Capture le code retour réel d'`apt update` (avant le pipe) pour que les
# chaînes `apt_update_silent || log_warn ...` se déclenchent en cas d'échec.
# Note : on évite le pipe direct car `|| true` final écrase PIPESTATUS.
apt_update_silent() {
local output rc
output=$(apt update -o Dpkg::Use-Pty=0 2>&1)
rc=$?
echo "$output" | grep -E '^(Err|W:)' || true
return "$rc"
}
# ---- Wrapper d'ajout de dépôt apt (L2 v8.8.24) ----
# Factorise le pattern Steam/Brave/Opera (3 occurrences identiques avant v8.8.24)
# Arguments : nom_dépôt, URL clé GPG, ligne deb, chemin keyring
ajouter_depot_apt() {
local nom="$1" url_key="$2" deb_line="$3" keyring="$4"
local list_file="/etc/apt/sources.list.d/${nom}.list"
if [ ! -f "$list_file" ]; then
log_info "Ajout dépôt $nom..."
curl -fsSL "$url_key" | gpg --dearmor -o "$keyring" \
|| log_warn "$nom : téléchargement clé GPG échoué"
echo "$deb_line" > "$list_file"
apt_update_silent || log_warn "Update dépôt $nom échoué"
else
log_info "Dépôt $nom déjà configuré"
fi
}
# ---- Wrapper snap install avec nouvelle tentative (v8.9.11) ----
# Le Snap Store échoue par intermittence (« unexpected EOF » en plein
# téléchargement, constaté au test réel du 2026-07-03 sur codium) :
# une 2e tentative après une courte pause absorbe presque toujours l'aléa.
# Usage : installer_snap <paquet> [options snap install]
installer_snap() {
local paquet="$1"; shift
if snap install "$paquet" "$@"; then
return 0
fi
log_info "$paquet : snap install en échec — nouvelle tentative dans 15 s..."
sleep 15
snap install "$paquet" "$@"
}
# ---- Wrapper gsettings (L1 v8.8.24) ----
# Factorise les 17 appels gsettings via sudo -u UTILISATEUR + DBUS_SESSION_BUS_ADDRESS
# Usage : gset org.gnome.nautilus.preferences show-hidden-files true
# La variable DBUS_PATH doit être définie avant l'appel (par la section appelante).
gset() {
sudo -u "$UTILISATEUR" DBUS_SESSION_BUS_ADDRESS="${DBUS_PATH:-unix:path=/run/user/${USER_UID:-1000}/bus}" \
gsettings set "$@"
}
################################################################################
# VÉRIFICATIONS PRÉALABLES
################################################################################
log_section "VÉRIFICATIONS PRÉALABLES"
# (vérification root déjà effectuée en tête de script, avant la création du log)
# Lit une clé de /etc/os-release SANS sourcing (v8.9.3 — cause racine enfin
# identifiée et reproduite) : ce script déclare « readonly VERSION » et
# os-release assigne VERSION="26.04 LTS…" → erreur readonly FATALE qui tue le
# sous-shell avant tout echo, d'où les valeurs vides aux tests réels
# v8.9.0/v8.9.1 (garde OS « blanche », VeraCrypt « Ubuntu /24.04/22.04 »).
lire_os_release() {
grep -oP "^${1}=\"?\K[^\"]+" /etc/os-release 2>/dev/null || true
}
# ---- Garde version OS : ce script cible Ubuntu 26.04 (v8.9.0) ----
# Lancé par mégarde sur une autre version (paquets, codenames, GNOME différents),
# il faut une confirmation explicite pour continuer.
OS_ID=$(lire_os_release ID)
OS_ID=${OS_ID:-inconnu}
OS_VERSION=$(lire_os_release VERSION_ID)
OS_VERSION=${OS_VERSION:-inconnue}
if [[ "$OS_ID" == "ubuntu" && "$OS_VERSION" == "26.04" ]]; then
log_ok "OS cible confirmé : Ubuntu 26.04"
else
log_warn "OS détecté : ${OS_ID} ${OS_VERSION} — ce script cible Ubuntu 26.04"
reponse_os=""
read -r -p "Continuer quand même ? (o/N) " reponse_os || true
if [[ ! "$reponse_os" =~ ^[oO]$ ]]; then
log_error "Exécution annulée — OS non cible (${OS_ID} ${OS_VERSION})"
exit 1
fi
log_warn "Exécution forcée sur ${OS_ID} ${OS_VERSION} (confirmé par l'utilisateur)"
fi
if ! id "$UTILISATEUR" &>/dev/null; then
log_error "L'utilisateur $UTILISATEUR n'existe pas"
exit 1
fi
USER_UID=$(id -u "$UTILISATEUR")
USER_GID=$(id -g "$UTILISATEUR")
log_ok "Utilisateur détecté : $UTILISATEUR (UID: $USER_UID, GID: $USER_GID)"
# ---- Nom de machine depuis le .conf (HOSTNAME_MACHINE — v8.9.0) ----
# Aligne le hostname système sur la configuration (postfix myhostname, sondes
# Centreon, mails de rapport) quel que soit le nom saisi à l'installation.
if [[ "$(hostname)" == "$HOSTNAME_MACHINE" ]]; then
log_ok "Nom de machine déjà correct : $HOSTNAME_MACHINE"
else
hostnamectl set-hostname "$HOSTNAME_MACHINE" \
&& log_ok "Nom de machine défini : $HOSTNAME_MACHINE (hostnamectl)" \
|| log_warn "hostnamectl set-hostname $HOSTNAME_MACHINE échoué"
# 127.0.1.1 doit suivre le nouveau nom (résolution locale du hostname)
if grep -qE '^127\.0\.1\.1[[:space:]]' /etc/hosts; then
sed -i -E "s|^127\.0\.1\.1[[:space:]].*|127.0.1.1\t${HOSTNAME_MACHINE}|" /etc/hosts
else
printf '127.0.1.1\t%s\n' "$HOSTNAME_MACHINE" >> /etc/hosts
fi
log_ok "/etc/hosts : 127.0.1.1 → $HOSTNAME_MACHINE"
fi
if [ "$NAS_PASSWORD" = "VOTRE_MOT_DE_PASSE_ICI" ]; then
log_error "Veuillez modifier le mot de passe NAS dans le script avant de l'exécuter"
exit 1
fi
# Détection réseau étendue : eno, enp, eth, wl
CONNEXION_NET=$(nmcli -t -f NAME,DEVICE connection show --active | grep -E 'enp|eth|eno|wl' | cut -d: -f1 | head -n1)
if [ -z "$CONNEXION_NET" ]; then
log_error "Aucune connexion réseau filaire/wifi active détectée"
log_error "Résultat nmcli : $(nmcli -t -f NAME,DEVICE connection show --active)"
exit 1
fi
log_ok "Connexion réseau active : $CONNEXION_NET"
################################################################################
# 1. CONFIGURATION DU MONTAGE DE /HOME SUR LVM VIA UUID + OPTIMISATION SSD
################################################################################
section_1() {
# Un seul backup pour toute la section (TS global C2)
FSTAB_BACKUP="/etc/fstab.backup.${TS}"
cp /etc/fstab "$FSTAB_BACKUP"
log_info "Backup fstab : $FSTAB_BACKUP"
# ---- Optimisation SSD : ajout de noatime sur / si pas déjà présent ----
if grep -qP "^\S+\s+/\s+ext4" /etc/fstab && ! grep -qP "^\S+\s+/\s+ext4.*noatime" /etc/fstab; then
sed -i -E '/^\S+\s+\/\s+ext4/ s/(defaults|errors=remount-ro)/\1,noatime/' /etc/fstab
log_ok "noatime ajouté pour la racine / (optimisation SSD)"
else
log_ok "/ : noatime déjà présent ou partition non ext4 — aucune modification"
fi
# ---- Montage /home sur LVM avec noatime ----
# Vérification du device avant toute action (R2 v8.8.24) : si le device LVM
# n'existe pas, on saute proprement la sous-section plutôt que d'écrire
# une entrée fstab cassée et de tenter un mount voué à l'échec.
if [ ! -b "$LVM_HOME_DEVICE" ]; then
log_warn "Device LVM absent ou inaccessible : $LVM_HOME_DEVICE"
log_warn " → Sous-section /home sur LVM ignorée (corrigez LVM_HOME_DEVICE dans le .conf)"
log_warn " → Pour vérifier : sudo lvdisplay"
else
HOME_UUID=$(blkid -s UUID -o value "$LVM_HOME_DEVICE" 2>/dev/null || true)
if [ -z "$HOME_UUID" ]; then
log_warn "Impossible de récupérer l'UUID de $LVM_HOME_DEVICE"
log_warn "Utilisation du nom de device : $LVM_HOME_DEVICE"
LVM_ENTRY="$LVM_HOME_DEVICE /home ext4 defaults,noatime 0 2"
else
log_ok "UUID trouvé pour /home : $HOME_UUID"
LVM_ENTRY="UUID=$HOME_UUID /home ext4 defaults,noatime 0 2"
fi
# Pattern strict : début de ligne, non-commenté, premier champ + espaces + /home + espaces
# (évite de matcher /home_old, /var/home, etc.)
HOME_PATTERN='^[^#][^[:space:]]*[[:space:]]+/home[[:space:]]'
if grep -qE "$HOME_PATTERN" /etc/fstab && ! grep -qF "$LVM_ENTRY" /etc/fstab; then
# Délimiteur | échappé (\|…|d) : HOME_PATTERN contient un / non échappé
# qui terminait l'adresse sed /…/ → la commande échouait (rc=1) et
# l'ancienne entrée /home restait, dupliquant le point de montage
sed -i -E "\|${HOME_PATTERN}|d" /etc/fstab \
|| log_warn "fstab : suppression de l'ancienne entrée /home échouée — vérifier les doublons"
fi
if grep -qF "$LVM_ENTRY" /etc/fstab; then
log_ok "Entrée LVM déjà présente dans /etc/fstab"
else
echo "" >> /etc/fstab
echo "# Partition /home sur LVM (via UUID) — noatime pour SSD" >> /etc/fstab
echo "$LVM_ENTRY" >> /etc/fstab
log_ok "Entrée LVM ajoutée à /etc/fstab (avec noatime)"
# Erreur visible si mount échoue : on conserve stderr pour diagnostic
if mount -a; then
log_ok "/home monté immédiatement"
else
log_warn "Montage immédiat échoué – redémarrage requis"
fi
fi
fi
}
################################################################################
# 2. MISE À JOUR SYSTÈME, DÉPÔTS DOCKER & STEAM, PAQUETS ET NAVIGATEURS
################################################################################
section_2() {
apt clean || true
rm -rf /var/lib/apt/lists/* || true
log_info "apt update + upgrade..."
apt_update_silent
apt upgrade -y \
|| { log_error "apt upgrade échoué — réseau ou dépôts défaillants"; exit 1; }
# Dépôt Docker Engine officiel
# Docker ne publie pas immédiatement pour les nouvelles versions Ubuntu :
# fallback explicite sur "noble" (24.04 LTS) jusqu'à publication officielle 26.04
log_info "Ajout dépôt Docker..."
apt install -y ca-certificates curl || true
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc \
|| log_warn "Téléchargement clé Docker échoué"
chmod a+r /etc/apt/keyrings/docker.asc
UBUNTU_CODENAME=$(lire_os_release VERSION_CODENAME)
case "$UBUNTU_CODENAME" in
noble|jammy|focal)
DOCKER_CODENAME="$UBUNTU_CODENAME"
;;
*)
# 26.04 "resolute" pas encore supporté par Docker → fallback noble
DOCKER_CODENAME="noble"
log_info "Codename '$UBUNTU_CODENAME' non supporté par Docker — fallback sur '$DOCKER_CODENAME'"
;;
esac
rm -f /etc/apt/sources.list.d/docker.*
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $DOCKER_CODENAME stable" \
> /etc/apt/sources.list.d/docker.list
apt_update_silent || log_warn "Update dépôt Docker échoué, continuation..."
log_info "Installation Docker Engine..."
apt install -y --no-install-recommends \
docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin \
|| log_warn "Erreur Docker, continuation..."
# Dépôt Steam officiel (via wrapper L2 v8.8.24)
ajouter_depot_apt "steam" \
"http://repo.steampowered.com/steam/archive/stable/steam.gpg" \
"deb [arch=amd64 signed-by=/usr/share/keyrings/steam.gpg] http://repo.steampowered.com/steam/ stable steam" \
"/usr/share/keyrings/steam.gpg"
apt install -y steam || log_warn "Erreur installation Steam, continuation..."
# Dépôt VirtualBox Oracle officiel (v8.9.19)
# Remplace le paquet virtualbox des dépôts Ubuntu : son virtualbox-dkms suit
# mal les nouveaux noyaux HWE (7.0.16 incompatible noyau 7.0 → postinst noyau
# en échec, initramfs jamais généré, kernel panic au boot — incident 2026-07-18).
# Le paquet Oracle embarque l'ISO Additions Invité (virtualbox-guest-additions-iso inutile)
# et recompile ses modules via le service vboxdrv à chaque nouveau noyau (pas de DKMS).
case "$UBUNTU_CODENAME" in
resolute|noble|jammy)
VBOX_CODENAME="$UBUNTU_CODENAME"
;;
*)
# Codename pas encore publié par Oracle → fallback noble (24.04 LTS)
VBOX_CODENAME="noble"
log_info "Codename '$UBUNTU_CODENAME' non supporté par le dépôt VirtualBox — fallback sur '$VBOX_CODENAME'"
;;
esac
ajouter_depot_apt "virtualbox" \
"https://www.virtualbox.org/download/oracle_vbox_2016.asc" \
"deb [arch=amd64 signed-by=/usr/share/keyrings/oracle-virtualbox-2016.gpg] https://download.virtualbox.org/virtualbox/debian $VBOX_CODENAME contrib" \
"/usr/share/keyrings/oracle-virtualbox-2016.gpg"
apt install -y virtualbox-7.2 || log_warn "Erreur installation VirtualBox 7.2, continuation..."
# Groupe requis par le paquet Oracle pour l'USB — prise d'effet à la reconnexion
usermod -aG vboxusers "$UTILISATEUR" || log_warn "Ajout de $UTILISATEUR au groupe vboxusers échoué"
# Paquets principaux — TABLEAU (robuste au word-splitting et à IFS=$'\n\t')
# Note 26.04 : musescore3 absent des dépôts officiels (installé via Flatpak à la section 10)
# chromium-browser (deb) supprimé → snap (section 2, ci-dessous)
# gnome-screensaver supprimé depuis Ubuntu 24.10 — verrouillage via loginctl lock-session
# tintii (plugin GIMP obsolète) et xsane (frontend X11) retirés
# libfuse2 → libfuse2t64 (transition time_t 64 bits, 24.04+)
# nagios-plugins-* → monitoring-plugins-* (paquets nagios-plugins transitionnels supprimés)
# qemu-kvm → qemu-system-x86 ; p7zip-rar, pinta et
# gnome-shell-extension-dashtodock absents des dépôts 26.04 — retirés
# (dash-to-dock : installer via Extension Manager) (v8.9.1, test réel)
# virtualbox + virtualbox-guest-additions-iso → paquet Oracle
# virtualbox-7.2 (bloc dépôt ci-dessus, v8.9.19)
log_info "Installation paquets principaux..."
# Préconfiguration debconf de postfix (v8.9.1) : sans preseed, l'écran
# interactif mailer_type bloquait l'installation groupée ci-dessous
# (stdin indisponible via « | tee ») et laissait dpkg dans un état interrompu
# → cascade d'échecs sur tous les paquets suivants (67 manquants au test réel)
echo "postfix postfix/main_mailer_type select Internet Site" | debconf-set-selections
echo "postfix postfix/mailname string $(hostname)" | debconf-set-selections
PAQUETS_PRINCIPAUX=(
torbrowser-launcher filezilla transmission plocate
postfix mailutils
gnome-software gnome-software-plugin-flatpak audacity calibre cdrdao dia gimp
gnome-tweaks guake gparted hplip imagemagick inkscape keepassxc manpages-fr
nautilus-image-converter net-tools okular xournalpp 7zip rar pdfsam
printer-driver-cups-pdf sharutils synaptic thunderbird timidity evince libreoffice-gnome
ubuntu-restricted-extras unace unrar uudeview vlc whois
gnome-shell-extensions cifs-utils openssh-server clamav clamav-daemon clamav-freshclam
vim htop glances iotop unattended-upgrades flatpak rsync gdebi libfuse2t64 ansible
qemu-system-x86 libvirt-daemon-system gedit pwgen tumbler ffmpegthumbnailer
gir1.2-gdkpixbuf-2.0 libvirt-clients bridge-utils virt-manager shellcheck
gnome-shell-extension-gsconnect hydrapaper
gnome-shell-extension-manager ncal figlet mpv
default-jdk scanmem git wakeonlan nodejs npm tree ncdu jq
network-manager-openvpn network-manager-openvpn-gnome pv snmpd
python3 python3-pip python3-venv fortune-mod cowsay lolcat nagios-nrpe-server nagios-nrpe-plugin
monitoring-plugins-basic monitoring-plugins-standard geany smartmontools
)
# Installation groupée (rapide) ; un seul nom invalide ferait avorter toute la
# transaction, d'où la reprise individuelle des paquets restés manquants.
apt install -y "${PAQUETS_PRINCIPAUX[@]}" \
|| log_warn "Installation groupée incomplète — reprise paquet par paquet"
# Vérifie qu'un paquet est bien installé
paquet_est_installe() {
dpkg-query -W -f='${Status}' "$1" 2>/dev/null | grep -q '^install ok installed'
}
# Compteur OK/KO + 2e passe individuelle (U3 v8.8.24) — diagnostic précis sans casser le flux
# Sortie apt VISIBLE (v8.9.1) : la redirection >/dev/null masquait la progression
# et rendait tout blocage/échec indiagnosticable en direct (test réel)
APT_OK=0; APT_KO=0; APT_KO_LIST=""
for pkg in "${PAQUETS_PRINCIPAUX[@]}"; do
paquet_est_installe "$pkg" || apt install -y "$pkg" || true
if paquet_est_installe "$pkg"; then
APT_OK=$((APT_OK + 1))
else
APT_KO=$((APT_KO + 1))
APT_KO_LIST+=" $pkg"
fi
done
APT_TOTAL=$((APT_OK + APT_KO))
if [ "$APT_KO" -eq 0 ]; then
log_ok "Paquets principaux installés : $APT_OK/$APT_TOTAL (tous présents)"
else
log_warn "Paquets principaux : $APT_OK/$APT_TOTAL ok — $APT_KO manquant(s) :$APT_KO_LIST"
fi
# Activation SSH
log_info "Activation SSH..."
systemctl enable --now ssh 2>/dev/null \
|| systemctl enable --now openssh-server 2>/dev/null \
|| log_warn "SSH : aucun service trouvé"
# VSCodium
log_info "Installation VSCodium..."
installer_snap codium --classic || log_warn "VSCodium snap échoué (2 tentatives)"
# Signal
log_info "Installation Signal..."
installer_snap signal-desktop || log_warn "Signal snap échoué (2 tentatives)"
# Chromium — snap (le paquet deb chromium-browser a été supprimé des dépôts Ubuntu 26.04)
log_info "Installation Chromium (snap)..."
installer_snap chromium || log_warn "Erreur installation Chromium snap (2 tentatives), continuation..."
log_ok "Chromium installé (snap)"
# Google Chrome — wget et apt chaînés : si wget échoue, apt n'est pas appelé
log_info "Installation Google Chrome..."
wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb -O /tmp/google-chrome.deb \
&& apt install -y /tmp/google-chrome.deb \
|| log_warn "Erreur installation Chrome (wget ou apt), continuation..."
rm -f /tmp/google-chrome.deb
rm -f /etc/apt/sources.list.d/google-chrome.list /etc/apt/sources.list.d/google.list
# Patch cron Google Chrome pour forcer Architectures: amd64 et éviter le warning i386
# Idempotent (N1 v8.8.34) : le cron n'est ré-exécuté que si le patch vient d'être appliqué
if [[ -f /etc/cron.daily/google-chrome ]]; then
if ! grep -q "Architectures: amd64" /etc/cron.daily/google-chrome; then
sed -i 's/Components: main$/Components: main\nArchitectures: amd64/' /etc/cron.daily/google-chrome
log_info "Patch cron Google Chrome appliqué (Architectures: amd64)"
/etc/cron.daily/google-chrome
else
log_info "Patch cron Google Chrome déjà appliqué"
fi
fi
apt_update_silent
# Brave Browser (via wrapper L2 v8.8.24)
ajouter_depot_apt "brave-browser-release" \
"https://brave-browser-apt-release.s3.brave.com/brave-browser-archive-keyring.gpg" \
"deb [arch=amd64 signed-by=/usr/share/keyrings/brave-browser-archive-keyring.gpg] https://brave-browser-apt-release.s3.brave.com/ stable main" \
"/usr/share/keyrings/brave-browser-archive-keyring.gpg"
apt install -y brave-browser || log_warn "Erreur installation Brave, continuation..."
log_ok "Brave Browser installé"
# Opera Browser (via wrapper L2 v8.8.24)
ajouter_depot_apt "opera-stable" \
"https://deb.opera.com/archive.key" \
"deb [arch=amd64 signed-by=/usr/share/keyrings/opera-archive-keyring.gpg] https://deb.opera.com/opera-stable/ stable non-free" \
"/usr/share/keyrings/opera-archive-keyring.gpg"
apt install -y opera-stable || log_warn "Erreur installation Opera, continuation..."
log_ok "Opera Browser installé"
# yt-dlp — binaire officiel GitHub (version apt toujours trop ancienne)
log_info "Installation yt-dlp (binaire GitHub)..."
apt remove -y yt-dlp 2>/dev/null || true # Supprimer l'ancienne version apt si présente
rm -f /usr/bin/yt-dlp # Nettoyer l'éventuel lien apt résiduel
wget -q https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp -O /usr/local/bin/yt-dlp \
&& chmod a+rx /usr/local/bin/yt-dlp \
&& hash -r \
&& log_ok "yt-dlp installé ($(yt-dlp --version 2>/dev/null || echo 'version inconnue'))" \
|| log_warn "yt-dlp : installation échouée — installer manuellement"
log_ok "Paquets et dépôts installés (ou au maximum possible)"
}
################################################################################
# 2a. OUTILS CLAUDE CODE
################################################################################
section_2a() {
# --- Outils Claude Code ---
# npm global sans sudo
sudo -u "$UTILISATEUR" mkdir -p "/home/${UTILISATEUR}/.npm-global"
sudo -u "$UTILISATEUR" npm config set prefix "/home/${UTILISATEUR}/.npm-global"
grep -qxF 'export PATH="$HOME/.npm-global/bin:$PATH"' "/home/${UTILISATEUR}/.bashrc" \
|| echo 'export PATH="$HOME/.npm-global/bin:$PATH"' >> "/home/${UTILISATEUR}/.bashrc"
# NPM_PATH (binaires npm-global via sudo -u env) : défini globalement après
# le chargement du .conf — voir le commentaire à sa définition (v8.9.0)
# ccstatusline (status-bar tokens)
sudo -u "$UTILISATEUR" env PATH="$NPM_PATH" \
npm install -g ccstatusline \
&& log_ok "ccstatusline installé" \
|| log_warn "ccstatusline : installation échouée (lancer manuellement après connexion)"
# Injection statusLine dans ~/.claude/settings.json (non-interactif)
sudo -u "$UTILISATEUR" python3 - << 'PYEOF'
import json, os
settings_path = os.path.expanduser("~/.claude/settings.json")
os.makedirs(os.path.dirname(settings_path), exist_ok=True)
try:
with open(settings_path, "r") as f:
settings = json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
settings = {}
settings["statusLine"] = {
"type": "command",
"command": "npx -y ccstatusline@latest",
"padding": 0
}
with open(settings_path, "w") as f:
json.dump(settings, f, indent=2)
print("✓ statusLine injecté dans ~/.claude/settings.json")
PYEOF
log_ok "Outils Claude Code configurés"
}
################################################################################
# 2b. CONFIGURATION FIREFOX SNAP — Restauration du profil utilisateur
# Note : Firefox snap stocke profiles.ini dans ~/snap/firefox/common/.mozilla/firefox/
# Le fichier est réécrit proprement pour garantir l'idempotence (pas de duplication).
################################################################################
section_2b() {
SNAP_PROFILES_DIR="/home/$UTILISATEUR/snap/firefox/common/.mozilla/firefox"
PROFILES_INI="$SNAP_PROFILES_DIR/profiles.ini"
PROFIL_CIBLE="b4r78nv8.default"
if [ -f "$PROFILES_INI" ]; then
# Idempotence : ne modifier que si le profil cible n'est pas déjà actif Default=1
if grep -qF "Path=$PROFIL_CIBLE" "$PROFILES_INI" \
&& awk -v p="$PROFIL_CIBLE" '
/^\[/ {section=$0}
/^Path=/ {path=$0}
/^Default=1/ && path == "Path="p {found=1; exit}
END {exit !found}
' "$PROFILES_INI"; then
log_ok "Firefox Snap : profil '$PROFIL_CIBLE' déjà actif — aucune modification"
else
log_info "profiles.ini Firefox Snap détecté — réécriture pour activer '$PROFIL_CIBLE'"
cp "$PROFILES_INI" "$PROFILES_INI.backup.${TS}"
cat <<EOF > "$PROFILES_INI"
[General]
StartWithLastProfile=1
Version=2
[Profile0]
Name=default
IsRelative=1
Path=$PROFIL_CIBLE
Default=1
EOF
chown "$UTILISATEUR":"$UTILISATEUR" "$PROFILES_INI"
log_ok "Firefox Snap : profil '$PROFIL_CIBLE' défini comme profil par défaut"
log_info "Données, extensions et historique Firefox restaurés au prochain lancement"
fi
else
log_warn "profiles.ini Firefox Snap non trouvé — Firefox n'a pas encore été lancé ou profil absent"
log_warn "Si Firefox a déjà été utilisé, vérifier : $SNAP_PROFILES_DIR"
fi
}
################################################################################
# 2c. CONFIGURATION PLOCATE (updatedb.conf)
################################################################################
section_2c() {
cat <<'EOF' > /etc/updatedb.conf
PRUNE_BIND_MOUNTS="no"
PRUNEPATHS="/tmp /var/spool /media /run/media /var/lib/os-prober /var/lib/ceph /home/.ecryptfs /var/lib/schroot"
PRUNEFS="NFS afs binfmt_misc ceph cgroup cgroup2 coda configfs curlftpfs debugfs devfs devpts devtmpfs ecryptfs ftpfs fuse.ceph fuse.cryfs fuse.encfs fuse.glusterfs fuse.gocryptfs fuse.gvfsd-fuse fuse.mfs fuse.rclone fuse.rozofs fuse.sshfs fusectl fusesmb hugetlbfs iso9660 lustre lustre_lite mfs mqueue ncpfs nfs nfs4 ocfs ocfs2 proc pstore rpc_pipefs securityfs shfs smbfs sysfs tmpfs tracefs udev udf usbfs"
EOF
updatedb &
log_ok "/etc/updatedb.conf configuré — indexation lancée en arrière-plan (/run/media ajouté)"
log_info "Si le reboot final interrompt l'indexation, le cron quotidien plocate la refera"
}
################################################################################
# 2d. MONITORING — NRPE, SNMP, CMA CENTREON
################################################################################
section_2d() {
# ─── PARTIE A — NRPE ────────────────────────────────────────────────────────
log_info "Configuration NRPE (nagios-nrpe-server)..."
tee /etc/nagios/nrpe.cfg > /dev/null << 'EOF'
log_facility=daemon
debug=0
pid_file=/run/nagios/nrpe.pid
server_port=5666
nrpe_user=nagios
nrpe_group=nagios
allowed_hosts=192.168.1.50,192.168.122.10,127.0.0.1
dont_blame_nrpe=1
allow_bash_command_substitution=0
command_timeout=60
connection_timeout=300
disable_syslog=0
# Commandes NRPE
command[check_users]=/usr/lib/nagios/plugins/check_users -w 5 -c 10
command[check_load]=/usr/lib/nagios/plugins/check_load -w 7,6,5 -c 9,8,7
command[check_disk_root]=/usr/lib/nagios/plugins/check_disk -w 20% -c 10% -p /
command[check_disk_home]=/usr/lib/nagios/plugins/check_disk -w 20% -c 10% -p /home
command[check_zombie_procs]=/usr/lib/nagios/plugins/check_procs -w 5 -c 10 -s Z
command[check_total_procs]=/usr/lib/nagios/plugins/check_procs -w 150 -c 200
command[check_procs]=/usr/lib/nagios/plugins/check_procs -w 600 -c 1000
command[check_ssh]=/usr/lib/nagios/plugins/check_ssh -4 -p 22 127.0.0.1
command[check_swap]=/usr/lib/nagios/plugins/check_swap -w 90 -c 10
command[check_mem.sh]=/usr/lib/nagios/plugins/check_mem.sh 80 90
command[check_mem.pl]=/usr/lib/nagios/plugins/check_mem.pl -u -w 80 -c 90
command[check_service]=/usr/lib/nagios/plugins/check_service.sh $ARG1$
command[check_apt]=/usr/lib/nagios/plugins/check_apt
command[check_entropie]=/usr/lib/nagios/plugins/check_entropie.sh
command[check_erreur_dmesg]=/usr/lib/nagios/plugins/check_erreurs_dmesg.sh
command[check_interface_reseau]=/usr/lib/nagios/plugins/check_erreurs_interface.sh -i $ARG1$
command[check_clef_ssh]=/usr/lib/nagios/plugins/check_cles_ssh.sh -u $ARG1$
command[check_sudoers]=/usr/lib/nagios/plugins/check_sudoers.sh
command[check_cron]=/usr/lib/nagios/plugins/check_cron.sh -f $ARG1$
command[check_lvm_vg]=/usr/lib/nagios/plugins/check_lvm.sh -v $ARG1$ -z
command[check_disque_inode]=/usr/lib/nagios/plugins/check_disque_inodes.sh -m $ARG1$
command[check_disque_io]=/usr/lib/nagios/plugins/check_disque_io.sh -d $ARG1$
command[check_disque_smart]=/usr/lib/nagios/plugins/check_disque_smart.sh -d $ARG1$
command[check_grub]=/usr/lib/nagios/plugins/check_grub.sh
command[check_utilisateurs]=/usr/lib/nagios/plugins/check_utilisateurs.sh -u $ARG1$
command[check_nvidia]=/usr/lib/nagios/plugins/check_gpu_nvidia.sh
# Fichiers supplémentaires
include=/etc/nagios/nrpe_local.cfg
include_dir=/etc/nagios/nrpe.d/
# --- NRPE4 : connecteur Centreon (centreon-plugins via NRPE) ---
command[check_centreon_plugins]=/usr/lib/centreon/plugins/centreon_linux_local.pl --plugin=$ARG1$ --mode=$ARG2$ $ARG3$
EOF
log_ok "nrpe.cfg écrit"
log_info "Création des sondes personnalisées Nagios/NRPE..."
tee /usr/lib/nagios/plugins/check_mem.sh > /dev/null << 'EOF'
#!/bin/bash
warning=$1
critical=$2
used=$(free | awk '/Mem:/ {print $3}')
total=$(free | awk '/Mem:/ {print $2}')
pourcentage=$((used * 100 / total))
if [ "$pourcentage" -ge "$critical" ]; then
echo "CRITICAL - RAM utilisée: ${pourcentage}% | ram=${pourcentage}%;${warning};${critical};0;100"
exit 2
elif [ "$pourcentage" -ge "$warning" ]; then
echo "WARNING - RAM utilisée: ${pourcentage}% | ram=${pourcentage}%;${warning};${critical};0;100"
exit 1
else
echo "OK - RAM utilisée: ${pourcentage}% | ram=${pourcentage}%;${warning};${critical};0;100"
exit 0
fi
EOF
chmod 755 /usr/lib/nagios/plugins/check_mem.sh
cat > /usr/lib/nagios/plugins/check_service.sh << 'EOF'
#!/bin/bash
# Sonde Centreon - vérification d'un service systemd
# Usage: check_service.sh <nom_du_service>
if [ -z "${1:-}" ]; then
echo "UNKNOWN ==> Aucun service spécifié"
exit 3
fi
SERVICE="$1"
# systemctl cat retourne 1 si l'unit n'existe pas (test fiable, sans faux positif grep)
if ! systemctl cat "${SERVICE}.service" &>/dev/null; then
echo "UNKNOWN ==> service $SERVICE introuvable"
exit 3
fi
STATE=$(systemctl is-active "$SERVICE")
case "$STATE" in
active) echo "OK ==> service $SERVICE UP"; exit 0 ;;
activating) echo "WARNING ==> service $SERVICE en cours de démarrage"; exit 1 ;;
*) echo "CRITICAL ==> service $SERVICE DOWN (état: $STATE)"; exit 2 ;;
esac
EOF
chmod 755 /usr/lib/nagios/plugins/check_service.sh
tee /usr/lib/nagios/plugins/check_mem.pl > /dev/null << 'EOF'
#!/usr/bin/perl -w
use strict;
use Getopt::Std;
my %opts;
getopts('uw:c:', \%opts);
my $warn = $opts{w} || 80;
my $crit = $opts{c} || 90;
open(my $fh, '<', '/proc/meminfo') or die "Impossible de lire /proc/meminfo: $!";
my %mem;
while (<$fh>) { $mem{$1} = $2 if /^(\w+):\s+(\d+)/; }
close($fh);
my $total = $mem{MemTotal};
my $available = $mem{MemAvailable} || ($mem{MemFree} + ($mem{Buffers}||0) + ($mem{Cached}||0));
my $used = $opts{u} ? ($total - $available) : ($total - $mem{MemFree});
my $pct = int($used * 100 / $total);
my $used_mb = int($used / 1024);
my $total_mb = int($total / 1024);
my $perf = "ram=${pct}%;${warn};${crit};0;100";
if ($pct >= $crit) { print "CRITICAL - RAM: ${pct}% (${used_mb}Mo/${total_mb}Mo) | $perf\n"; exit 2; }
elsif ($pct >= $warn) { print "WARNING - RAM: ${pct}% (${used_mb}Mo/${total_mb}Mo) | $perf\n"; exit 1; }
else { print "OK - RAM: ${pct}% (${used_mb}Mo/${total_mb}Mo) | $perf\n"; exit 0; }
EOF
chmod 755 /usr/lib/nagios/plugins/check_mem.pl
log_ok "Sondes check_mem.sh, check_mem.pl, check_service.sh créées"
mkdir -p /etc/default
echo "NRPE_OPTS=''" > /etc/default/nagios-nrpe-server
systemctl daemon-reload \
|| log_warn "daemon-reload échoué (section nrpe)"
systemctl restart nagios-nrpe-server \
|| log_warn "nagios-nrpe-server : redémarrage échoué"
log_ok "Warning NRPE_OPTS retiré"
systemctl enable --now nagios-nrpe-server >> "$LOG_FILE" 2>&1 \
&& log_ok "nagios-nrpe-server activé et démarré (port 5666)" \
|| log_warn "nagios-nrpe-server : activation échouée"
# ─── PARTIE B — SNMP ────────────────────────────────────────────────────────
log_info "Configuration snmpd..."
cat > /etc/snmp/snmpd.conf << 'EOF'
sysLocation Home Lab
sysContact utilisateur
sysServices 72
master agentx
agentaddress udp:161
view systemonly included .1.3.6.1.2.1.1
view systemonly included .1.3.6.1.2.1.25.1
rocommunity public 192.168.122.0/24
rocommunity public default -V systemonly
rocommunity6 public default -V systemonly
rouser authPrivUser authpriv -V systemonly
disk / 10%
disk /home 10%
disk /boot 10%
includeDir /etc/snmp/snmpd.conf.d
EOF
systemctl enable snmpd >> "$LOG_FILE" 2>&1 || log_warn "snmpd : activation échouée"
systemctl restart snmpd >> "$LOG_FILE" 2>&1 || log_warn "snmpd : redémarrage échoué"
log_ok "snmpd configuré"
# ─── PARTIE C — CMA (Centreon Monitoring Agent) — mode TLS ──────────────────
log_info "Installation Centreon Monitoring Agent (CMA) en mode TLS..."
apt install -y lsb-release ca-certificates apt-transport-https \
software-properties-common curl gpg gnupg2 wget \
|| log_warn "CMA : certaines dépendances ont échoué"
log_info "Ajout dépôt Centreon (bookworm forcé — seul codename supporté)..."
rm -f /etc/apt/sources.list.d/centreon.list /etc/apt/sources.list.d/centreon-plugins.list
# Nettoyage de l'ancienne clé à confiance globale (méthode dépréciée trusted.gpg.d)
rm -f /etc/apt/trusted.gpg.d/centreon.gpg
# Clé GPG dans un keyring dédié, référencé par signed-by= dans les dépôts
# (évite la confiance globale de trusted.gpg.d, cohérent avec Docker/Steam/Brave)
wget -q -O- https://apt-key.centreon.com \
| gpg --dearmor \
| tee /usr/share/keyrings/centreon-archive-keyring.gpg > /dev/null \
&& log_ok "Clé GPG Centreon ajoutée (/usr/share/keyrings/centreon-archive-keyring.gpg)" \
|| log_warn "CMA : ajout clé GPG Centreon échoué"
chmod a+r /usr/share/keyrings/centreon-archive-keyring.gpg
# Dépôt 25.10 (v8.9.4) : l'agent DOIT être aligné sur la version du serveur —
# un agent 24.10 sur serveur 25.10 se connecte mais l'auto-création d'hôte
# (create_host_auto) ne fonctionne pas (métadonnées host_template/ips absentes
# du protocole 24.10, vérifié au test réel). Nouveau format d'URL en 25.10 :
# suite « bookworm-25.10-stable » sous apt-standard/ (l'ancien
# apt-standard-25.10-stable renvoie 404).
tee /etc/apt/sources.list.d/centreon.list > /dev/null << 'EOF'
deb [signed-by=/usr/share/keyrings/centreon-archive-keyring.gpg] https://packages.centreon.com/apt-standard/ bookworm-25.10-stable main
EOF
tee /etc/apt/sources.list.d/centreon-plugins.list > /dev/null << 'EOF'
deb [signed-by=/usr/share/keyrings/centreon-archive-keyring.gpg] https://packages.centreon.com/apt-plugins-stable/ bookworm main
EOF
apt_update_silent || log_warn "CMA : apt update après dépôt Centreon échoué"
apt install -y centreon-monitoring-agent centreon-plugin-operatingsystems-linux-local \
&& log_ok "centreon-monitoring-agent installé" \
|| log_warn "CMA : installation échouée — vérifier le dépôt Centreon"
# sudo-rs (Ubuntu 25.10+) ne connaît pas « requiretty » : le fichier sudoers
# livré par centreon-plugin-operatingsystems-linux-sudoers (dépendance des
# plugins) déclenche un warning à CHAQUE sudo — non bloquant mais bruyant.
# La ligne est inutile (requiretty n'est plus un défaut depuis sudo 1.8.12)
# → commentée. Pas un conffile : une mise à jour du paquet peut la restaurer,
# d'où ce sed idempotent (v8.9.7).
SUDOERS_CENTREON="/etc/sudoers.d/sudoersCentreonLinuxPlugins"
if [[ -f "$SUDOERS_CENTREON" ]] && grep -q '^Defaults:.*!requiretty' "$SUDOERS_CENTREON"; then
sed -i 's|^Defaults:\(.*\)!requiretty|# requiretty inconnu de sudo-rs — commenté par postinstall\n# Defaults:\1!requiretty|' "$SUDOERS_CENTREON"
visudo -cf "$SUDOERS_CENTREON" >/dev/null 2>&1 \
&& log_ok "sudoersCentreonLinuxPlugins : ligne requiretty commentée (compat sudo-rs)" \
|| log_warn "sudoersCentreonLinuxPlugins : visudo invalide après modification — à vérifier"
fi
mkdir -p /etc/centreon-monitoring-agent
# Certificat TLS public du serveur Centreon (mode encryption: full)
# Copie littérale de /etc/centreon-monitoring-agent/centreon-agent.crt (prod)
cat > /etc/centreon-monitoring-agent/centreon-agent.crt << 'EOF'
-----BEGIN CERTIFICATE-----
MIIDIjCCAgqgAwIBAgIUFnsEM0CuM8J2OvpkRg36ZzgOqUIwDQYJKoZIhvcNAQEL
BQAwEzERMA8GA1UEAwwIY2VudHJlb24wHhcNMjYwNTMxMDk0NDIxWhcNMjcwNTMx
MDk0NDIxWjATMREwDwYDVQQDDAhjZW50cmVvbjCCASIwDQYJKoZIhvcNAQEBBQAD
ggEPADCCAQoCggEBALznDxbTRMqWy8suODUr1o+TOzNn6f8/BHvW3Q2zKWIZg4V8
9df3aqO3jt0bUsnPhSQ4UjnCFxxLeM9HYPW5+oEwxre90rqR6QliTT/qsq4WHCPT
1EiDuwfclIB/obAPcYBM9aji4XaCBJyAhqsycmrvf1SSAYP7gUszD8ykieJVSqNk
U3MBSa4Azhyg44ObeGd2qCmtQQ3+Dp2jOmVFRKDpp6dSe4z8juizqoVYb2wMC3Z8
8OOiYwNUF3Bwauf0wCQm6VBDWA/0WW9Hs5y/iZWi8m0AH4lGsFLPl6drXIoA8SQd
wlLoZWSKAUo3RKwXB+wBIxRq7PxHyVliKPcJn/MCAwEAAaNuMGwwHQYDVR0OBBYE
FOBHjBpRhhGyZ6Z/Afj588QzTWt0MB8GA1UdIwQYMBaAFOBHjBpRhhGyZ6Z/Afj5
88QzTWt0MA8GA1UdEwEB/wQFMAMBAf8wGQYDVR0RBBIwEIIIY2VudHJlb26HBMCo
AS4wDQYJKoZIhvcNAQELBQADggEBAGni5tTfokCD+XcJK/HVB4A9D5V/Uvyel+Jg
88xOp3bnPNM4n82wlT7GtTtpr2yXM5LFBMxI8xNNIYtmmmJc/lghdIC/7gWZkkMG
wKBXyCNpjefSpw4TVfrEKoQZ5QndUmAfoUGd/uDmUXk9fgMbrzzrPaSmNluYtdF7
SUjnV61xOCdJSI3UHg9jZeaHeDqiKI+mRdeMCKXEgOqdcm+BR/nLN10woLynJyY4
zGm6Lk7A6Z7qLMusWvBW9zC9paV8JTme0XnXqAa/FS2lv0T9D0adEjmTPTclOlEt
l+MTXLrzbj1wBvpYZoINQAfJfk8gzMV7SPG+7F+pNZF7c1jsAHI=
-----END CERTIFICATE-----
EOF
chmod 644 /etc/centreon-monitoring-agent/centreon-agent.crt
log_ok "centreon-agent.crt installé (certificat TLS)"
# Le certificat CMA est référencé directement par centagent.json (clé ca_certificate) :
# inutile de l'ajouter au store système (cela ferait confiance à une CA "centreon"
# pour TOUT le TLS du système). On purge l'éventuel résidu d'une exécution antérieure.
if [ -f /usr/local/share/ca-certificates/centreon-cma.crt ]; then
rm -f /usr/local/share/ca-certificates/centreon-cma.crt
update-ca-certificates --fresh >> "$LOG_FILE" 2>&1 \
&& log_ok "Ancienne CA Centreon retirée du store système (confiance TLS réduite)" \
|| log_warn "update-ca-certificates --fresh (purge) échoué"
fi
# Grep ancré : matche uniquement une entrée 'centreon' isolée (mot complet)
if ! grep -qE '^[^#]\S*\s+centreon(\s|$)' /etc/hosts; then
echo "192.168.1.50 centreon" >> /etc/hosts
log_ok "/etc/hosts : entrée 'centreon' (192.168.1.50) ajoutée"
else
log_info "/etc/hosts : entrée 'centreon' déjà présente"
fi
# umask 077 : centagent.json contient le token JWT (secret) — création en 600 puis 640
(
umask 077
cat > /etc/centreon-monitoring-agent/centagent.json << EOF
{
"log_level": "debug",
"endpoint": "${CMA_ENDPOINT}",
"host": "${CMA_HOST}",
"log_type": "file",
"log_file": "/var/log/centreon-monitoring-agent/centagent.log",
"encryption": "full",
"ca_certificate": "/etc/centreon-monitoring-agent/centreon-agent.crt",
"token": "${CMA_TOKEN}",
"plugins_directory": "/usr/lib/centreon/plugins/src",
"custom_check_file": "/etc/centreon-monitoring-agent/custom_checks.cfg"
}
EOF
)
# Groupe centreon-monitoring-agent (v8.9.3) : le service tourne sous cet
# utilisateur (User= du .service) — avec root:root 640 le fichier était
# ILLISIBLE pour lui → centagent en échec « Permission denied » au test réel
chown root:centreon-monitoring-agent /etc/centreon-monitoring-agent/centagent.json \
|| log_warn "centagent.json : chown root:centreon-monitoring-agent échoué (CMA non installé ?)"
chmod 640 /etc/centreon-monitoring-agent/centagent.json
log_ok "centagent.json configuré (token depuis .conf)"
tee /etc/centreon-monitoring-agent/custom_checks.cfg > /dev/null << 'EOF'
[custom_checks]
test_ok = /usr/local/bin/cma_test_ok.sh
test_ok_2 = /usr/local/bin/cma_test_ok.sh
test_ok_3 = /usr/local/bin/cma_test_ok.sh
test_ok_4 = /usr/local/bin/cma_test_ok.sh
test_ok_5 = /usr/local/bin/cma_test_ok.sh
check_service = /usr/lib/nagios/plugins/check_service.sh $ARG1$
check_service_ssh = /usr/lib/nagios/plugins/check_service.sh ssh
EOF
log_ok "custom_checks.cfg configuré"
tee /usr/local/bin/cma_test_ok.sh > /dev/null << 'EOF'
#!/bin/bash
echo "OK - CMA custom script works"
exit 0
EOF
chmod +x /usr/local/bin/cma_test_ok.sh
log_ok "cma_test_ok.sh créé"
systemctl enable --now centagent \
&& log_ok "CMA centagent activé et démarré" \
|| log_warn "centagent : activation échouée — vérifier /var/log/centreon-monitoring-agent/centagent.log"
systemctl is-active --quiet centagent \
&& log_ok "CMA centagent actif" \
|| log_warn "CMA centagent inactif — vérifier que le serveur Centreon est joignable sur centreon:4317"
log_ok "Section monitoring terminée — NRPE (port 5666) + SNMP + CMA configurés"
}
################################################################################
# 2e. CONFIGURATION SUPERVISION CENTREON — GROUPE, PERMISSIONS, SUDOERS
################################################################################
section_2e() {
# ─── Création du groupe monitoring ──────────────────────────────────────────
if ! getent group monitoring > /dev/null 2>&1; then
groupadd monitoring \
&& log_ok "Groupe monitoring créé" \
|| log_warn "Groupe monitoring : groupadd échoué"
else
log_info "Groupe monitoring déjà présent"
fi
# ─── Ajout nagios au groupe monitoring ──────────────────────────────────────
# tr + grep -qx (v8.9.10) : avec grep -qw, le tiret est une frontière de mot
# → « monitoring » matchait DANS « centreon-monitoring-agent » (groupe ajouté
# par le postinst CMA) : faux positif, usermod jamais exécuté, sudoers
# %monitoring inopérant au test réel (« nagios is not allowed to run sudo »).
if id nagios > /dev/null 2>&1; then
if id -nG nagios | tr ' ' '\n' | grep -qx monitoring; then
log_info "nagios déjà dans le groupe monitoring"
else
usermod -aG monitoring nagios \
&& log_ok "nagios ajouté au groupe monitoring" \
|| log_warn "nagios : usermod -aG monitoring échoué"
fi
else
log_warn "Utilisateur nagios absent — NRPE non installé ?"
fi
# ─── Ajout centreon-monitoring-agent au groupe monitoring ───────────────────
if id centreon-monitoring-agent > /dev/null 2>&1; then
# tr + grep -qx : même faux positif que nagios (le groupe primaire
# « centreon-monitoring-agent » contient le mot « monitoring »)
if id -nG centreon-monitoring-agent | tr ' ' '\n' | grep -qx monitoring; then
log_info "centreon-monitoring-agent déjà dans le groupe monitoring"
else
usermod -aG monitoring centreon-monitoring-agent \
&& log_ok "centreon-monitoring-agent ajouté au groupe monitoring" \
|| log_warn "centreon-monitoring-agent : usermod -aG monitoring échoué"
fi
else
log_warn "Utilisateur centreon-monitoring-agent absent — CMA non installé ?"
fi
# ─── Répertoire de cache Centreon ───────────────────────────────────────────
mkdir -p /var/cache/centreon
chown nagios\:monitoring /var/cache/centreon \
|| log_warn "/var/cache/centreon : chown nagios:monitoring échoué"
chmod 770 /var/cache/centreon
log_ok "/var/cache/centreon créé (nagios:monitoring, 770)"
# ─── Fichier sudoers monitoring-plugins-custom ──────────────────────────────
MONITORING_SUDOERS="/etc/sudoers.d/monitoring-plugins-custom"
log_info "Création ${MONITORING_SUDOERS}..."
# Validation visudo AVANT installation (même pattern que la section 9) :
# un fichier malformé déposé directement dans /etc/sudoers.d/ casserait TOUT sudo.
MONITORING_TMP=$(mktemp)
cat > "${MONITORING_TMP}" << 'SUDOERS_EOF'
# Droits sudo pour les sondes Centreon custom - Romain J.
# Groupe monitoring : nagios + centreon-monitoring-agent
# Généré par postinstall.26.04_v2.sh
#
# Principe : chaque commande est restreinte aux arguments strictement nécessaires
# pour éviter une privilege escalation en cas de compromis d'un compte du groupe.
#
# Limite connue : dans sudoers, le joker * matche aussi les espaces — il
# autorise donc des arguments supplémentaires. Accepté : binaires en lecture
# seule, usage personnel ; un durcissement exigerait des chemins de
# périphériques explicites, non portables.
#
# Compatibilité sudo-rs (sudo par défaut d'Ubuntu 25.10+, v8.9.3) :
# - joker PARTIEL interdit dans les arguments (/dev/* rejeté, * seul accepté)
# → smartctl/mdadm passent de « /dev/* » à « * » (delta sécurité minime :
# l'ancien * matchait déjà les espaces, cf. limite ci-dessus)
# - « : » séparateur de liste → échappé dans « chown nagios\:monitoring »
# Syntaxes validées par visudo -cf (sudo-rs 0.2.13) sur Ubuntu 26.04 réel.
# check_grub.sh — seulement le hash de la config grub
%monitoring ALL=(root) NOPASSWD: /usr/bin/sha256sum /boot/grub/grub.cfg
%monitoring ALL=(root) NOPASSWD: /usr/bin/sha256sum /boot/grub2/grub.cfg
# check_lvm.sh — lecture seule (vgs/lvs sans options dangereuses)
%monitoring ALL=(root) NOPASSWD: /sbin/vgs "", /sbin/vgs -o *
%monitoring ALL=(root) NOPASSWD: /sbin/lvs "", /sbin/lvs -o *
%monitoring ALL=(root) NOPASSWD: /usr/sbin/vgs "", /usr/sbin/vgs -o *
%monitoring ALL=(root) NOPASSWD: /usr/sbin/lvs "", /usr/sbin/lvs -o *
# check_sudoers.sh — uniquement la validation
%monitoring ALL=(root) NOPASSWD: /usr/sbin/visudo -c
# check_disque_smart.sh — lecture seule (info/health/attributs)
%monitoring ALL=(root) NOPASSWD: /usr/sbin/smartctl -H *, /usr/sbin/smartctl -i *, /usr/sbin/smartctl -A *, /usr/sbin/smartctl --all *
# check_raid.sh — lecture seule (detail/examine)
%monitoring ALL=(root) NOPASSWD: /sbin/mdadm --detail *, /sbin/mdadm --examine *
%monitoring ALL=(root) NOPASSWD: /usr/sbin/mdadm --detail *, /usr/sbin/mdadm --examine *
# check_pare_feu.sh et check_statut_pare_feu.sh — lecture seule
%monitoring ALL=(root) NOPASSWD: /usr/sbin/nft list ruleset, /usr/sbin/nft -j list ruleset, /usr/sbin/nft list table *
%monitoring ALL=(root) NOPASSWD: /sbin/iptables -L, /sbin/iptables -L -n, /sbin/iptables -L -n -v
%monitoring ALL=(root) NOPASSWD: /sbin/iptables-save
# /var/cache/centreon (création si absent) — chemin fixe, pas de wildcard
# « \: » : sudo-rs traite « : » comme séparateur de liste s'il n'est pas échappé
%monitoring ALL=(root) NOPASSWD: /bin/mkdir -p /var/cache/centreon
%monitoring ALL=(root) NOPASSWD: /bin/chown nagios\:monitoring /var/cache/centreon
SUDOERS_EOF
# ─── Validation puis installation atomique ──────────────────────────────────
if visudo -cf "${MONITORING_TMP}" >/dev/null 2>&1; then
install -m 0440 -o root -g root "${MONITORING_TMP}" "${MONITORING_SUDOERS}"
log_ok "${MONITORING_SUDOERS} installé (chmod 440, syntaxe validée par visudo)"
else
log_error "Syntaxe sudoers invalide — ${MONITORING_SUDOERS} non installé (sudo préservé)"
fi
rm -f "${MONITORING_TMP}"
log_ok "Section supervision Centreon terminée"
}
################################################################################
# 2f. ALERTES SMART PAR MAIL (SMARTD)
################################################################################
section_2f() {
# smartd (smartmontools, installé en section 2) surveille la santé SMART des
# disques mais sa conf par défaut alerte root en local — on alerte GMAIL_USER
# via le relay postfix (section 22). smartd-runner est le mécanisme Debian
# standard de dispatch des alertes (mail + scripts de /etc/smartmontools/run.d).
if [ -f /etc/smartd.conf ]; then
cp /etc/smartd.conf "/etc/smartd.conf.backup.${TS}"
LIGNE_DEVICESCAN="DEVICESCAN -d removable -n standby -m ${GMAIL_USER} -M exec /usr/share/smartmontools/smartd-runner"
if grep -qxF "$LIGNE_DEVICESCAN" /etc/smartd.conf; then
log_ok "smartd : DEVICESCAN déjà configuré pour ${GMAIL_USER}"
elif grep -qE '^DEVICESCAN' /etc/smartd.conf; then
sed -i -E "s|^DEVICESCAN.*|${LIGNE_DEVICESCAN}|" /etc/smartd.conf
log_ok "smartd : DEVICESCAN remplacé (alertes mail → ${GMAIL_USER})"
else
echo "$LIGNE_DEVICESCAN" >> /etc/smartd.conf
log_ok "smartd : DEVICESCAN ajouté (alertes mail → ${GMAIL_USER})"
fi
systemctl restart smartmontools 2>/dev/null \
|| systemctl restart smartd 2>/dev/null \
|| log_warn "smartd : redémarrage échoué — vérifier systemctl status smartmontools"
log_ok "Alertes SMART par mail configurées (panne disque imminente → ${GMAIL_USER})"
else
log_warn "/etc/smartd.conf absent — smartmontools non installé ? Alertes SMART ignorées"
fi
}
################################################################################
# 2g. ATTACHEMENT UBUNTU PRO (ESM-INFRA, ESM-APPS, LIVEPATCH)
# Le client `pro` (ubuntu-advantage-tools) est préinstallé sur Ubuntu 26.04.
# L'attachement souscrit la machine au contrat Ubuntu Pro et débloque les
# correctifs de sécurité étendus (ESM) et le patch de noyau à chaud (Livepatch).
################################################################################
section_2g() {
# Section sautée proprement si le token n'est pas renseigné (rétro-compatibilité
# avec les .conf antérieurs + machines sans abonnement Pro) — variable non
# critique (absente volontairement des VARS_CRITIQUES)
if [[ -z "${PRO_TOKEN:-}" || "${PRO_TOKEN}" == *PURGE* ]]; then
log_warn "PRO_TOKEN absent/non renseigné dans le .conf — Ubuntu Pro non configuré"
return 0
fi
# Client `pro` préinstallé sur Ubuntu 26.04 — installation de rattrapage sinon
if ! command -v pro &>/dev/null; then
apt install -y ubuntu-advantage-tools \
|| { log_warn "ubuntu-advantage-tools : installation échouée — Ubuntu Pro ignoré"; return 0; }
fi
# Idempotence : ne pas réattacher une machine déjà attachée — `pro attach`
# échouerait avec « This machine is already attached »
if pro status --format json 2>/dev/null | grep -qE '"attached":[[:space:]]*true'; then
log_ok "Ubuntu Pro : machine déjà attachée — pas de réattachement"
else
if pro attach "$PRO_TOKEN" >/dev/null 2>&1; then
log_ok "Ubuntu Pro : machine attachée au contrat"
else
log_warn "Ubuntu Pro : attachement échoué — vérifier le token et la connectivité (pro attach ...)"
return 0
fi
fi
# Activation explicite des services (l'attach auto-active déjà les services par
# défaut : on confirme/complète, idempotent — un service déjà actif renvoie un
# code non nul inoffensif consigné en INFO)
for service in esm-infra esm-apps livepatch; do
if pro enable "$service" --assume-yes >/dev/null 2>&1; then
log_ok "Ubuntu Pro : $service activé"
else
log_info "Ubuntu Pro : $service déjà actif ou indisponible sur ce contrat/noyau"
fi
done
# Récapitulatif lisible dans le journal
pro status 2>/dev/null | grep -E 'esm-infra|esm-apps|livepatch' || true
}
################################################################################
# 3. CONFIGURATION DOCKER, KVM ET GROUPES
################################################################################
section_3() {
usermod -aG docker "$UTILISATEUR" || log_warn "Groupe docker absent"
usermod -aG libvirt "$UTILISATEUR" 2>/dev/null || log_warn "Groupe libvirt absent"
usermod -aG kvm "$UTILISATEUR" || log_warn "Groupe kvm absent"
systemctl enable --now docker || log_warn "Docker service non démarré"
systemctl enable --now libvirtd 2>/dev/null \
|| systemctl enable --now virtqemud 2>/dev/null \
|| log_warn "libvirtd/virtqemud : aucun service trouvé"
virsh net-autostart default 2>/dev/null || true
virsh net-start default 2>/dev/null || true
# ==============================
# Restauration VM Centreon KVM
# ==============================
VM_NAME="centreon_25.10_debian12"
VM_XML="/home/$UTILISATEUR/kvm/debian12/centreon_25.10_debian12.xml"
# --- Définition de la VM si elle n'existe pas déjà
# log_info si XML absent (v8.9.12) : seules les machines hébergeant la VM
# Centreon possèdent ce fichier dans leur /home — son absence sur les autres
# n'est pas une anomalie (elle polluait le rapport final au test réel)
if [ -f "$VM_XML" ]; then
virsh list --all | grep -q "$VM_NAME" || \
virsh define "$VM_XML" || log_warn "virsh define $VM_XML échoué"
virsh autostart "$VM_NAME" || log_warn "virsh autostart $VM_NAME échoué"
else
log_info "Pas de VM Centreon à restaurer sur cette machine ($VM_XML absent)"
fi
# --- Activation service libvirt-guests
systemctl enable libvirt-guests || log_warn "libvirt-guests : activation échouée"
systemctl start libvirt-guests || log_warn "libvirt-guests : démarrage échoué"
# --- Configuration arrêt propre des VM au shutdown host
LIBVIRT_CONF_FILE="/etc/default/libvirt-guests"
# Idempotent : supprimer toute ligne ON_SHUTDOWN (commentée ou non, quelle que soit la valeur)
# puis ajouter notre directive. Évite les doublons sur ré-exécution.
if [ -f "$LIBVIRT_CONF_FILE" ]; then
sed -i -E '/^[[:space:]]*#?[[:space:]]*ON_SHUTDOWN=/d' "$LIBVIRT_CONF_FILE"
echo 'ON_SHUTDOWN=shutdown' >> "$LIBVIRT_CONF_FILE"
log_ok "libvirt-guests : ON_SHUTDOWN=shutdown configuré"
else
log_warn "$LIBVIRT_CONF_FILE introuvable — libvirt-daemon-system non installé ?"
fi
log_ok "Docker, KVM et groupes configurés"
}
################################################################################
# 4. CONFIGURATION RÉSEAU : IP STATIQUE (DHCP FIGÉ) + DNS
################################################################################
section_4() {
# ---- Passage de l'IP DHCP en statique + DNS (v8.9.15, demande Romain) ----
# L'IP obtenue par DHCP à l'installation est figée : l'adresse ne changera
# plus au gré des baux (indispensable pour Centreon, SSH, sondes…).
# Deux chemins selon l'origine de la connexion :
# - « netplan-* » (installeur subiquity 26.04) : les nmcli modify NE
# PERSISTENT PAS (la connexion est régénérée depuis /etc/netplan/*.yaml,
# vérifié au test réel : rc=0 mais method reste auto) → fichier de
# surcharge 99-postinstall-ip-statique.yaml (IP + passerelle + DNS)
# + netplan apply. L'adresse étant identique au bail, pas de coupure.
# - connexion NM native : nmcli modify + connection up en place.
NETPLAN_STATIQUE="/etc/netplan/99-postinstall-ip-statique.yaml"
ip_cidr=$(nmcli -g IP4.ADDRESS connection show "$CONNEXION_NET" 2>/dev/null | head -1)
passerelle=$(nmcli -g IP4.GATEWAY connection show "$CONNEXION_NET" 2>/dev/null | head -1)
device_net=$(nmcli -t -f NAME,DEVICE connection show --active 2>/dev/null | grep -F "$CONNEXION_NET:" | cut -d: -f2 | head -1)
if [[ -f "$NETPLAN_STATIQUE" ]]; then
log_ok "IP statique déjà configurée ($NETPLAN_STATIQUE présent) : $ip_cidr"
elif [[ -z "$ip_cidr" || -z "$passerelle" || -z "$device_net" ]]; then
log_warn "IP statique : IP/passerelle/interface introuvables — connexion laissée en DHCP"
elif [[ "$CONNEXION_NET" == netplan-* ]]; then
cat > "$NETPLAN_STATIQUE" << YAML
# Généré par postinstall.26.04_v2.sh — IP du bail DHCP figée en statique
# (surcharge le dhcp4: true de l'installeur, fusion netplan par ordre lexical)
network:
version: 2
renderer: NetworkManager
ethernets:
$device_net:
dhcp4: false
dhcp6: false
addresses: [$ip_cidr]
routes:
- to: default
via: $passerelle
nameservers:
addresses: [8.8.8.8, 8.8.4.4]
YAML
chmod 600 "$NETPLAN_STATIQUE"
if netplan generate 2>/dev/null && netplan apply 2>/dev/null; then
log_ok "IP DHCP figée en statique via netplan : $ip_cidr (passerelle $passerelle, DNS 8.8.8.8/8.8.4.4)"
else
rm -f "$NETPLAN_STATIQUE"
log_warn "IP statique : netplan generate/apply échoué — fichier retiré, connexion laissée en DHCP"
fi
else
nmcli connection modify "$CONNEXION_NET" \
ipv4.method manual \
ipv4.addresses "$ip_cidr" \
ipv4.gateway "$passerelle" \
ipv4.dns "8.8.8.8 8.8.4.4" \
ipv4.ignore-auto-dns yes \
&& log_ok "IP DHCP figée en statique via nmcli : $ip_cidr (passerelle $passerelle)" \
|| log_warn "IP statique : nmcli modify échoué — connexion laissée en DHCP"
# Réactivation EN PLACE (up sans down) : applique IP + DNS sans coupure
# (l'adresse figée est identique à celle du bail DHCP)
nmcli connection up "$CONNEXION_NET" >/dev/null 2>&1 \
|| log_warn "nmcli : réactivation $CONNEXION_NET échouée"
fi
# Contrôle de résolution avant de poursuivre
if getent hosts google.com >/dev/null 2>&1; then
log_ok "DNS Google configurés et résolution opérationnelle"
else
log_warn "DNS configurés mais résolution KO — vérifier la connexion réseau"
fi
}
################################################################################
# 5. MONTAGE NAS VIA FSTAB
################################################################################
section_5() {
# umask 077 : credentials CIFS créés directement en 600 (pas de fenêtre 644)
(
umask 077
# printf %s : aucune interprétation de $, ` ou \ dans les valeurs
# (un heredoc non quoté corromprait/exécuterait un mot de passe contenant ces caractères)
{
printf 'username=%s\n' "$NAS_USER"
printf 'password=%s\n' "$NAS_PASSWORD"
} > "$NAS_CREDENTIALS"
)
chown root:root "$NAS_CREDENTIALS"
added_count=0
for dossier in "${NAS_PARTAGES[@]}"; do
MOUNT_LINE="//${NAS_IP}/${dossier} /mnt/nas/${dossier} cifs credentials=${NAS_CREDENTIALS},iocharset=utf8,vers=3.1.1,uid=${USER_UID},gid=${USER_GID},_netdev,nofail,x-systemd.automount,x-systemd.mount-timeout=5 0 0"
if ! grep -q "^//${NAS_IP}/${dossier}[[:space:]]" /etc/fstab; then
echo "$MOUNT_LINE" >> /etc/fstab
added_count=$((added_count + 1))
fi
done
if [ $added_count -gt 0 ]; then
log_ok "$added_count montages NAS ajoutés à fstab"
else
log_ok "Montages NAS déjà présents"
fi
log_info "Création des dossiers /mnt/nas..."
# Généré depuis NAS_PARTAGES (source unique de vérité — plus de liste en dur)
mkdir -p /mnt/nas
for dossier in "${NAS_PARTAGES[@]}"; do
mkdir -p "/mnt/nas/${dossier}"
done
# Note : sur CIFS, chmod/chown ne sont pas persistants (cf. options uid/gid de fstab)
chown "$UTILISATEUR":"$UTILISATEUR" /mnt/nas/* 2>/dev/null || true
log_ok "Dossiers /mnt/nas créés (${#NAS_PARTAGES[@]} partages, depuis NAS_PARTAGES)"
}
################################################################################
# 6. CONFIGURATION GRUB DUAL-BOOT
################################################################################
section_6() {
cp /etc/default/grub "/etc/default/grub.backup.${TS}"
# Patch idempotent : test explicite avant sed.
# sed -i retourne 0 même si aucune substitution n'est faite → le `|| echo >>`
# n'était jamais déclenché si la directive était absente/commentée (bug v8.8.22).
patch_grub() {
local key="$1" value="$2"
if grep -qE "^${key}=" /etc/default/grub; then
sed -i "s|^${key}=.*|${key}=${value}|" /etc/default/grub
else
echo "${key}=${value}" >> /etc/default/grub
fi
}
patch_grub GRUB_TIMEOUT_STYLE menu
patch_grub GRUB_TIMEOUT 10
patch_grub GRUB_DISABLE_OS_PROBER false
update-grub || log_warn "update-grub échoué — vérifier la configuration GRUB"
log_ok "GRUB configuré pour dual-boot"
}
################################################################################
# 7. ÉDITEUR PAR DÉFAUT
################################################################################
section_7() {
if update-alternatives --set editor /usr/bin/vim.basic 2>/dev/null; then
log_ok "Éditeur par défaut : vim.basic"
elif update-alternatives --set editor /usr/bin/vim 2>/dev/null; then
log_ok "Éditeur par défaut : vim"
else
VIM_ALT=$(update-alternatives --list editor 2>/dev/null | grep -i vim | head -1 || true)
if [ -n "$VIM_ALT" ]; then
update-alternatives --set editor "$VIM_ALT"
log_ok "Éditeur par défaut : $VIM_ALT"
else
log_warn "vim non trouvé dans les alternatives, configuration ignorée"
fi
fi
}
################################################################################
# 8. CONFIGURATION .BASHRC + ALIASES
# Stratégie : backup horodaté + écrasement intégral
# - Backup .bashrc.backup.<TS> et .bash_aliases.backup.<TS> si existants
# - Réécriture intégrale des fichiers user (cat >)
# - Nettoyage : suppression .bashrc.postinstall / .bash_aliases.postinstall
# (résidus éventuels de v8.8.19 — stratégie abandonnée en v8.8.20)
# - chown UTILISATEUR + chmod 644
################################################################################
section_8() {
# ALIASES_USER et non BASH_ALIASES : BASH_ALIASES est un tableau associatif
# RÉSERVÉ de bash (il alimente les alias du shell) — l'écraser fonctionnait par
# chance (assignation implicite à l'élément [0]) mais shellcheck SC2178 a raison
ALIASES_USER="/home/$UTILISATEUR/.bash_aliases"
# ---- Backup horodaté si fichiers existants (TS global C2 v8.8.24) ----
if [ -f "$BASHRC_USER" ]; then
cp "$BASHRC_USER" "${BASHRC_USER}.backup.${TS}"
log_info "Backup : ${BASHRC_USER}.backup.${TS}"
fi
if [ -f "$ALIASES_USER" ]; then
cp "$ALIASES_USER" "${ALIASES_USER}.backup.${TS}"
log_info "Backup : ${ALIASES_USER}.backup.${TS}"
fi
# ---- Nettoyage des résidus v8.8.19 (stratégie .postinstall abandonnée) ----
rm -f "/home/$UTILISATEUR/.bashrc.postinstall" "/home/$UTILISATEUR/.bash_aliases.postinstall"
# ---- 1) Écriture intégrale de ~/.bashrc ----
cat > "$BASHRC_USER" << 'EOF'
# ~/.bashrc : géré par postinstall.26.04_v2.sh
# (réécrit à chaque exécution du script — backup horodaté dans ~/.bashrc.backup.*)
# Shells interactifs uniquement
case $- in
*i*) ;;
*) return;;
esac
# ==== HISTORIQUE BASH OPTIMISÉ ====
export HISTTIMEFORMAT="%F %T "
export HISTSIZE=500000
export HISTFILESIZE=1000000
export HISTCONTROL=ignoreboth:erasedups
export HISTIGNORE="ls:ll:la:l:pwd:cd:cd -:exit:clear:history*:bg:fg:jobs"
shopt -s histappend histverify checkwinsize globstar autocd direxpand nocaseglob
PROMPT_COMMAND='history -a; history -r'
[ -x /usr/bin/lesspipe ] && eval "$(SHELL=/bin/sh lesspipe)"
# Prompt dynamique avec info Git
# \001/\002 émis directement (équivalents internes de \[ \]) : bash décode les
# séquences \[ \] de PS1 AVANT l'expansion de $(prompt_git) — la sortie d'une
# substitution de commande n'est jamais re-décodée, echo -e "\[" affichait
# des \[ littéraux dans le prompt.
prompt_git() {
git rev-parse --is-inside-work-tree &>/dev/null || return
local branch
branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
[ -z "$branch" ] && return
if git diff --quiet 2>/dev/null; then
printf '\001\033[0;32m\002 [%s]' "$branch"
else
printf '\001\033[0;31m\002 [%s]' "$branch"
fi
}
if [ -x /usr/bin/tput ] && tput setaf 1 >/dev/null 2>&1; then
PS1='${debian_chroot:+($debian_chroot)}\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w$(prompt_git)\[\033[00m\]\$ '
else
PS1='${debian_chroot:+($debian_chroot)}\u@\h:\w$(prompt_git)\$ '
fi
case "$TERM" in
xterm*|rxvt*)
PS1="\[\e]0;${debian_chroot:+($debian_chroot)}\u@\h: \w\a\]$PS1"
;;
esac
# Couleurs ls/grep
if [ -x /usr/bin/dircolors ]; then
test -r ~/.dircolors && eval "$(dircolors -b ~/.dircolors)" || eval "$(dircolors -b)"
alias ls='ls --color=auto'
alias grep='grep --color=auto -n'
alias fgrep='fgrep --color=auto'
alias egrep='egrep --color=auto'
fi
alias ll='ls -alF'
alias la='ls -A'
alias l='ls -CF'
# Bash completion
if ! shopt -oq posix; then
if [ -f /usr/share/bash-completion/bash_completion ]; then
. /usr/share/bash-completion/bash_completion
elif [ -f /etc/bash_completion ]; then
. /etc/bash_completion
fi
fi
export PATH="$PATH:$HOME/.local/bin:$HOME/.npm-global/bin"
# Fortune + cowsay aléatoire
if command -v fortune &>/dev/null && command -v cowsay &>/dev/null; then
shopt -s nullglob
cow_files=(/usr/share/cowsay/cows/*.cow)
shopt -u nullglob
if (( ${#cow_files[@]} > 0 )); then
selected_cow="${cow_files[RANDOM % ${#cow_files[@]}]}"
fortune fr 2>/dev/null | cowsay -f "${selected_cow##*/}" | lolcat
else
fortune fr 2>/dev/null | lolcat
fi
fi
# Chargement des aliases
[ -f ~/.bash_aliases ] && . ~/.bash_aliases
EOF
# ---- 2) Écriture intégrale de ~/.bash_aliases ----
cat > "$ALIASES_USER" << 'EOF'
# ~/.bash_aliases : géré par postinstall.26.04_v2.sh
# (réécrit à chaque exécution du script — backup horodaté dans ~/.bash_aliases.backup.*)
# === Navigation ===
alias ..='cd ..'
alias ...='cd ../..'
alias ....='cd ../../..'
alias ~='cd ~'
# === Sécurité ===
alias rm='rm -I'
alias cp='cp -ip'
alias mv='mv -i'
# === Outils ===
alias df='df -h'
alias du='du -h --max-depth=1'
alias free='free -h'
alias path='echo -e ${PATH//:/\\n}'
# === Git ===
alias gs='git status'
alias ga='git add'
alias gc='git commit -m'
alias gl='git log --oneline --graph --decorate --all'
alias gco='git checkout'
alias gd='git diff'
alias cdgit='cd /mnt/nas/sauvegarde/git/'
# === Sauvegarde scripts ===
alias backupbash='/mnt/nas/sauvegarde/git/scripts/backup_bash.sh'
alias restorebash='/mnt/nas/sauvegarde/git/scripts/restore_bash.sh'
alias backuppostinstall='/mnt/nas/sauvegarde/git/scripts/backup_postinstall.sh'
alias restorepostinstall='/mnt/nas/sauvegarde/git/scripts/restore_postinstall.sh'
# === Divers ===
alias cls='clear'
alias h='history'
# IP publique : double-quote externe pour permettre les single-quotes du sed
alias ipext="wget -q -O - checkip.dyndns.org | sed -e 's/[^[:digit:]|.]//g'"
# === VeraCrypt — utilise les scripts générés en section [8b/31] ===
# (mot de passe lu depuis ~/.veracrypt_pass — voir ~/.mount_1984.sh)
alias 1984='~/.mount_1984.sh'
alias 1984q='~/.umount_1984.sh'
# === Alert ===
alias alert='notify-send --urgency=low -i "$([ $? = 0 ] && echo terminal || echo error)" "$(history|tail -n1|sed -e '\''s/^\s*[0-9]\+\s*//;s/[;&|]\s*alert$//'\'')"'
# === Mise à jour complète ===
alias update='sudo apt update && sudo apt upgrade -y && sudo apt autoremove -y && sudo snap refresh && flatpak update -y --noninteractive'
# === Verrouillage (Wayland/GNOME 50) ===
alias lok='loginctl lock-session'
# === Reboot / poweroff ===
alias reboot='sudo reboot'
alias poweroff='sudo poweroff'
# === ClamAV ===
# clamscan : standalone, tourne en root, supporte -r et --exclude-dir
# clamdscan : client du daemon, SANS -r. Pour un fichier précis rapidement.
# Le NAS (/mnt) possède son propre antivirus — exclu du scan automatique.
# Variables réutilisées (DRY) pour les exclusions
CLAM_EXCLUDES='--exclude-dir="^/mnt" --exclude-dir="^/media" --exclude-dir="^/run/media" --exclude-dir="^/snap" --exclude-dir="^/proc" --exclude-dir="^/sys" --exclude-dir="^/dev" --exclude-dir="^/run" --exclude-dir="^/boot" --exclude-dir="^/var/lib/docker" --exclude-dir="^/var/cache/apt"'
CLAM_MEDIA_EXCL='--exclude="\.(mkv|mp4|avi|mov|wmv|flv|webm|m4v|mpg|mpeg|ts|m2ts|vob|iso|img|bin|nrg|dmg|mp3|flac|ogg|aac|wav|wma|m4a|jpg|jpeg|png|gif|bmp|tiff|webp|svg|raw|cr2|nef|arw)$"'
alias cvup='sudo freshclam'
alias cvupd='sudo systemctl restart clamav-freshclam'
alias cvstat='sudo systemctl status clamav-freshclam'
alias cvscan='eval sudo clamscan -r -i --bell $CLAM_EXCLUDES'
alias cvsfast='sudo clamdscan -i --fdpass'
alias cvlog='eval sudo clamscan -r -i --log=/var/log/clamav/scan.log $CLAM_EXCLUDES'
alias cvq='eval sudo clamscan -r --move=$HOME/quarantaine -i --bell $CLAM_EXCLUDES'
alias cvconf='clamconf | less'
alias cvver='clamscan --version'
alias cvdaemon='sudo systemctl status clamav-daemon'
alias cvstart='sudo systemctl start clamav-daemon'
alias cvrestart='sudo systemctl restart clamav-daemon'
alias cvlogs='sudo journalctl -u clamav-daemon -n 80 --no-pager | less'
alias cvfreshlogs='sudo journalctl -u clamav-freshclam -n 60 --no-pager | less'
alias cvnas='eval sudo clamscan -r -i --bell $CLAM_MEDIA_EXCL /mnt/nas/'
alias cvnasslow='sudo clamscan -r -i --bell /mnt/nas/'
alias cvslow='sudo clamscan -r -i --bell'
alias cvslowq='sudo clamscan -r --move=$HOME/quarantaine -i'
alias cvnaslog='eval sudo clamscan -r -i $CLAM_MEDIA_EXCL --log=/var/log/clamav/nas.log /mnt/nas/'
alias cvnasq='eval sudo clamscan -r --move=$HOME/quarantaine -i --bell $CLAM_MEDIA_EXCL /mnt/nas/'
# === yt-dlp ===
alias mp3dl='yt-dlp -x --audio-format mp3 --audio-quality 0 --embed-thumbnail'
alias ytdlpup='sudo yt-dlp -U'
# === VeraCrypt — mise à jour manuelle (URL dynamique selon version Ubuntu) ===
veracryptup() {
local ubuntu_ver
# « ; » + fallback : si le sourcing échoue, la variable restait vide (v8.9.1)
ubuntu_ver=$(. /etc/os-release 2>/dev/null; echo "${VERSION_ID:-}")
[ -z "$ubuntu_ver" ] && ubuntu_ver="26.04"
local url=""
local data
data=$(curl -s https://api.github.com/repos/veracrypt/VeraCrypt/releases/latest)
local v
for v in "$ubuntu_ver" "24.04" "22.04"; do
url=$(echo "$data" | grep "browser_download_url.*console.*Ubuntu-${v}.*amd64.deb" | head -n1 | cut -d'"' -f4)
[ -n "$url" ] && break
done
if [ -z "$url" ]; then
echo "VeraCrypt : aucune URL trouvée pour Ubuntu $ubuntu_ver / 24.04 / 22.04" >&2
return 1
fi
echo "Téléchargement : $url"
wget -q "$url" -O /tmp/veracrypt.deb \
&& sudo apt install -y /tmp/veracrypt.deb \
&& rm -f /tmp/veracrypt.deb \
&& echo "VeraCrypt mis à jour"
}
# === Divers ===
alias pw='pwgen -s -y -c -n 20 16 -1'
# Cours de l'or (XAU/EUR) — LC_ALL=C comme variable d'environnement (pas en argument awk)
alias or='data=$(curl -s https://api.exchangerate.fun/latest); rate=$(echo "$data" | jq -r ".rates.XAU"); price=$(LC_ALL=C awk -v r="$rate" "BEGIN {printf \"%.2f\", 1/r}"); echo -e "💰 \033[1mOr (XAU/EUR)\033[0m : \033[1m${price}\033[0m € / once"'
# Cours de l'ADA (EUR)
alias ada='data=$(curl -s "https://api.coingecko.com/api/v3/simple/price?ids=cardano&vs_currencies=eur"); price=$(echo "$data" | jq -r ".cardano.eur"); echo -e "💎 \033[1mCardano (ADA/EUR)\033[0m : \033[1m${price}\033[0m €"'
# === Wake-on-LAN ===
alias wol='wakeonlan 9c:5c:8e:be:13:e5'
# === VPN ===
alias vpnsuisse='nmcli connection up "Suisse"'
alias vpnusa='nmcli connection up "USA"'
alias vpnfr='nmcli connection up "France"'
alias vpnru='nmcli connection up "Russie"'
alias vpnoff='for c in Suisse USA France Russie; do nmcli connection down "$c" 2>/dev/null; done'
alias vpnstat='nmcli connection show --active | grep -E "Suisse|USA|France|Russie" || echo "Aucun VPN actif"'
# === Supervision Centreon ===
# Réinitialise le hash de référence de la sonde check_grub après une
# modification légitime de grub.cfg (mise à jour noyau, update-grub…)
# Sonde déployée séparément via Centreon — l'alias est inerte tant qu'elle est absente
alias grub-reinit='sudo /usr/lib/nagios/plugins/check_grub.sh -i'
# === Docker NAS ===
# Met à jour la pile transmission-vpn (gluetun + transmission + autoheal) sur le NAS principal
# Nécessite les clés SSH échangées et sudo sans mot de passe sur 192.168.1.100
alias maj-transmission='ssh -p 22 utilisateur@192.168.1.100 "sudo /volume1/docker/transmission-vpn/maj.sh"'
EOF
# Aliases agents Claude Code : générés par boucle (DRY) — append au fichier
{
echo "# === Agents Claude Code (générés par boucle - suffixe 48 = Opus 4.8, f5 = Fable 5) ==="
for domaine in centreon:Centreon infra:Infrastructure scriptbash:ScriptBash sysadmin:Sysadmin webdev:WebDev; do
nom="${domaine%%:*}"
rep="${domaine##*:}"
echo "alias agent-${nom}-48='cd ~/Agents/${rep} && claude --dangerously-skip-permissions --model claude-opus-4-8'"
echo "alias agent-${nom}-f5='cd ~/Agents/${rep} && claude --dangerously-skip-permissions --model claude-fable-5'"
done
echo ""
cat <<'AIDE_EOF'
# Aide agents Claude Code
alias agent-help='echo -e "\e[1;33m=== AGENTS CLAUDE CODE ===\e[0m\n\n\e[1;36mLANCEMENT DES AGENTS (suffixe 48 = Opus 4.8, f5 = Fable 5)\e[0m\n\e[0;37m agent-centreon-48/f5\e[0m\n\e[0;37m agent-infra-48/f5\e[0m\n\e[0;37m agent-scriptbash-48/f5\e[0m\n\e[0;37m agent-sysadmin-48/f5\e[0m\n\e[0;37m agent-webdev-48/f5\e[0m\n\n\e[1;36mSLASH COMMANDS (à taper dans une session agent)\e[0m\n\e[0;37m /refs \e[0;90m→ liste toutes les références disponibles\e[0m\n\e[0;37m /clapi \e[0;90m→ référence CLAPI/API REST Centreon\e[0m\n\e[0;37m /sonde \e[0;90m→ template sonde Nagios/Centreon\e[0m\n\e[0;37m /template_bash \e[0;90m→ squelette complet d'\''un script bash (structure + aide)\e[0m\n\e[0;37m /ansible \e[0;90m→ référence Ansible\e[0m\n\e[0;37m /commandes \e[0;90m→ référence surveillance Infrastructure\e[0m\n\e[0;37m /templates \e[0;90m→ fonctions utilitaires Sysadmin (sudo, SSH, sauvegarde)\e[0m\n\n\e[1;36mPLUGIN CAVEMAN\e[0m\n\e[0;37m /caveman \e[0;90m→ activer mode caveman (réponses courtes -75% tokens)\e[0m\n\e[0;37m /caveman off \e[0;90m→ désactiver caveman\e[0m\n\e[0;37m /caveman ultra \e[0;90m→ mode ultra-compact\e[0m"'
# Audit système
alias audit-local='bash ~/Agents/Scripts/diagnostics/audit_localhost.sh'
AIDE_EOF
} >> "$ALIASES_USER"
# ---- 3) Permissions ----
chown "$UTILISATEUR":"$UTILISATEUR" "$BASHRC_USER" "$ALIASES_USER"
chmod 644 "$BASHRC_USER" "$ALIASES_USER"
# Dossier de quarantaine ClamAV — cible des alias cvq/cvslowq/cvnasq
# (le tilde de --move=~/... n'étant pas expansé, les alias utilisent $HOME)
mkdir -p "/home/$UTILISATEUR/quarantaine"
chown "$UTILISATEUR":"$UTILISATEUR" "/home/$UTILISATEUR/quarantaine"
log_ok ".bashrc et .bash_aliases écrasés (backups : .backup.${TS})"
}
################################################################################
# 8b. SCRIPTS VERACRYPT SÉCURISÉS (.mount_1984.sh / .umount_1984.sh)
# — Mot de passe lu depuis ~/.veracrypt_pass (non exposé dans les aliases)
################################################################################
section_8b() {
MOUNT_SCRIPT="/home/$UTILISATEUR/.mount_1984.sh"
UMOUNT_SCRIPT="/home/$UTILISATEUR/.umount_1984.sh"
# Création du script de montage si absent
# --text --non-interactive + --keyfiles="" (v8.9.8) : le binaire console
# (veracrypt-console, seul disponible en fallback 26.04) demandait
# interactivement « Enter keyfile [none]: » — tout paramètre non fourni
# devient un prompt. En non-interactif, tout est passé en argument.
if [ ! -f "$MOUNT_SCRIPT" ]; then
cat <<EOF > "$MOUNT_SCRIPT"
#!/bin/bash
# Volume déjà monté ? → message clair et sortie 0 (v8.9.9) : veracrypt-console
# renverrait sinon le message trompeur « L'emplacement du volume est indisponible »
if findmnt $VERACRYPT_MOUNTPOINT >/dev/null 2>&1; then
echo "Volume déjà monté sur $VERACRYPT_MOUNTPOINT (démonter avec : 1984q)"
exit 0
fi
veracrypt --text --non-interactive --mount \\
$VERACRYPT_CONTAINER \\
$VERACRYPT_MOUNTPOINT \\
--password="\$(cat $VERACRYPT_PASS_FILE)" \\
--pim=0 \\
--keyfiles="" \\
--protect-hidden=no
EOF
chmod 700 "$MOUNT_SCRIPT"
chown "$UTILISATEUR":"$UTILISATEUR" "$MOUNT_SCRIPT"
log_ok "Script .mount_1984.sh créé"
else
log_ok "Script .mount_1984.sh déjà présent — non modifié"
fi
# Création du script de démontage si absent
if [ ! -f "$UMOUNT_SCRIPT" ]; then
cat <<EOF > "$UMOUNT_SCRIPT"
#!/bin/bash
veracrypt --text -d $VERACRYPT_CONTAINER
EOF
chmod 700 "$UMOUNT_SCRIPT"
chown "$UTILISATEUR":"$UTILISATEUR" "$UMOUNT_SCRIPT"
log_ok "Script .umount_1984.sh créé"
else
log_ok "Script .umount_1984.sh déjà présent — non modifié"
fi
# Créer ~/.veracrypt_pass avec le mot de passe si absent ou vide
# umask 077 : fichier créé directement en 600 (pas de fenêtre 644)
if [ ! -f "$VERACRYPT_PASS_FILE" ] || [ ! -s "$VERACRYPT_PASS_FILE" ]; then
(
umask 077
printf '%s\n' "$VERACRYPT_PASS" > "$VERACRYPT_PASS_FILE"
)
chown "$UTILISATEUR":"$UTILISATEUR" "$VERACRYPT_PASS_FILE"
log_ok "Fichier ~/.veracrypt_pass créé (chmod 600 via umask)"
else
log_ok "Fichier ~/.veracrypt_pass déjà présent et non vide — non modifié"
fi
# Créer le point de montage VeraCrypt si absent
if [ ! -d "$VERACRYPT_MOUNTPOINT" ]; then
mkdir -p "$VERACRYPT_MOUNTPOINT"
chown "$UTILISATEUR":"$UTILISATEUR" "$VERACRYPT_MOUNTPOINT"
log_ok "Point de montage VeraCrypt créé : $VERACRYPT_MOUNTPOINT"
else
log_ok "Point de montage VeraCrypt déjà présent : $VERACRYPT_MOUNTPOINT"
fi
}
################################################################################
# 9. SUDO SANS MOT DE PASSE
################################################################################
section_9() {
# Validation visudo AVANT activation : un fichier sudoers malformé sous
# /etc/sudoers.d/ casserait TOUT sudo. On écrit en temporaire, on valide,
# puis on installe atomiquement en 0440 root:root.
SUDOERS_USER="/etc/sudoers.d/90-${UTILISATEUR}"
SUDOERS_TMP=$(mktemp)
printf '%s ALL=(ALL) NOPASSWD:ALL\n' "$UTILISATEUR" > "$SUDOERS_TMP"
if visudo -cf "$SUDOERS_TMP" >/dev/null 2>&1; then
install -m 0440 -o root -g root "$SUDOERS_TMP" "$SUDOERS_USER"
log_ok "Sudo NOPASSWD activé (${SUDOERS_USER}, syntaxe validée par visudo)"
else
log_error "Syntaxe sudoers invalide pour ${SUDOERS_USER} — non installé (sudo préservé)"
fi
rm -f "$SUDOERS_TMP"
}
################################################################################
# 10. FLATPAK ET APPLICATIONS
# Note 26.04 : musescore3 absent des dépôts apt officiels → Flatpak org.musescore.MuseScore
################################################################################
section_10() {
hash -r
FLATPAK_BIN=$(command -v flatpak || echo "")
if [ -z "$FLATPAK_BIN" ] || [ ! -x "$FLATPAK_BIN" ]; then
log_warn "flatpak introuvable dans PATH, tentative de réinstallation..."
apt install -y flatpak || log_warn "Réinstallation flatpak échouée"
hash -r
FLATPAK_BIN=$(command -v flatpak || echo "/usr/bin/flatpak")
fi
if [ -x "$FLATPAK_BIN" ]; then
"$FLATPAK_BIN" remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo \
|| log_warn "Flathub : remote-add échoué — les installations Flatpak vont échouer"
# Tableau d'applications : installation groupée (rapide) puis reprise app
# par app — une seule ref invalide avorte TOUTE la transaction flatpak,
# même pattern que les paquets APT de la section 2 (R1 v8.8.36)
FLATPAK_APPS=(
com.discordapp.Discord
org.videolan.VLC
com.bambulab.BambuStudio
com.usebottles.bottles
io.github.linx_systems.ClamUI
org.telegram.desktop
org.musescore.MuseScore
)
"$FLATPAK_BIN" install -y flathub "${FLATPAK_APPS[@]}" \
|| log_warn "Installation Flatpak groupée incomplète — reprise app par app"
FP_OK=0; FP_KO=0; FP_KO_LIST=""
for app in "${FLATPAK_APPS[@]}"; do
"$FLATPAK_BIN" info "$app" &>/dev/null \
|| "$FLATPAK_BIN" install -y flathub "$app" >/dev/null 2>&1 || true
if "$FLATPAK_BIN" info "$app" &>/dev/null; then
FP_OK=$((FP_OK + 1))
else
FP_KO=$((FP_KO + 1))
FP_KO_LIST+=" $app"
fi
done
if [ "$FP_KO" -eq 0 ]; then
log_ok "Apps Flatpak installées : $FP_OK/${#FLATPAK_APPS[@]} (toutes présentes)"
else
log_warn "Apps Flatpak : $FP_OK/${#FLATPAK_APPS[@]} ok — $FP_KO manquante(s) :$FP_KO_LIST"
fi
else
log_error "flatpak introuvable même après réinstallation — étape ignorée"
fi
}
################################################################################
# 11. MISES À JOUR AUTOMATIQUES
################################################################################
section_11() {
echo unattended-upgrades unattended-upgrades/enable_auto_updates boolean true | debconf-set-selections
dpkg-reconfigure -f noninteractive unattended-upgrades
log_ok "Mises à jour de sécurité automatiques activées"
}
################################################################################
# 12. NETTOYAGE PAQUETS
################################################################################
section_12() {
apt autoremove -y && apt autoclean -y
log_ok "Paquets obsolètes supprimés"
}
################################################################################
# 13. LIMITATION JOURNAL SYSTEMD
################################################################################
section_13() {
grep -qxF 'SystemMaxUse=500M' /etc/systemd/journald.conf || echo 'SystemMaxUse=500M' >> /etc/systemd/journald.conf
systemctl restart systemd-journald || log_warn "Redémarrage journald échoué"
log_ok "Journal limité à 500 Mo"
}
################################################################################
# 14. CONFIGURATION NAUTILUS, GNOME, EXTENSIONS + THÈME TERMINAL PTYXIS
# Note 26.04 : GNOME Terminal remplacé par Ptyxis (depuis Ubuntu 25.10)
# - Le thème Solarized Dark se configure via dconf org.gnome.Ptyxis
# - L'ID de profil Ptyxis se lit via dconf read /org/gnome/Ptyxis/default-profile-uuid
# - La session est Wayland-only (pas de fallback X11)
################################################################################
section_14() {
if command -v gsettings &>/dev/null; then
DBUS_PATH="unix:path=/run/user/${USER_UID}/bus"
# ---- Nautilus (via wrapper gset() — L1 v8.8.24) ----
gset org.gnome.nautilus.preferences show-hidden-files true \
|| log_warn "gset Nautilus show-hidden-files ignoré (DBUS/session indisponible ?)"
gset org.gnome.nautilus.preferences default-folder-viewer 'list-view' \
|| log_warn "gset Nautilus default-folder-viewer ignoré (DBUS/session indisponible ?)"
gset org.gnome.desktop.thumbnailers disable-all false \
|| log_warn "gset thumbnailers disable-all ignoré (DBUS/session indisponible ?)"
gset org.gnome.nautilus.preferences show-image-thumbnails 'always' \
|| log_warn "gset Nautilus show-image-thumbnails ignoré (DBUS/session indisponible ?)"
gset org.gnome.nautilus.preferences thumbnail-limit 1048576 \
|| log_warn "gset Nautilus thumbnail-limit ignoré (DBUS/session indisponible ?)"
log_ok "Préférences Nautilus configurées"
# ---- Extensions GNOME depuis extensions.gnome.org (v8.9.12) ----
GNOME_VERSION=$(gnome-shell --version 2>/dev/null | grep -oP '\d+' | head -n1 || echo "50")
log_info "Version GNOME Shell détectée : $GNOME_VERSION"
# Installe une extension depuis l'API extensions.gnome.org (URL dynamique
# selon la version du shell). Usage : installer_extension_ego <pk> <nom>
installer_extension_ego() {
local pk="$1" nom="$2" url
url=$(curl -s "https://extensions.gnome.org/extension-info/?pk=${pk}&shell_version=${GNOME_VERSION}" \
| python3 -c "
import sys, json
try:
d = json.load(sys.stdin)
print('https://extensions.gnome.org' + d.get('download_url', ''))
except:
print('')
" 2>/dev/null || echo "")
if [ -z "$url" ] || [ "$url" = "https://extensions.gnome.org" ]; then
log_warn "$nom : URL introuvable pour GNOME $GNOME_VERSION (installer via Extension Manager)"
return 1
fi
sudo -u "$UTILISATEUR" DBUS_SESSION_BUS_ADDRESS="$DBUS_PATH" \
gnome-extensions install --force "$url" 2>/dev/null \
&& log_ok "$nom installé" \
|| log_warn "$nom : installation échouée (installer via Extension Manager)"
}
log_info "Installation clipboard-indicator..."
installer_extension_ego 779 "clipboard-indicator" || true
# dash-to-dock retiré des dépôts 26.04 (paquet deb) → même canal (v8.9.12)
log_info "Installation dash-to-dock..."
installer_extension_ego 307 "dash-to-dock" || true
# ---- Activation des extensions ----
# Détection FILESYSTEM (v8.9.12) : « gnome-extensions list » ne reflète les
# nouvelles extensions qu'après rechargement du shell — au test réel, même
# clipboard-indicator fraîchement installé était déclaré absent.
# Activation ADDITIVE via gsettings : les UUID sont ajoutés à
# enabled-extensions sans écraser les extensions Ubuntu déjà actives
# (ubuntu-dock, appindicators…) ; prise en compte à la prochaine session.
log_info "Activation des extensions GNOME..."
EXT_USER_DIR="/home/$UTILISATEUR/.local/share/gnome-shell/extensions"
for ext in dash-to-dock@micxgx.gmail.com \
gsconnect@andyholmes.github.io \
clipboard-indicator@tudmotu.com; do
if [ -d "/usr/share/gnome-shell/extensions/$ext" ] || [ -d "$EXT_USER_DIR/$ext" ]; then
actuelles=$(sudo -u "$UTILISATEUR" DBUS_SESSION_BUS_ADDRESS="$DBUS_PATH" \
gsettings get org.gnome.shell enabled-extensions 2>/dev/null || echo "[]")
nouvelles=$(printf '%s' "$actuelles" | python3 -c "
import sys, ast
try:
lst = ast.literal_eval(sys.stdin.read().strip() or '[]')
except Exception:
lst = []
if not isinstance(lst, list):
lst = []
if '$ext' not in lst:
lst.append('$ext')
print(str(lst))
" 2>/dev/null || echo "")
if [ -n "$nouvelles" ] && sudo -u "$UTILISATEUR" DBUS_SESSION_BUS_ADDRESS="$DBUS_PATH" \
gsettings set org.gnome.shell enabled-extensions "$nouvelles" 2>/dev/null; then
log_ok "Extension activée (effective à la prochaine session) : $ext"
else
log_warn "Extension $ext : activation gsettings échouée (activer via Extension Manager)"
fi
else
log_warn "Extension absente : $ext (installer via Extension Manager si besoin)"
fi
done
# ---- Thème Solarized Dark pour Ptyxis (remplace GNOME Terminal depuis Ubuntu 25.10) ----
# La palette Solarized Dark est appliquée via dconf sur le profil Ptyxis par défaut.
# Le schéma de couleurs est défini comme une entrée "user-defined" dans org.gnome.Ptyxis.
log_info "Application thème Solarized Dark sur Ptyxis..."
PTYXIS_PROFILE_UUID=$(sudo -u "$UTILISATEUR" DBUS_SESSION_BUS_ADDRESS="$DBUS_PATH" \
dconf read /org/gnome/Ptyxis/default-profile-uuid 2>/dev/null | tr -d "'" || true)
if [ -z "$PTYXIS_PROFILE_UUID" ]; then
log_warn "Profil Ptyxis introuvable — Ptyxis n'a peut-être pas encore été lancé"
log_warn "Après le premier lancement de Ptyxis, appliquer manuellement :"
log_warn " UUID=\$(dconf read /org/gnome/Ptyxis/default-profile-uuid | tr -d \"'\")"
log_warn " dconf write /org/gnome/Ptyxis/Profiles/\$UUID/palette \"'Solarized'\""
else
PTYXIS_PATH="/org/gnome/Ptyxis/Profiles/$PTYXIS_PROFILE_UUID/"
# Ptyxis dispose d'un thème Solarized natif activable via la clé "palette"
sudo -u "$UTILISATEUR" DBUS_SESSION_BUS_ADDRESS="$DBUS_PATH" \
dconf write "${PTYXIS_PATH}palette" "'Solarized'"
sudo -u "$UTILISATEUR" DBUS_SESSION_BUS_ADDRESS="$DBUS_PATH" \
dconf write "${PTYXIS_PATH}dark-theme" "true"
log_ok "Thème Solarized Dark appliqué à Ptyxis (profil : $PTYXIS_PROFILE_UUID)"
fi
# Activer les secondes et le jour de la semaine dans l'horloge GNOME
gset org.gnome.desktop.interface clock-show-seconds true \
|| log_warn "gset horloge clock-show-seconds ignoré (DBUS/session indisponible ?)"
gset org.gnome.desktop.interface clock-show-weekday true \
|| log_warn "gset horloge clock-show-weekday ignoré (DBUS/session indisponible ?)"
log_ok "Horloge GNOME : secondes et jour de la semaine activés"
log_ok "Configuration GNOME, extensions et thème terminal Ptyxis terminés"
else
log_warn "gsettings indisponible — configuration GNOME ignorée"
fi
}
################################################################################
# 15. DÉSACTIVATION VEILLE
################################################################################
section_15() {
if command -v gsettings &>/dev/null; then
DBUS_PATH="unix:path=/run/user/${USER_UID}/bus"
# Wrapper gset() — L1 v8.8.24
gset org.gnome.settings-daemon.plugins.power sleep-inactive-ac-timeout 0 \
|| log_warn "gsettings power ignoré (schéma absent sur GNOME 50)"
gset org.gnome.settings-daemon.plugins.power sleep-inactive-battery-timeout 0 \
|| log_warn "gsettings power ignoré (schéma absent sur GNOME 50)"
gset org.gnome.desktop.session idle-delay 0 \
|| log_warn "gset session idle-delay ignoré (DBUS/session indisponible ?)"
gset org.gnome.settings-daemon.plugins.power idle-dim false \
|| log_warn "gsettings power ignoré (schéma absent sur GNOME 50)"
log_ok "Veille désactivée"
fi
}
################################################################################
# 16. DÉSACTIVATION UFW
################################################################################
section_16() {
ufw disable || log_warn "UFW déjà désactivé ou absent"
log_ok "UFW désactivé"
}
################################################################################
# 17. SCRIPT NETTOYAGE AUTOMATIQUE
################################################################################
section_17() {
cat <<'SCRIPT' > /usr/local/bin/cleanup.sh
#!/bin/bash
set -euo pipefail
echo "=== Nettoyage système $(date) ==="
# DPkg::Lock::Timeout : attendre le verrou dpkg au lieu d'échouer immédiatement
# (update-system et cleanup-system peuvent partir au même boot, Persistent=true)
apt -o DPkg::Lock::Timeout=600 autoremove -y && apt -o DPkg::Lock::Timeout=600 autoclean -y
snap set system refresh.retain=2 || true
snap list --all | awk '/disabled/{print $1" --revision "$3}' | xargs -r snap remove || true
flatpak uninstall --unused -y || true
# Nettoyage Docker uniquement si le service est actif
if systemctl is-active --quiet docker; then
docker system prune -f
docker builder prune -f --keep-storage 10GB
else
echo "Docker inactif — nettoyage Docker ignoré"
fi
find /tmp /var/tmp -type f -atime +7 -delete 2>/dev/null || true
find /home -maxdepth 3 -path "*/.cache/thumbnails" -type f -atime +30 -delete 2>/dev/null || true
find /var/log -type f -name "*.log.*" -mtime +30 -delete 2>/dev/null || true
echo "=== Fin nettoyage $(date) ==="
SCRIPT
chmod +x /usr/local/bin/cleanup.sh
cat <<'EOF' > /etc/systemd/system/cleanup-system.timer
[Unit]
Description=Nettoyage hebdomadaire
[Timer]
OnCalendar=Sun *-*-* 03:00:00
Persistent=true
[Install]
WantedBy=timers.target
EOF
cat <<'EOF' > /etc/systemd/system/cleanup-system.service
[Unit]
Description=Nettoyage système
After=docker.service network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/cleanup.sh
Nice=19
EOF
systemctl daemon-reload
systemctl enable --now cleanup-system.timer
log_ok "Nettoyage automatique planifié (dimanche 3h)"
}
################################################################################
# 18. SCRIPT MISE À JOUR AUTOMATIQUE (SYSTÈME)
# — Sans Ollama/Open WebUI (remplacés par Claude Code)
# — OnBootSec=5min pour laisser la session GNOME s'établir
################################################################################
section_18() {
cat <<'SCRIPT' > /usr/local/bin/update-system.sh
#!/bin/bash
set -euo pipefail
echo "=== Mise à jour système $(date) ==="
echo "• Mise à jour APT..."
# DPkg::Lock::Timeout : attendre le verrou dpkg (unattended-upgrades ou
# cleanup-system peuvent tourner en même temps au boot, Persistent=true)
apt -o DPkg::Lock::Timeout=600 update && apt -o DPkg::Lock::Timeout=600 upgrade -y \
|| echo " → APT en échec — poursuite des autres mises à jour"
echo "• Mise à jour Snap..."
snap refresh || true
echo "• Mise à jour Flatpak..."
flatpak update -y --noninteractive || true
echo "• Mise à jour Claude Code..."
npm update -g @anthropic-ai/claude-code 2>/dev/null \
&& echo " ✓ Claude Code mis à jour" \
|| echo " → Claude Code : mise à jour ignorée (non installé ou erreur)"
echo "• Mise à jour yt-dlp..."
yt-dlp -U 2>/dev/null \
&& echo " ✓ yt-dlp mis à jour" \
|| echo " → yt-dlp : mise à jour ignorée (non installé ou erreur)"
# Mise à jour VeraCrypt (binaire GitHub) — URL dynamique selon Ubuntu + fallback
# || true OBLIGATOIRE : sous set -euo pipefail, une assignation dont le pipeline
# échoue (VeraCrypt absent, réseau HS, API GitHub rate-limitée) tuerait le
# script ici et sauterait le message final (même esprit que N3 v8.8.34)
VERACRYPT_INSTALLED=$(dpkg -l veracrypt 2>/dev/null | awk '/^ii/{print $3}') || true
DATA=$(curl -s https://api.github.com/repos/veracrypt/VeraCrypt/releases/latest) || true
VERACRYPT_LATEST=$(echo "$DATA" | grep '"tag_name"' | cut -d'"' -f4 | sed 's/VeraCrypt_//') || true
if [[ -n "$VERACRYPT_LATEST" && "$VERACRYPT_INSTALLED" != "$VERACRYPT_LATEST" ]]; then
echo "[$(date '+%F %T')] [INFO] Mise à jour VeraCrypt : $VERACRYPT_INSTALLED → $VERACRYPT_LATEST"
# « ; » + fallback : si le sourcing échoue, la variable restait vide (v8.9.1)
UBUNTU_VER=$(. /etc/os-release 2>/dev/null; echo "${VERSION_ID:-}")
[ -z "$UBUNTU_VER" ] && UBUNTU_VER="26.04"
URL=""
for v in "$UBUNTU_VER" "24.04" "22.04"; do
# || true : grep sans correspondance ferait sortir le script (set -e + pipefail)
URL=$(echo "$DATA" | grep "browser_download_url.*console.*Ubuntu-${v}.*amd64.deb" | head -n1 | cut -d'"' -f4) || true
[ -n "$URL" ] && break
done
if [ -n "$URL" ]; then
wget -q "$URL" -O /tmp/veracrypt.deb \
&& apt -o DPkg::Lock::Timeout=600 install -y /tmp/veracrypt.deb \
&& echo "[$(date '+%F %T')] [OK] VeraCrypt mis à jour vers $VERACRYPT_LATEST" \
|| echo "[$(date '+%F %T')] [WARN] Mise à jour VeraCrypt échouée"
rm -f /tmp/veracrypt.deb
else
echo "[$(date '+%F %T')] [WARN] VeraCrypt : aucune URL pour Ubuntu ${UBUNTU_VER}/24.04/22.04"
fi
else
echo "[$(date '+%F %T')] [OK] VeraCrypt à jour ($VERACRYPT_INSTALLED)"
fi
echo "=== Fin mise à jour $(date) ==="
SCRIPT
chmod +x /usr/local/bin/update-system.sh
cat <<'EOF' > /etc/systemd/system/update-system.service
[Unit]
Description=Mise à jour automatique système
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/update-system.sh
Nice=19
EOF
cat <<'EOF' > /etc/systemd/system/update-system.timer
[Unit]
Description=Mise à jour quotidienne système
[Timer]
OnCalendar=daily
Persistent=true
RandomizedDelaySec=1h
OnBootSec=5min
[Install]
WantedBy=timers.target
EOF
systemctl daemon-reload
systemctl enable --now update-system.timer
log_ok "Mise à jour automatique planifiée (quotidienne, délai 5min au boot)"
}
################################################################################
# 19. REDIRECTION DOSSIERS VERS NAS + SIGNETS
################################################################################
section_19() {
USER_DIRS_FILE="/home/$UTILISATEUR/.config/user-dirs.dirs"
USER_DIRS_CONF="/home/$UTILISATEUR/.config/user-dirs.conf"
# ---- Dossiers XDG : tous pointent vers $HOME (locaux) ----
# Ne pas pointer XDG_DOCUMENTS_DIR / MUSIC / PICTURES vers le NAS :
# cela supprime les entrées "Documents", "Musique", "Images" de la barre latérale
# et provoque un mélange dans les signets Nautilus.
# Les dossiers NAS sont accessibles via les signets file:///mnt/nas/ ci-dessous.
cat <<EOF > "$USER_DIRS_FILE"
XDG_DESKTOP_DIR="\$HOME/Bureau"
XDG_DOWNLOAD_DIR="\$HOME/Téléchargements"
XDG_TEMPLATES_DIR="\$HOME/Modèles"
XDG_PUBLICSHARE_DIR="\$HOME/Public"
XDG_DOCUMENTS_DIR="\$HOME/Documents"
XDG_MUSIC_DIR="\$HOME/Musique"
XDG_PICTURES_DIR="\$HOME/Images"
XDG_VIDEOS_DIR="\$HOME/Vidéos"
EOF
cat <<EOF > "$USER_DIRS_CONF"
enabled=False
EOF
chown "$UTILISATEUR":"$UTILISATEUR" "$USER_DIRS_FILE" "$USER_DIRS_CONF"
# Créer explicitement chaque dossier XDG local et appliquer les droits
for dir in Bureau Téléchargements Modèles Public Documents Musique Images Vidéos; do
mkdir -p "/home/$UTILISATEUR/$dir"
chown "$UTILISATEUR":"$UTILISATEUR" "/home/$UTILISATEUR/$dir"
done
log_ok "Dossiers XDG locaux créés : Bureau, Téléchargements, Modèles, Public, Documents, Musique, Images, Vidéos"
# Pas de xdg-user-dirs-update : no-op avec enabled=False dans user-dirs.conf
# (les dossiers et user-dirs.dirs sont gérés statiquement ci-dessus)
sudo -u "$UTILISATEUR" nautilus -q 2>/dev/null || true
# ---- Signets Nautilus : URI file:///mnt/nas/<partage> ----
# On utilise file:// pointant sur les montages CIFS déjà authentifiés via fstab.
# Les URI smb:// forcent Nautilus à se connecter via GVfs et redemandent le mot
# de passe NAS — à éviter puisque le montage fstab est déjà actif et authentifié.
# Les noms documents/musique/photographie sont suffixés "NAS" pour les distinguer
# des dossiers locaux XDG homonymes rétablis dans $HOME ci-dessus.
# veracrypt1 est listé en premier (montage local, pas NAS).
BOOKMARKS_FILE="/home/$UTILISATEUR/.config/gtk-3.0/bookmarks"
mkdir -p "$(dirname "$BOOKMARKS_FILE")"
# Backup unique si fichier existe (préserve customisations utilisateur)
[ -f "$BOOKMARKS_FILE" ] && cp "$BOOKMARKS_FILE" "${BOOKMARKS_FILE}.backup.${TS}"
touch "$BOOKMARKS_FILE"
declare -A NOM_SIGNET
NOM_SIGNET[animation]="🎬 Animation"
NOM_SIGNET[documents]="📄 Documents NAS"
NOM_SIGNET[downloads]="⬇️ Téléchargements NAS"
NOM_SIGNET[films]="🎥 Films"
NOM_SIGNET[Imprimante]="🖨️ Imprimante"
NOM_SIGNET[jeux]="🎮 Jeux"
NOM_SIGNET[livres]="📚 Livres"
NOM_SIGNET[musique]="🎵 Musique NAS"
NOM_SIGNET[perso]="👤 Perso"
NOM_SIGNET[photographie]="📸 Photographie NAS"
NOM_SIGNET[sauvegarde]="💾 Sauvegarde"
NOM_SIGNET[seriestv]="📺 Séries TV"
NOM_SIGNET[surveillance]="📹 Surveillance"
NOM_SIGNET[Technique]="🔧 Technique"
# Ajout idempotent : on n'ajoute que les signets absents (préserve les bookmarks user)
ajoute_signet() {
local uri="$1"
local nom="$2"
if ! grep -qF "$uri" "$BOOKMARKS_FILE"; then
printf '%s %s\n' "$uri" "$nom" >> "$BOOKMARKS_FILE"
fi
}
ajoute_signet "file:///media/veracrypt1" "🔐 VeraCrypt"
for dossier in "${NAS_PARTAGES[@]}"; do
ajoute_signet "file:///mnt/nas/${dossier}" "${NOM_SIGNET[$dossier]:-$dossier}"
done
chown "$UTILISATEUR":"$UTILISATEUR" "$BOOKMARKS_FILE"
log_ok "Signets NAS ajoutés sans écraser les bookmarks user existants"
}
################################################################################
# 20. VÉRIFICATIONS SERVICES
################################################################################
section_20() {
# Boucle de vérification des services critiques (R3 v8.8.24)
# Format spécial pour libvirt : libvirtd OU virtqemud (selon la version)
SRV_OK=0; SRV_KO=0; SRV_KO_LIST=""
for svc in ssh docker postfix nagios-nrpe-server snmpd centagent clamav-freshclam smartmontools; do
if systemctl is-active --quiet "$svc" 2>/dev/null; then
log_ok "Service actif : $svc"
SRV_OK=$((SRV_OK + 1))
else
log_warn "Service inactif : $svc"
SRV_KO=$((SRV_KO + 1))
SRV_KO_LIST+=" $svc"
fi
done
# libvirt traité à part (deux noms possibles selon la version)
if systemctl is-active --quiet libvirtd 2>/dev/null || systemctl is-active --quiet virtqemud 2>/dev/null; then
log_ok "Service actif : libvirt (libvirtd ou virtqemud)"
SRV_OK=$((SRV_OK + 1))
else
log_warn "Service inactif : libvirt (ni libvirtd ni virtqemud)"
SRV_KO=$((SRV_KO + 1))
SRV_KO_LIST+=" libvirt"
fi
# Vérification binaire ansible (pas un service)
if command -v ansible &>/dev/null; then
log_ok "Ansible installé ($(ansible --version | head -1))"
else
log_warn "Ansible absent"
fi
SRV_TOTAL=$((SRV_OK + SRV_KO))
if [ "$SRV_KO" -eq 0 ]; then
log_ok "Services critiques : $SRV_OK/$SRV_TOTAL actifs"
else
log_warn "Services critiques : $SRV_OK/$SRV_TOTAL actifs — inactifs :$SRV_KO_LIST"
fi
}
################################################################################
# 21. NETTOYAGE INTERMÉDIAIRE
################################################################################
section_21() {
apt autoremove -y && apt autoclean -y
log_ok "Nettoyage effectué"
}
################################################################################
# 22. CONFIGURATION POSTFIX RELAY GMAIL
################################################################################
section_22() {
if ! command -v postfix &>/dev/null; then
log_warn "Postfix absent, installation forcée..."
# Type « select » (v8.9.1) : main_mailer_type est un template select,
# le type « string » ne préchargeait pas la réponse → écran interactif
debconf-set-selections <<< "postfix postfix/main_mailer_type select Internet Site"
debconf-set-selections <<< "postfix postfix/mailname string $(hostname)"
DEBIAN_FRONTEND=noninteractive apt install -y postfix mailutils \
|| log_error "Installation Postfix échouée — relay Gmail non configuré"
hash -r
fi
# Plus d'exit 1 ici (v8.8.32) : sans Postfix on saute la configuration du relay
# mais les sections suivantes (23-31, dont le résumé final) s'exécutent.
if command -v postfix &>/dev/null; then
mkdir -p /etc/postfix
SASL_PASSWD="/etc/postfix/sasl_passwd"
# umask 077 : sasl_passwd contient le mot de passe Gmail App — créé directement en 600
(
umask 077
# printf %s : pas d'interprétation de $, ` ou \ dans le mot de passe applicatif Gmail
printf '[smtp.gmail.com]:587\t%s:%s\n' "$GMAIL_USER" "$GMAIL_APP_PASSWORD" > "$SASL_PASSWD"
)
chown root:root "$SASL_PASSWD"
# postmap génère .db hérite des permissions du parent (postfix gère lui-même)
postmap "$SASL_PASSWD" \
&& chmod 600 "${SASL_PASSWD}.db" \
|| log_error "postmap échoué — relay Gmail cassé, .db absent"
cat <<'EOF' > /etc/postfix/main.cf
# ---------------- Configuration de base ----------------
compatibility_level = 3.6
myhostname = ma-machine.local
mydestination = $myhostname, localhost
mynetworks = 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128
inet_interfaces = loopback-only
inet_protocols = all
# ---------------- Pas de réception de mails ----------------
biff = no
append_dot_mydomain = no
readme_directory = no
# ---------------- Relay vers Gmail (smarthost) ----------------
relayhost = [smtp.gmail.com]:587
# ---------------- Authentification SASL ----------------
smtp_sasl_auth_enable = yes
smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd
smtp_sasl_security_options = noanonymous
smtp_sasl_mechanism_filter = plain, login
# ---------------- Sécurité TLS ----------------
smtp_tls_security_level = encrypt
smtp_tls_CAfile = /etc/ssl/certs/ca-certificates.crt
smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache
# ---------------- Restrictions ----------------
smtpd_relay_restrictions = permit_mynetworks permit_sasl_authenticated defer_unauth_destination
# ---------------- Divers ----------------
mailbox_size_limit = 0
recipient_delimiter = +
alias_maps = hash:/etc/aliases
alias_database = hash:/etc/aliases
EOF
# myhostname depuis le .conf (HOSTNAME_MACHINE — v8.9.0) : le heredoc
# ci-dessus est quoté ('EOF', pour préserver ${data_directory} destiné à
# postfix), d'où le sed après écriture plutôt qu'une expansion directe
sed -i "s/^myhostname = .*/myhostname = ${HOSTNAME_MACHINE}.local/" /etc/postfix/main.cf
if grep -q "^root:" /etc/aliases; then
sed -i "s|^root:.*|root: $UTILISATEUR|" /etc/aliases
else
echo "root: $UTILISATEUR" >> /etc/aliases
fi
newaliases || log_warn "newaliases échoué — vérifier /etc/aliases"
systemctl enable --now postfix || log_warn "Postfix enable échoué"
systemctl restart postfix || log_warn "Postfix restart échoué"
echo "Test Postfix post-install $(date)" | mail -s "[postinstall] Postfix OK - $(hostname)" "$GMAIL_USER" \
&& log_ok "Mail de test envoyé à $GMAIL_USER" \
|| log_warn "Envoi test échoué – vérifier : journalctl -u postfix -n 30"
log_ok "Postfix configuré (relay Gmail)"
else
log_error "Postfix indisponible — section 22 ignorée (rapport final par mail indisponible)"
fi
}
################################################################################
# 23. INSTALLATION CLAUDE CODE
################################################################################
section_23() {
# Node.js >= 18 requis par Claude Code (v8.9.1) : au test réel, npm et
# l'installation Claude Code échouaient — la version nodejs des dépôts Ubuntu
# peut être trop ancienne → passage par le dépôt NodeSource (Node 22 LTS)
node_ver=$(node --version 2>/dev/null | grep -oP '^v\K[0-9]+') || true
if [[ -z "${node_ver:-}" || "${node_ver}" -lt 18 ]]; then
log_info "Node.js absent ou < 18 (version : ${node_ver:-absente}) — installation via NodeSource 22.x"
if curl -fsSL https://deb.nodesource.com/setup_22.x | bash -; then
apt install -y nodejs || log_warn "nodejs (NodeSource) : installation échouée"
hash -r
else
log_warn "Dépôt NodeSource : configuration échouée — npm/Claude Code risquent d'échouer"
fi
fi
# Mise à jour npm vers version stable
npm install -g npm@latest || log_warn "Mise à jour npm échouée, continuation..."
# Installation Claude Code via npm
if npm install -g @anthropic-ai/claude-code; then
log_ok "Claude Code installé ($(claude --version 2>/dev/null || echo 'version inconnue'))"
log_ok "Lancer avec : claude"
log_ok "Authentification requise au premier lancement : claude auth"
else
log_warn "Installation Claude Code échouée — installer manuellement après reboot : npm install -g @anthropic-ai/claude-code"
fi
# caveman (plugin Claude Code - réduction tokens) — déplacé ici en v8.8.32 :
# en section 2a il s'installait AVANT Claude Code et échouait systématiquement
# --yes + </dev/null (v8.9.2) : npx demandait « Ok to proceed? » et attendait —
# blocage indiagnosticable car sudo-rs (26.04) alloue un NOUVEAU pty : la
# réponse tapée dans le terminal n'atteint jamais le prompt. </dev/null coupe
# aussi tout prompt ultérieur de claudepluginhub (échec géré par log_warn).
sudo -u "$UTILISATEUR" env PATH="$NPM_PATH" \
npx --yes claudepluginhub juliusbrussee/caveman --plugin caveman </dev/null \
&& log_ok "Plugin caveman installé" \
|| log_warn "Plugin caveman : installation échouée (lancer manuellement après connexion)"
}
################################################################################
# 24. INSTALLATION VERACRYPT
################################################################################
section_24() {
VERACRYPT_VER=$(curl -s "https://api.github.com/repos/veracrypt/VeraCrypt/releases/latest" \
| grep '"tag_name"' | grep -oP '(?<=VeraCrypt_)[\d.]+') || true
if [ -n "$VERACRYPT_VER" ]; then
# Détection version Ubuntu + fallback (VeraCrypt suit la LTS, pas la dernière release)
# lire_os_release (v8.9.3) : le sourcing était tué par « readonly VERSION »
# → « aucun paquet trouvé pour Ubuntu /24.04/22.04 » aux tests réels
UBUNTU_VER=$(lire_os_release VERSION_ID)
if [[ -z "$UBUNTU_VER" ]]; then
UBUNTU_VER="26.04"
log_warn "VERSION_ID vide dans /etc/os-release — fallback 26.04"
fi
INSTALLED=0
for v in "$UBUNTU_VER" "24.04" "22.04"; do
VERACRYPT_DEB="veracrypt-console-${VERACRYPT_VER}-Ubuntu-${v}-amd64.deb"
URL="https://github.com/veracrypt/VeraCrypt/releases/download/VeraCrypt_${VERACRYPT_VER}/${VERACRYPT_DEB}"
if wget -q --spider "$URL"; then
wget -q "$URL" -O /tmp/veracrypt.deb \
&& apt install -y /tmp/veracrypt.deb \
&& log_ok "VeraCrypt ${VERACRYPT_VER} installé (paquet Ubuntu-${v})" \
&& INSTALLED=1 \
&& break \
|| log_warn "VeraCrypt : install paquet Ubuntu-${v} échouée — tentative suivante"
fi
done
[ "$INSTALLED" -eq 0 ] && log_warn "VeraCrypt : aucun paquet trouvé pour Ubuntu ${UBUNTU_VER}/24.04/22.04"
rm -f /tmp/veracrypt.deb
else
log_warn "VeraCrypt : version introuvable via GitHub — installer manuellement depuis https://www.veracrypt.fr"
fi
}
################################################################################
# 25. INSTALLATION FORTUNE-MOD + COWSAY + FORTUNES FRANÇAISES
################################################################################
section_25() {
# Bibliothèque de citations PERSO (v8.9.14) : fortunes-fr a disparu des dépôts
# Ubuntu 26.04 et le .deb du pool 24.04 finira par disparaître aussi — les
# 30 catégories de textes (contenu GPL du paquet) sont désormais VERSIONNÉES
# dans le dépôt git du NAS, à côté de ce script. Installation : copie des
# textes + index .dat régénérés par strfile (fortune-mod, toujours dans les
# dépôts). Plus AUCUNE dépendance à un paquet fortunes-fr.
# Source des citations : le dépôt git du NAS (source de vérité, toujours à
# jour), avec REPLI sur une copie locale fortunes-fr/ à côté du script si le
# NAS est indisponible (v8.9.17). Surchargeable via FORTUNES_FR_SRC.
if [[ -z "${FORTUNES_FR_SRC:-}" ]]; then
FORTUNES_FR_SRC="/mnt/nas/sauvegarde/git/scripts/Postinstall_Ubuntu_26.04/Script_avec_variables_dans_un_fichier_conf/fortunes-fr"
if [[ ! -d "$FORTUNES_FR_SRC" ]]; then
FORTUNES_FR_SRC="$(dirname "$(realpath "$0")")/fortunes-fr"
log_info "Source NAS indisponible — repli sur la copie locale : $FORTUNES_FR_SRC"
fi
fi
FORTUNES_FR_DEST="/usr/share/games/fortunes/fr"
if [[ -d "$FORTUNES_FR_SRC" ]] && command -v strfile &>/dev/null; then
mkdir -p "$FORTUNES_FR_DEST"
nb_categories=0
for src in "$FORTUNES_FR_SRC"/*; do
[[ -f "$src" ]] || continue
nom=$(basename "$src")
cp "$src" "$FORTUNES_FR_DEST/$nom" \
&& strfile "$FORTUNES_FR_DEST/$nom" "$FORTUNES_FR_DEST/$nom.dat" >/dev/null 2>&1 \
&& nb_categories=$((nb_categories + 1)) \
|| log_warn "fortunes-fr : échec sur la catégorie $nom"
done
if [[ $nb_categories -gt 0 ]]; then
log_ok "Fortune-mod + cowsay + citations françaises installés ($nb_categories catégories depuis le NAS)"
else
log_warn "fortunes-fr : aucune catégorie installée depuis $FORTUNES_FR_SRC"
fi
else
log_warn "fortunes-fr : source NAS introuvable ($FORTUNES_FR_SRC) ou strfile absent — citations françaises non installées"
fi
}
################################################################################
# 26. INSTALLATION DE L'AGENT DWSERVICE
################################################################################
section_26() {
# echo "1" (v8.9.1) : l'installeur affiche un menu (1=Installer, 2=Exécuter,
# 3=Refuser) et attend une réponse — stdin indisponible via « | tee »,
# le script restait bloqué indéfiniment. Réponse « Installer » passée en stdin.
# log_ok conditionné à la réussite effective (message mensonger au test réel).
if curl -fsSL https://www.dwservice.net/download/dwagent.sh -o /tmp/dwagent.sh \
&& chmod +x /tmp/dwagent.sh \
&& echo "1" | /tmp/dwagent.sh -console; then
log_ok "Agent DWService installé en mode console"
else
log_warn "DWService : téléchargement ou installation échoué"
fi
rm -f /tmp/dwagent.sh
}
################################################################################
# 26b. GNOME REMOTE DESKTOP — RDP NATIF WAYLAND (v8.9.16)
# Deux modes complémentaires (identifiants RDP_USER/RDP_PASS depuis le .conf) :
# - SYSTÈME (headless) : connexion possible dès l'écran de login, sans
# session ouverte — administration à distance de la machine.
# - SESSION : partage de la session graphique ouverte de $UTILISATEUR.
# GNOME gère le port 3389 en handover : le daemon système délègue au daemon
# de session quand une session partagée est active.
################################################################################
section_26b() {
# Section sautée proprement si les identifiants manquent (rétro-compatibilité
# avec les .conf antérieurs à v8.9.16 — variables non critiques)
if [[ -z "${RDP_USER:-}" || -z "${RDP_PASS:-}" ]]; then
log_warn "RDP_USER/RDP_PASS absents du .conf — GNOME Remote Desktop non configuré"
return 0
fi
# Préinstallé sur Ubuntu 26.04 desktop — installation de rattrapage sinon
paquet_est_installe gnome-remote-desktop \
|| apt install -y gnome-remote-desktop \
|| log_warn "gnome-remote-desktop : installation échouée"
# Wrapper : commande exécutée dans le contexte utilisateur (bus + runtime)
grd_user() {
sudo -u "$UTILISATEUR" env \
DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/${USER_UID}/bus" \
XDG_RUNTIME_DIR="/run/user/${USER_UID}" "$@"
}
# ---- Mode SYSTÈME (headless) ----
# Le daemon système n'écoute PAS sans certificat TLS (vérifié au test réel :
# « TLS certificate: (null) », port 3389 fermé) → auto-signé 10 ans dans
# /var/lib/gnome-remote-desktop (home de l'utilisateur de service).
GRD_DIR="/var/lib/gnome-remote-desktop"
if [[ ! -f "$GRD_DIR/tls.crt" ]]; then
openssl req -new -newkey rsa:4096 -days 3650 -nodes -x509 \
-subj "/CN=${HOSTNAME_MACHINE}" \
-out "$GRD_DIR/tls.crt" -keyout "$GRD_DIR/tls.key" 2>/dev/null \
&& log_ok "Certificat TLS RDP système généré (auto-signé, 10 ans)" \
|| log_warn "RDP système : génération du certificat TLS échouée"
chown gnome-remote-desktop:gnome-remote-desktop "$GRD_DIR/tls.crt" "$GRD_DIR/tls.key" 2>/dev/null || true
chmod 600 "$GRD_DIR/tls.key" 2>/dev/null || true
fi
grdctl --system rdp set-tls-cert "$GRD_DIR/tls.crt" || log_warn "RDP système : set-tls-cert échoué"
grdctl --system rdp set-tls-key "$GRD_DIR/tls.key" || log_warn "RDP système : set-tls-key échoué"
grdctl --system rdp set-credentials "$RDP_USER" "$RDP_PASS" || log_warn "RDP système : set-credentials échoué"
grdctl --system rdp enable || log_warn "RDP système : enable échoué"
systemctl enable --now gnome-remote-desktop.service || log_warn "RDP : service système non démarré"
systemctl restart gnome-remote-desktop.service || true
sleep 2
if ss -tln | grep -q ':3389'; then
log_ok "RDP headless actif — port 3389 en écoute (connexion possible dès l'écran de login)"
else
log_warn "RDP : port 3389 pas en écoute — vérifier : journalctl -u gnome-remote-desktop"
fi
# ---- Mode SESSION (partage de la session de $UTILISATEUR) ----
# Le daemon de session a son propre certificat (~/.local/share/…). Les
# credentials vont dans le TROUSSEAU : il doit être déverrouillé — OK quand le
# script est lancé depuis la session graphique ; sinon (SSH) le set-credentials
# échoue (« locked collection ») → consigne manuelle loguée.
GRD_USER_DIR="/home/$UTILISATEUR/.local/share/gnome-remote-desktop"
sudo -u "$UTILISATEUR" mkdir -p "$GRD_USER_DIR"
if [[ ! -f "$GRD_USER_DIR/rdp-tls.crt" ]]; then
sudo -u "$UTILISATEUR" openssl req -new -newkey rsa:4096 -days 3650 -nodes -x509 \
-subj "/CN=${HOSTNAME_MACHINE}" \
-out "$GRD_USER_DIR/rdp-tls.crt" -keyout "$GRD_USER_DIR/rdp-tls.key" 2>/dev/null \
&& sudo -u "$UTILISATEUR" chmod 600 "$GRD_USER_DIR/rdp-tls.key" \
&& log_ok "Certificat TLS RDP session généré" \
|| log_warn "RDP session : génération du certificat échouée"
fi
grd_user grdctl rdp set-tls-cert "$GRD_USER_DIR/rdp-tls.crt" || log_warn "RDP session : set-tls-cert échoué"
grd_user grdctl rdp set-tls-key "$GRD_USER_DIR/rdp-tls.key" || log_warn "RDP session : set-tls-key échoué"
if grd_user grdctl rdp set-credentials "$RDP_USER" "$RDP_PASS" 2>/dev/null; then
log_ok "RDP session : identifiants enregistrés dans le trousseau"
else
log_warn "RDP session : trousseau verrouillé — après ouverture de session, lancer : grdctl rdp set-credentials $RDP_USER '<mot de passe du .conf>'"
fi
grd_user grdctl rdp enable || log_warn "RDP session : enable échoué"
grd_user grdctl rdp disable-view-only || true
grd_user systemctl --user enable --now gnome-remote-desktop.service 2>/dev/null || true
log_ok "GNOME Remote Desktop configuré — client RDP (Remmina/mstsc) vers ${HOSTNAME_MACHINE}:3389"
}
################################################################################
# 27. CONFIGURATION CRONTAB RAPPELS QUOTIDIENS + SCAN CLAMAV HEBDOMADAIRE
# Note 26.04 : Ubuntu 26.04 est Wayland-only (GNOME 50).
# - DISPLAY=:0 est conservé pour XWayland (les apps X11 continuent de fonctionner)
# - WAYLAND_DISPLAY=wayland-0 est ajouté pour notify-send natif Wayland
# - Les deux variables garantissent la compatibilité dans les deux cas
################################################################################
section_27() {
# Rappels user : fichier dans /etc/cron.d/ (idempotent, ne touche pas au crontab user)
# Format /etc/cron.d : <minute> <heure> <jour> <mois> <jour_semaine> <user> <commande>
CRON_RAPPELS="/etc/cron.d/postinstall-rappels"
DBUS_ADDR="unix:path=/run/user/${USER_UID}/bus"
NOTIFY_ENV="DISPLAY=:0 WAYLAND_DISPLAY=wayland-0 DBUS_SESSION_BUS_ADDRESS=${DBUS_ADDR}"
{
echo "# Géré par postinstall.26.04_v2.sh — ne pas modifier"
echo "# Rappels boulot (lun-ven 04h00 → 09h00, toutes les 30 min) et coucher (22h30 → 02h00)"
echo "# DISPLAY=:0 (XWayland) + WAYLAND_DISPLAY=wayland-0 → compatibilité GNOME 50 Wayland"
echo "SHELL=/bin/bash"
echo "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
echo ""
echo "# === Rappels matin (boulot) — lun-ven, 04:00 à 09:00 par tranches de 30 min ==="
for h in 4 5 6 7 8; do
for m in 0 30; do
echo "${m} ${h} * * 1-5 ${UTILISATEUR} ${NOTIFY_ENV} notify-send \"[Prépare toi à aller au boulot, il est l'heure]\" --hint=string:sound-name:alarm-clock-elapsed"
done
done
echo "0 9 * * 1-5 ${UTILISATEUR} ${NOTIFY_ENV} notify-send \"[Prépare toi à aller au boulot, il est l'heure]\" --hint=string:sound-name:alarm-clock-elapsed"
echo ""
echo "# === Rappels soir (coucher) — tous les jours, 22:30 à 02:00 ==="
echo "30 22 * * * ${UTILISATEUR} ${NOTIFY_ENV} notify-send \"[Prépare toi à aller au lit, il est tard]\" --hint=string:sound-name:alarm-clock-elapsed"
for h in 23 0 1; do
for m in 0 30; do
echo "${m} ${h} * * * ${UTILISATEUR} ${NOTIFY_ENV} notify-send \"[Prépare toi à aller au lit, il est tard]\" --hint=string:sound-name:alarm-clock-elapsed"
done
done
echo "0 2 * * * ${UTILISATEUR} ${NOTIFY_ENV} notify-send \"[Prépare toi à aller au lit, il est tard]\" --hint=string:sound-name:alarm-clock-elapsed"
} > "$CRON_RAPPELS"
chown root:root "$CRON_RAPPELS"
chmod 644 "$CRON_RAPPELS"
log_ok "Crontab rappels installé dans $CRON_RAPPELS (ne touche pas au crontab user)"
# Crontab root : scan ClamAV hebdomadaire — fichier dédié /etc/cron.d/ (préserve crontab root existant)
# Tous les % sont échappés (\%) car cron interprète % non-escape comme fin de commande + newline.
CRON_CLAMAV="/etc/cron.d/postinstall-clamav"
cat > "$CRON_CLAMAV" << EOF
# Géré par postinstall.26.04_v2.sh — Scan antivirus ClamAV hebdomadaire
# samedi 14h00 (freshclam) + 14h30 (scan système local)
# /mnt exclu : le NAS possède son propre antivirus
# Note 26.04 : /run/media remplace /media pour les montages auto de périphériques externes
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO=""
0 14 * * 6 root /usr/bin/freshclam --quiet 2>/dev/null || true
30 14 * * 6 root LOG=/tmp/clamav-\$(date +\%Y\%m\%d).log; /usr/bin/clamscan -r -i --exclude-dir="^/mnt" --exclude-dir="^/media" --exclude-dir="^/run/media" --exclude-dir="^/snap" --exclude-dir="^/proc" --exclude-dir="^/sys" --exclude-dir="^/dev" --exclude-dir="^/run" --exclude-dir="^/boot" --exclude-dir="^/var/lib/docker" --exclude-dir="^/var/cache/apt" / > "\$LOG" 2>&1; find /tmp -name 'clamav-*.log' -mtime +30 -delete 2>/dev/null; grep -q "FOUND" "\$LOG" && printf 'Menaces detectees sur \%s le \%s :\n\n\%s\n\nLog complet : \%s\n' "\$(hostname)" "\$(date +'\%d/\%m/\%Y \%H:\%M')" "\$(grep FOUND \"\$LOG\")" "\$LOG" | mail -s "[ClamAV] Menace detectee sur \$(hostname)" $GMAIL_USER
EOF
chown root:root "$CRON_CLAMAV"
chmod 644 "$CRON_CLAMAV"
log_ok "Scan ClamAV hebdomadaire planifié dans $CRON_CLAMAV (sam. 14h00 freshclam + 14h30 scan)"
# Activation explicite des services ClamAV (v8.9.3) : sur 26.04, le paquet
# laisse clamav-freshclam DISABLED — le service tournait pendant l'installation
# (démarré par le postinst du paquet) mais ne revenait pas après reboot
systemctl enable --now clamav-freshclam \
&& log_ok "clamav-freshclam activé (mise à jour des signatures au boot)" \
|| log_warn "clamav-freshclam : activation échouée"
systemctl enable --now clamav-daemon \
|| log_warn "clamav-daemon : activation échouée"
}
################################################################################
# 28. CONFIGURATION OPENVPN (Suisse + USA + France + Russie)
################################################################################
section_28() {
# ====================== FONCTION ======================
# Les certificats / clés / CA sont stockés dans postinstall.26.04_v2.conf (v8.8.24)
# Le mapping nom → remote CyberGhost et indirection variable est fait via nameref bash.
configure_vpn() {
local name="$1"
local upper="${name^^}"
local user pass ovpn ca cert key remote
case "$name" in
Suisse) remote="87-1-ch.cg-dialup.net 443" ;;
USA) remote="87-1-us.cg-dialup.net 443" ;;
France) remote="87-1-fr.cg-dialup.net 443" ;;
Russie) remote="87-1-ru.cg-dialup.net 443" ;;
*) echo "Erreur : VPN inconnu ($name)"; return 1 ;;
esac
# Récupération via indirection (variables sourcées depuis le .conf)
local user_var="VPN_${upper}_USER" pass_var="VPN_${upper}_PASS"
local cert_var="VPN_${upper}_CERT" key_var="VPN_${upper}_KEY"
user="${!user_var}"
pass="${!pass_var}"
cert="${!cert_var}"
key="${!key_var}"
ca="$VPN_COMMON_CA"
# OVPN spécifique = OVPN commun + ligne remote propre au serveur
ovpn="$VPN_COMMON_OVPN
remote $remote"
# Utiliser /home/$UTILISATEUR — $HOME vaut /root quand on tourne en sudo
local cert_dir="/home/$UTILISATEUR/.cert/nm-openvpn-${name,,}"
mkdir -p "$cert_dir"
chown "$UTILISATEUR":"$UTILISATEUR" "$cert_dir"
chmod 700 "$cert_dir"
log_info "Configuration VPN : $name"
# Idempotence : supprimer toute connexion préexistante portant ce nom
# ou nommée "openvpn" résiduelle d'un import précédent
nmcli connection show "$name" &>/dev/null && nmcli connection delete "$name" >/dev/null 2>&1
nmcli connection show "openvpn" &>/dev/null && nmcli connection delete "openvpn" >/dev/null 2>&1
# umask 077 sur le répertoire temporaire : clé privée jamais en 644
local tmp_dir
tmp_dir=$(mktemp -d)
chmod 700 "$tmp_dir"
(
umask 077
printf '%s\n' "$ovpn" > "$tmp_dir/openvpn.ovpn"
printf '%s\n' "$ca" > "$tmp_dir/ca.crt"
printf '%s\n' "$cert" > "$tmp_dir/client.crt"
printf '%s\n' "$key" > "$tmp_dir/client.key"
)
nmcli connection import type openvpn file "$tmp_dir/openvpn.ovpn" >/dev/null 2>&1 || true
sleep 1
# Renommer la connexion "openvpn" (nom auto issu du fichier .ovpn) en "$name"
if nmcli -g NAME connection show | grep -qx "openvpn"; then
nmcli connection modify "openvpn" connection.id "$name" 2>/dev/null || true
fi
# Copier les fichiers de certificats dans cert_dir (umask déjà 077)
(
umask 077
cp "$tmp_dir/ca.crt" "$cert_dir/${name}-ca.crt"
cp "$tmp_dir/client.crt" "$cert_dir/${name}-client.crt"
cp "$tmp_dir/client.key" "$cert_dir/${name}-client.key"
)
chown "$UTILISATEUR":"$UTILISATEUR" "$cert_dir/${name}-"*
# Configurer vpn.data SANS le password (pas exposé dans ps aux)
nmcli connection modify "$name" \
vpn.data "ca=$cert_dir/${name}-ca.crt,cert=$cert_dir/${name}-client.crt,key=$cert_dir/${name}-client.key,connection-type=password-tls,username=$user,password-flags=0,auth=SHA256,remote-cert-tls=server,ping=5" \
|| log_warn "nmcli modify VPN $name échoué"
# Password injecté via l'éditeur nmcli (commandes set/save/quit sur stdin —
# secret absent de la ligne de commande, donc invisible dans ps aux).
# Repli sur `modify +vpn.secrets` si l'éditeur échoue (le secret y est alors
# visible dans ps aux, mais seulement en dernier recours).
printf 'set vpn.secrets password %s\nsave\nquit\n' "$pass" \
| nmcli connection edit "$name" >/dev/null 2>&1 \
|| nmcli connection modify "$name" +vpn.secrets "password=$pass" >/dev/null 2>&1 \
|| log_warn "Injection password VPN $name échouée"
nmcli connection reload || log_warn "nmcli reload VPN $name échoué"
# Garde : ne jamais lancer rm -rf sur une variable potentiellement vide
[ -n "${tmp_dir:-}" ] && rm -rf "$tmp_dir"
log_ok "VPN $name configuré"
}
# ====================== EXÉCUTION ======================
configure_vpn "Suisse"
configure_vpn "USA"
configure_vpn "France"
configure_vpn "Russie"
log_ok "Configuration OpenVPN terminée (Suisse + USA + France + Russie)"
log_info "Aliases disponibles : vpnsuisse, vpnusa, vpnfr, vpnru, vpnoff, vpnstat"
}
################################################################################
# 29. NETTOYAGE FINAL AVANT REBOOT
################################################################################
section_29() {
apt autoremove -y && apt autoclean -y
log_ok "Système nettoyé"
}
################################################################################
# 29b. VALIDATION POST-REBOOT AUTOMATIQUE (TIMER + MAIL)
################################################################################
section_29b() {
# Au premier boot suivant la post-installation, validate_postinstall.sh est
# exécuté automatiquement (en ${UTILISATEUR}, comme prévu par son en-tête) et
# son rapport est envoyé par mail, puis le timer se désactive (one-shot).
VALIDATE_SRC="$(dirname "$(realpath "$0")")/validate_postinstall.sh"
if [ ! -f "$VALIDATE_SRC" ]; then
log_warn "validate_postinstall.sh introuvable à côté du script — validation post-reboot non planifiée"
return 0
fi
install -m 0755 -o root -g root "$VALIDATE_SRC" /usr/local/bin/validate-postinstall.sh
log_ok "validate-postinstall.sh installé dans /usr/local/bin"
# Wrapper exécuté en root par le service : lance la validation via runuser
# (le script de validation est conçu pour ${UTILISATEUR} + sudo NOPASSWD),
# retire les codes couleur ANSI pour le mail, puis désactive le timer.
# Heredoc non quoté : VERSION/CONF_FILE/UTILISATEUR/GMAIL_USER figés à la
# génération ; les \$ sont évalués à l'exécution post-reboot.
cat > /usr/local/bin/postinstall-validation-mail.sh << EOF
#!/bin/bash
# Validation post-reboot — généré par postinstall.26.04_v2.sh v${VERSION}
# Lancé une seule fois par postinstall-validation.timer au boot suivant
set -uo pipefail
RAPPORT_BRUT=\$(mktemp)
RAPPORT="/var/log/postinstall/validation-mail-\$(date +%Y%m%d_%H%M%S).txt"
# POSTINSTALL_CONF : le .conf est resté là où la post-installation a été lancée
export POSTINSTALL_CONF="${CONF_FILE}"
runuser -u ${UTILISATEUR} -- /usr/local/bin/validate-postinstall.sh > "\$RAPPORT_BRUT" 2>&1
rc=\$?
# Suppression des séquences ANSI (couleurs) pour un mail lisible
sed -e 's/\x1b\[[0-9;]*m//g' "\$RAPPORT_BRUT" > "\$RAPPORT"
rm -f "\$RAPPORT_BRUT"
if [ "\$rc" -eq 0 ]; then statut="OK"; else statut="ECHEC (FAIL detectes)"; fi
mail -s "[postinstall] Validation post-reboot \$statut - \$(hostname)" "${GMAIL_USER}" < "\$RAPPORT" \\
|| echo "Envoi du mail de validation échoué — rapport conservé : \$RAPPORT"
# Auto-désactivation : la validation ne tourne qu'au premier boot suivant
systemctl disable --now postinstall-validation.timer 2>/dev/null || true
exit 0
EOF
chmod 755 /usr/local/bin/postinstall-validation-mail.sh
log_ok "Wrapper postinstall-validation-mail.sh créé"
cat > /etc/systemd/system/postinstall-validation.service << 'EOF'
[Unit]
Description=Validation post-installation (rapport par mail)
Wants=network-online.target
After=network-online.target postfix.service
[Service]
Type=oneshot
ExecStart=/usr/local/bin/postinstall-validation-mail.sh
EOF
cat > /etc/systemd/system/postinstall-validation.timer << 'EOF'
[Unit]
Description=Validation post-installation au premier boot suivant
[Timer]
OnBootSec=4min
[Install]
WantedBy=timers.target
EOF
# enable SANS --now : OnBootSec=4min du boot COURANT est déjà dépassé, --now
# déclencherait la validation immédiatement (avant le reboot) — on veut
# qu'elle parte 4 min après le PROCHAIN démarrage.
systemctl daemon-reload
systemctl enable postinstall-validation.timer >> "$LOG_FILE" 2>&1 \
&& log_ok "Validation post-reboot planifiée (4 min après le prochain démarrage, rapport → ${GMAIL_USER})" \
|| log_warn "postinstall-validation.timer : activation échouée"
}
################################################################################
# 30. RÉSUMÉ FINAL + EMPLACEMENT DU LOG
################################################################################
section_30() {
# ---- Calcul de la durée totale d'exécution (U1 v8.8.24) ----
DUREE=$(( $(date +%s) - START ))
DUREE_FMT=$(printf '%dh %02dm %02ds' $((DUREE/3600)) $((DUREE%3600/60)) $((DUREE%60)))
echo ""
echo "=========================================="
echo " POST-INSTALLATION TERMINÉE (v${VERSION}) !"
echo " Ubuntu 26.04 LTS — Resolute Raccoon"
echo " ⏱ Durée totale : $DUREE_FMT"
echo "=========================================="
echo ""
echo "📋 ACTIONS RÉALISÉES (par catégorie — U2 v8.8.24) :"
echo ""
echo " 🖥 SYSTÈME"
echo " • /home monté sur LVM (UUID) + noatime SSD sur / et /home"
echo " • GRUB dual-boot configuré (patch_grub idempotent)"
echo " • Sudo NOPASSWD pour $UTILISATEUR"
echo " • Dossiers XDG locaux (Documents, Musique, Images, Vidéos dans ~/)"
echo " • Historique Bash optimisé + prompt Git dynamique"
echo " • Journal systemd limité à 500 Mo"
echo " • Mises à jour quotidiennes auto + nettoyage hebdomadaire"
echo ""
echo " 🌐 RÉSEAU & NAS"
echo " • DNS Google (8.8.8.8 + 8.8.4.4)"
echo " • ${#NAS_PARTAGES[@]} partages NAS montés (CIFS) + signets file:///mnt/nas/"
echo " • VPN OpenVPN : Suisse + USA + France + Russie (certs depuis .conf)"
echo " • SSH activé"
echo ""
echo " 🔐 SÉCURITÉ"
echo " • UFW désactivé (choix utilisateur)"
echo " • ClamAV installé + scan hebdomadaire (samedi 14h30)"
echo " • VeraCrypt + scripts ~/.mount_1984.sh / ~/.umount_1984.sh"
echo " • Postfix relay Gmail configuré (→ $GMAIL_USER)"
echo " • Mises à jour sécurité automatiques (unattended-upgrades)"
echo ""
echo " 🛠 OUTILS DEV"
echo " • Docker Engine (dernière version)"
echo " • KVM/libvirt + virt-manager"
echo " • Ansible, Vim, Git, nodejs, npm, jq, tree, ncdu, pv"
echo " • Claude Code (npm global) + ccstatusline + caveman plugin"
echo ""
echo " 🎬 MULTIMÉDIA & BUREAU"
echo " • Navigateurs : Chrome, Chromium (snap), Brave, Opera, Firefox snap"
echo " • Flatpak : Discord, VLC, Bambu Studio, Bottles, ClamUI, Telegram, MuseScore"
echo " • yt-dlp (binaire GitHub) + alias mp3dl"
echo " • GNOME : Nautilus, extensions (gsconnect, clipboard-indicator — dash-to-dock : manuel)"
echo " • Ptyxis thème Solarized Dark, horloge (secondes + jour)"
echo " • fortune fr | cowsay | lolcat à chaque terminal"
echo ""
echo " 📡 SUPERVISION (Centreon)"
echo " • NRPE actif sur port 5666 (allowed: 192.168.1.50, 192.168.122.10)"
echo " • SNMP configuré"
echo " • Centreon Monitoring Agent (CMA) installé"
echo " • Groupe monitoring + sudoers restreints (smartctl, nft, iptables, ...)"
echo ""
echo "🎵 yt-dlp — télécharger une vidéo en MP3 :"
echo " mp3dl https://www.youtube.com/watch?v=..."
echo ""
echo "🤖 Claude Code — lancer avec : claude (authentif. au 1er lancement : claude auth)"
echo ""
echo "🖥 NOTES UBUNTU 26.04 (Resolute Raccoon) :"
echo " • Terminal par défaut : Ptyxis (thème Solarized si Ptyxis lancé avant ce script)"
echo " • Chromium via snap, MuseScore via Flatpak"
echo " • Wayland-only — gnome-screensaver remplacé par loginctl lock-session"
echo ""
echo "📄 LOG COMPLET : $LOG_FILE"
echo " Erreurs/warnings : grep -E 'WARN|ERREUR' $LOG_FILE"
echo ""
echo "🔧 APRÈS REBOOT :"
echo " • claude auth → authentifier Claude Code"
echo " • Steam → Paramètres → Stockage → ajouter bibliothèque (~/snap/steam/common/.local/share/Steam/steamapps)"
echo " • GSConnect → appairer avec KDE Connect Android"
echo " • VirtualBox (Oracle 7.2) → Additions Invité incluses (menu Périphériques) ;"
echo " se reconnecter pour activer le groupe vboxusers (USB)"
echo " • DWService → lancer 'dwagent' et entrer le code"
echo " • Dash to Dock → installer via Extension Manager (absent des dépôts 26.04)"
echo ""
log_ok "Post-installation v${VERSION} terminée en $DUREE_FMT — log : $LOG_FILE"
# ---- Récapitulatif des anomalies (fonction partagée avec le trap INT/TERM) ----
afficher_anomalies
# Envoi du log complet par email
log_section "Envoi du rapport par email"
# Construire un résumé court (pas le log brut complet — fuite de données)
# Inclut uniquement : statut global, anomalies, et les lignes [OK/WARN/ERREUR]
RAPPORT="/tmp/postinstall-rapport-$(date +%Y%m%d_%H%M%S).txt"
{
echo "Post-installation v${VERSION} — $(hostname) — $(date '+%F %T') — Durée : $DUREE_FMT"
echo ""
if [ ${#ANOMALIES[@]} -eq 0 ]; then
echo "Statut : ✅ AUCUNE ANOMALIE"
else
echo "Statut : ❌ ${#ANOMALIES[@]} ANOMALIE(S)"
echo ""
echo "--- Anomalies ---"
printf ' %s\n' "${ANOMALIES[@]}"
fi
echo ""
echo "--- Dernières 50 entrées OK/WARN/ERREUR ---"
grep -E '\[(OK|WARN|ERREUR)\]' "$LOG_FILE" | tail -50 || true
echo ""
echo "Log complet sur $(hostname) : $LOG_FILE"
} > "$RAPPORT"
mail -s "[postinstall] Rapport v${VERSION} - $(hostname)" "$GMAIL_USER" < "$RAPPORT" \
&& log_ok "Rapport envoyé à $GMAIL_USER" \
|| log_warn "Envoi du rapport échoué — vérifier Postfix"
rm -f "$RAPPORT"
}
################################################################################
# 31. REBOOT
################################################################################
section_31() {
# Désarmement du trap INT/TERM (dispatcher) : pendant le compte à rebours,
# Ctrl+C doit ANNULER le reboot — pas déclencher le récapitulatif d'interruption
trap - INT TERM
if [[ "${REBOOT_AUTO:-true}" == "true" ]]; then
echo "Redémarrage dans 20 secondes (Ctrl+C pour annuler)..."
for i in {20..1}; do
printf "\rRedémarrage dans %2d secondes..." "$i"
sleep 1
done
echo ""
sync
reboot
else
log_info "REBOOT_AUTO != true — redémarrage automatique désactivé"
log_info "Pensez à redémarrer manuellement : sudo reboot"
fi
}
################################################################################
# DISPATCHER — EXÉCUTION ORDONNÉE DES SECTIONS
################################################################################
# Interruption (Ctrl+C, kill) : loguer la section en cours + récapitulatif
# des anomalies avant de sortir. Désarmé par la section 31 (compte à rebours).
SECTION_COURANTE="initialisation"
trap 'echo ""; log_error "Exécution interrompue par signal pendant la section : ${SECTION_COURANTE}"; afficher_anomalies; exit 130' INT TERM
TOTAL_SECTIONS=${#SECTIONS[@]}
INDEX_SECTION=0
DEMARRAGE_ATTEINT=true
[[ -n "$FROM_ID" ]] && DEMARRAGE_ATTEINT=false
[[ -n "$FROM_ID" ]] && log_info "Mode reprise : exécution à partir de la section $FROM_ID"
[[ -n "$ONLY_ID" ]] && log_info "Mode section unique : exécution de la seule section $ONLY_ID"
for entree_section in "${SECTIONS[@]}"; do
id_section="${entree_section%%|*}"
titre_section="${entree_section#*|}"
INDEX_SECTION=$((INDEX_SECTION + 1))
# Filtre --only : tout sauf la section demandée est ignoré
if [[ -n "$ONLY_ID" && "$id_section" != "$ONLY_ID" ]]; then
continue
fi
# Filtre --from : ignorer jusqu'à la section de reprise (incluse ensuite)
if [[ "$DEMARRAGE_ATTEINT" == false ]]; then
if [[ "$id_section" == "$FROM_ID" ]]; then
DEMARRAGE_ATTEINT=true
else
continue
fi
fi
SECTION_COURANTE="$id_section — $titre_section"
log_section "[$INDEX_SECTION/$TOTAL_SECTIONS] $titre_section"
debut_section=$(date +%s)
"section_${id_section}"
log_info "⏱ Section $id_section terminée en $(( $(date +%s) - debut_section ))s"
done
Le modèle de configuration — postinstall.26.04_v2.conf.example (complet)
À copier en postinstall.26.04_v2.conf puis à remplir. C’est le seul fichier qui contiendra vos secrets — il est exclu du versionnage et verrouillé root:600 par le script.
################################################################################
# postinstall.26.04_v2.conf — Configuration du script de post-installation
# Ubuntu 26.04 LTS "Resolute Raccoon"
#
# Fait avec amour par Romain J.
#
# ⚠️ CE FICHIER CONTIENT DES MOTS DE PASSE EN CLAIR
# Permissions recommandées : chmod 600 postinstall.26.04_v2.conf
# Ne jamais versionner dans un dépôt public (Git, GitHub, etc.)
#
# Utilisation :
# sudo bash postinstall.26.04_v2.sh
# (le script cherche postinstall.26.04_v2.conf dans son propre répertoire)
#
# Ou avec un chemin personnalisé :
# POSTINSTALL_CONF=/chemin/vers/mon.conf sudo bash postinstall.26.04_v2.sh
#
# Checklist avant exécution :
# 1. ⚠️ NAS_PASSWORD → mot de passe du NAS
# 2. ⚠️ GMAIL_APP_PASSWORD → App Password Gmail (16 caractères, espaces inclus)
# 3. ⚠️ VERACRYPT_PASS → mot de passe du conteneur VeraCrypt
# 4. ⚠️ VPN_*_USER / PASS → identifiants CyberGhost par connexion
# 5. UTILISATEUR → nom du compte principal (actuellement : utilisateur)
# 6. LVM_HOME_DEVICE → vérifier avec : sudo lvdisplay
################################################################################
# ==============================================================================
# UTILISATEUR PRINCIPAL
# ==============================================================================
UTILISATEUR="utilisateur"
# Nom de machine appliqué via hostnamectl (postfix myhostname, /etc/hosts,
# supervision Centreon) — quel que soit le nom saisi pendant l'installation
HOSTNAME_MACHINE="ma-machine"
# ==============================================================================
# COMPORTEMENT DU SCRIPT
# ==============================================================================
# Redémarrage automatique en fin de post-installation (true/false)
# Mettre à false pour terminer sans reboot (redémarrage manuel ensuite)
REBOOT_AUTO=true
# ==============================================================================
# NAS
# ==============================================================================
NAS_IP="192.168.1.100"
# ⚠️ À MODIFIER — mot de passe du partage CIFS
NAS_USER="ma-machine"
NAS_PASSWORD='***PURGE***' # ⚠️ À REMPLACER AVANT EXÉCUTION
# Liste des partages NAS (ordre affiché dans les signets Nautilus)
NAS_PARTAGES=(
animation
documents
downloads
films
Imprimante
jeux
livres
musique
perso
photographie
sauvegarde
seriestv
surveillance
Technique
)
# Emplacement du fichier credentials CIFS (créé automatiquement par le script)
NAS_CREDENTIALS="/root/.smbcredentials"
# ==============================================================================
# MONTAGE /home SUR LVM
# ==============================================================================
# Vérifier l'existence du device : sudo lvdisplay /dev/home-vg/home_lv
LVM_HOME_DEVICE="/dev/home-vg/home_lv"
# ==============================================================================
# GMAIL — RELAY POSTFIX
# ==============================================================================
GMAIL_USER="prenom.nom@gmail.com"
# ⚠️ À MODIFIER — App Password Gmail (16 chars, généré sur myaccount.google.com)
# Format avec espaces : "xxxx xxxx xxxx xxxx"
GMAIL_APP_PASSWORD="***PURGE***" # ⚠️ À REMPLACER (App Password Gmail)
# ==============================================================================
# VERACRYPT
# ==============================================================================
# Fichier qui stockera le mot de passe VeraCrypt sur le système (chmod 600)
VERACRYPT_PASS_FILE="/home/$UTILISATEUR/.veracrypt_pass"
# ⚠️ À MODIFIER — mot de passe du conteneur VeraCrypt
VERACRYPT_PASS="***PURGE***" # ⚠️ À REMPLACER
# Chemin du conteneur chiffré sur le NAS
VERACRYPT_CONTAINER="/mnt/nas/chemin/vers/conteneur.vc" # ⚠️ À ADAPTER
# Point de montage VeraCrypt
VERACRYPT_MOUNTPOINT="/media/veracrypt1"
# ==============================================================================
# CENTREON MONITORING AGENT (CMA)
# ==============================================================================
# ⚠️ Token JWT du broker Centreon — secret long-lived (exp: null)
# À régénérer via l'interface Centreon en cas de compromission
CMA_TOKEN="***PURGE***" # ⚠️ À REMPLACER (généré dans Centreon)
# Endpoint du serveur Centreon (host:port)
CMA_ENDPOINT="centreon:4317"
# Nom d'hôte affiché dans Centreon
CMA_HOST="MaMachine_avec_CMA"
# ==============================================================================
# UBUNTU PRO
# ==============================================================================
# ⚠️ Token d'attachement Ubuntu Pro — récupérable sur https://ubuntu.com/pro/dashboard
# À régénérer via le dashboard en cas de compromission.
# Laisser vide pour ne PAS attacher la machine (section 2g sautée avec warn).
PRO_TOKEN="***PURGE***" # ⚠️ À REMPLACER (token Ubuntu Pro) ou laisser vide
# ==============================================================================
# VPN — IDENTIFIANTS OPENVPN (CyberGhost)
# ==============================================================================
# ⚠️ À MODIFIER — identifiants spécifiques à chaque connexion CyberGhost
VPN_SUISSE_USER="***PURGE***" # ⚠️ À REMPLACER
VPN_SUISSE_PASS="***PURGE***" # ⚠️ À REMPLACER
VPN_USA_USER="***PURGE***" # ⚠️ À REMPLACER
VPN_USA_PASS="***PURGE***" # ⚠️ À REMPLACER
VPN_FRANCE_USER="***PURGE***" # ⚠️ À REMPLACER
VPN_FRANCE_PASS="***PURGE***" # ⚠️ À REMPLACER
VPN_RUSSIE_USER="***PURGE***" # ⚠️ À REMPLACER
VPN_RUSSIE_PASS="***PURGE***" # ⚠️ À REMPLACER
# ==============================================================================
# VPN — CERTIFICATS ET CLÉS OPENVPN (CyberGhost)
# ==============================================================================
# ⚠️ SECRETS — déplacés ici en v8.8.24 (avant : embarqués dans le .sh)
# Le .conf doit rester en chmod 600 (cf. checklist en tête du script)
# Configuration OVPN commune aux 4 serveurs
read -r -d '' VPN_COMMON_OVPN << 'OVPN_EOF' || true
client
dev tun
proto udp
auth-user-pass
resolv-retry infinite
redirect-gateway def1
persist-key
persist-tun
nobind
data-ciphers AES-256-GCM:AES-128-GCM:AES-256-CBC
data-ciphers-fallback AES-256-CBC
auth SHA256
ping 5
explicit-exit-notify 2
script-security 2
remote-cert-tls server
route-delay 5
verb 4
ca ca.crt
cert client.crt
key client.key
OVPN_EOF
# CA CyberGhost (commune aux 4 serveurs)
read -r -d '' VPN_COMMON_CA << 'CA_EOF' || true
-----BEGIN CERTIFICATE-----
MIIGWjCCBEKgAwIBAgIJAJxUG61mxDS7MA0GCSqGSIb3DQEBDQUAMHsxCzAJBgNV
BAYTAlJPMRIwEAYDVQQHEwlCdWNoYXJlc3QxGDAWBgNVBAoTD0N5YmVyR2hvc3Qg
Uy5BLjEbMBkGA1UEAxMSQ3liZXJHaG9zdCBSb290IENBMSEwHwYJKoZIhvcNAQkB
FhJpbmZvQGN5YmVyZ2hvc3Qucm8wHhcNMTcwNjE5MDgxNzI1WhcNMzcwNjE0MDgx
NzI1WjB7MQswCQYDVQQGEwJSTzESMBAGA1UEBxMJQnVjaGFyZXN0MRgwFgYDVQQK
Ew9DeWJlckdob3N0IFMuQS4xGzAZBgNVBAMTEkN5YmVyR2hvc3QgUm9vdCBDQTEh
MB8GCSqGSIb3DQEJARYSaW5mb0BjeWJlcmdob3N0LnJvMIICIjANBgkqhkiG9w0B
AQEFAAOCAg8AMIICCgKCAgEA7O8+mji2FlQhJXn/G4VLrKPjGtxgQBAdjo0dZEQz
KX08q14dLkslmOLgShStWKrOiLXGAvB1rPvvk613jtA0KjQLpgyLy9lIWohQKYjj
5jrJYXMZMkbSHBYI9L8L7iezBEFYrjYKdDo51nq99wRFhKdbyKKjDh3e2L2SVEZL
T1ogkK5gWzjvH+mjjtjUUicK+YjGwWOz6I+KKaG4Ve/D/cE6nCLbhHIMMnargZEu
7sqA6BFeS4kEP/ZdCZoTSX2n43XV1q63nJt/v0KDetbZDciFVW9h9SVPG4qT44p0
550N+Mom7zTX7S/ID5T9dplgU8sRGtIMrG0cIMD9zmpFgUnMusCrR7jJFr0sMAve
TbgZg95LmstV6R6WKZkSFdUrE0DHl4dHoZvTFX+1LhwhHgjgDLaosX0vhG/C/7Lp
oVWimd6RRQT3M9o4Fa1TuhfvBzQ20QHrmRV/yKvGNK0xckZ6EZ/QY7Z55ORU15Tg
ab4ebnblYPWoEmn0mIYP3LFFeoR5OS1EX7+j4kPv+bwPGsmpHjxmZyq2Y7sJBpbO
CJgbkn52WZdPBIRDpPdIHQ8pAJC4T0iMK9xvAwWNl/V6EYYNpR97osyEDXn+BTdA
HlhJ5fck9KlwI9mb1Kg1bhbvbmaIAiOLenSULYf3j6rI1ygo3R2cCyybtuAq8M7z
0OECAwEAAaOB4DCB3TAdBgNVHQ4EFgQU6tdK1g/He5qzjeAoM5eHt4in9iUwga0G
A1UdIwSBpTCBooAU6tdK1g/He5qzjeAoM5eHt4in9iWhf6R9MHsxCzAJBgNVBAYT
AlJPMRIwEAYDVQQHEwlCdWNoYXJlc3QxGDAWBgNVBAoTD0N5YmVyR2hvc3QgUy5B
LjEbMBkGA1UEAxMSQ3liZXJHaG9zdCBSb290IENBMSEwHwYJKoZIhvcNAQkBFhJp
bmZvQGN5YmVyZ2hvc3Qucm+CCQCcVButZsQ0uzAMBgNVHRMEBTADAQH/MA0GCSqG
SIb3DQEBDQUAA4ICAQDNyQ92kj4qiNjnHk99qvnFw9qGfwB9ofaPL74zh0G5hEe3
Wgb2o4fqUGnvUNgOu53gJksz3DcPQ8t40wfmm9I1Z8tiM9qrqvkuQ+nKcLgdooXt
EsTybPIYDZ2cWR/5E0TKRvC7RFzKgQ4D77Vbi4TdaHiDV7ZNfU1iLCoBGcYm80hc
UHEs5KIVLwUmcSOTmbZBySJxcSD0yUpS7nlZGwLY6VQrU+JFwDSisbXT4DXf3iSz
p7FzW0/u/SFvWsPHrjE0hkPoZPalYvouaJEHKAhip0ZwSmitlxbBnmm8+K/3c9mL
A5/uXrirfpuhhs8V3lyV2mczVtSiTl6gpi88gc//JY80JeHdupjO25T3XEzY9cpx
ecmkWaUEjLMx4wVoXQuUiPonfILM6OLwi+zUS8gQErdFeGvcQXbncPa4SdJuHkF8
lgiX2i8S8fPGdXvU37E9bdAXwP5nZriYq1s0D59Qfvz+vLXVkmyZp6ztxjKjKole
mPMak0Y5c1Q4RjNF6tmQoFuy/ACSkWy14Tzu2dFp7UiVbGg1FOvKhfs48zC2/IUQ
v1arqmPT/9LVq3B2DVT9UKXRUXX/f/jSSsVjkz4uUe2jUyL+XHX1nSmROTPHSAJ+
oKf0BLnfqUxFkEUTwLnayssP2nwGgq35b7wEbTFIXdrjHGFUVQIDeERz8UThew==
-----END CERTIFICATE-----
CA_EOF
# Certificats et clés par serveur — chaque utilisateur CyberGhost a sa paire
# ⚠️ À REMPLACER par les certificats/clés téléchargés depuis l'espace CyberGhost
read -r -d '' VPN_SUISSE_CERT << 'SUISSE_CERT_EOF' || true
-----BEGIN CERTIFICATE-----
COLLER_ICI_LE_CERTIFICAT_CLIENT_SUISSE
-----END CERTIFICATE-----
SUISSE_CERT_EOF
read -r -d '' VPN_SUISSE_KEY << 'SUISSE_KEY_EOF' || true
-----BEGIN PRIVATE KEY-----
COLLER_ICI_LA_CLE_PRIVEE_SUISSE
-----END PRIVATE KEY-----
SUISSE_KEY_EOF
read -r -d '' VPN_USA_CERT << 'USA_CERT_EOF' || true
-----BEGIN CERTIFICATE-----
COLLER_ICI_LE_CERTIFICAT_CLIENT_USA
-----END CERTIFICATE-----
USA_CERT_EOF
read -r -d '' VPN_USA_KEY << 'USA_KEY_EOF' || true
-----BEGIN PRIVATE KEY-----
COLLER_ICI_LA_CLE_PRIVEE_USA
-----END PRIVATE KEY-----
USA_KEY_EOF
read -r -d '' VPN_FRANCE_CERT << 'FRANCE_CERT_EOF' || true
-----BEGIN CERTIFICATE-----
COLLER_ICI_LE_CERTIFICAT_CLIENT_FRANCE
-----END CERTIFICATE-----
FRANCE_CERT_EOF
read -r -d '' VPN_FRANCE_KEY << 'FRANCE_KEY_EOF' || true
-----BEGIN PRIVATE KEY-----
COLLER_ICI_LA_CLE_PRIVEE_FRANCE
-----END PRIVATE KEY-----
FRANCE_KEY_EOF
read -r -d '' VPN_RUSSIE_CERT << 'RUSSIE_CERT_EOF' || true
-----BEGIN CERTIFICATE-----
COLLER_ICI_LE_CERTIFICAT_CLIENT_RUSSIE
-----END CERTIFICATE-----
RUSSIE_CERT_EOF
read -r -d '' VPN_RUSSIE_KEY << 'RUSSIE_KEY_EOF' || true
-----BEGIN PRIVATE KEY-----
COLLER_ICI_LA_CLE_PRIVEE_RUSSIE
-----END PRIVATE KEY-----
RUSSIE_KEY_EOF
# ============================================================
# GNOME Remote Desktop (RDP) — section 26b (v8.9.16)
# Identifiants de connexion RDP (headless + partage de session)
# ============================================================
RDP_USER="VOTRE_UTILISATEUR_RDP"
RDP_PASS="VOTRE_MOT_DE_PASSE_RDP"
La validation post-reboot — validate_postinstall.sh (complet)
Exécuté automatiquement 4 minutes après le premier redémarrage par un timer systemd à usage unique ; le rapport arrive par mail.
#!/bin/bash
# =============================================================================
# Fait avec amour par Romain J.
# -----------------------------------------------------------------------------
# Nom : validate_postinstall.sh
# Description : Validation post-installation Ubuntu 26.04 — contrôle automatique
# de tout ce qu'a fait postinstall.26.04_v2.sh
# Date : 2026-06-09
# Version : 1.2
# Cible : postinstall.26.04_v2.sh v8.8.29+ (CMA en mode TLS)
# Usage : ./validate_postinstall.sh
# Contexte : exécuté par utilisateur (pas root) après postinstall + reboot
# =============================================================================
set -uo pipefail
IFS=$'\n\t'
# ── Localisation du .conf (source unique de vérité, avec repli) ─────────────────
readonly CONF_FILE="${POSTINSTALL_CONF:-$(dirname "$(realpath "$0")")/postinstall.26.04_v2.conf}"
# Lit une variable scalaire du .conf : lecture directe si lisible, sinon via sudo -n.
# Renvoie une chaîne vide si le .conf est inaccessible (600 root + pas de sudo en cache).
# Limite : ne gère que les valeurs sans quotes ou entre doubles quotes — une valeur
# entre quotes simples (ex. NAS_PASSWORD) renverrait une chaîne vide.
lire_conf() {
local cle="$1" val=""
if [[ -r "${CONF_FILE}" ]]; then
val=$(grep -oP "^${cle}=\"?\K[^\"]+" "${CONF_FILE}" 2>/dev/null | head -n1)
elif sudo -n true 2>/dev/null; then
val=$(sudo grep -oP "^${cle}=\"?\K[^\"]+" "${CONF_FILE}" 2>/dev/null | head -n1)
fi
printf '%s' "${val}"
}
# ── Variables globales ────────────────────────────────────────────────────────
# Déclaration séparée du readonly (SC2155 : un readonly combiné masquerait
# le code retour de la substitution de commande)
DATE_EXECUTION=$(date +%Y%m%d_%H%M%S)
readonly DATE_EXECUTION
readonly REPERTOIRE_LOG="/var/log/postinstall"
readonly JOURNAL="${REPERTOIRE_LOG}/validation-${DATE_EXECUTION}.log"
# Dérivées du .conf quand il est lisible, sinon valeurs de repli (cohérence avec le .sh)
_user=$(lire_conf UTILISATEUR); readonly UTILISATEUR_ATTENDU="${_user:-utilisateur}"
_nasip=$(lire_conf NAS_IP); readonly NAS_IP="${_nasip:-192.168.1.100}"
unset _user _nasip
# Couleurs ANSI
readonly C_VERT='\033[0;32m'
readonly C_ORANGE='\033[0;33m'
readonly C_ROUGE='\033[0;31m'
readonly C_BLEU='\033[0;34m'
readonly C_GRAS='\033[1m'
readonly C_RESET='\033[0m'
# Compteurs et listes
NB_OK=0
NB_WARN=0
NB_FAIL=0
NB_SKIP=0
POINTS_MANUELS=()
# ── Préparation du journal ────────────────────────────────────────────────────
preparer_journal() {
if [[ ! -d "${REPERTOIRE_LOG}" ]]; then
sudo mkdir -p "${REPERTOIRE_LOG}" 2>/dev/null || {
# Fallback : log dans /tmp si /var/log/postinstall inaccessible
echo "AVERTISSEMENT : impossible de créer ${REPERTOIRE_LOG}, fallback /tmp" >&2
local nouveau_journal="/tmp/validation-${DATE_EXECUTION}.log"
# Réassigner via variable globale non readonly de fallback
JOURNAL_EFFECTIF="${nouveau_journal}"
return
}
sudo chown "${UTILISATEUR_ATTENDU}:${UTILISATEUR_ATTENDU}" "${REPERTOIRE_LOG}" 2>/dev/null || true
fi
sudo touch "${JOURNAL}" 2>/dev/null || true
sudo chown "${UTILISATEUR_ATTENDU}:${UTILISATEUR_ATTENDU}" "${JOURNAL}" 2>/dev/null || true
JOURNAL_EFFECTIF="${JOURNAL}"
}
# ── Fonctions de journalisation ───────────────────────────────────────────────
# Écrit dans le journal (sans couleur)
ecrire_journal() {
local message="$1"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ${message}" >> "${JOURNAL_EFFECTIF}" 2>/dev/null || true
}
# Affiche un titre de section
val_section() {
local titre="$1"
echo ""
echo -e "${C_BLEU}${C_GRAS}═══════════════════════════════════════════════════════════════════════════════${C_RESET}"
echo -e "${C_BLEU}${C_GRAS} ${titre}${C_RESET}"
echo -e "${C_BLEU}${C_GRAS}═══════════════════════════════════════════════════════════════════════════════${C_RESET}"
ecrire_journal "===== ${titre} ====="
}
# Affiche un test OK (vert)
val_ok() {
local message="$1"
echo -e " ${C_VERT}[ OK ]${C_RESET} ${message}"
ecrire_journal "[OK] ${message}"
NB_OK=$((NB_OK + 1))
}
# Affiche un avertissement (orange) — non bloquant
val_warn() {
local message="$1"
echo -e " ${C_ORANGE}[WARN]${C_RESET} ${message}"
ecrire_journal "[WARN] ${message}"
NB_WARN=$((NB_WARN + 1))
}
# Affiche un échec (rouge)
val_fail() {
local message="$1"
echo -e " ${C_ROUGE}[FAIL]${C_RESET} ${message}"
ecrire_journal "[FAIL] ${message}"
NB_FAIL=$((NB_FAIL + 1))
}
# Affiche un test ignoré (sudo indisponible, etc.)
val_skip() {
local message="$1"
echo -e " ${C_ORANGE}[SKIP]${C_RESET} ${message}"
ecrire_journal "[SKIP] ${message}"
NB_SKIP=$((NB_SKIP + 1))
}
# Ajoute un point à vérifier manuellement
ajouter_point_manuel() {
POINTS_MANUELS+=("$1")
}
# ── Helpers ───────────────────────────────────────────────────────────────────
# Vérifie si sudo est utilisable sans mot de passe (cache valide)
sudo_dispo() {
sudo -n true 2>/dev/null
}
# Vérifie si un paquet apt est installé
paquet_installe() {
dpkg -s "$1" 2>/dev/null | grep -q "^Status: install ok installed"
}
# Vérifie si un service systemd est actif
service_actif() {
systemctl is-active --quiet "$1" 2>/dev/null
}
# ── Section 1 : SYSTÈME DE BASE ───────────────────────────────────────────────
valider_systeme_base() {
val_section "[1] SYSTÈME DE BASE"
# Utilisateur utilisateur
if id "${UTILISATEUR_ATTENDU}" &>/dev/null; then
val_ok "Utilisateur ${UTILISATEUR_ATTENDU} existe"
# Groupes
local groupes_attendus=(docker kvm libvirt sudo)
local groupes_utilisateur
groupes_utilisateur=$(id -nG "${UTILISATEUR_ATTENDU}" 2>/dev/null || echo "")
for groupe in "${groupes_attendus[@]}"; do
if echo " ${groupes_utilisateur} " | grep -q " ${groupe} "; then
val_ok "Groupe '${groupe}' présent pour ${UTILISATEUR_ATTENDU}"
else
val_fail "Groupe '${groupe}' MANQUANT pour ${UTILISATEUR_ATTENDU}"
fi
done
else
val_fail "Utilisateur ${UTILISATEUR_ATTENDU} introuvable"
fi
# /home sur LVM
if mount | grep -E "on /home " | grep -q "mapper" 2>/dev/null; then
val_ok "/home monté sur LVM"
elif mount | grep -qE "on /home "; then
val_warn "/home monté mais peut-être pas sur LVM (vérifier : mount | grep /home)"
else
val_fail "/home n'est pas un point de montage séparé"
fi
# noatime sur / et /home
local options_root
options_root=$(findmnt -no OPTIONS / 2>/dev/null || echo "")
if echo "${options_root}" | grep -q "noatime"; then
val_ok "noatime actif sur /"
else
val_warn "noatime absent sur / (options : ${options_root})"
fi
if findmnt /home &>/dev/null; then
local options_home
options_home=$(findmnt -no OPTIONS /home 2>/dev/null || echo "")
if echo "${options_home}" | grep -q "noatime"; then
val_ok "noatime actif sur /home"
else
val_warn "noatime absent sur /home (options : ${options_home})"
fi
else
val_warn "/home pas un montage séparé — noatime non vérifiable"
fi
# Espace disque /
local espace_libre_gib
espace_libre_gib=$(df -BG / 2>/dev/null | awk 'NR==2 {gsub("G",""); print $4}' || echo "0")
if [[ "${espace_libre_gib}" -gt 5 ]]; then
val_ok "Espace libre sur / : ${espace_libre_gib} GiB (> 5 GiB)"
else
val_fail "Espace libre sur / : ${espace_libre_gib} GiB (≤ 5 GiB)"
fi
# Attachement Ubuntu Pro (section 2g) — SKIP si aucun token dans le .conf
local pro_token
pro_token=$(lire_conf PRO_TOKEN)
if [[ -z "${pro_token}" || "${pro_token}" == *PURGE* ]]; then
val_skip "Ubuntu Pro : PRO_TOKEN non renseigné dans le .conf — attachement non attendu"
elif ! command -v pro &>/dev/null; then
val_fail "Ubuntu Pro : client 'pro' introuvable"
elif pro status --format json 2>/dev/null | grep -qE '"attached":[[:space:]]*true'; then
val_ok "Ubuntu Pro : machine attachée au contrat"
else
val_fail "Ubuntu Pro : machine NON attachée (relancer : sudo bash postinstall.26.04_v2.sh --only 2g)"
fi
}
# ── Section 2 : PAQUETS ESSENTIELS ────────────────────────────────────────────
valider_paquets() {
val_section "[2] PAQUETS ESSENTIELS"
local paquets=(
git vim htop curl jq tree ncdu rsync ansible
docker-ce qemu-system-x86 virt-manager flatpak
postfix clamav nagios-nrpe-server snmpd smartmontools
veracrypt-console nodejs npm pwgen
)
# Note : yt-dlp n'est PAS un paquet apt (binaire GitHub dans /usr/local/bin) —
# il est validé via `command -v yt-dlp` en section [10].
# wget et snapd retirés : préinstallés Ubuntu, jamais installés par le .sh
# (évite un faux négatif). snapd est couvert via `snap list` en section [6].
for paquet in "${paquets[@]}"; do
if paquet_installe "${paquet}"; then
val_ok "Paquet installé : ${paquet}"
else
val_fail "Paquet MANQUANT : ${paquet}"
fi
done
}
# ── Section 3 : SERVICES SYSTEMD ──────────────────────────────────────────────
valider_services() {
val_section "[3] SERVICES SYSTEMD"
local services=(
ssh docker postfix
nagios-nrpe-server snmpd
clamav-freshclam smartmontools
)
for service in "${services[@]}"; do
if service_actif "${service}"; then
val_ok "Service actif : ${service}"
else
val_fail "Service INACTIF : ${service}"
fi
done
# Services tolérés inactifs (dépendances externes) → WARN, pas FAIL :
# - centagent : inactif si le serveur Centreon est injoignable
# - clamav-daemon : peut ne pas démarrer tant que la base virale n'est pas prête
for service in centagent clamav-daemon; do
if service_actif "${service}"; then
val_ok "Service actif : ${service}"
else
val_warn "Service inactif : ${service} (toléré — dépendance externe)"
fi
done
# libvirtd ou virtqemud (selon version)
if service_actif "libvirtd"; then
val_ok "Service actif : libvirtd"
elif service_actif "virtqemud"; then
val_ok "Service actif : virtqemud (remplace libvirtd)"
else
val_fail "Service INACTIF : libvirtd ET virtqemud"
fi
# Timer update-system
if systemctl is-active --quiet "update-system.timer" 2>/dev/null; then
val_ok "Timer actif : update-system.timer"
else
val_fail "Timer INACTIF : update-system.timer"
fi
}
# ── Section 4 : RÉSEAU ET NAS ─────────────────────────────────────────────────
valider_reseau() {
val_section "[4] RÉSEAU ET NAS"
# IP statique (v8.9.15) : le postinstall fige l'IP DHCP en manuel
local conn_active methode_ip ip_actuelle
conn_active=$(nmcli -t -f NAME,DEVICE connection show --active 2>/dev/null | grep -E 'enp|eth|eno|wl' | cut -d: -f1 | head -n1)
if [[ -n "${conn_active}" ]]; then
methode_ip=$(nmcli -g ipv4.method connection show "${conn_active}" 2>/dev/null)
ip_actuelle=$(nmcli -g IP4.ADDRESS connection show "${conn_active}" 2>/dev/null | head -1)
if [[ "${methode_ip}" == "manual" ]]; then
val_ok "IP statique configurée : ${ip_actuelle} (${conn_active})"
else
val_fail "IP en DHCP (${methode_ip:-inconnue}) sur ${conn_active} — le postinstall doit la figer"
fi
else
val_fail "Aucune connexion réseau active détectée (nmcli)"
fi
# DNS
if ping -c 1 -W 3 google.com &>/dev/null; then
val_ok "Résolution DNS Google opérationnelle"
else
val_fail "Résolution DNS Google échoue (ping google.com)"
fi
# GNOME Remote Desktop (v8.9.16) : service système + port RDP
if systemctl is-active --quiet gnome-remote-desktop.service 2>/dev/null; then
if ss -tln 2>/dev/null | grep -q ':3389'; then
val_ok "GNOME Remote Desktop actif (port 3389 en écoute)"
else
val_fail "GNOME Remote Desktop actif mais port 3389 fermé (certificat TLS ?)"
fi
else
val_warn "GNOME Remote Desktop inactif (RDP non configuré ou service arrêté)"
fi
# Montages NAS (x-systemd.automount → montage différé : on déclenche l'accès
# avant le test pour éviter un faux négatif juste après reboot)
for partage in sauvegarde Technique; do
ls "/mnt/nas/${partage}" >/dev/null 2>&1 || true
if mountpoint -q "/mnt/nas/${partage}" 2>/dev/null; then
val_ok "NAS monté : /mnt/nas/${partage}"
else
val_fail "NAS NON monté : /mnt/nas/${partage}"
fi
done
# SSH NAS
if ssh -o ConnectTimeout=5 -o BatchMode=yes -o StrictHostKeyChecking=no \
"${UTILISATEUR_ATTENDU}@${NAS_IP}" -p 22 "true" &>/dev/null; then
val_ok "Connexion SSH NAS (${NAS_IP}:22) opérationnelle"
else
val_warn "Connexion SSH NAS (${NAS_IP}:22) échoue (vérifier clé SSH)"
fi
# Port NRPE 5666
if ss -tln 2>/dev/null | grep -q ":5666 "; then
val_ok "Port NRPE 5666 en écoute"
else
val_fail "Port NRPE 5666 absent (NRPE inopérant)"
fi
# Port SNMP 161
if ss -uln 2>/dev/null | grep -q ":161 "; then
val_ok "Port SNMP 161 en écoute"
else
val_fail "Port SNMP 161 absent (snmpd inopérant)"
fi
# Port CMA 4317 sur centreon (nc si présent, sinon repli /dev/tcp de bash)
local centreon_ok=1
if command -v nc &>/dev/null; then
nc -z -w 3 centreon 4317 &>/dev/null || centreon_ok=0
else
timeout 3 bash -c 'exec 3<>/dev/tcp/centreon/4317' &>/dev/null || centreon_ok=0
fi
if [[ "${centreon_ok}" -eq 1 ]]; then
val_ok "Serveur Centreon joignable (centreon:4317)"
else
val_fail "Serveur Centreon INJOIGNABLE (centreon:4317)"
fi
# Entrée /etc/hosts pour 'centreon' (ajoutée par le .sh, section 2d)
if grep -qE '^[^#]\S*\s+centreon(\s|$)' /etc/hosts 2>/dev/null; then
val_ok "/etc/hosts : entrée 'centreon' présente"
else
val_fail "/etc/hosts : entrée 'centreon' MANQUANTE"
fi
}
# ── Section 5 : MONITORING CENTREON ───────────────────────────────────────────
valider_centreon() {
val_section "[5] MONITORING CENTREON"
# Fichier centagent.json
local centagent_json="/etc/centreon-monitoring-agent/centagent.json"
if [[ -f "${centagent_json}" ]] || sudo test -f "${centagent_json}" 2>/dev/null; then
if sudo_dispo; then
if sudo jq empty "${centagent_json}" 2>/dev/null; then
val_ok "centagent.json présent et JSON valide"
# Mode de chiffrement attendu : full (TLS)
local enc_mode
enc_mode=$(sudo jq -r '.encryption // "absent"' "${centagent_json}" 2>/dev/null || echo "absent")
if [[ "${enc_mode}" == "full" ]]; then
val_ok "centagent.json : encryption=full (TLS actif)"
else
val_fail "centagent.json : encryption=${enc_mode} (attendu : full)"
fi
else
val_fail "centagent.json présent mais JSON INVALIDE"
fi
else
val_skip "centagent.json présent (sudo indisponible pour valider JSON/encryption)"
fi
else
val_fail "centagent.json INTROUVABLE (${centagent_json})"
fi
# Service centagent (déjà testé en [3] mais utile ici)
if service_actif "centagent"; then
val_ok "Service centagent actif"
else
val_fail "Service centagent INACTIF"
fi
# Certificat TLS centreon-agent.crt (mode encryption: full)
local centreon_crt="/etc/centreon-monitoring-agent/centreon-agent.crt"
if [[ -f "${centreon_crt}" ]] || sudo test -f "${centreon_crt}" 2>/dev/null; then
if sudo_dispo; then
if sudo openssl x509 -checkend 86400 -noout -in "${centreon_crt}" &>/dev/null; then
val_ok "centreon-agent.crt valide (non expiré dans les 24h)"
else
val_fail "centreon-agent.crt EXPIRÉ ou expire sous 24h"
fi
else
val_skip "centreon-agent.crt présent (sudo indisponible pour vérifier expiration)"
fi
else
val_fail "centreon-agent.crt INTROUVABLE (${centreon_crt})"
fi
# nrpe.cfg
if [[ -f "/etc/nagios/nrpe.cfg" ]] || sudo test -f "/etc/nagios/nrpe.cfg" 2>/dev/null; then
val_ok "nrpe.cfg présent (/etc/nagios/nrpe.cfg)"
else
val_fail "nrpe.cfg INTROUVABLE"
fi
# Sondes
local sondes=(
/usr/lib/nagios/plugins/check_mem.sh
/usr/lib/nagios/plugins/check_mem.pl
/usr/lib/nagios/plugins/check_service.sh
/usr/local/bin/cma_test_ok.sh
)
for sonde in "${sondes[@]}"; do
if [[ -x "${sonde}" ]]; then
val_ok "Sonde présente et exécutable : ${sonde}"
else
val_fail "Sonde MANQUANTE ou non exécutable : ${sonde}"
fi
done
# Test check_mem.sh
if [[ -x "/usr/lib/nagios/plugins/check_mem.sh" ]]; then
local sortie_mem
sortie_mem=$(/usr/lib/nagios/plugins/check_mem.sh 2>&1 || true)
if echo "${sortie_mem}" | grep -qE "^(OK|WARNING|CRITICAL)"; then
val_ok "check_mem.sh retourne un état Nagios valide"
else
val_warn "check_mem.sh : sortie inattendue (${sortie_mem:0:60})"
fi
fi
# Test check_service.sh ssh
if [[ -x "/usr/lib/nagios/plugins/check_service.sh" ]]; then
local sortie_svc
sortie_svc=$(/usr/lib/nagios/plugins/check_service.sh ssh 2>&1 || true)
if echo "${sortie_svc}" | grep -qE "^OK"; then
val_ok "check_service.sh ssh : OK"
else
val_warn "check_service.sh ssh : non OK (${sortie_svc:0:60})"
fi
fi
}
# ── Section 6 : NAVIGATEURS ET APPLICATIONS ───────────────────────────────────
valider_applications() {
val_section "[6] NAVIGATEURS ET APPLICATIONS"
# Navigateurs (binaires)
local navigateurs=(google-chrome brave-browser opera)
for nav in "${navigateurs[@]}"; do
if command -v "${nav}" &>/dev/null; then
val_ok "Navigateur installé : ${nav}"
else
val_fail "Navigateur MANQUANT : ${nav}"
fi
done
# Flatpak apps
if command -v flatpak &>/dev/null; then
local flatpak_apps=(
"com.discordapp.Discord:Discord"
"org.videolan.VLC:VLC"
"org.telegram.desktop:Telegram"
)
local flatpak_liste
flatpak_liste=$(flatpak list --app --columns=application 2>/dev/null || echo "")
for entree in "${flatpak_apps[@]}"; do
local app_id="${entree%%:*}"
local nom="${entree##*:}"
if echo "${flatpak_liste}" | grep -q "^${app_id}$"; then
val_ok "Flatpak installé : ${nom} (${app_id})"
else
val_fail "Flatpak MANQUANT : ${nom} (${app_id})"
fi
done
else
val_fail "flatpak introuvable — impossible de vérifier les apps Flatpak"
fi
# Snap apps
if command -v snap &>/dev/null; then
local snap_liste
snap_liste=$(snap list 2>/dev/null | awk 'NR>1 {print $1}' || echo "")
local snaps=(codium signal-desktop chromium)
for s in "${snaps[@]}"; do
if echo "${snap_liste}" | grep -q "^${s}$"; then
val_ok "Snap installé : ${s}"
else
val_fail "Snap MANQUANT : ${s}"
fi
done
else
val_fail "snap introuvable — impossible de vérifier les snaps"
fi
}
# ── Section 7 : BASH ET ALIASES ───────────────────────────────────────────────
valider_bash() {
val_section "[7] BASH ET ALIASES"
local bashrc="/home/${UTILISATEUR_ATTENDU}/.bashrc"
local aliases="/home/${UTILISATEUR_ATTENDU}/.bash_aliases"
# .bashrc
if [[ -s "${bashrc}" ]]; then
val_ok "~/.bashrc présent et non vide"
if grep -q "bash_aliases" "${bashrc}" 2>/dev/null; then
val_ok "~/.bashrc source bien ~/.bash_aliases"
else
val_warn "~/.bashrc ne semble pas sourcer ~/.bash_aliases"
fi
else
val_fail "~/.bashrc absent ou vide"
fi
# .bash_aliases
if [[ -s "${aliases}" ]]; then
val_ok "~/.bash_aliases présent"
local aliases_critiques=(lok pw mp3dl ytdlpup veracryptup update vpnsuisse cvscan)
for a in "${aliases_critiques[@]}"; do
if grep -qE "^\s*alias\s+${a}=" "${aliases}" 2>/dev/null \
|| grep -qE "^\s*${a}\(\)" "${aliases}" 2>/dev/null; then
val_ok "Alias/fonction définie : ${a}"
else
val_fail "Alias/fonction MANQUANTE : ${a}"
fi
done
else
val_fail "~/.bash_aliases absent"
fi
# fortune + cowsay + lolcat — PATH étendu à /usr/games : lancé par le timer
# systemd, le PATH ne contient pas /usr/games → faux négatifs (test réel)
for binaire in fortune cowsay lolcat; do
if PATH="${PATH}:/usr/games" command -v "${binaire}" &>/dev/null; then
val_ok "Binaire présent : ${binaire}"
else
val_fail "Binaire MANQUANT : ${binaire}"
fi
done
}
# ── Section 8 : SÉCURITÉ ──────────────────────────────────────────────────────
valider_securite() {
val_section "[8] SÉCURITÉ"
# /root/.smbcredentials
if sudo_dispo; then
if sudo test -f /root/.smbcredentials; then
local perms_smb
perms_smb=$(sudo stat -c "%a" /root/.smbcredentials 2>/dev/null || echo "?")
if [[ "${perms_smb}" == "600" ]]; then
val_ok "/root/.smbcredentials présent et chmod 600"
else
val_fail "/root/.smbcredentials présent mais chmod ${perms_smb} (attendu : 600)"
fi
else
val_fail "/root/.smbcredentials INTROUVABLE"
fi
else
val_skip "/root/.smbcredentials (sudo indisponible)"
fi
# ~/.veracrypt_pass
local veracrypt_pass="/home/${UTILISATEUR_ATTENDU}/.veracrypt_pass"
if [[ -f "${veracrypt_pass}" ]]; then
local perms_vc
perms_vc=$(stat -c "%a" "${veracrypt_pass}" 2>/dev/null || echo "?")
if [[ "${perms_vc}" == "600" ]]; then
val_ok "~/.veracrypt_pass présent et chmod 600"
else
val_fail "~/.veracrypt_pass présent mais chmod ${perms_vc} (attendu : 600)"
fi
else
val_fail "~/.veracrypt_pass INTROUVABLE"
fi
# /etc/sudoers.d/monitoring-plugins-custom
local sudoers_mon="/etc/sudoers.d/monitoring-plugins-custom"
if sudo_dispo; then
if sudo test -f "${sudoers_mon}"; then
local perms_sud
perms_sud=$(sudo stat -c "%a" "${sudoers_mon}" 2>/dev/null || echo "?")
if [[ "${perms_sud}" == "440" ]]; then
val_ok "${sudoers_mon} présent et chmod 440"
else
val_fail "${sudoers_mon} présent mais chmod ${perms_sud} (attendu : 440)"
fi
else
val_fail "${sudoers_mon} INTROUVABLE"
fi
else
val_skip "${sudoers_mon} (sudo indisponible)"
fi
# /etc/sudoers.d/90-<utilisateur> (NOPASSWD)
local sudoers_user="/etc/sudoers.d/90-${UTILISATEUR_ATTENDU}"
if sudo_dispo; then
if sudo test -f "${sudoers_user}"; then
local perms_u
perms_u=$(sudo stat -c "%a" "${sudoers_user}" 2>/dev/null || echo "?")
if [[ "${perms_u}" == "440" ]]; then
val_ok "${sudoers_user} présent et chmod 440"
else
val_warn "${sudoers_user} présent mais chmod ${perms_u} (attendu : 440)"
fi
else
val_fail "${sudoers_user} INTROUVABLE"
fi
else
val_skip "${sudoers_user} (sudo indisponible)"
fi
# VeraCrypt installé
if command -v veracrypt &>/dev/null; then
val_ok "VeraCrypt installé"
else
val_fail "VeraCrypt MANQUANT"
fi
# UFW désactivé
if command -v ufw &>/dev/null; then
if sudo_dispo; then
# LANG=C : en locale française « État : inactif » ne matche pas
# « inactive » → faux positif « UFW actif » (test réel)
if sudo LANG=C ufw status 2>/dev/null | grep -qi "inactive"; then
val_ok "UFW désactivé (conforme)"
else
val_warn "UFW actif (attendu : inactif)"
fi
else
val_skip "Statut UFW (sudo indisponible)"
fi
else
val_ok "UFW non installé (équivalent à désactivé)"
fi
}
# ── Section 9 : DÉVELOPPEMENT ─────────────────────────────────────────────────
valider_developpement() {
val_section "[9] DÉVELOPPEMENT"
# Claude Code — installé dans ~/.npm-global/bin, souvent hors PATH non interactif
local claude_bin=""
if command -v claude &>/dev/null; then
claude_bin="$(command -v claude)"
elif [[ -x "/home/${UTILISATEUR_ATTENDU}/.npm-global/bin/claude" ]]; then
claude_bin="/home/${UTILISATEUR_ATTENDU}/.npm-global/bin/claude"
fi
if [[ -n "${claude_bin}" ]]; then
val_ok "Claude Code installé (${claude_bin})"
else
val_fail "Claude Code (claude) introuvable (PATH et ~/.npm-global/bin)"
fi
# Node.js >= 18
if command -v node &>/dev/null; then
local node_ver
node_ver=$(node --version 2>/dev/null | sed 's/^v//' || echo "0")
local node_major="${node_ver%%.*}"
if [[ "${node_major}" =~ ^[0-9]+$ ]] && [[ "${node_major}" -ge 18 ]]; then
val_ok "Node.js v${node_ver} (>= 18)"
else
val_fail "Node.js v${node_ver} (< 18 requis)"
fi
else
val_fail "Node.js introuvable"
fi
# npm
if command -v npm &>/dev/null; then
local npm_ver
npm_ver=$(npm --version 2>/dev/null || echo "?")
val_ok "npm installé (v${npm_ver})"
else
val_fail "npm introuvable"
fi
# Docker hello-world
if command -v docker &>/dev/null; then
if docker run --rm hello-world &>/dev/null; then
val_ok "Docker fonctionne (hello-world OK)"
else
val_fail "Docker présent mais 'hello-world' échoue (utilisateur dans le groupe docker ?)"
fi
else
val_fail "Docker introuvable"
fi
}
# ── Section 10 : MISE À JOUR AUTOMATIQUE ──────────────────────────────────────
valider_maj_auto() {
val_section "[10] MISE À JOUR AUTOMATIQUE"
local script_maj="/usr/local/bin/update-system.sh"
if [[ -x "${script_maj}" ]]; then
val_ok "${script_maj} présent et exécutable"
else
val_fail "${script_maj} MANQUANT ou non exécutable"
fi
if systemctl is-active --quiet "update-system.timer" 2>/dev/null; then
val_ok "update-system.timer actif"
else
val_fail "update-system.timer INACTIF"
fi
# Timer de nettoyage hebdomadaire (section 17 du .sh)
if systemctl is-active --quiet "cleanup-system.timer" 2>/dev/null; then
val_ok "cleanup-system.timer actif"
else
val_fail "cleanup-system.timer INACTIF"
fi
# Crons postinstall (rappels quotidiens + scan ClamAV hebdomadaire)
for cron in /etc/cron.d/postinstall-rappels /etc/cron.d/postinstall-clamav; do
if [[ -f "${cron}" ]] || sudo test -f "${cron}" 2>/dev/null; then
val_ok "Cron présent : ${cron}"
else
val_fail "Cron MANQUANT : ${cron}"
fi
done
if command -v yt-dlp &>/dev/null; then
local ytdlp_ver
ytdlp_ver=$(yt-dlp --version 2>/dev/null || echo "?")
val_ok "yt-dlp installé (version : ${ytdlp_ver})"
else
val_fail "yt-dlp introuvable"
fi
}
# ── Section 11 : VPN ──────────────────────────────────────────────────────────
valider_vpn() {
val_section "[11] VPN"
local profils=(Suisse USA France Russie)
local liste_nm
liste_nm=$(nmcli -t -f NAME connection show 2>/dev/null || echo "")
for profil in "${profils[@]}"; do
if echo "${liste_nm}" | grep -Fxq "${profil}"; then
val_ok "Profil VPN NetworkManager présent : ${profil}"
else
val_fail "Profil VPN NetworkManager MANQUANT : ${profil}"
fi
done
}
# ── Section 12 : VÉRIFICATIONS MANUELLES REQUISES ─────────────────────────────
afficher_verifications_manuelles() {
val_section "[12] VÉRIFICATIONS MANUELLES REQUISES"
ajouter_point_manuel "Connexion VPN fonctionnelle → tester : vpnsuisse puis vpnoff"
ajouter_point_manuel "Envoi de mail Gmail → echo test | mail -s \"test postinstall\" prenom.nom@gmail.com"
# Nom d'hôte CMA lu depuis centagent.json (plus de valeur en dur : le nom
# varie selon la machine — MaMachine_avec_CMA n'était vrai que sur le poste principal)
local hote_cma
hote_cma=$(sudo -n grep -oP '"host"\s*:\s*"\K[^"]+' /etc/centreon-monitoring-agent/centagent.json 2>/dev/null || echo "voir centagent.json")
ajouter_point_manuel "CMA visible dans Centreon → vérifier l'interface web (hôte : ${hote_cma})"
ajouter_point_manuel "Montage VeraCrypt → tester : 1984 puis 1984q"
ajouter_point_manuel "Extensions GNOME chargées → vérifier visuellement (gnome-extensions list)"
ajouter_point_manuel "GSConnect apparié → vérifier appariement avec téléphone Android"
ajouter_point_manuel "DWService opérationnel → lancer : dwagent et tester accès distant"
echo ""
echo -e "${C_ORANGE} Les points suivants ne sont pas testables automatiquement.${C_RESET}"
echo -e "${C_ORANGE} Ils sont listés à nouveau dans le résumé final ci-dessous.${C_RESET}"
}
# ── Résumé final ──────────────────────────────────────────────────────────────
afficher_resume() {
local total=$((NB_OK + NB_WARN + NB_FAIL))
echo ""
echo -e "${C_BLEU}${C_GRAS}═══════════════════════════════════════════════════════════════════════════════${C_RESET}"
echo -e "${C_BLEU}${C_GRAS} RÉSUMÉ DE LA VALIDATION POST-INSTALLATION${C_RESET}"
echo -e "${C_BLEU}${C_GRAS}═══════════════════════════════════════════════════════════════════════════════${C_RESET}"
echo ""
echo -e " Total de tests automatiques : ${C_GRAS}${total}${C_RESET}"
echo -e " ${C_VERT}[ OK ]${C_RESET} : ${C_VERT}${C_GRAS}${NB_OK}${C_RESET}"
echo -e " ${C_ORANGE}[WARN]${C_RESET} : ${C_ORANGE}${C_GRAS}${NB_WARN}${C_RESET}"
echo -e " ${C_ROUGE}[FAIL]${C_RESET} : ${C_ROUGE}${C_GRAS}${NB_FAIL}${C_RESET}"
echo -e " ${C_ORANGE}[SKIP]${C_RESET} : ${C_ORANGE}${NB_SKIP}${C_RESET} (sudo indisponible ou non testable)"
echo ""
if [[ "${NB_FAIL}" -eq 0 && "${NB_WARN}" -eq 0 ]]; then
echo -e " ${C_VERT}${C_GRAS}✔ INSTALLATION VALIDÉE — aucun point bloquant détecté.${C_RESET}"
elif [[ "${NB_FAIL}" -eq 0 ]]; then
echo -e " ${C_ORANGE}${C_GRAS}⚠ INSTALLATION VALIDÉE AVEC ${NB_WARN} AVERTISSEMENT(S) — à examiner.${C_RESET}"
else
echo -e " ${C_ROUGE}${C_GRAS}✘ ${NB_FAIL} POINT(S) À CORRIGER — installation incomplète.${C_RESET}"
fi
echo ""
echo -e "${C_BLEU}${C_GRAS}─── Vérifications manuelles à effectuer ──────────────────────────────────────${C_RESET}"
local i=1
for point in "${POINTS_MANUELS[@]}"; do
echo -e " ${C_ORANGE}${i}.${C_RESET} ${point}"
i=$((i + 1))
done
echo ""
echo -e " Journal complet : ${C_GRAS}${JOURNAL_EFFECTIF}${C_RESET}"
echo ""
echo -e "${C_BLEU} Fait avec amour par Romain J.${C_RESET}"
echo ""
# Trace dans le journal
ecrire_journal "===== RÉSUMÉ ====="
ecrire_journal "OK=${NB_OK} WARN=${NB_WARN} FAIL=${NB_FAIL} SKIP=${NB_SKIP}"
}
# ── Code principal ────────────────────────────────────────────────────────────
main() {
# Avertissement si lancé en root
if [[ "${EUID}" -eq 0 ]]; then
echo -e "${C_ORANGE}AVERTISSEMENT : ce script est conçu pour être lancé par ${UTILISATEUR_ATTENDU}, pas root.${C_RESET}"
fi
# Pré-chauffe sudo AVANT toute opération sudo (préparation du journal incluse)
if sudo -n true 2>/dev/null; then
: # sudo déjà en cache
else
echo ""
echo -e "${C_ORANGE}Certains tests nécessitent sudo. Si refus, ils seront marqués [SKIP].${C_RESET}"
sudo -v 2>/dev/null || true
fi
preparer_journal
echo -e "${C_BLEU}${C_GRAS}Validation post-installation Ubuntu 26.04${C_RESET}"
echo -e "Date : $(date '+%Y-%m-%d %H:%M:%S')"
echo -e "Utilisateur : $(id -un)"
echo -e "Hôte : $(hostname)"
echo -e "Journal : ${JOURNAL_EFFECTIF}"
ecrire_journal "Démarrage validation — utilisateur $(id -un) sur $(hostname)"
valider_systeme_base || true
valider_paquets || true
valider_services || true
valider_reseau || true
valider_centreon || true
valider_applications || true
valider_bash || true
valider_securite || true
valider_developpement || true
valider_maj_auto || true
valider_vpn || true
afficher_verifications_manuelles || true
afficher_resume
# Code de sortie : 1 si au moins un FAIL (utile en automatisation/CI), 0 sinon
[[ "${NB_FAIL}" -eq 0 ]]
}
main "$@"
Conclusion
Ce script n’est pas un produit : c’est un outil personnel, publié tel quel parce que son squelette — configuration séparée et verrouillée, sections rejouables pilotées par un tableau unique, journalisation exhaustive, validation automatique après reboot — se transpose à n’importe quel environnement. Supprimez les sections qui ne vous concernent pas, remplissez le .conf, et faites-en le vôtre. Et si vous ne repartez qu’avec une idée, prenez celle-ci : un /home sur son propre LVM et un script idempotent transforment la réinstallation, corvée redoutée, en opération de routine d’une heure, café compris.


