Développement

PHP2Shell : pourquoi j’ai quitté les images Bitnami pour remettre mes WordPress en ligne

Le 21 octobre au soir, deux failles critiques de PHP tombent : CVE-2026-109618 et CVE-2026-109619, vite surnommées « PHP2Shell ». Le correctif existe : PHP 8.4.27, 8.3.36 ou 8.2.35. Encore faut-il pouvoir l'installer. Ce soir-là, je coupe deux sites WordPress que j'héberge sur mon cluster Kubernetes, ce blog compris. Pas par excès de prudence : leur image ne recevrait jamais ce correctif.

Voici comment je les ai remis en ligne le lendemain, sur une image qui, elle, se met à jour.

Le vrai problème : une image que plus personne ne reconstruit

Mes deux WordPress tournaient avec le chart Helm de Bitnami. Pendant des années, c'était le choix évident : un helm install, un fichier de valeurs, et le site tourne, base MariaDB comprise.

Sauf que Bitnami ne publie plus librement ses images sur Docker Hub depuis 2025. Ce qui reste disponible, ce sont des images figées (le dépôt bitnamilegacy) ou un tag latest qui n'avance plus. Résultat : mon blog tournait sur une image de WordPress 6.8.2 arrêtée dans le temps, et le second site sur un latest qui n'avait de « dernier » que le nom.

Tant que rien ne casse, on ne s'en aperçoit pas. Le jour où une faille critique sort, on découvre qu'on n'a aucun moyen d'appliquer le correctif. C'est exactement ce qui s'est passé.

Une image de conteneur n'est pas un logiciel qu'on installe une fois. C'est un abonnement à des mises à jour. Quand l'éditeur arrête de les publier, l'abonnement est résilié, même si le site continue de tourner.

La cible : l'image officielle wordpress

L'image officielle wordpress du Docker Hub est maintenue par la communauté Docker et reconstruite à chaque nouvelle version de PHP. Le lendemain de l'annonce, le tag wordpress:php8.4-apache embarquait déjà PHP 8.4.27. C'est tout ce que je demande à une image : qu'elle suive.

Le prix à payer : plus de chart Helm tout fait. J'écris moi-même un Deployment, un Service, un volume et un CronJob. Une centaine de lignes de YAML, mais je sais exactement ce qui tourne.

La méthode : monter la nouvelle installation à côté de l'ancienne

Je ne remplace rien en place. La nouvelle installation se monte dans le même namespace, à côté de l'ancienne, sans être exposée. Elle se teste en interne, et l'ingress ne bascule vers elle qu'une fois tout vérifié. L'ancienne reste là, arrêtée : c'est le retour arrière.

  1. Relever l'existant : nom de la base, utilisateur, préfixe des tables, Secret qui porte le mot de passe. Tout est lu dans l'ancien Deployment, rien n'est recopié à la main.
  2. Sauvegarder la base avec un mariadb-dump, et vérifier que le fichier se termine bien par « Dump completed ».
  3. Copier wp-content vers un nouveau volume. Thèmes, extensions et médias suivent. L'ancien volume est monté en lecture seule : la copie ne peut pas l'abîmer.
  4. Déployer l'image officielle sur ce volume, branchée sur la même base MariaDB.
  5. Tester en interne par un port-forward : code HTTP, titre de la page, version de PHP.
  6. Basculer l'ingress, mais seulement si PHP affiche 8.4.27 ou plus. Le contrôle est automatique.
  7. Remplacer WP-Cron par un CronJob Kubernetes qui appelle wp-cron.php toutes les cinq minutes.

Toutes ces étapes vivent dans un Makefile, une cible par étape : make releve, make backup-db, make copy, make deploy, make test, make switch. Je n'ai plus de commandes à copier-coller depuis une note, et le second site a repris le même fichier avec quatre noms changés.

Le cœur du Deployment

L'image officielle se configure par variables d'environnement. Le mot de passe de la base reste dans son Secret Kubernetes, il n'est écrit nulle part ailleurs :

containers:
  - name: wordpress
    image: wordpress:php8.4-apache
    imagePullPolicy: Always
    env:
      - { name: WORDPRESS_DB_HOST, value: "mon-site-mariadb:3306" }
      - { name: WORDPRESS_DB_NAME, value: "bitnami_wordpress" }
      - { name: WORDPRESS_DB_USER, value: "bn_wordpress" }
      - name: WORDPRESS_DB_PASSWORD
        valueFrom:
          secretKeyRef: { name: mon-site-mariadb, key: mariadb-password }
      - { name: WORDPRESS_TABLE_PREFIX, value: "wp_" }
      - name: WORDPRESS_CONFIG_EXTRA
        value: |
          define('DISABLE_WP_CRON', true);
    readinessProbe:
      httpGet: { path: /wp-includes/images/blank.gif, port: 80 }

Deux détails qui comptent. La sonde de disponibilité vise un fichier statique plutôt que wp-login.php : elle ne réveille pas PHP toutes les dix secondes, et elle ne tombe pas quand une extension ralentit la page de connexion. Et imagePullPolicy: Always fait qu'un simple kubectl rollout restart récupère la prochaine image corrigée.

Les pièges rencontrés

Sur le papier, la procédure tenait en une page. En vrai, chaque site a eu sa surprise.

  • Le mot de passe qui n'était plus une variable. Le chart Bitnami le plus récent ne passe plus le mot de passe de la base en variable d'environnement, mais en fichier monté depuis le Secret. Le script ne le trouvait pas et s'est arrêté net, ce qui était exactement son rôle.
  • Le site qui redirigeait vers 127.0.0.1. Les images Bitnami récentes enregistrent 127.0.0.1 comme adresse du site en base, puis la corrigent dans leur propre wp-config.php. Avec l'image officielle, ce correctif disparaît et WordPress renvoie le visiteur vers https://127.0.0.1/. La solution, sans toucher à la base : fixer WP_HOME et WP_SITEURL dans la configuration.
  • Les envois limités à 2 Mo. L'image officielle garde la limite par défaut de PHP. Un petit uploads.ini monté par ConfigMap la remonte à 40 Mo, comme l'ingress.
  • Le WP-Cron fantôme. L'ancien chart avait son propre CronJob, qui continuait d'appeler l'ancien Service, désormais vide. Il faut le suspendre, et surtout ne plus lancer de helm upgrade sur l'ancienne release, qui le réactiverait.

Ce qui reste à faire

Les deux sites sont en ligne sur PHP 8.4.27, extensions et contenus intacts. Mais le travail n'est pas fini : MariaDB tourne encore sur une image Bitnami qui n'est plus maintenue. Elle n'est pas concernée par cette faille et n'est joignable que depuis son namespace, donc je la traite à froid, avec un dump et une restauration sur l'image officielle mariadb.

La leçon

Un site qui tourne n'est pas un site maintenu. La question à se poser pour chaque service hébergé n'est pas « est-ce que ça marche ? », mais « si une faille sort demain, qui publie le correctif, et en combien de temps je l'applique ? ». Le jour où la réponse est « personne », il est temps de changer d'image. Mieux vaut le découvrir un mardi tranquille qu'un soir de faille critique.

Si vous hébergez vous-même un WordPress, un Nextcloud ou n'importe quel service PHP, prenez cinq minutes cette semaine pour regarder d'où vient votre image et quand elle a été construite pour la dernière fois. Et si vous préférez qu'on le regarde ensemble, c'est mon métier.