Dans cet article, nous allons construire de bout en bout une infrastructure WordPress hautement disponible et scalable, entierement sous Debian 13 « Trixie », en s’appuyant au maximum sur les paquets officiels du depot Trixie. Aucun maillon ne doit constituer un point de defaillance unique (SPOF) : chaque couche est doublee et bascule automatiquement.
L’architecture se decompose en quatre couches, toutes redondees :
- 2 load-balancers HAProxy en frontal, en SSL offload, en HA via Keepalived (VIP
10.0.0.10) ; - 2 serveurs Web Nginx + PHP-FPM + WordPress ;
- 2 load-balancers HAProxy pour la base de donnees sur le port 3306, en HA via Keepalived (VIP
10.0.0.30) ; - 2 serveurs MariaDB en replication Master/Master, avec un Redis sur chacun pour le cache objet ;
- 2 serveurs NFS repliques en DRBD et pilotes par Pacemaker/Corosync (VIP
10.0.0.60) pour servir le dossierwp-content.
Schema de l’infrastructure

Plan d’adressage
Voici l’inventaire complet des machines. Tous les serveurs sont sur le reseau 10.0.0.0/24.
| Role | Hostname | IP | VIP |
|---|---|---|---|
| LB Web (HAProxy / SSL offload) | lb-web-01 | 10.0.0.11 | 10.0.0.10 |
| LB Web (HAProxy / SSL offload) | lb-web-02 | 10.0.0.12 | |
| Serveur Web (Nginx/PHP-FPM/WP) | web-01 | 10.0.0.20 | – |
| Serveur Web (Nginx/PHP-FPM/WP) | web-02 | 10.0.0.21 | – |
| LB BDD (HAProxy 3306) | lb-bdd-01 | 10.0.0.31 | 10.0.0.30 |
| LB BDD (HAProxy 3306) | lb-bdd-02 | 10.0.0.32 | |
| BDD MariaDB M/M + Redis | bdd-01 | 10.0.0.40 | – |
| BDD MariaDB M/M + Redis | bdd-02 | 10.0.0.41 | – |
| NFS (DRBD + Pacemaker) | nfs-01 | 10.0.0.61 | 10.0.0.60 |
| NFS (DRBD + Pacemaker) | nfs-02 | 10.0.0.62 |
Note : les mots de passe utilises dans cet article (ReplPass, WpDbPass, RedisStrongPass, etc.) sont des exemples. Remplacez-les imperativement par des secrets robustes et uniques.
1. Prerequis communs (sur TOUS les serveurs)
On part d’une installation minimale de Debian 13. Sur chaque machine, on met le systeme a jour, on installe les outils de base et on synchronise l’horloge (indispensable pour la replication et le cluster) :
apt update && apt full-upgrade -y apt install -y curl wget gnupg vim chrony timedatectl set-timezone Europe/Paris systemctl enable --now chrony
On definit le hostname de chaque machine (exemple pour web-01) :
hostnamectl set-hostname web-01
On renseigne ensuite le fichier /etc/hosts de maniere identique sur tous les serveurs afin que la resolution de noms fonctionne meme sans DNS interne :
10.0.0.10 vip-web 10.0.0.11 lb-web-01 10.0.0.12 lb-web-02 10.0.0.20 web-01 10.0.0.21 web-02 10.0.0.30 vip-bdd 10.0.0.31 lb-bdd-01 10.0.0.32 lb-bdd-02 10.0.0.40 bdd-01 10.0.0.41 bdd-02 10.0.0.60 vip-nfs 10.0.0.61 nfs-01 10.0.0.62 nfs-02
2. Couche base de donnees : MariaDB Master/Master + Redis
On installe MariaDB 11.8 LTS (version du depot Trixie) sur bdd-01 et bdd-02 :
apt install -y mariadb-server mariadb-client mariadb-secure-installation
2.1. Configuration de la replication
Par defaut, Debian fait ecouter MariaDB sur 127.0.0.1. On cree un fichier de configuration dedie a la replication. Sur bdd-01, creez /etc/mysql/mariadb.conf.d/90-replication.cnf :
[mysqld] server-id = 1 log_bin = /var/log/mysql/mariadb-bin log_bin_index = /var/log/mysql/mariadb-bin.index relay_log = /var/log/mysql/relay-bin relay_log_index = /var/log/mysql/relay-bin.index binlog_format = ROW sync_binlog = 1 expire_logs_days = 7 # Master/Master : on decale les auto-increment pour eviter les collisions de cles auto_increment_increment = 2 auto_increment_offset = 1 # Ecoute sur le reseau (protege par pare-feu) bind-address = 0.0.0.0 # On ne replique que la base WordPress replicate_do_db = wordpress
Sur bdd-02, le fichier est identique sauf ces deux lignes :
server-id = 2 auto_increment_offset = 2
On cree le repertoire des logs binaires (sinon MariaDB refuse de demarrer) puis on redemarre, sur les deux noeuds :
mkdir -p /var/log/mysql chown mysql:mysql /var/log/mysql systemctl restart mariadb
2.2. Comptes et amorçage de la replication croisee
On cree le compte de replication ainsi que le compte de controle utilise par HAProxy (sans privilege), sur les deux noeuds :
mariadb -e "CREATE USER 'repl'@'10.0.0.%' IDENTIFIED BY 'ReplPass';" mariadb -e "GRANT REPLICATION SLAVE ON *.* TO 'repl'@'10.0.0.%';" mariadb -e "CREATE USER 'haproxy_check'@'10.0.0.%';" mariadb -e "FLUSH PRIVILEGES;"
On recupere la position du journal binaire sur chaque noeud (notez le File et la Position retournes) :
mariadb -e "SHOW MASTER STATUS;"
On declare ensuite chaque serveur comme esclave de l’autre. Sur bdd-01, on pointe vers bdd-02 (10.0.0.41) en reportant le File/Position lus sur bdd-02 :
mariadb -e "STOP SLAVE;" mariadb -e "CHANGE MASTER TO MASTER_HOST='10.0.0.41', MASTER_USER='repl', MASTER_PASSWORD='ReplPass', MASTER_LOG_FILE='mariadb-bin.000001', MASTER_LOG_POS=328;" mariadb -e "START SLAVE;"
Sur bdd-02, symetriquement, on pointe vers bdd-01 (10.0.0.40) avec le File/Position lus sur bdd-01 :
mariadb -e "STOP SLAVE;" mariadb -e "CHANGE MASTER TO MASTER_HOST='10.0.0.40', MASTER_USER='repl', MASTER_PASSWORD='ReplPass', MASTER_LOG_FILE='mariadb-bin.000001', MASTER_LOG_POS=328;" mariadb -e "START SLAVE;"
On verifie sur chaque noeud que la replication est active (les deux lignes doivent indiquer Yes) :
mariadb -e "SHOW SLAVE STATUSG" | grep -E 'Slave_IO_Running|Slave_SQL_Running|Seconds_Behind'
2.3. Base WordPress
Comme la replication est croisee, il suffit de creer la base et l’utilisateur applicatif sur un seul noeud (bdd-01) : tout sera repercute sur bdd-02.
mariadb -e "CREATE DATABASE wordpress CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" mariadb -e "CREATE USER 'wpuser'@'10.0.0.%' IDENTIFIED BY 'WpDbPass';" mariadb -e "GRANT ALL PRIVILEGES ON wordpress.* TO 'wpuser'@'10.0.0.%';" mariadb -e "FLUSH PRIVILEGES;"
2.4. Redis sur chaque serveur de base
On installe Redis sur bdd-01 et bdd-02. Il servira de backend au plugin Redis Object Cache de WordPress.
apt install -y redis-server
On adapte /etc/redis/redis.conf (memes valeurs sur les deux noeuds) :
bind 0.0.0.0 -::1 protected-mode yes port 6379 requirepass RedisStrongPass maxmemory 512mb maxmemory-policy allkeys-lru # Cache uniquement : pas de persistance disque necessaire appendonly no save ""
On applique :
systemctl restart redis-server systemctl enable redis-server redis-cli -a RedisStrongPass ping
3. Couche d’equilibrage base de donnees (lb-bdd-01 / lb-bdd-02)
Ces deux noeuds presentent une VIP unique 10.0.0.30 aux serveurs Web, a la fois pour MySQL (3306) et pour Redis (6379). Le choix retenu est volontairement actif/passif cote backend : HAProxy envoie tout le trafic vers bdd-01 et ne bascule sur bdd-02 qu’en cas de panne. Cela garantit qu’un seul noeud recoit les ecritures a un instant donne, ce qui evite les conflits de cles propres au Master/Master, tout en partageant le meme Redis entre les deux serveurs Web.
apt install -y haproxy keepalived
Pour qu’HAProxy puisse se lier a la VIP meme lorsqu’elle n’est pas encore presente sur la machine, on active le bind non-local :
echo 'net.ipv4.ip_nonlocal_bind = 1' > /etc/sysctl.d/99-haproxy.conf sysctl --system
3.1. Configuration HAProxy (identique sur les deux LB BDD)
Fichier /etc/haproxy/haproxy.cfg :
global
log /dev/log local0
maxconn 4096
user haproxy
group haproxy
daemon
defaults
log global
mode tcp
option tcplog
option dontlognull
retries 3
timeout connect 5s
timeout client 50s
timeout server 50s
# ---- MariaDB : ecriture sur un seul master a la fois ----
frontend ft_mysql
bind 10.0.0.30:3306
default_backend bk_mysql
backend bk_mysql
option mysql-check user haproxy_check
server bdd-01 10.0.0.40:3306 check inter 2s rise 2 fall 3
server bdd-02 10.0.0.41:3306 check inter 2s rise 2 fall 3 backup
# ---- Redis : on suit le meme noeud actif que MariaDB ----
frontend ft_redis
bind 10.0.0.30:6379
default_backend bk_redis
backend bk_redis
option tcp-check
tcp-check connect
tcp-check send AUTH RedisStrongPassrn
tcp-check expect string +OK
tcp-check send PINGrn
tcp-check expect string +PONG
tcp-check send QUITrn
server bdd-01 10.0.0.40:6379 check inter 2s rise 2 fall 3
server bdd-02 10.0.0.41:6379 check inter 2s rise 2 fall 3 backup
# ---- Page de stats (interne) ----
listen stats
bind *:8404
mode http
stats enable
stats uri /
stats refresh 10s
stats auth admin:StatsPass
On valide la syntaxe et on (re)demarre :
haproxy -c -f /etc/haproxy/haproxy.cfg systemctl enable --now haproxy
3.2. Haute disponibilite de la VIP avec Keepalived
Keepalived assure le heartbeat (protocole VRRP) entre les deux LB et porte la VIP 10.0.0.30. Un script de controle abaisse la priorite si HAProxy tombe, declenchant la bascule.
Sur lb-bdd-01 (MASTER), creez /etc/keepalived/keepalived.conf :
vrrp_script chk_haproxy {
script "/usr/bin/killall -0 haproxy"
interval 2
weight 2
fall 2
rise 2
}
vrrp_instance VI_DB {
state MASTER
interface eth0
virtual_router_id 30
priority 110
advert_int 1
authentication {
auth_type PASS
auth_pass DbVrrpPass
}
virtual_ipaddress {
10.0.0.30/24
}
track_script {
chk_haproxy
}
}
Sur lb-bdd-02 (BACKUP), le fichier est identique sauf l’etat et la priorite :
state BACKUP
priority 100
On adapte interface eth0 au nom reel de l’interface (verifiable avec ip a), puis on demarre :
systemctl enable --now keepalived ip a show eth0 | grep 10.0.0.30
La VIP doit apparaitre sur lb-bdd-01. Un test de bascule consiste a arreter HAProxy sur le master : la VIP doit migrer sur lb-bdd-02 en moins de 3 secondes.
4. Couche stockage partage NFS (nfs-01 / nfs-02)
Les deux serveurs Web doivent partager le dossier wp-content (themes, plugins, uploads). On le place sur un NFS hautement disponible : le bloc disque est replique en temps reel par DRBD, et Pacemaker/Corosync orchestre la bascule (montage du systeme de fichiers, demon NFS, export et VIP 10.0.0.60). On suppose un disque dedie /dev/sdb sur chaque noeud.
apt install -y drbd-utils nfs-kernel-server pacemaker corosync pcs systemctl disable --now nfs-server
On desactive nfs-server au demarrage : c’est Pacemaker qui le pilotera.
4.1. Replication bloc avec DRBD
Sur les deux noeuds, creez la ressource /etc/drbd.d/wpcontent.res :
resource wpcontent {
protocol C;
device /dev/drbd0;
disk /dev/sdb;
meta-disk internal;
net {
verify-alg sha256;
csums-alg sha256;
# Politique de recuperation apres split-brain
after-sb-0pri discard-zero-changes;
after-sb-1pri discard-secondary;
after-sb-2pri disconnect;
}
on nfs-01 {
address 10.0.0.61:7789;
}
on nfs-02 {
address 10.0.0.62:7789;
}
}
On initialise les metadonnees et on active la ressource sur les deux noeuds :
modprobe drbd drbdadm create-md wpcontent drbdadm up wpcontent
Uniquement sur nfs-01, on force la premiere synchronisation complete, puis on formate le peripherique DRBD :
drbdadm primary --force wpcontent mkfs.ext4 /dev/drbd0
On suit l’avancement de la synchronisation initiale (doit afficher UpToDate/UpToDate a la fin), puis on repasse nfs-01 en secondaire pour laisser Pacemaker gerer la promotion :
drbdadm status wpcontent drbdadm secondary wpcontent
On cree enfin le point de montage sur les deux noeuds :
mkdir -p /srv/wp-content
4.2. Cluster Pacemaker / Corosync
On definit un mot de passe identique pour l’utilisateur hacluster sur les deux noeuds, puis on active le service pcsd :
echo 'hacluster:ClusterPass' | chpasswd systemctl enable --now pcsd
Depuis nfs-01, on authentifie les noeuds et on cree le cluster :
pcs host auth nfs-01 nfs-02 -u hacluster -p ClusterPass pcs cluster setup wpcluster nfs-01 nfs-02 pcs cluster start --all pcs cluster enable --all
Sur un cluster a deux noeuds en lab, on desactive le STONITH et on ignore le quorum (a reactiver imperativement en production avec un mecanisme de fencing materiel) :
pcs property set stonith-enabled=false pcs property set no-quorum-policy=ignore
4.3. Ressources du cluster
On declare la ressource DRBD et son clone promouvable (un seul Primary a la fois) :
pcs resource create wp_drbd ocf:linbit:drbd drbd_resource=wpcontent op monitor interval=20s role=Promoted op monitor interval=30s role=Unpromoted pcs resource promotable wp_drbd promoted-max=1 promoted-node-max=1 clone-max=2 clone-node-max=1 notify=true
On cree le systeme de fichiers, le serveur NFS, l’export (vers le reseau Web) et la VIP, regroupes pour demarrer ensemble sur le meme noeud :
pcs resource create wp_fs ocf:heartbeat:Filesystem device=/dev/drbd0 directory=/srv/wp-content fstype=ext4 pcs resource create wp_nfsd ocf:heartbeat:nfsserver nfs_shared_infodir=/srv/wp-content/nfsinfo nfs_no_notify=true pcs resource create wp_export ocf:heartbeat:exportfs clientspec=10.0.0.0/24 options=rw,sync,no_subtree_check,no_root_squash directory=/srv/wp-content/data fsid=100 pcs resource create wp_vip ocf:heartbeat:IPaddr2 ip=10.0.0.60 cidr_netmask=24 pcs resource group add wp_group wp_fs wp_nfsd wp_export wp_vip
On ajoute les contraintes : le groupe NFS ne demarre que sur le noeud ou DRBD est Primary, et apres la promotion de DRBD :
pcs constraint colocation add wp_group with Promoted wp_drbd-clone INFINITY pcs constraint order promote wp_drbd-clone then start wp_group
On verifie l’etat du cluster (toutes les ressources doivent etre Started sur le meme noeud) :
pcs status
Enfin, on cree le sous-dossier data qui sera reellement exporte (sur le noeud actif, ou DRBD est monte) :
mkdir -p /srv/wp-content/data /srv/wp-content/nfsinfo chown -R www-data:www-data /srv/wp-content/data
5. Couche Web (web-01 / web-02)
On installe Nginx, PHP-FPM 8.4 et les extensions necessaires a WordPress et au cache Redis, ainsi que le client NFS :
apt install -y nginx php8.4-fpm php8.4-mysql php8.4-redis php8.4-curl php8.4-gd php8.4-mbstring php8.4-xml php8.4-zip php8.4-intl php8.4-bcmath php8.4-imagick nfs-common
5.1. Montage du wp-content via NFS
On monte l’export HA (VIP 10.0.0.60) sur le futur wp-content. Ajoutez dans /etc/fstab des deux serveurs Web :
10.0.0.60:/srv/wp-content/data /var/www/wordpress/wp-content nfs4 _netdev,rw,hard,nfsvers=4.2,noatime,timeo=15,retrans=3 0 0
On prepare l’arborescence et on monte :
mkdir -p /var/www/wordpress/wp-content mount -a mount | grep wp-content
5.2. Installation du coeur WordPress
On telecharge la derniere version officielle. Sur web-01 (NFS deja monte, donc vide), on deploie la totalite : le wp-content par defaut (theme et plugins de base) va ainsi peupler le partage NFS.
cd /tmp wget https://wordpress.org/latest.tar.gz tar xzf latest.tar.gz rsync -a wordpress/ /var/www/wordpress/ chown -R www-data:www-data /var/www/wordpress
Sur web-02, le wp-content est deja peuple via le NFS : on deploie donc le coeur en excluant wp-content pour ne pas ecraser le partage :
cd /tmp wget https://wordpress.org/latest.tar.gz tar xzf latest.tar.gz rsync -a --exclude wp-content wordpress/ /var/www/wordpress/ chown -R www-data:www-data /var/www/wordpress
5.3. Durcissement de PHP-FPM
Dans /etc/php/8.4/fpm/php.ini, on applique quelques reglages de securite et de taille d’upload :
expose_php = Off cgi.fix_pathinfo = 0 upload_max_filesize = 64M post_max_size = 64M memory_limit = 256M max_execution_time = 120 session.cookie_httponly = 1 session.cookie_secure = 1 session.use_strict_mode = 1
Dans le pool /etc/php/8.4/fpm/pool.d/www.conf, on verrouille les fonctions dangereuses et on restreint l’acces disque :
listen = /run/php/php8.4-fpm.sock listen.owner = www-data listen.group = www-data pm = dynamic pm.max_children = 30 pm.start_servers = 6 pm.min_spare_servers = 4 pm.max_spare_servers = 10 pm.max_requests = 500 php_admin_value[disable_functions] = exec,passthru,shell_exec,system,proc_open,popen,parse_ini_file,show_source php_admin_value[open_basedir] = /var/www/wordpress:/tmp:/usr/share/php php_admin_flag[expose_php] = off
On applique :
systemctl restart php8.4-fpm
5.4. Virtual host Nginx securise
Le SSL etant termine sur HAProxy, Nginx ecoute en clair sur le port 80 (uniquement joignable depuis les LB). Creez /etc/nginx/sites-available/wordpress.conf sur les deux serveurs Web :
server {
listen 80;
server_name www.example.com;
root /var/www/wordpress;
index index.php;
# SSL termine sur HAProxy : on recupere l'IP reelle du visiteur
set_real_ip_from 10.0.0.11;
set_real_ip_from 10.0.0.12;
real_ip_header X-Forwarded-For;
client_max_body_size 64M;
access_log /var/log/nginx/wp.access.log;
error_log /var/log/nginx/wp.error.log;
# Masquer la version de Nginx
server_tokens off;
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ .php$ {
try_files $uri =404;
include fastcgi_params;
fastcgi_pass unix:/run/php/php8.4-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param HTTPS on;
fastcgi_read_timeout 120s;
}
# ===== Durcissement WordPress =====
# Interdire l'execution de PHP depuis les uploads et wp-content
location ~* /wp-content/.*.(?:php|phtml|php3|php4|php5|php7|phps)$ { deny all; }
location ~* /wp-includes/.*.php$ { deny all; }
# Bloquer l'acces aux fichiers sensibles
location = /wp-config.php { deny all; }
location ~* /(?:wp-config.php|readme.html|license.txt|wp-config-sample.php)$ { deny all; }
# xmlrpc.php : vecteur de brute-force et d'amplification, on le coupe
location = /xmlrpc.php { deny all; }
# Fichiers caches (sauf .well-known pour les certificats)
location ~ /.(?!well-known).* { deny all; }
# Pas de PHP dans le dossier uploads (double securite)
location = /wp-content/uploads/ { deny all; }
# Assets statiques : cache navigateur, pas de log
location ~* .(?:js|css|png|jpe?g|gif|ico|svg|webp|woff2?)$ {
expires max;
access_log off;
log_not_found off;
}
}
On active le site, on retire le site par defaut, on teste et on recharge :
ln -s /etc/nginx/sites-available/wordpress.conf /etc/nginx/sites-enabled/ rm -f /etc/nginx/sites-enabled/default nginx -t systemctl reload nginx
5.5. Configuration de WordPress (wp-config.php)
On part du fichier d’exemple. Generez d’abord des cles de securite uniques :
cp /var/www/wordpress/wp-config-sample.php /var/www/wordpress/wp-config.php curl -s https://api.wordpress.org/secret-key/1.1/salt/
Collez le bloc retourne a la place des cles d’exemple, puis renseignez la connexion a la base (via la VIP du LB BDD), le cache Redis et les directives de securite. Voici les directives a placer dans wp-config.php (avant la ligne /* That's all, stop editing! */) :
define( 'DB_NAME', 'wordpress' );
define( 'DB_USER', 'wpuser' );
define( 'DB_PASSWORD', 'WpDbPass' );
define( 'DB_HOST', '10.0.0.30:3306' );
define( 'DB_CHARSET', 'utf8mb4' );
define( 'DB_COLLATE', '' );
define( 'WP_HOME', 'https://www.example.com' );
define( 'WP_SITEURL', 'https://www.example.com' );
/* SSL offload : HAProxy transmet le protocole reel via X-Forwarded-Proto */
if ( isset( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https' ) {
$_SERVER['HTTPS'] = 'on';
}
/* Cache objet Redis (via la VIP du LB BDD, port 6379) */
define( 'WP_REDIS_HOST', '10.0.0.30' );
define( 'WP_REDIS_PORT', 6379 );
define( 'WP_REDIS_PASSWORD', 'RedisStrongPass' );
define( 'WP_REDIS_PREFIX', 'blog:' );
define( 'WP_REDIS_DATABASE', 0 );
define( 'WP_REDIS_TIMEOUT', 1 );
define( 'WP_REDIS_READ_TIMEOUT', 1 );
define( 'WP_CACHE', true );
/* Durcissement WordPress */
define( 'DISALLOW_FILE_EDIT', true );
define( 'FORCE_SSL_ADMIN', true );
define( 'WP_AUTO_UPDATE_CORE', 'minor' );
Le fichier wp-config.php est propre a chaque serveur Web (il reside sur le disque local, pas sur le NFS) : reportez-le donc a l’identique sur web-01 et web-02.
5.6. Installation via WP-CLI et activation du cache Redis
On installe WP-CLI :
curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar chmod +x wp-cli.phar mv wp-cli.phar /usr/local/bin/wp
On finalise l’installation de WordPress depuis web-01 uniquement (la base et le wp-content sont partages) :
cd /var/www/wordpress sudo -u www-data wp core install --url=https://www.example.com --title="Mon Blog" --admin_user=admin --admin_password='AdminPass' --admin_email=admin@example.com
On installe et active le plugin Redis Object Cache, puis on active le drop-in (il est cree dans wp-content, donc partage avec web-02) :
sudo -u www-data wp plugin install redis-cache --activate sudo -u www-data wp redis enable sudo -u www-data wp redis status
La commande wp redis status doit indiquer Status: Connected et Drop-in: Valid.
6. Couche frontale : HAProxy SSL offload (lb-web-01 / lb-web-02)
C’est le point d’entree public. HAProxy termine le TLS (SSL offload), applique un durcissement SSL moderne, ajoute les en-tetes de securite, repartit la charge sur les serveurs Web et bascule en HA via Keepalived (VIP 10.0.0.10).
apt install -y haproxy keepalived echo 'net.ipv4.ip_nonlocal_bind = 1' > /etc/sysctl.d/99-haproxy.conf sysctl --system
6.1. Certificat et parametres Diffie-Hellman
HAProxy attend un seul fichier PEM contenant la chaine complete suivie de la cle privee. Avec un certificat Let’s Encrypt :
mkdir -p /etc/haproxy/certs cat /etc/letsencrypt/live/www.example.com/fullchain.pem /etc/letsencrypt/live/www.example.com/privkey.pem > /etc/haproxy/certs/www.example.com.pem chmod 600 /etc/haproxy/certs/www.example.com.pem openssl dhparam -out /etc/haproxy/dhparam.pem 2048
6.2. Configuration HAProxy (identique sur les deux LB Web)
Fichier /etc/haproxy/haproxy.cfg :
global
log /dev/log local0
maxconn 20000
user haproxy
group haproxy
daemon
# ---- Durcissement SSL (cipher suites modernes) ----
ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305
ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
ssl-default-bind-options ssl-min-ver TLSv1.2 no-tls-tickets
ssl-dh-param-file /etc/haproxy/dhparam.pem
defaults
log global
mode http
option httplog
option dontlognull
option forwardfor
option http-server-close
timeout connect 5s
timeout client 30s
timeout server 30s
timeout http-request 10s
retries 3
# ---- Redirection HTTP -> HTTPS ----
frontend ft_http
bind 10.0.0.10:80
http-request redirect scheme https code 301 unless { ssl_fc }
# ---- Frontend HTTPS (SSL offload) ----
frontend ft_https
bind 10.0.0.10:443 ssl crt /etc/haproxy/certs/www.example.com.pem alpn h2,http/1.1
# Indiquer au backend que la connexion d'origine est en HTTPS
http-request set-header X-Forwarded-Proto https
http-request set-header X-Forwarded-Port 443
# ---- En-tetes de securite ----
http-response set-header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
http-response set-header X-Frame-Options "SAMEORIGIN"
http-response set-header X-Content-Type-Options "nosniff"
http-response set-header Referrer-Policy "strict-origin-when-cross-origin"
http-response set-header Permissions-Policy "geolocation=(), microphone=(), camera=()"
http-response set-header Content-Security-Policy "upgrade-insecure-requests"
# Retirer les bannieres qui fuitent des informations
http-after-response del-header Server
http-after-response del-header X-Powered-By
# ---- Anti brute-force basique (limitation de debit par IP) ----
stick-table type ip size 200k expire 60s store http_req_rate(10s)
http-request track-sc0 src
http-request deny deny_status 429 if { sc_http_req_rate(0) gt 100 }
default_backend bk_web
backend bk_web
balance roundrobin
option httpchk GET /wp-login.php
http-check expect status 200
# Affinite de session (utile pour l'admin)
cookie SRVID insert indirect nocache
server web-01 10.0.0.20:80 check cookie web01
server web-02 10.0.0.21:80 check cookie web02
listen stats
bind *:8404
mode http
stats enable
stats uri /
stats refresh 10s
stats auth admin:StatsPass
On valide et on demarre :
haproxy -c -f /etc/haproxy/haproxy.cfg systemctl enable --now haproxy
6.3. Keepalived (VIP 10.0.0.10)
Sur lb-web-01 (MASTER), /etc/keepalived/keepalived.conf :
vrrp_script chk_haproxy {
script "/usr/bin/killall -0 haproxy"
interval 2
weight 2
fall 2
rise 2
}
vrrp_instance VI_WEB {
state MASTER
interface eth0
virtual_router_id 10
priority 110
advert_int 1
authentication {
auth_type PASS
auth_pass WebVrrpPass
}
virtual_ipaddress {
10.0.0.10/24
}
track_script {
chk_haproxy
}
}
Sur lb-web-02 (BACKUP) : state BACKUP et priority 100. On demarre :
systemctl enable --now keepalived
6.4. Renouvellement du certificat
Comme HAProxy lit un PEM concatene, on ajoute un hook de deploiement Certbot qui reconstruit le fichier et recharge HAProxy. Creez /etc/letsencrypt/renewal-hooks/deploy/haproxy.sh :
#!/bin/bash set -e DOM=www.example.com cat /etc/letsencrypt/live/$DOM/fullchain.pem /etc/letsencrypt/live/$DOM/privkey.pem > /etc/haproxy/certs/$DOM.pem chmod 600 /etc/haproxy/certs/$DOM.pem systemctl reload haproxy
chmod +x /etc/letsencrypt/renewal-hooks/deploy/haproxy.sh
7. Tests et verifications
On valide l’ensemble de la chaine, de bas en haut :
# Replication MariaDB (sur bdd-01 et bdd-02) : doit afficher Yes / Yes mariadb -e "SHOW SLAVE STATUSG" | grep Running # Etat du cluster NFS : ressources Started sur le meme noeud pcs status # Cache Redis cote WordPress : Status Connected sudo -u www-data wp redis status # En-tetes de securite et redirection HTTPS depuis l'exterieur curl -sI http://www.example.com | grep -i location curl -sI https://www.example.com | grep -iE 'strict-transport|x-frame|x-content|referrer|content-security'
Pour evaluer la qualite TLS (protocoles et cipher suites acceptes) :
nmap --script ssl-enum-ciphers -p 443 www.example.com
Tests de bascule a mener un par un (le site doit rester accessible a chaque fois) :
- LB Web :
systemctl stop haproxysur lb-web-01 -> la VIP 10.0.0.10 migre sur lb-web-02. - Serveur Web : arret de Nginx sur web-01 -> HAProxy sort le backend et route tout vers web-02.
- LB BDD : arret de HAProxy sur lb-bdd-01 -> la VIP 10.0.0.30 migre sur lb-bdd-02.
- BDD : arret de MariaDB sur bdd-01 -> HAProxy bascule sur bdd-02 (backup).
- NFS :
pcs node standby nfs-01-> DRBD promeut nfs-02, le NFS et la VIP 10.0.0.60 suivent, les serveurs Web ne voient qu’une breve pause I/O.
8. Points de vigilance
- Master/Master, un seul ecrivain a la fois. Le decalage
auto_increment_offsetevite les collisions de cles, mais deux ecritures simultanees sur les deux noeuds peuvent toujours creer des conflits applicatifs. C’est pourquoi le LB BDD est volontairement en actif/passif : tout le trafic va sur bdd-01, bdd-02 ne prend le relais qu’en cas de panne. - Cache Redis et bascule. Le Redis suit le meme noeud actif que MariaDB via le LB. Lors d’une bascule, le cache repart a froid sur l’autre noeud : c’est sans danger (le cache objet se reconstruit), mais attendez-vous a un pic de charge SQL transitoire.
- STONITH / fencing. On a desactive le STONITH pour le lab. En production, activez-le (IPMI, watchdog SBD…) : sans fencing, un split-brain DRBD peut corrompre
wp-content. La politiqueafter-sb-*du fichier DRBD limite les degats mais ne remplace pas le fencing. - Quorum a deux noeuds.
no-quorum-policy=ignoreest acceptable a deux noeuds mais augmente le risque de split-brain : un troisieme noeud (ou un quorum device) est recommande en production. - Pare-feu. Segmentez avec nftables : seuls les LB Web exposent 80/443 a Internet ; 3306/6379 ne doivent etre joignables que depuis le reseau interne ; le port 7789 (DRBD) et VRRP (protocole 112) doivent etre autorises entre les paires concernees.
- WP-CRON. Sur plusieurs serveurs, desactivez le pseudo-cron HTTP (
define( 'DISABLE_WP_CRON', true )) et declenchezwp cron event run --due-nowvia une tache systemd sur un seul noeud pour eviter les doublons.
9. Recapitulatif
L’infrastructure obtenue n’a aucun point de defaillance unique :
- Entree HTTPS redondee (2x HAProxy + Keepalived, SSL offload durci, en-tetes de securite) ;
- Couche applicative redondee (2x Nginx/PHP-FPM/WordPress) ;
- Acces base redonde (2x HAProxy + Keepalived) vers une base MariaDB Master/Master ;
- Cache objet Redis suivant le noeud actif ;
- Stockage
wp-contentpartage et hautement disponible (NFS + DRBD + Pacemaker/Corosync).
Le tout sur Debian 13 « Trixie », avec les paquets officiels : HAProxy 3.0, Nginx, PHP-FPM 8.4, MariaDB 11.8 LTS, Redis, DRBD, Pacemaker/Corosync.
Conclusion
Cette architecture montre comment combiner des briques open-source eprouvees pour heberger un WordPress reellement resilient et capable d’encaisser la charge. Elle reste extensible : on peut ajouter des serveurs Web a la volee (il suffit de monter le NFS et de les declarer dans le backend HAProxy), placer un cache de page (Varnish) devant Nginx, ou remplacer la replication Master/Master par un cluster Galera pour un multi-master synchrone. A vous de l’adapter a vos besoins, et n’oubliez pas : une infra HA ne vaut que si l’on a teste ses bascules.
