Migration HTTPS sabotée : quand le CDN sert encore les images en HTTP
Mercredi 14h. L'équipe infrastructure d'un retailer français (12 000 pages, 480K sessions organiques/mois) termine la migration HTTP → HTTPS. Le certificat Let's Encrypt est valide. La redirection 301 globale fonctionne. Le cadenas vert apparaît sur la homepage. Le Slack interne affiche un emoji champagne. Cinq semaines plus tard, Search Console révèle −34% de clics sur les pages catégorie. Le cadenas n'a jamais été vert sur 73% du catalogue.
Vendredi suivant, 9h12 — le premier signal ignoré
Un développeur front remarque un warning dans la console Chrome sur une fiche produit. Il le mentionne en stand-up. Personne ne relève. Le warning :
Mixed Content: The page at 'https://www.example-retailer.fr/chaussures/running-trail-x400' was loaded over HTTPS, but requested an insecure image 'http://cdn.example-retailer.fr/media/products/trail-x400-main.jpg'. This content should also be served over HTTPS.
Le CTO regarde rapidement. Le site affiche bien https:// dans la barre d'adresse. Le certificat couvre www.example-retailer.fr. Conclusion rapide : "C'est juste un warning, pas un blocage. On s'en occupe au sprint suivant."
Deux semaines passent.
Le lead SEO ouvre son rapport Search Console hebdomadaire. Les impressions sur les pages catégorie ont baissé de 18%. Il vérifie les positions : stables. Il vérifie l'indexation : pas de page désindexée. Il pense à un effet saisonnier et note le point dans son dashboard.
Semaine 3. La baisse atteint 27% sur les clics. Les positions commencent à glisser — pas en chute libre, mais un effritement constant. Position moyenne sur les catégories : de 8.2 à 11.4. Les pages produit avec images hero semblent les plus touchées.
Le lead SEO lance un crawl Screaming Frog en mode "Insecure Content". 8 427 URLs d'images remontent en http://. Toutes pointent vers le même sous-domaine : cdn.example-retailer.fr.
Le problème n'est pas le certificat du site. Le problème est le CDN.
Le CDN — un bucket S3 derrière CloudFront, configuré trois ans plus tôt — sert les images sur un domaine séparé. Ce domaine a bien un certificat SSL. Mais les URLs stockées en base de données, dans le CMS (un Magento 2.4), pointent toutes vers http://cdn.example-retailer.fr/.... La migration HTTPS du domaine principal n'a jamais touché aux URLs du CDN inscrites dans le catalogue produit.
Résultat : le navigateur charge la page en HTTPS, puis demande chaque image en HTTP. Mixed content passif. Chrome ne bloque pas les images (contrairement au mixed content actif — scripts, iframes). Mais il supprime le cadenas. Et Google enregistre la page comme partiellement non sécurisée.
Le bug : une URL en dur dans trois couches de la stack
Le diagnostic complet a pris deux jours. Le mixed content venait de trois sources distinctes, toutes liées au même sous-domaine CDN.
Source 1 — Les URLs produit dans la base Magento
Le champ media_gallery de chaque produit stocke le chemin relatif de l'image. Mais la base URL du média est configurée dans core_config_data :
SELECT * FROM core_config_data WHERE path LIKE '%base_media_url%';
Résultat :
+----------+-------------------------------------------+---------------------------------------------+
| scope | path | value |
+----------+-------------------------------------------+---------------------------------------------+
| default | web/unsecure/base_media_url | http://cdn.example-retailer.fr/media/ |
| default | web/secure/base_media_url | http://cdn.example-retailer.fr/media/ |
+----------+-------------------------------------------+---------------------------------------------+
Les deux lignes — unsecure et secure — pointaient vers http://. La migration HTTPS avait mis à jour web/secure/base_url et web/unsecure/base_url vers https://www.example-retailer.fr/, mais personne n'avait touché les URLs média. Le ticket de migration ne mentionnait pas le CDN.
Source 2 — Le template Luma surchargé
Le thème custom contenait un partial pour les balises Open Graph :
<!-- app/design/frontend/Custom/theme/Magento_Catalog/templates/product/view/opengraph/general.phtml -->
<meta property="og:image" content="http://cdn.example-retailer.fr/media/catalog/product/<?= $block->getImage() ?>" />
L'URL du CDN était codée en dur dans le template. Même après correction de la config Magento, cette balise aurait continué à émettre du http://.
Source 3 — Les descriptions CMS avec URLs absolues
Les descriptions longues de 2 300 produits contenaient des images insérées via l'éditeur WYSIWYG de Magento. Les URLs avaient été copiées-collées au fil des années :
<p>Découvrez notre gamme trail :</p>
<img src="http://cdn.example-retailer.fr/media/wysiwyg/collection-trail-2024.jpg" alt="Collection trail 2024" />
<p>Conçue pour les terrains techniques...</p>
Ces URLs vivaient dans la table catalog_product_entity_text. Aucune configuration globale ne pouvait les corriger automatiquement.
Ce que voyait le navigateur vs ce que voyait Googlebot
Dans Chrome, les images s'affichaient normalement. Le navigateur charge le mixed content passif (images, vidéos, audio) même en HTTPS — il retire simplement le cadenas et affiche un warning en console. L'utilisateur moyen ne remarque rien.
Googlebot, lui, voit autre chose. Lors du rendu, le crawler détecte les requêtes HTTP sur une page HTTPS. La page est indexée, mais avec un signal de sécurité dégradé. Depuis que HTTPS est un signal de ranking — même léger — cette dégradation s'applique à chaque page affectée.
Pour vérifier ce que Googlebot percevait, l'équipe a utilisé l'outil d'inspection d'URL dans Search Console. Le rendu montrait les images, mais l'onglet "Plus d'informations" listait les ressources chargées en HTTP :
Ressources bloquées ou en erreur :
- http://cdn.example-retailer.fr/media/catalog/product/trail-x400-main.jpg (mixed content)
- http://cdn.example-retailer.fr/media/catalog/product/trail-x400-sole.jpg (mixed content)
[... 6 ressources supplémentaires sur cette page]
Pourquoi les tests pré-migration n'avaient rien vu
L'équipe avait testé la migration HTTPS sur un staging. Mais le staging utilisait un CDN différent — un bucket de dev, déjà en HTTPS. Le test passait. La checklist de migration incluait "Vérifier le certificat SSL" et "Tester les redirections 301". Elle n'incluait pas "Auditer les URLs d'assets sur domaines tiers".
Le rapport Lighthouse du staging affichait un score sécurité parfait. Le rapport Lighthouse de la production aussi — Lighthouse teste la page courante dans le navigateur actuel, pas le comportement de Googlebot face aux mixed content passifs.
Screaming Frog en mode standard ne flag pas le mixed content par défaut. Il faut activer l'option "Check Insecure Content" dans Configuration > Spider > Advanced, ou utiliser un custom search sur les réponses HTML pour src="http://. L'équipe ne l'avait pas fait.
Un audit complet via la commande CLI suivante aurait permis de détecter le problème en moins de cinq minutes sur un export HTML :
# Chercher toutes les références HTTP dans les pages rendues
curl -s https://www.example-retailer.fr/chaussures/running-trail-x400 \
| grep -oP 'src="http://[^"]+' \
| sort -u
Résultat attendu :
src="http://cdn.example-retailer.fr/media/catalog/product/trail-x400-main.jpg
src="http://cdn.example-retailer.fr/media/catalog/product/trail-x400-sole.jpg
src="http://cdn.example-retailer.fr/media/catalog/product/trail-x400-lace.jpg
Sur un site de 12 000 pages, cette vérification pouvait être scriptée sur un sitemap complet. Elle ne l'a pas été.
Le fix : trois corrections, une purge, et beaucoup de patience
Correction 1 — Config Magento core_config_data
UPDATE core_config_data
SET value = 'https://cdn.example-retailer.fr/media/'
WHERE path IN (
'web/unsecure/base_media_url',
'web/secure/base_media_url'
);
Suivi d'un flush du cache Magento :
php bin/magento cache:flush
php bin/magento cache:clean
php bin/magento indexer:reindex
Cette correction a couvert environ 6 100 des 8 427 URLs d'images problématiques — celles générées dynamiquement par Magento à partir du chemin relatif + la base URL.
Correction 2 — Template Open Graph
Le partial a été réécrit pour utiliser la méthode native de Magento :
<!-- app/design/frontend/Custom/theme/Magento_Catalog/templates/product/view/opengraph/general.phtml -->
<?php
$imageUrl = $block->getImage()->getImageUrl();
// Force HTTPS si le CDN est configuré correctement
$imageUrl = str_replace('http://', 'https://', $imageUrl);
?>
<meta property="og:image" content="<?= $escaper->escapeUrl($imageUrl) ?>" />
Le str_replace en filet de sécurité peut sembler brutal. Mais dans le contexte d'une correction d'urgence avec un CDN qui supporte déjà HTTPS, c'est le patch le plus sûr. L'équipe a ajouté un TODO pour passer à $block->getImage()->getSecureUrl() au sprint suivant.
Correction 3 — URLs en dur dans les descriptions produit
Celle-ci était la plus lourde. 2 300 produits avec des URLs http:// dans le champ description. Un script SQL ciblé :
UPDATE catalog_product_entity_text
SET value = REPLACE(value, 'http://cdn.example-retailer.fr/', 'https://cdn.example-retailer.fr/')
WHERE value LIKE '%http://cdn.example-retailer.fr/%';
Avant exécution, l'équipe a compté les lignes impactées :
SELECT COUNT(*) FROM catalog_product_entity_text
WHERE value LIKE '%http://cdn.example-retailer.fr/%';
-- Résultat : 3 847 lignes (certains produits avaient plusieurs attributs texte)
Backup complet de la table avant le UPDATE. Vérification post-exécution sur 50 produits aléatoires.
Purge CDN et cache Varnish
Le CDN (CloudFront) cachait les headers de la réponse. Même après correction, les anciennes pages HTML avec les URLs http:// restaient en cache Varnish côté serveur et en cache CloudFront côté CDN.
# Invalidation CloudFront complète
aws cloudfront create-invalidation \
--distribution-id E1XXXXXXXXXX \
--paths "/*"
# Purge Varnish
varnishadm "ban req.url ~ ."
L'invalidation CloudFront a pris 12 minutes pour se propager sur tous les edge locations.
Ajout d'un header CSP en mode report
Pour détecter tout mixed content résiduel, l'équipe a ajouté un header Content-Security-Policy en mode report-only :
Content-Security-Policy-Report-Only: default-src https:; report-uri /csp-report-endpoint
Ce header ne bloque rien, mais envoie un rapport JSON à chaque violation. En 48h, l'équipe a identifié 34 URLs supplémentaires avec du mixed content — des images de blog insérées par l'équipe marketing via un plugin WordPress alimentant une section éditoriale du Magento.
Récupération du trafic
La timeline de récupération :
- Jour 0 : déploiement des trois corrections + purge cache.
- Jour 3 : Search Console arrête de signaler de nouvelles pages avec mixed content.
- Jour 7 : les premières pages corrigées sont re-crawlées. L'inspection d'URL ne montre plus de ressources HTTP.
- Jour 14 : les positions commencent à revenir. Position moyenne catégories : 10.1 (vs 11.4 au pic de la baisse).
- Jour 28 : retour à 9.0 de position moyenne. Clics à −12% par rapport au niveau pré-incident.
- Jour 42 : stabilisation à −5%. L'équipe attribue le delta résiduel à la saisonnalité et au core update de mai 2026 qui a redistribué certaines positions.
Six semaines pour récupérer ce qu'un REPLACE SQL aurait prévenu en cinq minutes le jour de la migration.
Process ajoutés post-incident
L'équipe a intégré trois garde-fous :
-
Checklist de migration étendue : chaque migration de protocole ou de domaine inclut désormais un audit
grepsur les URLs d'assets dans la base, les templates, et les contenus CMS. Ce point rejoint les recommandations décrites dans notre article sur le stress test des environnements de staging. -
Header CSP en production : un
Content-Security-Policy: upgrade-insecure-requestspermanent qui force le navigateur à transformer toute requêtehttp://enhttps://. Ce n'est pas un fix — c'est un filet de sécurité. Le header ne change pas ce que Googlebot voit dans le HTML source. Mais il protège l'expérience utilisateur immédiate. -
Crawl Screaming Frog hebdomadaire avec l'option "Insecure Content" activée, et alerte Slack automatique si plus de 0 URLs remontent.
Pour les équipes ayant vécu des régressions similaires lors de changements d'architecture URL, les cas documentés sur les migrations www vers non-www ou les canonicals pointant vers le staging montrent le même pattern : une modification incomplète qui affecte des milliers de pages sans déclencher la moindre alerte.
Ce qu'on en retient
Le mixed content est un bug de migration paresseux. Pas un problème de certificat, pas un problème de serveur — un problème de checklist incomplète. Le certificat SSL protège le transport. Il ne corrige pas les URLs stockées en base depuis trois ans.
Le pattern est toujours le même : la page principale passe en HTTPS, les assets tiers restent en HTTP, les tests visuels ne détectent rien parce que les navigateurs sont tolérants. Googlebot, lui, note tout.
Un monitoring continu type Seogard détecte ce type de divergence entre le HTML servi et les attentes HTTPS en quelques minutes — pas cinq semaines après le déploiement.
La vraie leçon : chaque migration de protocole est une migration de données. Et les données vivent dans la base, dans les templates, dans les contenus éditoriaux, et dans le CDN. Pas seulement dans le .htaccess.