Infrastructure haute disponibilité
Un site WordPress qui reste en ligne quand un serveur tombe : 2 serveurs web derrière HAProxy, une base MariaDB Galera répliquée sur 3 nœuds, GLPI, et un réseau découpé en 4 VLAN filtrés par pfSense.
🎯 Contexte et objectifs
Le problème
Une organisation héberge un site vitrine WordPress et un outil de gestion de parc GLPI. Chaque application repose sur un seul serveur web et une seule base de données : une mise à jour ratée, un disque plein ou un redémarrage, et le service s'arrête.
Client ──► Serveur web ──► Base de données
(SPOF) (SPOF)
Les objectifs
- Supprimer les points uniques de défaillance (SPOF) des couches web et données.
- Répliquer la base de manière synchrone, sans perte de données.
- Répartir la charge et écarter automatiquement un serveur en panne.
- Segmenter le réseau avec un filtrage deny-by-default.
- Prouver la tolérance aux pannes par des tests reproductibles.
- Documenter pour qu'un tiers puisse reconstruire le lab.
🏗️ Architecture
Pour voir les flux circuler, ouvrir la fiche de chaque machine et simuler une panne : le schéma interactif →
Plan d'adressage
| VM | IP | VLAN | Rôle |
|---|---|---|---|
| VLAN 301 · vWEB · 10.0.10.0/24 · passerelle .254 | |||
| WEB-01 | 10.0.10.1 | 301 | Apache + WordPress |
| HAProxy | 10.0.10.2 | 301 | Répartiteur HTTP (:80, :8080) et SQL (:3306) |
| GLPI-01 | 10.0.10.3 | 301 | Apache + GLPI |
| WEB-02 | 10.0.10.4 | 301 | Apache + WordPress |
| VLAN 302 · vBDD · 10.0.20.0/24 | |||
| MYSQL-01 / 02 / 03 | 10.0.20.1 – .3 | 302 | MariaDB 10.11 + Galera 4 |
| VLAN 303 · vADMIN · 10.0.30.0/24 — VLAN 304 · vUSER · 10.0.40.0/24 | |||
| ADM-01 | 10.0.30.1 | 303 | Contrôleur de domaine Active Directory paugy.lan |
| Guacamole | 10.0.30.2 | 303 | Bastion d'accès distant |
| ZABBIX | 10.0.30.3 | 303 | Supervision Zabbix (12 hôtes) et journaux centralisés : Rsyslog + LogAnalyzer |
| CLI-01 | 10.0.40.1 | 304 | Poste Windows membre du domaine |
| pfSense | WAN 10.4.0.198 | trunk | Routage inter-VLAN, NAT :8081 → 10.0.10.2:80 (WordPress), :8082 → 10.0.10.3:80 (GLPI), :2231 (Zabbix), :2233 (LogAnalyzer), filtrage |
⚙️ Fonctionnement de la haute disponibilité
Couche web
WEB-01 et WEB-02 servent le même site. HAProxy teste chaque serveur toutes les 2 s : après 3 échecs, il est retiré (mesuré : 9 s, 6 requêtes sur 6 servies par l'autre serveur) ; après 2 succès, il est réintégré.
Couche données
Galera réplique chaque transaction avant de la valider : aucune donnée confirmée n'est perdue. Le cluster reste actif tant qu'il a la majorité (2 nœuds sur 3) : c'est le quorum, qui empêche deux moitiés de diverger (split-brain).
Point d'entrée unique
Applications et clients ne connaissent que 10.0.10.2. WordPress et GLPI se connectent à la base via 10.0.10.2:3306. Les requêtes vont à MYSQL-01, les deux autres nœuds restent en secours (pas de conflit d'écriture) : si MYSQL-01 tombe, HAProxy bascule sans toucher aux applications.
DROP TABLE accidentel est répliqué instantanément sur les 3 nœuds : seule une sauvegarde permet de revenir en arrière.📡 Supervision et journaux
La haute disponibilité cache les pannes : un cluster peut tourner sur 2 nœuds sans que personne ne le voie. La VM ZABBIX (10.0.30.3, zone d'administration) surveille donc les 12 VM et réunit leurs journaux, sans VM ni adresse supplémentaire : trois services, chacun sur son port.
Zabbix 7.0 LTS
12 hôtes supervisés : agent sur les 8 VM Linux, ping pour pfSense et l'annuaire. Interface publiée sur :2231. Prochaine étape : les alertes.
Rsyslog central
Les 8 VM Linux envoient automatiquement une copie de leurs journaux en TCP 514. pfSense n'ouvre ce port qu'aux serveurs prévus, et jamais sur le WAN.
LogAnalyzer
Lecture des journaux de toutes les machines au même endroit. Accès par :2233, avec un compte et depuis un réseau d'administration seulement.
📘 Procédures
/ toutes les 2 s, pas encore de page de statistiques (8404) ni d'en-tête X-Served-By, SQL envoyé à MYSQL-01 seul (les deux autres en secours), fichiers WordPress pas encore synchronisés.Chaque commande indique la machine sur laquelle elle s'exécute. Les mots de passe sont remplacés par <PASSWORD>. Version complète, avec les fichiers de configuration et les scripts : documentation sur GitHub ↗.
🧭 0. Ordre de déploiement Les 16 étapes et leurs points de contrôle
L'ordre suit les dépendances : le réseau avant les serveurs, la base avant les applications, les tests de panne à la fin. Chaque étape est validée avant de passer à la suivante.
- Proxmox : 12 VM créées, bridge
vmbr0« VLAN aware ». - pfSense installé, WAN
10.4.0.198. - VLAN 301 à 304 : chaque VM pingue sa passerelle
.254. - MYSQL-01 : MariaDB installé,
bind-address = 10.0.20.1. - MYSQL-02 : idem avec
10.0.20.2. - MYSQL-03 : idem avec
10.0.20.3. - Galera formé :
cluster_size = 3sur chaque nœud. - HAProxy : configuration valide, SQL réparti sur les 3 nœuds.
- WEB-01 : Apache + PHP.
- WEB-02 : Apache + PHP.
- WordPress : installé une fois, site identique sur les deux, servi via
10.0.10.2. - GLPI via
10.0.10.2:8080. - NAT :
http://10.4.0.198:8081affiche WordPress,http://10.4.0.198:8082affiche GLPI. - Filtrage deny-by-default, flux interdits vérifiés.
- Tests nominaux.
- Tests de bascule (web et Galera).
🔥 1. pfSense : VLAN, NAT et filtrage Segmentation réseau
VLAN
- Proxmox : cocher VLAN aware sur
vmbr0. La carte de pfSense reste sans tag (elle reçoit le trunk) ; chaque VM reçoit le tag de son VLAN (Hardware → Network Device → VLAN Tag). - pfSense, Interfaces → Assignments → VLANs : créer 301, 302, 303 et 304 sur
vtnet1. - Assigner chaque VLAN à une interface, l'activer, IP statique :
10.0.10.254/24,10.0.20.254/24,10.0.30.254/24,10.0.40.254/24.
# SUR L'HÔTE PROXMOX : poser le tag 302 sur la VM MYSQL-01 (VMID 105) qm set 105 --net0 virtio,bridge=vmbr0,tag=302 # SUR MYSQL-01 : vérifier ping -c 2 10.0.20.254
NAT vers HAProxy
Firewall → NAT → Port Forward : WAN, TCP, port 8081 → 10.0.10.2:80, avec règle de filtrage associée. On cible HAProxy et jamais un serveur web : sinon la panne de ce serveur couperait le site.
Règles principales (deny-by-default)
| Interface | Source → Destination | Port | Pourquoi |
|---|---|---|---|
| vWEB | HAProxy → nœuds Galera | 3306 | Seul HAProxy atteint les bases |
| vADMIN | ADM-01, Guacamole → vWEB, vBDD | 22 | Administration SSH |
| vADMIN | ADM-01 → HAProxy | 80, 3306, 8080, 8404 | Tests et statistiques |
| vUSER | Postes → HAProxy | 80, 8080 | Services publiés uniquement |
| Tous | VLAN → réseaux privés | tout | Bloqué (sauf règles ci-dessus) |
🗄️ 2. Cluster MariaDB Galera (3 nœuds) Installation, configuration, bootstrap
Installation (sur les 3 nœuds)
# SUR MYSQL-01, MYSQL-02 et MYSQL-03
sudo apt update
sudo apt install -y mariadb-server mariadb-client galera-4 rsync
sudo mariadb-secure-installation
rsync est indispensable : c'est lui qui copie les données vers un nœud qui rejoint le cluster (SST). Sans lui, le nœud ne peut pas entrer (incident vécu).Écouter sur l'IP du VLAN : /etc/mysql/mariadb.conf.d/50-server.cnf
# SUR MYSQL-01 (10.0.20.2 sur MYSQL-02, 10.0.20.3 sur MYSQL-03) bind-address = 10.0.20.1 # et non 127.0.0.1 (valeur par défaut)
Configuration Galera : /etc/mysql/mariadb.conf.d/60-galera.cnf
[galera] wsrep_on = ON wsrep_provider = /usr/lib/galera/libgalera_smm.so wsrep_cluster_name = "mon_cluster_galera" # identique partout wsrep_cluster_address = "gcomm://10.0.20.1,10.0.20.2,10.0.20.3" # identique partout wsrep_node_address = "10.0.20.1" # propre au nœud wsrep_node_name = "MYSQL-01" # propre au nœud wsrep_sst_method = rsync binlog_format = ROW # réplication des lignes, déterministe default_storage_engine = InnoDB # seul moteur répliqué de façon fiable innodb_autoinc_lock_mode = 2 # indispensable en multi-maître
Bootstrap : une seule fois, sur un seul nœud
# SUR LES 3 NŒUDS sudo systemctl stop mariadb # SUR MYSQL-01 UNIQUEMENT : crée le cluster sudo galera_new_cluster # SUR MYSQL-02, puis SUR MYSQL-03 : rejoignent le cluster sudo systemctl start mariadb # SUR N'IMPORTE QUEL NŒUD : vérifier sudo mariadb -e "SHOW STATUS WHERE Variable_name IN ('wsrep_cluster_size','wsrep_cluster_status','wsrep_local_state_comment','wsrep_ready');" # Attendu : 3 / Primary / Synced / ON
galera_new_cluster sur un deuxième nœud : cela crée un second cluster indépendant et les données divergent. Un nœud qui redémarre rejoint le cluster avec un simple systemctl start mariadb.Test de réplication
# SUR MYSQL-01 sudo mariadb -e "CREATE DATABASE ha_test; CREATE TABLE ha_test.t (id INT AUTO_INCREMENT PRIMARY KEY, origine VARCHAR(20)); INSERT INTO ha_test.t (origine) VALUES ('MYSQL-01');" # SUR MYSQL-03 : la ligne est déjà là sudo mariadb -e "SELECT * FROM ha_test.t;"
🔀 3. HAProxy : répartition HTTP et SQL Point d'entrée unique 10.0.10.2
Installation
# SUR HAPROXY (compte d'admin « haproxyadmin », PAS « haproxy » : réservé au service) sudo apt install -y haproxy mariadb-client socat curl # SUR MYSQL-01 : compte de health-check, sans mot de passe ni privilège sudo mariadb -e "CREATE USER 'haproxy_check'@'10.0.10.2';"
Configuration : /etc/haproxy/haproxy.cfg (extrait)
frontend fe_wordpress
bind 10.0.10.2:80
option forwardfor
default_backend be_wordpress
backend be_wordpress
balance roundrobin
option httpchk
http-check send meth GET uri /wp-login.php ver HTTP/1.1 hdr Host 10.0.10.2
http-check expect status 200-399
cookie SRV_WP insert indirect nocache httponly
http-response set-header X-Served-By %[srv_name] # rend la bascule visible
default-server inter 3s fall 3 rise 2
server WEB-01 10.0.10.1:80 check cookie web01
server WEB-02 10.0.10.4:80 check cookie web02
listen galera_cluster
bind 10.0.10.2:3306
mode tcp
balance roundrobin
option mysql-check user haproxy_check post-41
default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions
server MYSQL-01 10.0.20.1:3306 check
server MYSQL-02 10.0.20.2:3306 check
server MYSQL-03 10.0.20.3:3306 check
frontend fe_glpi
bind 10.0.10.2:8080
default_backend be_glpi
backend be_glpi
option httpchk
server GLPI-01 10.0.10.3:80 check
Appliquer et vérifier
# SUR HAPROXY sudo haproxy -c -f /etc/haproxy/haproxy.cfg # toujours vérifier AVANT sudo systemctl reload haproxy # reload : sans couper les connexions # Le SQL est-il bien réparti ? (un nœud différent à chaque connexion) for i in 1 2 3 4 5 6; do mariadb -h 10.0.10.2 -u haproxy_check -N -e 'SELECT @@hostname'; done
'wordpress'@'10.0.10.2'.🌐 4. WordPress sur 2 serveurs Base commune, fichiers synchronisés
Base et compte (une seule fois, répliqué par Galera)
# SUR MYSQL-01
sudo mariadb -e "CREATE DATABASE wordpress CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'wordpress'@'10.0.10.2' IDENTIFIED BY '<PASSWORD>';
GRANT ALL PRIVILEGES ON wordpress.* TO 'wordpress'@'10.0.10.2';"
Serveurs web
# SUR WEB-01 et WEB-02 sudo apt install -y apache2 libapache2-mod-php php-mysql php-curl php-gd php-mbstring php-xml php-zip php-intl rsync sudo a2enmod rewrite remoteip # vhost DocumentRoot /var/www/wordpress, AllowOverride All, IP réelle via X-Forwarded-For sudo a2ensite wordpress && sudo a2dissite 000-default sudo apachectl configtest && sudo systemctl reload apache2
wp-config.php : identique sur les deux serveurs
define( 'DB_HOST', '10.0.10.2:3306' ); // la base via HAProxy, jamais un nœud define( 'DB_PASSWORD', '<PASSWORD>' ); // Clés et sels IDENTIQUES sur WEB-01 et WEB-02 : la session reste valide sur les deux define( 'DISALLOW_FILE_EDIT', true ); // une modif sur un seul serveur = divergence define( 'AUTOMATIC_UPDATER_DISABLED', true ); // idem pour les mises à jour
Synchroniser les fichiers de WEB-01 vers WEB-02
Galera réplique la base, pas les fichiers : une image téléversée sur WEB-01 n'existe pas sur WEB-02. L'installation, les extensions et les médias passent donc par WEB-01, puis sont copiés :
# SUR WEB-01 : simulation (-n) puis exécution réelle
sudo rsync -aAXn --delete /var/www/wordpress/ <ADMIN_USER>@10.0.10.4:/var/www/wordpress/ --rsync-path="sudo rsync"
sudo rsync -aAX --delete /var/www/wordpress/ <ADMIN_USER>@10.0.10.4:/var/www/wordpress/ --rsync-path="sudo rsync"
🎫 5. GLPI derrière HAProxy Apache, réécritures, base Galera
# SUR MYSQL-01 sudo mariadb -e "CREATE DATABASE glpi CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER 'glpi'@'10.0.10.2' IDENTIFIED BY '<PASSWORD>'; GRANT ALL PRIVILEGES ON glpi.* TO 'glpi'@'10.0.10.2';" # SUR GLPI-01 sudo apt install -y apache2 libapache2-mod-php php-mysql php-curl php-gd php-intl php-ldap php-mbstring php-xml php-zip php-bz2 php-bcmath php-apcu sudo a2enmod rewrite
vhost : seul le dossier public/ est exposé
<VirtualHost *:80>
DocumentRoot /var/www/glpi/public
<Directory /var/www/glpi/public>
Require all granted
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ index.php [QSA,L] # tout passe par le routeur GLPI
</Directory>
</VirtualHost>
Dans l'assistant d'installation : serveur SQL 10.0.10.2 (HAProxy), compte glpi. Ensuite : changer les mots de passe des comptes par défaut et supprimer install/install.php. GLPI est ensuite accessible sur http://10.0.10.2:8080.
🧪 6. Tester la bascule Panne d'un serveur web, panne d'un nœud Galera
Panne de WEB-01 : WEB-02 doit continuer
# SUR ADM-01 : observer en continu quel serveur répond while true; do curl -sI http://10.0.10.2 | grep -Ei '^HTTP/|x-served-by' | tr -d '\r' | tr '\n' ' '; echo; sleep 1; done # SUR WEB-01 : provoquer la panne sudo systemctl stop apache2 # → ADM-01 n'affiche plus que "X-Served-By: WEB-02", sans aucune erreur # SUR WEB-01 : retour sudo systemctl start apache2 # → après ~6 s, l'alternance WEB-01 / WEB-02 reprend toute seule
Même test dans l'autre sens (WEB-02 arrêté → WEB-01 répond). La page de statistiques http://10.0.10.2:8404/stats montre le serveur passer au rouge puis revenir au vert.
Panne d'un nœud Galera : le site doit rester en ligne
# SUR MYSQL-03 sudo systemctl stop mariadb # SUR MYSQL-01 : 2 nœuds, toujours Primary (2/3 = majorité) sudo mariadb -e "SHOW STATUS LIKE 'wsrep_cluster_s%';" # → WordPress et GLPI fonctionnent toujours # SUR MYSQL-03 : il rejoint seul et rattrape son retard (PAS de bootstrap) sudo systemctl start mariadb
test-web01-failure.sh, test-web02-failure.sh et test-galera-node.sh. En mode guidé, ils indiquent la commande à lancer et vérifient le résultat.🚑 7. Reprise après une panne totale de Galera grastate.dat, quorum, bootstrap sûr
Si les 3 nœuds s'arrêtent (coupure de l'hôte Proxmox par exemple), il faut redémarrer le cluster depuis le nœud le plus à jour. Si on se trompe de nœud, les transactions les plus récentes sont perdues.
# SUR CHAQUE NŒUD : état sauvegardé sudo cat /var/lib/mysql/grastate.dat # safe_to_bootstrap: 1 → dernier nœud arrêté proprement : c'est lui # seqno: -1 → arrêt brutal : lire la vraie position : sudo galera_recovery # --wsrep_start_position=<uuid>:1842 → comparer ce numéro sur les 3 nœuds # SUR LE NŒUD LE PLUS À JOUR UNIQUEMENT sudo sed -i 's/^safe_to_bootstrap: 0/safe_to_bootstrap: 1/' /var/lib/mysql/grastate.dat sudo galera_new_cluster # SUR LES DEUX AUTRES sudo systemctl start mariadb
non-Primary et refuse les requêtes n'est pas une panne de Galera : c'est la protection contre le split-brain. Il faut diagnostiquer avant d'agir.🔐 8. Sécurité SSH, moindre privilège, secrets
- SSH par clé uniquement, pas de connexion root, accepté depuis le VLAN d'administration seulement.
- Un compte SQL par application, limité à sa base et à l'IP de HAProxy ; jamais
'%'niroot. - Bases non exposées : seul HAProxy atteint le port 3306 du VLAN vBDD.
- Aucun secret sur GitHub : configurations publiées en
.exampleavec<PASSWORD>,.gitignorestrict, contrôle automatique des secrets dans la CI.
# SUR CHAQUE VM : /etc/ssh/sshd_config.d/10-hardening.conf PermitRootLogin no PasswordAuthentication no # vérifier la syntaxe puis recharger (garder une session ouverte !) sudo sshd -t && sudo systemctl reload ssh
✅ Tests
Une matrice de 35 tests (T01 à T35) couvre le réseau, SSH, Galera, HAProxy, WordPress, GLPI, le NAT, le filtrage, les pannes, la supervision et les journaux centralisés. Les scripts Bash du dépôt affichent un résultat clair :
[INFO] Checking Galera status on local... [OK] wsrep_cluster_size = 3 [OK] wsrep_cluster_status = Primary [OK] wsrep_local_state_comment = Synced [OK] wsrep_ready = ON
| Scénario | Action | Résultat attendu |
|---|---|---|
| Panne de WEB-01 | systemctl stop apache2 | WEB-02 sert tout le trafic, aucune erreur côté client |
| Panne de WEB-02 | systemctl stop apache2 | WEB-01 sert tout le trafic |
| Retour d'un serveur | systemctl start apache2 | Réintégré automatiquement en ~6 s |
| Panne d'un nœud Galera | systemctl stop mariadb | Cluster à 2 nœuds, toujours Primary, site en ligne |
| Flux interdits | nc -zv 10.0.20.1 3306 depuis vUSER | Bloqué par pfSense |
| Journaux centralisés | logger -t SYSLOG-AUTO "TEST AUTOMATIQUE DEPUIS $(hostname)" sur les 8 VM Linux | 8 messages sur 8 visibles dans LogAnalyzer (réussi le 06/11/2026) |
Les preuves (captures et sorties de commandes) sont archivées au fur et à mesure dans le dossier evidence/ ↗. La matrice de validation ↗ donne le statut réel de chaque test.
🛠️ Incidents rencontrés et résolus
Cluster Galera « non-Primary »
Cause : perte du quorum après l'arrêt brutal de plusieurs nœuds.
Solution : analyse de grastate.dat et galera_recovery, bootstrap du nœud le plus à jour uniquement.
« Connection refused » sur 3306
Cause : bind-address = 127.0.0.1 par défaut : MariaDB n'écoutait qu'en local.
Solution : chaque nœud écoute sur sa propre IP (10.0.20.1, .2, .3).
« dpkg was interrupted »
Cause : installation coupée en cours de route.
Solution : dpkg --configure -a puis apt --fix-broken install ; installations longues dans tmux.
« rsync: command not found »
Cause : paquet absent sur une Debian minimale, alors que Galera (SST) et la synchro WordPress en ont besoin.
Solution : installation sur tous les nœuds concernés.
GLPI : erreur 404
Cause : règles de réécriture Apache mal placées, DocumentRoot hors de public/.
Solution : mod_rewrite activé, règles dans le bloc <Directory> du dossier public/.
SSH « Permission denied (publickey) »
Cause : clé privée placée dans authorized_keys, qui ne doit contenir que des clés publiques.
Solution : ssh-copy-id avec la clé publique, nouvelle paire de clés.
« REMOTE HOST IDENTIFICATION HAS CHANGED »
Cause : VM réinstallée, donc nouvelle clé d'hôte.
Solution : empreinte vérifiée depuis la console Proxmox avant ssh-keygen -R (c'est aussi le symptôme d'une attaque).
Conflit avec le compte « haproxy »
Cause : utilisateur humain nommé haproxy, comme le compte système du service.
Solution : compte d'administration haproxyadmin, compte système laissé au paquet.
LogAnalyzer : « 403 Forbidden »
Cause : Apache n'autorisait que la zone d'administration ; le NAT de pfSense garde l'adresse du poste, qui était donc refusée (AH01630).
Solution : réseau du poste ajouté à Require ip, en gardant l'authentification obligatoire (Require valid-user).
Journaux : rien n'arrive du serveur web
Cause : le fichier de transfert 90-centralisation.conf existait, mais le paquet Rsyslog n'était pas installé sur la Debian.
Solution : apt install rsyslog, service activé, puis test logger : 8 VM sur 8 reçues.
⚠️ Limites et améliorations
Ce qui reste à améliorer
- HAProxy unique → second HAProxy + Keepalived (IP virtuelle partagée).
- pfSense unique → deux pare-feu en CARP.
- Health-check SQL → vérifier l'état Galera (Synced), pas seulement que MariaDB répond.
- Fichiers WordPress → synchronisation planifiée ou stockage partagé (NFS, GlusterFS) : pas encore en place.
- HTTPS, alertes Zabbix, sauvegardes automatisées.
- Journaux : les 8 serveurs Linux sont centralisés (Rsyslog + LogAnalyzer) ; restent pfSense, les postes Windows et un accès chiffré.
- Un seul hôte Proxmox : la redondance est logique, pas physique.
📚 Compétences BTS SIO mobilisées
Bloc 1 — Support
- Gérer le patrimoine informatique
- Répondre aux incidents
- Travailler en mode projet
- Mettre à disposition un service
Bloc 2 — SISR
- Concevoir une solution d'infrastructure réseau
- Installer, tester et déployer
- Exploiter, dépanner et superviser
Bloc 3 — Cybersécurité
- Garantir la disponibilité et l'intégrité des services
- Sécuriser l'infrastructure (filtrage, SSH, moindre privilège)