Sécurité
La sécurité par conception — les niveaux, ce qui est livré et ce qui est prévu
La sécurité fait partie de l'ADN de PNeX : chaque couche a été pensée chiffrée, cloisonnée et vérifiable dès sa conception — y compris sur un Raspberry Pi dans un atelier. Chaque niveau ci-dessous affiche son état réel ; rien n'est annoncé qui ne soit livré.
Les huit niveaux
| # | Niveau | Ce qu'il garantit | État |
|---|---|---|---|
| 1 | Transport | TLS partout ; point d'entrée devices dédié en TLS mutuel (certificat client par appareil) ; CA épinglée dans le firmware | Livré |
| 2 | Lien appareil | Canal Noise authentifié et anti-rejeu dans le TLS (clé par appareil, couche post-quantique), anti-clone, OTA signée Ed25519 et anti-rétrogradation | Livré |
| 3 | Identité | Fournisseur OIDC dédié (Rauthy), PKCE, passkeys ; tickets à usage unique pour les websockets du navigateur | Livré |
| 4 | Autorisation | Isolation par organisation, rôles owner/admin/membre/lecteur, une seule source d'écriture par pin, IA en lecture seule | Livré |
| 5 | Exécution | JavaScript en sandbox et Starlark hermétique, requêtes sortantes filtrées par une garde réseau, builds de firmware custom encadrés | Livré |
| 6 | Réseau | Seul nginx fait face au réseau (ports 80, 443 et 4443 pour les appareils) ; bases et identité restent sur le réseau privé | Livré — installeur de production ; la pile de développement publie encore les ports des bases |
| 7 | Chaîne d'approvisionnement | Rust de bout en bout, images Wolfi (Chainguard), dépendances auditées | Livré |
| 8 | Données au repos | Coffre de secrets par organisation, XChaCha20-Poly1305, trousseau avec rotation | Livré |
Le lien appareil décrit ci-dessous (TLS mutuel, Noise, OTA signée, point d'entrée
devices) est livré depuis la 0.1.0-beta.8. Un appareil flashé en 0.1.0-beta.7 ou
avant est refusé : passer au-delà de la beta.7 impose de recompiler et reflasher en
USB chaque appareil, voir
Mises à jour.
TLS partout, deux portes d'entrée
nginx termine le TLS de tout ce qui parle à PNeX, sur deux portes distinctes :
- 443 : navigateurs et apps natives (desktop et Android) — interface, API, WebSockets du navigateur, fournisseur d'identité ;
- le point d'entrée devices : un
servernginx dédié (port 4443 sur une installationinstall.sh/Compose,devices.<publicHost>derrière un LoadBalancer avec le chart Helm). Il ne sert que/ws/device,/ws/cameraet/api/v1/ota/firmware/, et demande à chaque appareil son certificat client (TLS mutuel). Toute autre route y répond 404.
Un appareil ne passe jamais par un reverse proxy, un CDN ou un Ingress : le
TLS y serait terminé et le certificat client perdu. Le nom compilé dans le firmware
est la variable serveur PNEX_DEVICE_HOST (hôte[:port], ou :port seul en LAN).
TLS 1.2 reste activé car l'ESP8266 n'a pas TLS 1.3 ; de petits records TLS sont
négociés pour son tampon de 2 Ko. ws:// n'existe plus : le firmware ne parle que
wss:// et https://, sans setInsecure, et le serveur ferme en 4013 un appareil
arrivé hors de l'edge TLS. nginx joint à chaque requête un secret partagé
(X-Pnex-Edge, généré par l'installeur) : sans lui, le serveur ignore les en-têtes
TLS et certificat, si bien qu'un client qui atteint directement le port du serveur
ne peut pas se faire passer pour l'edge.
Autorité épinglée. La CA du point d'entrée devices est injectée dans chaque
build et vérifiée, avec le nom d'hôte, pour le canal WebSocket comme pour l'OTA. Elle
dépend du mode : CA locale (Compose --tls local), ISRG Root X1 (Compose
--tls cloud, le point d'entrée sert le même certificat Let's Encrypt que
l'interface), CA privée générée par le chart (Helm, par défaut). Renouveler le
certificat sous la même autorité est transparent ; changer d'autorité impose une
recompilation et un reflash. Voir Auto-hébergement.
Websockets du navigateur : tickets à usage unique
Les vues live du navigateur (notifications, direct caméra, édition partagée des pages) ne mettent jamais votre jeton de session dans une URL, où il finirait dans les journaux des proxys et des accès. L’application demande d’abord un ticket à l’API : valable 60 secondes, lié à votre utilisateur et à l’organisation active, utilisable une seule fois. Le websocket s’ouvre avec ce ticket ; un ticket rejoué ou expiré est refusé.
Identité appareil : X.509 et TLS mutuel
- Une CA par organisation : ECDSA P-256, valable 30 ans, créée au premier build ; sa clé privée vit dans le coffre de secrets de l'organisation.
- Un certificat par build : clé et certificat client émis à chaque compilation (CN = identifiant de l'appareil, validité 10 ans), compilés dans le firmware ; la clé privée ne vit que dans l'image. Chaque certificat est inscrit dans un registre (empreinte, numéro de série) qui permet de le révoquer.
- Vérification serveur : chaîne contre la CA de l'organisation de l'appareil,
empreinte contre le registre, révocation, appartenance à cet appareil. Sans
certificat valide : fermeture
4014. - Rotation de CA : pas outillée en V1. Une CA compromise impose aujourd'hui une nouvelle CA, puis la recompilation et le reflash de tous les appareils de l'organisation.
Lien chiffré appareil (Noise)
Sous le TLS, l'appareil et le serveur ouvrent un second canal chiffré :
Noise_NNpsk0_25519_ChaChaPoly_SHA256, le motif qu'utilise aussi ESPHome.
- Clé partagée : 32 octets aléatoires générés à l'enregistrement de l'appareil, intégrés à son firmware au build ; elle ne transite jamais sur le réseau. Mélangée dès le premier message, elle apporte une confidentialité post-quantique : le TLS échange ses clés en ECDHE, qu'un ordinateur quantique pourra casser, pas une clé symétrique de 256 bits. Cette couche n'est donc pas une redondance.
- Authentifié et anti-rejeu : ChaCha20-Poly1305, nonces = compteurs Noise par
sens, prologue lié à l'identifiant de l'appareil. Une trame modifiée, rejouée ou
issue d'une autre connexion reçoit
ERROR:decryption_failed; une poignée de main refusée (mauvaise clé, autre appareil, firmware antérieur) ferme en4011. - Périmètre : canal appareil (annonce, commandes de pins, métriques et commandes custom, ordres OTA), ingestion de télémétrie et caméras. Les agents edge utilisent le même lien.
- Limites de taille : une trame porte au plus 4 Ko de clair sur ESP32 et 1 Ko sur ESP8266 ; le firmware refuse d'envoyer plus gros.
- Plus de mode en clair : l'ancien format ChaCha20 non authentifié est supprimé partout ; un firmware compilé avant est refusé et doit être reflashé.
Token et anti-clone
Le token de l'appareil voyage brut dans l'en-tête Authorization: Bearer — jamais
dans l'URL (aucun device_id en paramètre, plus de base64 sur le fil) : il ne finit
pas dans les journaux d'accès. Les tokens sont uniques par appareil, jamais
réutilisés, et désactivables : le serveur revalide les sessions actives (toutes les
30 secondes par défaut) et ferme en 4005 celle dont le token a été révoqué.
Deux cartes flashées avec le même binaire portent la même identité. Le serveur impose donc un bail « premier live gagne », tenu dans Valkey et partagé par toutes les instances du serveur. Il n'est pris qu'après une poignée de main Noise réussie : un token volé sans la clé ne peut pas bloquer le vrai appareil.
- tant que l'appareil d'origine est connecté, une seconde connexion avec la même
identité est refusée (
4003) ; - une déconnexion propre libère le bail immédiatement ;
- après un silence plus long que le bail (10 secondes par défaut, soit deux heartbeats manqués), le bail expire et une reconnexion est acceptée.
Limite assumée : une copie exacte de l'image (certificat, clé et token compris) peut prendre la place d'un original silencieux. Le certificat par build lie l'identité à une image, pas à une puce physique.
Codes de fermeture du lien appareil
| Code | Cause | Que faire |
|---|---|---|
4001 | Token inconnu ou inactif (y compris un token encodé en base64 par un ancien firmware) | Recompiler et reflasher l'appareil |
4002 | En-tête Authorization: Bearer absent | Firmware antérieur au contrat actuel : recompiler et reflasher |
4003 | Appareil déjà connecté (bail tenu) | Chercher un clone ; attendre l'expiration du bail |
4005 | Token révoqué pendant la session | Réactiver le token ou recompiler |
4011 | Poignée de main Noise refusée (mauvaise clé, autre appareil, ancien firmware) | Recompiler et reflasher |
4013 | Arrivé hors de l'edge TLS | Viser le point d'entrée devices, jamais le port du serveur |
4014 | Certificat client absent ou d'un autre appareil | Viser le point d'entrée devices (pas le 443, pas de proxy) ; recompiler si le certificat a été révoqué |
OTA signée
Chaque image OTA est signée en Ed25519 sur l'identifiant de l'appareil, la version et le SHA-256 de l'image. L'appareil vérifie la signature avant de basculer ; une image non signée ou mal signée est refusée, une version inférieure aussi (anti-rétrogradation). Le téléchargement passe par le point d'entrée devices, avec token et certificat client. Détails : Firmware & OTA.
Coffre de secrets
Chaque organisation dispose d'un coffre unique pour tous ses secrets : tokens et mots de passe des canaux de notification, identifiants des nœuds HTTP des flows, mots de passe Wi-Fi, clés des fournisseurs LLM et géographiques, et secrets d’accès des flux audio. L’usage au quotidien est décrit sur la page Secrets.
- Chiffrement : XChaCha20-Poly1305 avec un nonce aléatoire de 24 octets à chaque écriture. L'organisation et l'identifiant du secret sont liés en données associées : une ligne recopiée vers une autre organisation ou un autre secret ne se déchiffre plus.
- Clé hors base : le trousseau de clés maîtresses vient de l'environnement
(
PNEX_SECRETS_KEYS, généré par l'installeur). Le serveur refuse de démarrer sans lui. Un dump de la base seul ne révèle aucun secret. - Des références, pas des valeurs : un champ de formulaire qui porte un secret stocke une référence typée vers une entrée du coffre. Aucune valeur secrète n'apparaît dans les flows déployés, dans la file de jobs ni entre instances du serveur.
- Runtime des flows : il ne reçoit que des références et résout chacune au démarrage, uniquement pour les secrets utilisés par un flow déployé de cette organisation. La clé maîtresse n'atteint jamais le runtime, et les nœuds fonction (JavaScript, Starlark) n'ont pas accès aux secrets.
- Écriture seule : la page Secrets liste chaque secret avec ce qui l'utilise, jamais sa valeur. Aucun rôle ne peut relire une valeur. Owners et admins créent et remplacent les secrets et les rattachent à un champ ; les membres gardent les secrets déjà rattachés à ce qu’ils modifient, sans jamais en rattacher un ni pointer un nœud ou un canal qui en porte un vers une autre adresse ; les lecteurs ne voient que les noms. Un secret encore utilisé ne peut pas être supprimé.
- Rotation : placez une nouvelle clé en tête du trousseau et redémarrez, puis lancez le rechiffrement depuis la page d'état de la plateforme. Le rechiffrement ne tourne jamais au démarrage : des instances mises à jour une par une continuent toutes de lire le coffre. La page d'état compte les secrets encore sous une ancienne clé ; retirez cette clé quand le compteur est à zéro.
Changer la valeur d'un secret ne déclenche rien en cascade : les flows la prennent à leur prochain déploiement, et les appareils gardent le mot de passe Wi-Fi avec lequel ils ont été flashés jusqu'à leur recompilation.
Hors du coffre, volontairement : les secrets d'infrastructure (URL de base de données, clés de stockage, le trousseau lui-même) restent dans l'environnement, et les tokens et clés Noise des appareils sont des identifiants machine pas encore chiffrés au repos. La clé de la CA de chaque organisation et la clé de signature OTA de l'instance, elles, sont dans le coffre.
Frontières de moindre privilège
- Le serveur n'écrit jamais un pin de sa propre initiative.
- L'assistant IA lit, n'écrit jamais sur les appareils.
- Les requêtes sortantes choisies par les utilisateurs (nœud requête HTTP, webhooks de notification, fournisseurs LLM et géographiques) passent par une garde réseau : les adresses du serveur lui-même, les métadonnées cloud et les noms de services internes sont refusés.
- Les builds de firmware custom partent d'un projet généré par le serveur, avec contrôle du source, catalogue de bibliothèques et sandbox sans réseau optionnel — et la fonction est désactivée par défaut. Voir Firmware & OTA.
Modèle de menaces
Un appareil prouve son identité trois fois : certificat client émis par la CA de son organisation, token dans l'en-tête, clé partagée du lien Noise. L'OTA n'accepte qu'une image signée par l'instance. Le fournisseur d'identité tourne comme composant séparé (Rauthy), donc l'identité des humains reste gérable et auditable indépendamment du serveur de la plateforme.

