---
title: "SRI sur Hugo : hashes automatiques, auto-update et alerting BetterStack"
description: "Comment j'ai implémenté le Subresource Integrity (SRI) sur arleo.eu avec Hugo et LoveIt, puis automatisé la détection de mises à jour CDN, le rebuild, la purge Cloudflare et l'alerting BetterStack — en zéro intervention manuelle."
url: "https://www.arleo.eu/posts/sri-cdn-hugo-automate/"
language: "fr"
datePublished: "2026-05-17T00:00:00Z"
dateModified: "2026-05-17T00:00:00Z"
tags: ["security","hugo","cdn","cloudflare","monitoring","homelab","infrastructure"]
categories: ["sécurité","homelab"]
contentSignal: "ai-train=no, search=yes, ai-input=yes"
---

# SRI sur Hugo : hashes automatiques, auto-update et alerting BetterStack
Comment j'ai implémenté le Subresource Integrity (SRI) sur arleo.eu avec Hugo et LoveIt, puis automatisé la détection de mises à jour CDN, le rebuild, la purge Cloudflare et l'alerting BetterStack — en zéro intervention manuelle.



## Pourquoi le SRI ?

Quand votre site charge des ressources depuis un CDN tiers — FontAwesome, Mermaid, Animate.css — vous faites confiance à un tiers sur lequel vous n'avez aucun contrôle. Si jsdelivr.net est compromis ou si une version « immuable » est mutée silencieusement, votre site peut devenir vecteur d'attaque.

Le **Subresource Integrity** (SRI) résout ça simplement : chaque balise `<link>` ou `<script>` porte un attribut `integrity="sha256-…"` que le navigateur vérifie avant d'exécuter la ressource. Si le hash ne correspond pas, le navigateur bloque le chargement.

Sur arleo.eu, le thème LoveIt charge **9 URLs jsdelivr** via ses partials Hugo. Aucune n'avait de hash SRI. Ce post documente comment j'ai corrigé ça — et comment j'ai poussé le concept jusqu'à un pipeline entièrement automatisé.

---

## Architecture LoveIt et le problème CDN

LoveIt ne code pas ses URLs CDN en dur dans les templates. L'architecture est la suivante :

```
themes/LoveIt/assets/data/cdn/jsdelivr.yml   ← liste des libs et versions
themes/LoveIt/layouts/_partials/init.html    ← charge le YAML, construit $cdn
themes/LoveIt/layouts/_partials/plugin/script.html  ← émet les <script>
themes/LoveIt/layouts/_partials/plugin/style.html   ← émet les <link>
```

Le fichier `jsdelivr.yml` ressemble à :

```yaml
prefix:
  libFiles: https://cdn.jsdelivr.net/npm/
libFiles:
  fontawesomeCSS: "@fortawesome/fontawesome-free@7.2.0/css/all.min.css"
  animateCSS: animate.css@4.1.1/animate.min.css
  mermaidJS: mermaid@11.15.0/dist/mermaid.min.js
  # ...
```

Les partials `plugin/script.html` et `plugin/style.html` émettent les balises sans `integrity=`. Ce sont eux qu'il faut surcharger — **sans toucher au submodule git du thème**.

---

## Étape 1 — Calcul des hashes SRI

Pour chaque URL jsdelivr identifiée dans `public/*.html` :

```bash
curl -fsSL https://cdn.jsdelivr.net/npm/mermaid@11.15.0/dist/mermaid.min.js \
  | openssl dgst -sha256 -binary \
  | base64
```

On stocke tous les hashes dans un fichier `data/sri.yaml` propre au site (pas dans le thème) :

```yaml
"https://cdn.jsdelivr.net/npm/mermaid@11.15.0/dist/mermaid.min.js": "sha256-cBN+d7snO7LvlyuG6LBADMqL5TyyW/xFkRoYbcmGZd4="
"https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@7.2.0/css/all.min.css": "sha256-MVopmdyC2tYTiJ8wlktf0uh0v4NgT+vNdyVFepi7Q0c="
# ...
```

---

