---
title: "De 46 hashes à zéro : migration CSP vers les nonces dynamiques"
description: "Migration CSP de 46 hashes statiques vers des nonces dynamiques sur nginx et Cloudflare : zéro maintenance, sécurité renforcée pour un homelab auto-hébergé."
url: "https://www.arleo.eu/posts/csp-nonce/"
language: "fr"
datePublished: "2026-04-15T13:22:00+02:00"
dateModified: "2026-05-15T23:51:25+02:00"
tags: ["cloudflare","nginx","security","infrastructure"]
categories: ["Infrastructure"]
contentSignal: "ai-train=no, search=yes, ai-input=yes"
---

# De 46 hashes à zéro : migration CSP vers les nonces dynamiques
Migration CSP de 46 hashes statiques vers des nonces dynamiques sur nginx et Cloudflare : zéro maintenance, sécurité renforcée pour un homelab auto-hébergé.



## ⚡ En bref

La CSP initiale listait 46 hashes SHA-256 pour couvrir les scripts et styles inline — ingérable, fragile, et à la limite des 4 096 caractères de Cloudflare. Cette migration vers des **nonces dynamiques** réduit la CSP à ~600 caractères, supprime toute maintenance manuelle des hashes, et ajoute un reporting structuré des violations dans BetterStack.

Le plugin Grav utilisé est disponible sur GitHub :
🔌 Plugin : [jmrGrav/grav-plugin-csp-nonce](https://github.com/jmrGrav/csp-nonce)

## 🧠 Pourquoi

Quand on implémente une Content Security Policy stricte sur un site Grav CMS servi via Cloudflare, l'approche naïve consiste à lister les hashes SHA-256 de chaque script inline. C'est fonctionnel, mais ça devient vite ingérable. Plusieurs problèmes cumulés :

- **Limite Cloudflare** : les Transform Rules ont une limite de 4 096 caractères. Avec 46 hashes, on frôlait le plafond.
- **`strict-dynamic` mal compris** : avec `strict-dynamic`, les domaines dans `script-src` sont ignorés par les navigateurs CSP Level 3.
- **Fragile** : un espace ou un attribut supplémentaire dans un tag `<script>` invalide le hash.
- **Pas de reporting fonctionnel** : `Reporting-Endpoints` était absent, les violations ne remontaient pas.

## 🔧 Ce qui a été fait

### Architecture finale

```
Visiteur
  → Cloudflare (cache CSS/JS/images, bypass HTML — no-store)
  → nginx (lit X-CSP-Nonce depuis FastCGI, compose le header CSP)
  → PHP-FPM / Grav (génère le nonce, l'injecte dans <script> et <style>)
  → Violations CSP → /csp-report → nginx log JSON → Vector → BetterStack
```

### Plugin Grav : csp-nonce

Un plugin minimal abonné à `onOutputGenerated` :

```php
public function onPluginsInitialized(): void
{
    if ($this->isAdmin()) return;
    $this->nonce = base64_encode(random_bytes(16));
    $this->enable([
        'onTwigVariables'    => ['onTwigVariables', 0],
        'onOutputGenerated'  => ['onOutputGenerated', 0],
    ]);
}

public function onOutputGenerated(): void
{
    $nonce = $this->nonce;
    $output = preg_replace_callback('/<script(\s[^>]*)?(>)/i', function($m) use ($nonce) {
        if (strpos($m[1] ?? '', 'nonce=') !== false) return $m[0];
        return '<script' . ($m[1] ?? '') . ' nonce="' . $nonce . '">';
    }, $this->grav->output);
    $this->grav->output = $output;
    if (!headers_sent()) header('X-CSP-Nonce: ' . $nonce);
}
```

Le nonce est généré avec `random_bytes(16)` — 128 bits d'entropie, impossible à deviner.

### nginx : composition du header CSP

```nginx
set $csp_nonce $upstream_http_x_csp_nonce;
fastcgi_hide_header X-CSP-Nonce;

add_header Content-Security-Policy "
  default-src 'none';
  script-src 'self' 'nonce-$csp_nonce' 'report-sample';
  style-src 'self' 'nonce-$csp_nonce' 'report-sample';
  font-src 'self';
  img-src 'self' www.gravatar.com data:;
  object-src 'none';
  upgrade-insecure-requests;
  report-to csp-endpoint;
" always;
```

### Cloudflare : bypass du cache HTML

```nginx
# Dans le bloc location ~ \.php$
add_header Cache-Control "no-store" always;
```

### Élimination de unsafe-hashes

- `footer.html.twig` : `style="display: flex; ..."` → classe CSS `.footer-content`
- `hero.html.twig` : `style="background-image: url(...)"` → attribut `data-hero-bg` lu par JS
- `cookie-banner.js` : `<style>` dynamique → CSS déplacé dans `custom.css`

### Bilan

| Critère | Avant | Après |
|---|---|---|
| Hashes scripts | 46 | 0 |
| Hashes styles | 5 | 0 |
| `unsafe-hashes` | présent | supprimé |
| Taille CSP | ~4 000 chars | ~600 chars |
| Maintenance | manuelle | automatique |
| Reporting | non fonctionnel | BetterStack structuré |
| Cache HTML Cloudflare | actif | bypass (no-store) |

## 🏁 Conclusion

La migration vers les nonces dynamiques simplifie radicalement la maintenance de la CSP tout en renforçant sa sécurité. Chaque visiteur reçoit un nonce unique par requête — impossible à prévoir et à réutiliser.

**Pour aller plus loin :**

- 💡 Ajouter une alerte BetterStack sur les violations CSP répétées pour détecter une tentative d'injection XSS réelle
- 💡 Implémenter le mode `Content-Security-Policy-Report-Only` en parallèle pour tester les futures modifications CSP sans impacter les utilisateurs



## Tags

- cloudflare
- nginx
- security
- infrastructure

## Categories

- Infrastructure
