Bêta 0.2.0-beta.2, pas encore pour un process critique. Voir la roadmap
PNeX logo
Documentation de la version en développement, pas encore publiée. Voir la version stable (0.1.0)

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é.

CLIENTSEDGE · TLSCŒURDONNÉESHTTPS · WSSHTTPSWSS · mTLS :4443WSScerts/auth/v1/ · /wsOIDCsuperviseSQL · jobsingest · querylast valuesliveartifacts · mediaApp webDioxus · WASMApps nativesDioxus · same codeAppareilsESP32 · ESP8266Votre codePublic APIEdge TLSnginx:443 web · :4443 mTLSCertificatsCA · Let’s EncryptIdentitéRauthyServeur PNeXRust · Loco + SeaORMMoteur de flowsEdgeLink · RustBase de donnéesPostgreSQLTélémétrieOpenObserveCache liveValkeyStockage objetS3-compatible
Chiffré TLS sur le réseau Réseau privé interneDocker Compose · Helm 0.3.0 (edge devices dédié) · images Wolfi (Chainguard) · Raspberry Pi → cloud

Les huit niveaux

#NiveauCe qu'il garantitÉtat
1TransportTLS partout ; point d'entrée devices dédié en TLS mutuel (certificat client par appareil) ; CA épinglée dans le firmwareLivré
2Lien appareilCanal Noise authentifié et anti-rejeu dans le TLS (clé par appareil, couche post-quantique), anti-clone, OTA signée Ed25519 et anti-rétrogradationLivré
3IdentitéFournisseur OIDC dédié (Rauthy), PKCE, passkeys ; tickets à usage unique pour les websockets du navigateurLivré
4AutorisationIsolation par organisation, rôles owner/admin/membre/lecteur, une seule source d'écriture par pin, IA en lecture seuleLivré
5ExécutionJavaScript en sandbox et Starlark hermétique, requêtes sortantes filtrées par une garde réseau, builds de firmware custom encadrésLivré
6RéseauSeul 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
7Chaîne d'approvisionnementRust de bout en bout, images Wolfi (Chainguard), dépendances auditéesLivré
8Données au reposCoffre de secrets par organisation, XChaCha20-Poly1305, trousseau avec rotationLivré

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 server nginx dédié (port 4443 sur une installation install.sh/Compose, devices.<publicHost> derrière un LoadBalancer avec le chart Helm). Il ne sert que /ws/device, /ws/camera et /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 en 4011.
  • 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

CodeCauseQue faire
4001Token inconnu ou inactif (y compris un token encodé en base64 par un ancien firmware)Recompiler et reflasher l'appareil
4002En-tête Authorization: Bearer absentFirmware antérieur au contrat actuel : recompiler et reflasher
4003Appareil déjà connecté (bail tenu)Chercher un clone ; attendre l'expiration du bail
4005Token révoqué pendant la sessionRéactiver le token ou recompiler
4011Poignée de main Noise refusée (mauvaise clé, autre appareil, ancien firmware)Recompiler et reflasher
4013Arrivé hors de l'edge TLSViser le point d'entrée devices, jamais le port du serveur
4014Certificat client absent ou d'un autre appareilViser 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.

On this page