## Étape 2 — Surcharge des partials LoveIt

Hugo résout les partials en priorité dans le dossier `layouts/` du site avant le thème. On crée deux fichiers qui surchargent les originaux :

**`layouts/_partials/plugin/script.html`** (extrait) :

```go-html-template
{{- $sri := site.Data.sri | default dict -}}
{{- $src := .Source -}}
{{- $hash := index $sri $src | default "" -}}
{{- if and $src (hasPrefix $src "http") $hash -}}
<script src="{{ $src }}" crossorigin="anonymous" integrity="{{ $hash }}"
  {{- with .Defer }} defer{{ end -}}
  {{- with .Async }} async{{ end -}}
></script>
{{- else if and $src (hasPrefix $src "http") -}}
<script src="{{ $src }}"
  {{- with .Defer }} defer{{ end -}}
  {{- with .Async }} async{{ end -}}
></script>
{{- else -}}
{{/* ressource locale — inchangée */}}
{{- end -}}
```

La logique est simple : si l'URL est présente dans `data/sri.yaml`, on injecte `integrity=` + `crossorigin="anonymous"`. Sinon, on laisse passer sans modifier. Les ressources locales (fingerprinting Hugo) ne sont pas touchées.

Même logique pour `plugin/style.html`, avec propagation des attributs sur les deux balises `<link>` générées (preload + stylesheet).

Après un `hugo --minify`, toutes les balises CDN portent leur hash :

```html
<script src="https://cdn.jsdelivr.net/npm/mermaid@11.15.0/dist/mermaid.min.js"
  crossorigin="anonymous"
  integrity="sha256-cBN+d7snO7LvlyuG6LBADMqL5TyyW/xFkRoYbcmGZd4=">
```

---

## Étape 3 — Script de surveillance hebdomadaire

Avoir des hashes SRI c'est bien. Les garder à jour sans intervention manuelle, c'est mieux. Le script `/home/jm/scripts/check-sri-versions.sh` (cron lundi 8h) fait trois choses :

### 3.1 Vérification des hashes live

Pour chaque URL de `data/sri.yaml`, le script recalcule le hash en direct et compare :

```bash
live=$(curl -fsSL --max-time 30 "$url" \
  | openssl dgst -sha256 -binary | base64)
if [ "sha256-$live" != "$stored" ]; then
    # WARN — hash mismatch : CDN muté ou compromis
fi
```

Un mismatch sur une version épinglée (ex. `mermaid@11.15.0`) signifie que jsdelivr a **modifié un fichier théoriquement immuable** — c'est un signal de sécurité critique. Ce cas n'est **jamais auto-fixé** : il crée un incident BetterStack pour review humaine.

### 3.2 Détection des versions outdated

Pour chaque lib active dans `public/*.html`, le script interroge l'API jsdelivr :

```bash
latest=$(curl -fsSL "https://data.jsdelivr.com/v1/packages/npm/$pkg" \
  | jq -r '.tags.latest')
```

Les libs inactives (définies dans `jsdelivr.yml` mais non activées dans `hugo.toml`) sont loggées `INFO (inactive, skipped)` sans déclencher d'alerte — ce qui évite le bruit sur les features optionnelles de LoveIt.

Trois catégories de résultat :

| Situation | Action |
|---|---|
| Version à jour | `OK` |
| Minor/patch outdated (même major) | Auto-fix complet |
| Major bump (ex. echarts 5→6) | `WARN` → incident humain |

### 3.3 Auto-fix minor/patch

C'est la partie intéressante. Pour une lib minor/patch outdated, le script :

1. **Backup** de `data/sri.yaml` + `assets/data/cdn/jsdelivr.yml`
2. **Bump** de la version dans `jsdelivr.yml` (URL + commentaire)
3. **Fetch + recalcul** du nouveau hash SRI
4. **Mise à jour** de `data/sri.yaml` avec la nouvelle URL et le nouveau hash
5. **`hugo --minify --cleanDestinationDir`**
6. **`bash deploy.sh`** (rsync vers nginx)
7. **Purge Cloudflare** via API (`purge_everything: true`, zone arleo.eu)
8. **Vérification live** : re-check de tous les hashes depuis jsdelivr CDN
9. Si OK → heartbeat BetterStack pingé, incident précédent auto-résolu
10. Si échec à n'importe quelle étape → **rollback** (restore backup + rebuild + redeploy + repurge CF) + incident BetterStack

