# OPTIM-09 — Revue hardening/cache (repo jurerotar + WP Super Cache) adaptée à NOTRE stack

**Date** : 2026-06-25 · **Statut** : 📊 ANALYSE + config préparée (à appliquer sur validation).
**Sources** : `github.com/jurerotar/Wordpress-security-and-performance` (bundle Apache .htaccess/wp-config/functions) ·
`jetpack.com/support/wp-super-cache/initial-wp-super-cache-setup-and-configuration`.
**Principe** : ne RIEN dédoubler — on a déjà beaucoup. Stack : Apache 2.4 + PHP-FPM 8.3 + MariaDB + Redis (object cache) + WP Super Cache (page cache). WPML retiré.

## Couches actuelles (constat) — déjà saines
- **Compression** : Apache **brotli + deflate** sur text/html, css, js… (vhost). WPSC compression = OFF (`$cache_compression=0`) → pas de double. ✅
- **Cache navigateur** : `mod_expires` → assets statiques **1 an immutable** (css/js/img/fonts). ✅
- **Page cache** : WP Super Cache **mode Simple** (`$wp_cache_mod_rewrite=0`, supercache ON) → HTML statique pour anonymes, sans règles .htaccess (pas de conflit avec notre durcissement). ✅
- **Object cache** : Redis (redis-cache) Connected. ✅ Complémentaire (couche DB, derrière le page cache).
- **Un seul page cache** (WPSC) + redis-cache. Pas de W3TC/Rocket/LiteSpeed/Autoptimize. ✅
- **Headers sécu déjà servis** : X-Frame-Options SAMEORIGIN, X-Content-Type-Options nosniff, **HSTS** (max-age 1an + preload), Referrer-Policy strict-origin-when-cross-origin, X-XSS-Protection. expose_php=Off (pas de X-Powered-By). ✅

## 🔧 2 conflits "couches qui s'empiètent" trouvés (à corriger)
1. **`ExpiresDefault "access plus 2 days"` (vhost L53)** colle un **cache navigateur de 2 j sur le HTML dynamique** → en double avec le `Cache-Control: max-age=3` de WP (2 headers Cache-Control contradictoires) → risque : un visiteur ne voit pas un nouvel événement/agenda pendant 48 h.
   **Fix** : supprimer `ExpiresDefault` (les `ExpiresByType` explicites couvrent déjà tous les statiques) → le HTML retombe sur le Cache-Control court de WP (revalidé). Les assets gardent leur 1 an.
2. **`$wpsc_cookies = array("wp-wpml_current_language")`** (résidu WPML) → en-tête `Vary: Cookie` → fragmente le cache par un cookie qui **n'existe plus**.
   **Fix** : `$wpsc_cookies = array();` → meilleur taux de hit, `Vary` propre.

## Source 1 — repo jurerotar : tri pour NOTRE cas
| Mesure | Verdict |
|--------|---------|
| Headers sécu (X-Frame/X-Content-Type/HSTS/Referrer) | ✅ déjà en place |
| ServerSignature Off / ServerTokens Prod | ✅ déjà (SECURITY-05) |
| Bloc xmlrpc / anti-énum auteur+REST | ✅ déjà (SECURITY-05/mu-plugin) |
| Deny wp-config/.bak / deny PHP uploads | ✅ déjà (SECURITY-05/07) |
| mod_deflate / mod_expires | ✅ déjà (mieux : brotli en plus) |
| Force HTTPS | ✅ déjà (vhost + HSTS) |
| **Permissions-Policy** (header) | 🟢 **À AJOUTER** (seul header manquant ; faible risque) |
| `X-Permitted-Cross-Domain-Policies: none` | 🟢 optionnel (mineur) |
| **CSP** (Content-Security-Policy) | 🔴 **ÉVITER tel quel** : la CSP du repo est ultra-permissive (`default-src *`,'unsafe-inline','unsafe-eval') = quasi inutile, et une vraie CSP casse Divi → chantier dédié, plus tard |
| Blacklist ~500 user-agents bots | 🔴 ÉVITER : maintenance + faux positifs ; fail2ban + UFW couvrent déjà le brute-force |
| Bloquer exécution `plugins/*.php` & `themes/*.php` | 🔴 ÉVITER : casse des endpoints légitimes de plugins (notre deny uploads suffit) |
| IP-allowlist sur wp-login.php | 🔴 ÉVITER : équipe sans IP fixe → lockout. fail2ban gère le brute-force |
| Hotlink protection (img/css/js) | ⚪ inutile ici (pas de souci bande passante) |
| Blocage `wp-includes/` direct | ⚪ optionnel, gain marginal |
| Rewrite anti-XSS (GLOBALS/_REQUEST) | ⚪ marginal + faux positifs ; on a corrigé les vraies XSS (SECURITY-08) |

