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 dossier wp-content.

Schema de l’infrastructure

Architecture WordPress haute disponibilite : HAProxy SSL offload, MariaDB Master/Master, Redis, NFS DRBD/Pacemaker

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 haproxy sur 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_offset evite 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 politique after-sb-* du fichier DRBD limite les degats mais ne remplace pas le fencing.
  • Quorum a deux noeuds. no-quorum-policy=ignore est 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 declenchez wp cron event run --due-now via 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-content partage 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.