```bash
# Extrait — purge Cloudflare
curl -s -X POST \
  "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/purge_cache" \
  -H "Authorization: Bearer $CF_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"purge_everything":true}'
```

Le token CF est réutilisé depuis `/etc/crowdsec/cf-sync.env` (scope `Zone:Cache Purge` vérifié au préalable via `/user/tokens/verify`).

---

## Étape 4 — Intégration BetterStack

Le script suit un cycle d'alerting complet :

```
Run WARN
  → POST /api/v2/incidents (summary + description détaillée)
  → ID de l'incident écrit dans ~/.config/sri-check.open-incidents (chmod 600)

Run OK suivant
  → POST /api/v2/incidents/{id}/resolve pour chaque ID tracké
  → ~/.config/sri-check.open-incidents supprimé
  → GET heartbeat BetterStack pingé
```

Robustesse du state file :
- `404` (incident supprimé manuellement côté BS) → dropé silencieusement
- `5xx` ou réseau down → ID conservé pour retry au prochain run
- Plusieurs incidents en parallèle → tous résolus au prochain run OK

---

## Fichiers créés/modifiés

| Fichier | Rôle |
|---|---|
| `data/sri.yaml` | Map URL → hash sha256 (source de vérité SRI) |
| `assets/data/cdn/jsdelivr.yml` | Override local du YAML du thème (versions bumped) |
| `layouts/_partials/plugin/script.html` | Surcharge LoveIt — injection `integrity=` sur `<script>` |
| `layouts/_partials/plugin/style.html` | Surcharge LoveIt — injection `integrity=` sur `<link>` |
| `/home/jm/scripts/check-sri-versions.sh` | Script de surveillance + auto-fix (cron lundi 8h) |
| `/home/jm/.config/sri-check.env` | Secrets BS + CF (chmod 600) |
| `/home/jm/.config/sri-check.open-incidents` | State file incidents (créé/supprimé à la volée) |
| `/etc/logrotate.d/sri-check` | Rotation weekly, 4 semaines, compress |

Aucune modification dans `themes/LoveIt/` (submodule git intact).

---

## Pour étendre à de nouvelles libs

Ajouter une ligne dans `data/sri.yaml` suffit :

```bash
url="https://cdn.jsdelivr.net/npm/nouvelle-lib@1.2.3/dist/lib.min.js"
hash=$(curl -fsSL "$url" | openssl dgst -sha256 -binary | base64)
echo "\"$url\": \"sha256-$hash\"" >> data/sri.yaml
```

Pas de template à toucher. Pas de rebuild manuel — le prochain cron s'en charge.

---

## Résultat

- **9 libs CDN** protégées par SRI, hashes injectés automatiquement à chaque build Hugo
- **0 modification** dans le submodule LoveIt
- **Pipeline entièrement automatisé** : détection → bump → rebuild → deploy → purge CF → vérif live → alerting BS → auto-résolution
- **Score ImmuniWeb/Qualys** : A+ maintenu, disparition des avertissements SRI manquants

---

## Plugin hugo-mcp sri-check

Le script bash a depuis été intégré comme plugin natif du serveur MCP hugo-mcp. Il expose le tool `check_sri_versions(auto_fix, dry_run)` directement depuis Claude.ai, et délègue la purge Cloudflare au plugin CF existant plutôt que de l'appeler en direct depuis le bash.

Code source et PR : [github.com/jmrGrav/hugo-mcp — PR #1 feat/sri-check-plugin](https://github.com/jmrGrav/hugo-mcp/pull/1)


## Tags

- security
- hugo
- cdn
- cloudflare
- monitoring
- homelab
- infrastructure

## Categories

- sécurité
- homelab
