WordPress en Docker compromis : image, volumes et secrets
Reconstruire l’image ne lave pas un volume wp-content déjà sale. Les secrets dans l’env, les ports publiés et une image wordpress:latest vieille de deux ans restent le cœur du dossier. On inspecte la pile, on ne se contente pas d’un docker compose up --build.
Rebuild l'image ne suffit pas si le volume wp-content est sale. Inspectez volumes, env, ports publiés. Une image « wordpress:latest » d'il y a deux ans n'est pas un cœur propre.
Ce que « rebuild » ne touche pas
docker compose build ou un pull wordpress:latest recrée le système de fichiers de l’image. Les volumes nommés, les bind mounts (./wp-content, ./uploads), le réseau, les fichiers .env : ils survivent. C’est voulu — c’est pour ça que vos articles ne disparaissent pas à chaque déploiement. C’est aussi pour ça que le mu-plugin et le PHP dans uploads reviennent au redémarrage.
Les dossiers que nous reprenons commencent souvent par « on a rebuild, c’est revenu en 48 h ». Le volume était sale, ou le secret FTP/admin n’avait pas changé, ou un cron sur l’hôte réécrivait le bind mount. Le geste a l’air technique. Il n’a pas traité la porte.
Le protocole reste celui d’un WordPress piraté : copier (volume + dump SQL), changer les accès (registry, hôte, panel, admin CMS), trouver l’entrée, nettoyer ou recreer le volume depuis une copie saine, puis seulement rebuild. Inverser, c’est de la mise en scène.
- Lister les volumes et bind mounts (compose, docker volume ls).
- Lister les ports publiés (0.0.0.0:80, 3306, 22 dans le container).
- Lire .env et les environment: du compose — comme un wp-config.
Image, couches et wordpress:latest
`wordpress:latest` d’il y a deux ans n’est plus latest sur votre disque. Le tag a été tiré une fois, le daemon garde l’ancien digest jusqu’au pull. Un cœur PHP 7.4 / WP 5.9 dans un container « officiel » est un cœur obsolète. Rebuild sans pull ne le rajeunit pas.
Une image « wordpress sécurisé » trouvée sur un forum, ou un Dockerfile qui COPY un thème nulled, arrive déjà sale. Comparez le cœur aux zips officiels une fois le volume monté — ou partez d’une image officielle pinnée (wordpress:6.7.2-php8.2-apache, exemple) et remontez thèmes / plugins depuis une source propre.
Les couches intermédiaires (apt, composer) peuvent contenir des binaires que vous n’avez jamais choisis. Après incident, on ne « fait confiance » à une image custom que si vous la construisez depuis un Dockerfile relu, sans ADD d’une URL inconnue. Sinon : image officielle + volumes de contenu inspectés.
Volumes : là où le malware reste
wp-content (plugins, themes, uploads, mu-plugins) est le volume qui revient dans tous les tickets. object-cache.php et advanced-cache.php à la racine de wp-content aussi. Un bind ./ :/var/www/html monte tout le document root : un PHP à la racine survit au rebuild exactement comme sur un mutualisé.
Copiez le volume hors du serveur (docker run --rm -v … ou archive du bind) avant de supprimer. C’est votre preuve et votre filet. Restaurer « le volume d’hier » replace l’infection. On compare, on extrait le métier (uploads légitimes, thème enfant relu), on ne remonte pas le volume tel quel.
Un volume « anonyme » oublié après un down -v mal compris, ou un second compose sur le même projet, laisse deux wp-content. Le container que vous regardez n’est pas celui qui sert le trafic. docker ps, labels, ports : qui écoute vraiment 443.
- mu-plugins et drop-ins.
- PHP dans uploads.
- Thèmes en double (nulled + enfant).
- Fichiers à la racine si le mount le permet.
Env, secrets et wp-config
WORDPRESS_DB_PASSWORD, clés AUTH, SMTP, identifiants S3, jetons CI : souvent dans .env, parfois dans un secret Docker mal restreint, parfois en clair dans un compose commité. Après incident, tournez-les tous. Un rebuild avec le même .env laisse la base et les API ouvertes.
wp-config.php peut être généré au premier boot puis rester dans le volume. Un include ajouté là survit. Relisez-le comme sur un FTP classique. Les mots de passe d’application WordPress sont dans la base, pas dans l’image : rebuild ignorant.
Le socket Docker exposé au container WordPress (rare, catastrophique) ou un utilisateur hôte trop large : l’attaquant sort du CMS. Cherchez qui a le droit de monter /var/run/docker.sock. Ce n’est pas un réglage « de perf ». Si vous le voyez, déléguez.
Ports publiés et reverse proxy
Publier 3306 ou 22 sur 0.0.0.0 « pour debug » et oublier : l’entrée n’est plus WordPress. Refermez. WordPress derrière Traefik / nginx proxy : les journaux du CMS montrent l’IP du proxy. Configurez les en-têtes réels — voir reverse proxy — pour lire l’entrée, pas pour « hacker ».
Deux stacks (staging et prod) sur le même hôte, mêmes secrets, port staging ouvert au monde : c’est un sous-domaine oublié en version compose. Coupez le publish, htpasswd, ou éteignez staging.
Let’s Encrypt dans un sidecar : un cert mort n’est pas Safe Browsing (cert vs trompeur). Un volume certs partagé trop permissif est un secret de plus à tourner.
La base : un service à part, souvent oublié
Le container mysql/mariadb a son propre volume. Options WordPress injectées, admins fantômes, posts spam : rebuild de l’image wordpress ne les touche pas. Dump, lecture, nettoyage ou restore d’un dump d’avant l’entrée — pas du dump d’hier soir.
WORDPRESS_DB_HOST=db : le réseau compose. Si db est aussi publié, le mot de passe .env vaut un accès distant. Après incident : plus de publish 3306, mot de passe tourné, utilisateurs MySQL listés.
Un second site sur le même serveur MySQL (autre compose, même instance) : voisin de volume SQL. Inventaire des bases, comme un panel mutualisé.
Ordre de travail sur une pile compromise
1) Copie volumes + dump + compose + .env, hors machine. 2) Rotation secrets hôte / registry / DB / CMS / SMTP. 3) Identifier l’entrée (plugin, upload, port, secret). 4) Extraire le métier, jeter le volume CMS sale ou le comparer fichier à fichier. 5) Image officielle pinnée, up. 6) Tests visiteur + inspection, puis listes Google.
Mettre le site en maintenance sur le proxy le temps du volume est légitime. Couper tout le daemon Docker de l’hôte si d’autres projets propres y tournent : mesurez. Un down ciblé vaut mieux qu’un coup de sang.
Les premiers gestes s’appliquent : ne pas déposer un réexamen tant que l’origine (IP du proxy) sert encore le HTML sale. Purgez le cache du proxy.
Refermer : tags d’image, volumes propres, moins de privilèges
Pinez les tags (digest si vous pouvez). Plus de latest en prod. Utilisateur non-root dans l’image officielle si votre pile le permet. Pas de docker.sock. Uploads sans exécution PHP (réglage nginx/apache du container ou du proxy).
Sauvegardes : volume + SQL, hors hôte, testées. Une sauvegarde du volume infecté n’est pas un PRA. Moins d’admins WP, 2FA après le nettoyage, pas comme substitut.
Surveillez trente jours : nouveaux fichiers dans le bind, nouveaux admins, CPU du container, hostnames bizarres si vous êtes derrière un wildcard.
Quand la pile dépasse le bricolage
Kubernetes, Swarm, secrets Vault, plusieurs origines : ce n’est plus un article de vitrine. Déléguez si vous ne savez pas dire quel volume est monté sur le container qui reçoit le trafic public.
Une boutique (commandes, cartes tokenisées) dans cette pile : le coût d’une erreur dépasse l’intervention. Constat écrit, accès révocable.
Créer un espace : on demande compose et accès au moment de l’intervention, pas à l’inscription. Le guide WordPress reste la checklist métier une fois dans le volume.