La surface d'attaque d'un système IoT
Jour 1 · Attaquer pour comprendre · 25 min
Les manipulations offensives de M5 portent exclusivement sur votre propre maquette — le broker, les capteurs et la plateforme que vous avez construits depuis M1. Rejouer ces techniques sur un système que vous n'exploitez pas est illégal, quelle que soit l'intention.
Cette règle n'est pas une formalité : elle fait partie de ce qui est évalué à la revue de module.
Un système IoT présente plus de surfaces qu'une application classique, parce qu'il déborde du réseau : il a un corps physique, une radio, et il se met à jour tout seul.
L'OWASP IoT Top 10, appliqué à Thermeo
L'OWASP publie une liste des dix faiblesses les plus répandues. Confrontons-la à ce que vous avez construit — l'exercice est instructif.
| Faiblesse OWASP | Votre maquette | Traité en |
|---|---|---|
| Mots de passe faibles ou par défaut | corrigé — comptes nominatifs | M1 |
| Services réseau non sécurisés | corrigé — TLS, port 1883 fermé | M1 |
| Interfaces d'écosystème non sûres | partiel — l'API de M4 n'a pas d'authentification | M4 |
| Absence de mise à jour sûre | non traité — c'est l'objet du jour 2 | M5 |
| Composants obsolètes | à vérifier — vos images Docker sont-elles à jour ? | M5 |
| Protection insuffisante de la vie privée | à examiner — que révèlent vos relevés ? | M5 |
| Transfert et stockage non chiffrés | partiel — MQTT chiffré, InfluxDB en clair | M5 |
| Absence de gestion du parc | non traité — pas d'inventaire des certificats | M5 |
| Réglages par défaut non sûrs | à vérifier — mot de passe Grafana, jetons | M5 |
| Absence de durcissement physique | hors périmètre en simulé, mais à documenter | M5 |
Vous avez traité les deux premières faiblesses dès M1, ce qui est déjà mieux que beaucoup de déploiements réels. Les six suivantes restent ouvertes — c'est le travail du module.
Ce que l'IoT a de particulier
- L'objet est physiquement accessible. Un capteur sur un toit peut être emporté, ouvert, son firmware extrait et lu. Tout secret qu'il contient est un secret public.
- Le parc est nombreux et durable. Cent objets déployés pour dix ans : une faille découverte en 2029 devra être corrigée sur du matériel installé en 2026.
- La mise à jour est une arme. Le mécanisme qui vous permet de corriger une faille permet aussi, s'il est mal protégé, d'installer n'importe quoi sur tout le parc.
Le firmware que vous écrivez sera lisible par quiconque tient l'objet. Un mot de passe en dur, une clé d'API, un jeton : considérez-les comme publics dès l'instant où vous les compilez.
C'est pourquoi le certificat client de M1 est supérieur au mot de passe : il est propre à chaque objet et se révoque individuellement.
Quiz — Surface d'attaque
Jour 1 · Attaquer pour comprendre · 7 min
1. Pourquoi un mot de passe en dur dans le firmware est-il un problème ?
2. En quoi le certificat client de M1 est-il supérieur au mot de passe ?
3. Quelle surface d'attaque touche potentiellement tout le parc d'un coup ?
4. Votre InfluxDB stocke les relevés en clair sur le disque. Est-ce grave ?
TP 1 — Auditer sa propre maquette
Jour 1 · Attaquer pour comprendre · 1 h 15
Passer votre installation au crible des dix faiblesses OWASP, avec des preuves.
Un audit n'est pas une opinion : chaque constat s'appuie sur une commande exécutée et son résultat. Consignez les deux.
- Inventoriez ce qui écoute sur votre poste. Toute écoute non justifiée est un constat :
docker ps --format 'table {{{{.Names}}}}\t{{{{.Ports}}}}' lsof -iTCP -sTCP:LISTEN -P -n | grep -E '1883|8883|8086|3000|1880|8000' - Pour chaque port, répondez par écrit : qui doit y accéder, et qu'est-ce qui l'en empêche aujourd'hui ?
- Vérifiez les réglages par défaut. Grafana, InfluxDB, Node-RED sont-ils protégés ?
curl -s -o /dev/null -w 'grafana %{{http_code}}\n' http://localhost:3000/api/health curl -s -o /dev/null -w 'influx %{{http_code}}\n' http://localhost:8086/api/v2/buckets curl -s -o /dev/null -w 'nodered %{{http_code}}\n' http://localhost:1880/flows - Node-RED répond très probablement sans authentification, et il peut publier sur votre broker. Mesurez ce que cela permet à quelqu'un sur le même réseau.
- Testez votre API de M4 : est-elle authentifiée ?
curl -s http://localhost:8000/api/etat | head -c 200 - Cherchez les secrets qui traînent dans votre dépôt — la faiblesse la plus répandue :
git log --all -p | grep -iE 'token|password|BEGIN (RSA|PRIVATE)' | head -20 git ls-files | xargs grep -lIE 'token|passwd|\.key$' 2>/dev/null - Examinez la question de la vie privée : à partir de vos relevés de température de bureau, que peut-on déduire ? Écrivez la réponse — horaires d'occupation, absences, week-ends. C'est le constat que les équipes oublient systématiquement.
- Vérifiez l'âge de vos images :
docker images --format '{{{{.Repository}}}}:{{{{.Tag}}}}\t{{{{.CreatedSince}}}}' - Produisez un tableau d'audit : une ligne par faiblesse OWASP, avec constat, preuve, gravité et correction proposée. C'est le premier livrable du module.
- Combien de vos constats viennent d'un oubli, et combien d'un choix assumé pour la maquette ?
- Lequel corrigeriez-vous en premier si la maquette partait en production demain ?
Écouter, rejouer, injecter
Jour 1 · Attaquer pour comprendre · 20 min
Trois attaques suffisent à comprendre pourquoi le chiffrement seul ne protège pas.
L'écoute
Sans TLS, tout transite en clair : valeurs, identifiants, topics. Vous l'avez déjà constaté au TP 3 de M1 avec Wireshark. C'est l'attaque la plus simple, et la parade est connue.
Le rejeu
Plus subtile, parce qu'elle survit au chiffrement dans certains montages. Le principe : capturer un message légitime et le réémettre plus tard.
Si un attaquant obtient un jour un identifiant valide, il peut republier indéfiniment un message correctement formé. Le broker n'a aucun moyen de distinguer « le capteur dit 21,4 °C » de « quelqu'un rejoue un ancien 21,4 °C ».
Sur une mesure, c'est gênant. Sur une commande — « ouvrir la vanne » — c'est grave.
Trois parades, à combiner :
- Horodatage dans la charge utile, et rejet de tout message trop ancien. Impose des horloges synchronisées.
- Compteur croissant par capteur : un numéro déjà vu est refusé.
- Signature du message incluant l'horodatage, pour qu'on ne puisse pas le modifier.
L'injection
Publier de fausses valeurs sur un topic légitime. C'est le scénario évoqué en M1 : une température falsifiée qui trompe la régulation.
La parade est celle que vous avez déjà en place — les ACL par certificat.
Encore faut-il qu'elles soient strictes : un capteur qui a le droit d'écrire sur
thermeo/# peut usurper tous les autres.
Une valeur injectée est souvent aberrante — un saut brutal, une valeur physiquement impossible. Le filtrage médian de M2 et les seuils de plausibilité rendent l'injection beaucoup plus difficile à exploiter : c'est de la défense en profondeur.
TP 2 — Rejouer contre soi-même
Jour 1 · Attaquer pour comprendre · 1 h 15
Monter une attaque par rejeu sur votre maquette, puis la rendre inopérante.
Uniquement sur votre installation. Aucune de ces manipulations ne doit viser un système que vous n'exploitez pas.
- Placez-vous dans la peau d'un attaquant qui a obtenu un identifiant. Capturez un message légitime :
mosquitto_sub -h localhost -p 8883 --cafile certs/ca.crt \ --cert certs/capt-01.crt --key certs/capt-01.key \ -t 'thermeo/lyon/etage1/capt-01/temperature' -C 1 -v - Rejouez-le une heure plus tard, tel quel :
mosquitto_pub -h localhost -p 8883 --cafile certs/ca.crt \ --cert certs/capt-01.crt --key certs/capt-01.key \ -t 'thermeo/lyon/etage1/capt-01/temperature' -m '21.4' - Regardez Grafana : la valeur rejouée est indiscernable d'une mesure réelle. Le système est chiffré, authentifié, et pourtant trompé.
- Ajoutez maintenant les défenses. Enrichissez la charge utile du capteur avec un horodatage et un compteur :
import time, json compteur = 0 def charge(valeur): global compteur compteur += 1 return json.dumps({ "valeur": valeur, "ts": int(time.time()), "n": compteur, }) - Écrivez côté réception un contrôle qui rejette les messages suspects :
import time FENETRE = 60 # secondes de tolerance vus = {} # capteur -> dernier compteur accepte def accepter(capteur, msg): if abs(time.time() - msg["ts"]) > FENETRE: return False, "horodatage hors fenetre" if msg["n"] <= vus.get(capteur, 0): return False, "compteur deja vu" vus[capteur] = msg["n"] return True, "ok" - Rejouez le message capturé : il doit être rejeté deux fois — par l'horodatage et par le compteur. Consignez les deux refus.
- Éprouvez la limite : rejouez immédiatement, dans la fenêtre de 60 secondes. Seul le compteur vous sauve. Que se passe-t-il si le capteur redémarre et repart à zéro ? Traitez ce cas — il est réel, un capteur en deep sleep redémarre à chaque réveil.
- Ajoutez un contrôle de plausibilité : rejeter toute valeur hors de −30 à +60 °C, ou s'écartant de plus de 10 °C de la précédente. Testez avec une injection aberrante.
- Le compteur ne survit pas au redémarrage d'un objet en deep sleep. Quelle solution ?
- L'horodatage suppose des horloges synchronisées. Comment un capteur sur pile obtient-il l'heure ?
- Vos trois parades — horodatage, compteur, plausibilité — sont-elles redondantes ou complémentaires ?
Point de contrôle — fin du jour 1
Jour 1 · Attaquer pour comprendre · 15 min
Mettre à jour sans se tirer dessus
Jour 2 · Durcir et déployer · 25 min
Cent capteurs sur des toits. Une faille est publiée. Vous devez corriger — et le mécanisme qui vous le permet est précisément celui qui peut détruire le parc.
Les trois exigences
1. La signature
L'objet doit vérifier que la mise à jour vient de vous. Le principe est celui des certificats de M1, appliqué à un fichier : vous signez le firmware avec une clé privée, l'objet vérifie avec la clé publique qu'il embarque.
Quiconque atteint l'objet — même réseau, DNS détourné, serveur de mise à jour compromis — installe son propre code. Et ce code s'exécute avec tous les privilèges, sur un objet qui a un certificat valide pour votre broker.
2. La double partition
On n'écrase jamais la version qui fonctionne. La nouvelle s'écrit dans la partition libre, et la bascule n'a lieu qu'une fois l'écriture vérifiée. Une coupure en cours de transfert laisse l'ancienne version intacte.
3. Le retour arrière
La nouvelle version doit confirmer son bon démarrage — se connecter au broker, publier une fois. Sans confirmation dans un délai donné, l'objet repart sur l'ancienne partition.
Sans confirmation explicite, un firmware qui démarre mais ne parvient plus à se connecter est un objet perdu : il tourne, donc il ne redémarre pas, mais il est injoignable. Il faut aller le chercher physiquement.
La confirmation transforme une panne logicielle en simple retour arrière.
Le déploiement par vagues
Même signé, même avec retour arrière, on ne déploie jamais sur cent objets d'un coup.
| Vague | Objets | Attente | Ce qu'on vérifie |
|---|---|---|---|
| 1 | 1 objet, accessible facilement | 24 h | Qu'il fonctionne vraiment |
| 2 | 10 objets, sites variés | 48 h | Comportement sur configurations différentes |
| 3 | le reste | — | Surveillance renforcée |
Gardez un objet identique au parc, accessible physiquement, sur lequel vous déployez toujours en premier. C'est votre canari : s'il tombe, vous le récupérez à la main plutôt que d'aller chercher cent capteurs.
Ce que le simulateur ne fera pas
Wokwi n'expose pas de serveur de mise à jour : nous ne réaliserons pas d'OTA par le réseau. En revanche, vous implémenterez pour de vrai la signature et sa vérification — c'est la partie qui compte, et celle qu'on rate.
La double partition et le retour arrière sont des mécanismes de l'ESP-IDF que vous documenterez sans les exécuter.
TP 3 — Signer et vérifier un firmware
Jour 2 · Durcir et déployer · 1 h
Implémenter la chaîne de confiance d'une mise à jour, de la signature au refus.
- Générez la paire de clés de signature. Elle est distincte de l'autorité TLS de M1 : signer du code et authentifier un service sont deux usages différents.
openssl genrsa -out certs/firmware-signature.key 2048 openssl rsa -in certs/firmware-signature.key -pubout \ -out certs/firmware-signature.pub - Fabriquez un firmware factice et signez-le :
echo 'FIRMWARE v2 — corrige la faille du parseur' > firmware-v2.bin openssl dgst -sha256 -sign certs/firmware-signature.key \ -out firmware-v2.sig firmware-v2.bin - Écrivez le vérificateur — c'est ce que ferait l'objet avant d'installer :
openssl dgst -sha256 -verify certs/firmware-signature.pub \ -signature firmware-v2.sig firmware-v2.bin - La commande répond
Verified OK. Maintenant, attaquez : modifiez un octet du firmware et relancez la vérification. - Le contrôle échoue. C'est exactement ce qui doit se produire :
echo 'code malveillant' >> firmware-v2.bin openssl dgst -sha256 -verify certs/firmware-signature.pub \ -signature firmware-v2.sig firmware-v2.bin # Verification Failure - Écrivez maintenant un script
maj.pyqui reproduit la logique complète d'un objet recevant une mise à jour :# 1. telecharger firmware + signature # 2. verifier la signature -> si echec, ABANDON, on ne touche a rien # 3. verifier que la version est superieure a l'actuelle # 4. ecrire dans la partition inactive # 5. marquer 'a essayer' et redemarrer # 6. au demarrage : si pas de confirmation en 60 s -> retour arriere # # Simulez les partitions par deux fichiers et l'etat par un troisieme. - Éprouvez les quatre scénarios et consignez le comportement de chacun : signature valide, signature invalide, version inférieure ou égale, et démarrage sans confirmation.
- Le cas de la version inférieure est le plus souvent oublié : sans lui, un attaquant réinstalle une ancienne version signée — qui contenait la faille que vous venez de corriger. C'est une attaque par retour en arrière.
- Où stockeriez-vous la clé privée de signature, et qui doit y avoir accès ?
- Pourquoi séparer la clé de signature de firmware de l'autorité TLS de M1 ?
- Comment un objet sait-il quelle est sa version actuelle après un retour arrière ?
L'industriel n'est pas l'informatique
Jour 2 · Durcir et déployer · 20 min
Thermeo intervient dans des bâtiments tertiaires. Le jour où le client est une usine, les règles changent — et l'erreur classique est d'y appliquer les réflexes de l'informatique de gestion.
Deux mondes, deux priorités
| Informatique de gestion (IT) | Industriel (OT) | |
|---|---|---|
| Priorité | Confidentialité → Intégrité → Disponibilité | Disponibilité → Intégrité → Confidentialité |
| Durée de vie | 3 à 5 ans | 15 à 30 ans |
| Mise à jour | automatique, fréquente | planifiée, à l'arrêt de production |
| Redémarrer | acceptable | arrête la ligne — coûte des milliers d'euros |
| Protocoles | HTTP, TLS | Modbus, PROFINET, OPC-UA |
En OT, la disponibilité prime sur la confidentialité. Un correctif de sécurité qui impose un redémarrage peut être refusé, parce qu'arrêter la production coûte plus cher que le risque estimé.
Ce n'est pas de la négligence : c'est un arbitrage économique explicite, et il faut savoir composer avec.
Les protocoles que vous rencontrerez
| Protocole | Nature | Sécurité native |
|---|---|---|
| Modbus | Très ancien, très répandu, très simple | aucune — ni authentification ni chiffrement |
| PROFINET | Ethernet industriel temps réel | limitée, dépend de l'installation |
| OPC-UA | Moderne, orienté services et modèle de données | intégrée — authentification, chiffrement, signature |
Quiconque atteint le réseau peut lire et écrire n'importe quel registre, sans s'identifier. Le protocole date de 1979 et n'a jamais prévu d'attaquant.
La parade n'est pas dans le protocole : elle est dans la segmentation réseau — on isole, on filtre, on ne l'expose jamais.
La segmentation, principe central de l'OT
Puisqu'on ne peut pas sécuriser les équipements eux-mêmes — trop anciens, trop critiques pour être touchés — on sécurise ce qui les entoure. Le modèle de référence empile des niveaux, du terrain à la bureautique, avec une zone démilitarisée entre l'industriel et l'informatique de gestion.
Votre passerelle IoT se place précisément là : elle lit les équipements industriels et n'expose que ce qui doit sortir. Elle ne doit jamais permettre d'écrire dans le sens inverse sans contrôle explicite.
Le programme mentionne les réseaux corporels. La logique y est la même — disponibilité d'abord, un équipement médical ne se redémarre pas pendant un soin — avec en plus un régime réglementaire strict sur les données de santé. La segmentation et le principe du moindre privilège s'appliquent à l'identique.
Quiz — Mise à jour et industriel
Jour 2 · Durcir et déployer · 7 min
1. Pourquoi refuser une mise à jour dont la version est inférieure à l'actuelle ?
2. À quoi sert la confirmation de bon démarrage ?
3. En milieu industriel, quelle propriété prime généralement ?
4. Comment sécuriser un équipement Modbus ?
TP 4 — Le plan de durcissement
Jour 2 · Durcir et déployer · 1 h 30
Corriger ce que l'audit a révélé et produire le dossier de sécurité du système.
- Reprenez votre tableau d'audit du TP 1 et classez les constats par gravité, en croisant impact et facilité d'exploitation.
- Corrigez les trois plus graves. Typiquement : authentifier Node-RED, protéger l'API de M4, retirer les secrets du dépôt.
- Pour l'API, ajoutez une authentification par en-tête :
from fastapi import Header, HTTPException import os, secrets def verifier(x_api_key: str = Header(None)): attendu = os.environ["API_KEY"] if not x_api_key or not secrets.compare_digest(x_api_key, attendu): raise HTTPException(401, "cle invalide") @app.get("/api/etat", dependencies=[Depends(verifier)]) def etat(): ... - Notez l'usage de
compare_digestplutôt que==: la comparaison à temps constant évite de révéler la clé caractère par caractère. Expliquez pourquoi dans votre rendu. - Si des secrets sont dans votre historique Git, le retrait du fichier ne suffit pas — ils restent dans les commits antérieurs. Documentez la procédure :
# le fichier retire reste dans l'historique : git log --all --full-history -- certs/ca.key # la reecriture d'historique est necessaire, et les secrets exposes # doivent de toute facon etre consideres comme compromis et regeneres - Ajoutez le contrôle de plausibilité du TP 2 dans votre chaîne d'ingestion, en production.
- Rédigez le dossier de sécurité du système, en quatre parties : la surface d'attaque et les mesures en place ; le tableau d'audit avec ce qui est corrigé et ce qui est accepté ; la procédure de mise à jour, signature comprise ; et la conduite à tenir en cas de compromission d'un capteur.
- Rédigez enfin une note d'une page sur le passage en environnement industriel : ce qui changerait dans votre architecture si Thermeo équipait une usine plutôt qu'un immeuble de bureaux.
- Quels constats avez-vous choisi de ne pas corriger, et sur quel argument ?
- Un capteur est compromis. Décrivez les cinq premières minutes.
- Votre dossier de sécurité serait-il exploitable par quelqu'un d'autre que vous ?
Point de contrôle — fin du module M5
Jour 2 · Durcir et déployer · 20 min
Votre encadrant lira votre dossier de sécurité comme le lirait un client. Un constat sans preuve, ou une correction annoncée sans vérification, sera relevé.
Le dernier critère n'est pas une formalité : la déontologie fait partie du métier, et un parcours de sécurité l'évalue.
Le projet fil rouge démarre jeudi 20 — deux jours pour assembler les cinq modules et préparer votre soutenance du vendredi 21.