## Source 2 — WP Super Cache : config recommandée (post-WPML, sans overlap)
| Réglage | Actuel | Recommandé | Pourquoi |
|---------|--------|-----------|----------|
| Mode | Simple (mod_rewrite=0) | **garder Simple** | coexiste avec notre durcissement + Redis ; mod_rewrite/Expert ajouterait des règles .htaccess = overlap inutile |
| Ne pas cacher les connectés | oui (`not_logged_in=2`) | garder | éditeurs voient l'état réel |
| Compress pages (WPSC) | OFF | **garder OFF** | Apache brotli/deflate compresse déjà (sinon double) |
| `$wpsc_cookies` | `[wp-wpml_current_language]` | **`[]`** (vider) | résidu WPML → fragmente le cache |
| Cache rebuild | (à confirmer) | **ON** | sert l'ancienne version pendant la régénération (pas de pic) |
| `cache_max_time` | 1800 s | **3600 s** | contenu festival pas minute-par-minute → moins de régénérations, meilleur TTFB |
| Preload | OFF | **optionnel ON, interval long (1×/jour)** | garde au chaud les ~600 pages CPT (artistes/events/instruments) → TTFB des pages froides (crawl/SEO). Coût : charge périodique + disque |
| CDN (ossdlcdn) | OFF | **garder OFF** | pas de CDN (QUIC.cloud écarté) |

## Modèle de couches cible (chacune son rôle, zéro chevauchement)
- **Transport** → Apache brotli/deflate (compression de toute réponse texte).
- **Cache navigateur** → mod_expires : statiques 1 an immutable ✅ ; **HTML court/revalidé** (après retrait ExpiresDefault).
- **Page cache** → WPSC Simple : HTML statique anonyme, court-circuite PHP/DB au hit.
- **Object cache** → Redis : résultats DB pour connectés + cache-miss + admin.

## ✅ APPLIQUÉ (2026-06-25)
- Vhost : retrait `ExpiresDefault "access plus 2 days"` + ajout `Permissions-Policy` (geolocation/micro/caméra/paiement/usb/capteurs = off). configtest OK + reload. (NB : une CSP `default-src 'self'` raisonnable + `unset X-Powered-By` existaient déjà → non touchés.)
- WPSC : `$wpsc_cookies = array();` + `$cache_max_time = 3600` (`$cache_rebuild_files` déjà à 1). php -l OK, chown www-data.
- Vérif : HTML = **1 seul** `Cache-Control: max-age=3, must-revalidate` (fini le doublon 172800) ; assets 1 an immutable ; Permissions-Policy présent ; pages 200 ; WPSC sert le cache. `Vary: Cookie` résiduel = WP core (inoffensif sans CDN).
- Snapshots : `snapshots/apache/toulouse.conf.{before,after}-cache`, `snapshots/wordpress/wp-cache-config.{before,after}-cache.php`.
- ✅ **Preload WPSC activé (2026-06-25)** : `$wp_cache_preload_on=1`, `$wp_cache_preload_posts='all'`, `$wp_cache_preload_interval=1440` (1×/jour), taxonomies off. Amorcé via `wpsc_enable_preload()` (entrée officielle, après nettoyage du `preload_mutex.tmp`). Vérifié : compteur qui avance + chaîne via WP-Cron + **115+ pages supercache** générées (`index-https.html`, ex. `evenement/…`). Snapshot `snapshots/wordpress/wp-cache-config.{before,after}-preload.php`.
  - NB technique : les fichiers supercache HTTPS = `index-https.html` (pas `index.html`). Le crawl traverse aussi les post types internes (acf-field/oembed/flamingo) sans générer de page = normal. Arrêt d'urgence possible via `wp-content/cache/stop_preload.txt`.

## Plan d'application proposé (low-risk)
1. Vhost : retirer `ExpiresDefault "access plus 2 days"` + ajouter `Header set Permissions-Policy` (+ optionnel X-Permitted-Cross-Domain-Policies). configtest + reload.
2. WPSC : `$wpsc_cookies = array();` + `cache_max_time = 3600` (+ activer Cache rebuild si off). chown wp-cache-config.
3. (Optionnel) activer Preload interval quotidien.
4. Vérif : 1 seul Cache-Control sur HTML (court), assets toujours 1 an, `Vary` sans Cookie, pages 200, hit WPSC.
Snapshots before/after dans `snapshots/`. NE PAS toucher : CSP, bot list, blocages plugins/themes/login (écartés ci-dessus).
