🔌 Diagnostic rĂ©seau OT/IT · au shell Linux

Chapitre 09
OPC-UA au shell

Objectifs du chapitre

1. OPC-UA en bref

OPC-UA (Unified Architecture) est le protocole d'Ă©change applicatif standard entre un automate, une supervision et le monde IT. Il fonctionne en client/serveur : notre CPU S7-1500 expose un serveur OPC-UA, et notre poste de diagnostic joue le client. Le transport par dĂ©faut est opc.tcp — un protocole binaire (UA-TCP) posĂ© directement sur TCP, sur le port 4840. Ce n'est pas du HTTP : on ne le lira pas avec curl, mais on peut parfaitement l'observer et le dialoguer au shell.

Une connexion ne s'ouvre pas d'un coup : elle se construit en plusieurs couches empilĂ©es, chacune pouvant Ă©chouer pour sa propre raison. C'est prĂ©cisĂ©ment ce qui fait la richesse — et la difficultĂ© — du diagnostic OPC-UA. Les Ă©tapes :

  1. Hello / Acknowledge — la couche transport UA-TCP se met d'accord sur les tailles de buffer et la version. Si ça passe, le port 4840 rĂ©pond bien un serveur OPC-UA.
  2. OpenSecureChannel — Ă©tablissement du canal sĂ©curisĂ© : c'est ici que la Security Policy et les certificats entrent en jeu. PremiĂšre Ă©tape qui peut ĂȘtre rejetĂ©e pour raison de sĂ©curitĂ©.
  3. CreateSession — le serveur crĂ©e une session applicative et renvoie son certificat.
  4. ActivateSession — le client s'authentifie (anonyme, login/mot de passe, ou certificat utilisateur) et active la session.
  5. Services — une fois la session active : Browse (naviguer), Read, Write, souscriptions


Retiens cet ordre : oĂč la connexion s'arrĂȘte dans cette sĂ©quence te dit pourquoi elle Ă©choue.

Client (poste .71) Serveur (CPU 192.168.0.1:4840) Hello → ← Acknowledge OpenSecureChannel + cert client → ← ServiceFault : BadCertificateUntrusted cert non approuvĂ© → stop — si le certificat est approuvĂ©, la sĂ©quence se poursuit — CreateSession → / ← cert serveur ActivateSession (auth) → / ← OK Browse / Read / Write ⇄
Figure 9.1. La sĂ©quence d'une connexion OPC-UA. La couche transport (Hello/Ack, teal) valide le port ; le canal sĂ©curisĂ© (violet) est l'Ă©tape critique oĂč les certificats s'Ă©changent : un certificat client non approuvĂ© cĂŽtĂ© serveur casse la connexion par un ServiceFault (rouge), avant mĂȘme la session. Si le canal passe, session (vert) puis services (bleu) s'enchaĂźnent.

2. La sécurité OPC-UA, source n°1 de pannes

Si une seule chose est Ă  retenir de ce chapitre, c'est celle-ci : la grande majoritĂ© des connexions OPC-UA qui « ne marchent pas » Ă©chouent sur la sĂ©curitĂ©, pas sur le rĂ©seau. Deux rĂ©glages indĂ©pendants dĂ©crivent le niveau de protection d'un endpoint :

DĂšs qu'on quitte le mode None, l'authentification applicative repose sur des certificats X.509 mutuels. Le client prĂ©sente son certificat au serveur, le serveur prĂ©sente le sien au client, et chacun doit faire confiance au certificat de l'autre. C'est un rejet rĂ©ciproque : si le certificat du client n'est pas dans la liste de confiance du serveur, le serveur refuse — mĂȘme si le mot de passe est bon, mĂȘme si le rĂ©seau est parfait. C'est le piĂšge classique du premier raccordement.

Deux erreurs indĂ©pendantes se ressemblent sur le terrain : « le serveur refuse ma politique » (BadSecurityPolicyRejected) et « le serveur ne fait pas confiance Ă  mon certificat » (BadCertificateUntrusted / BadSecurityChecksFailed). La premiĂšre se rĂšgle en choisissant une politique publiĂ©e par le serveur ; la seconde en approuvant le certificat cĂŽtĂ© serveur. Ne les confonds pas.

3. Découvrir les endpoints du serveur

Avant de se battre avec des certificats, la premiĂšre question est : que le serveur accepte-t-il ? Un serveur OPC-UA publie une liste d'endpoints, chacun avec sa Security Policy et son Security Mode. La lib asyncua (installĂ©e au chapitre 1) fournit des outils CLI pour ça, sans Ă©crire une ligne de code. Le plus utile est uadiscover :

$ uadiscover opc.tcp://192.168.0.1:4840
# Application: urn:S7-1500:OPCUA:Server  (ProductUri: SIEMENS.S71500)

Endpoints:
  opc.tcp://192.168.0.1:4840
    SecurityPolicy : None
    SecurityMode   : None
    UserTokens     : Anonymous, UserName
  opc.tcp://192.168.0.1:4840
    SecurityPolicy : Basic256Sha256
    SecurityMode   : SignAndEncrypt
    UserTokens     : UserName, Certificate
  opc.tcp://192.168.0.1:4840
    SecurityPolicy : Aes128_Sha256_RsaOaep
    SecurityMode   : SignAndEncrypt
    UserTokens     : UserName, Certificate

Cette sortie est une mine d'informations. On y lit que le serveur expose un endpoint ouvert (None/None, souvent dĂ©sactivĂ© sur une vraie install), et deux endpoints sĂ©curisĂ©s. Elle rĂ©pond aussi Ă  la question « quelle authentification ? » via UserTokens : anonyme, login/mot de passe (UserName), ou certificat utilisateur. Choisis toujours une combinaison publiĂ©e ici — en demander une autre, c'est le BadSecurityPolicyRejected assurĂ©.

Lance uadiscover en tout premier. Il ne demande aucun certificat, ne crée aucune session, et te dit exactement ce que le serveur sait faire. Tu évites des heures à te battre avec une politique ou un token que le CPU n'accepte de toute façon pas. Découvre d'abord, connecte ensuite.

4. Naviguer et lire un nƓud

Une fois l'endpoint choisi, deux besoins : parcourir l'espace d'adressage pour trouver les nƓuds, et lire leur valeur. En restant sur l'endpoint None (le plus simple pour dĂ©buter), les outils CLI d'asyncua suffisent :

$ uals -u opc.tcp://192.168.0.1:4840
# browse depuis Objects (ns=0;i=85)
Objects
  ├─ Server
  └─ DeviceSet
       └─ PLC_1
            ├─ DataBlocksGlobal
            │    └─ Moteur1  (ns=3)
            │         ├─ Vitesse   ns=3;i=1001
            │         └─ Marche    ns=3;i=1002
            └─ ...

Le nƓud Moteur1.Vitesse porte l'identifiant ns=3;i=1001 (espace de noms 3, identifiant numĂ©rique 1001 — la convention Siemens pour les variables issues d'un bloc de donnĂ©es). Pour lire sa valeur :

$ uaread -u opc.tcp://192.168.0.1:4840 -n "ns=3;i=1001"
1450.0

Pour aller plus loin (boucler, formater, intĂ©grer Ă  un script de supervision), quelques lignes de Python avec asyncua font le mĂȘme travail et se prĂȘtent Ă  l'automatisation :

import asyncio
from asyncua import Client

async def main():
    async with Client("opc.tcp://192.168.0.1:4840") as c:
        node = c.get_node("ns=3;i=1001")
        print(await node.read_value())

asyncio.run(main())

Sur l'endpoint None, aucun certificat n'est requis : la lecture marche du premier coup si le port est ouvert. DĂšs qu'on vise un endpoint sĂ©curisĂ©, il faut fournir un certificat client (paramĂštre set_security cĂŽtĂ© asyncua) — et c'est lĂ  que commencent les vraies aventures du §6.

Lire est sans danger ; Ă©crire ne l'est pas. Un uawrite sur un nƓud comme Moteur1.Marche agit rĂ©ellement sur le procĂ©dĂ© — tu peux dĂ©marrer un moteur. Sur une installation vivante, contente-toi de lectures tant que tu n'as pas la certitude absolue de ce que fait le nƓud visĂ©. Lecture d'abord, toujours.

5. Capturer la poignée de main

Quand une connexion Ă©choue et que les messages du client restent vagues, la vĂ©ritĂ© est sur le fil. tshark dĂ©code nativement OPC-UA : on filtre le port 4840 en capture, et on n'affiche que les trames OPC-UA en analyse :

$ sudo tshark -i eth0 -f 'tcp port 4840' -Y opcua
  1  0.000  192.168.0.71 → 192.168.0.1  OpcUa  Hello message
  2  0.002  192.168.0.1 → 192.168.0.71  OpcUa  Acknowledge message
  3  0.004  192.168.0.71 → 192.168.0.1  OpcUa  OpenSecureChannel request
  4  0.021  192.168.0.1 → 192.168.0.71  OpcUa  ServiceFault : BadCertificateUntrusted

Cette capture raconte toute l'histoire : Hello et Acknowledge passent — donc le port est ouvert et un serveur OPC-UA rĂ©pond bien. Mais dĂšs l'OpenSecureChannel, le serveur renvoie un ServiceFault avec le code BadCertificateUntrusted : il ne fait pas confiance au certificat client. On n'atteint jamais CreateSession. Le code de statut est le diagnostic ; voici les plus frĂ©quents :

Code de statutCause probableCorrection
BadCertificateUntrusted
BadSecurityChecksFailed
Certificat client non approuvé cÎté serveur. Approuver le certificat client dans la liste de confiance du CPU (TIA).
BadSecurityPolicyRejected Politique demandée non supportée par le serveur. Choisir une politique publiée par uadiscover.
BadIdentityTokenRejected Mode d'authentification refusé (ex. anonyme interdit). Utiliser un UserToken autorisé (login ou certificat).
BadTimeout / connexion refusée
(aucune trame en retour)
Port 4840 fermé, pare-feu, ou serveur OPC-UA désactivé. Vérifier l'ouverture du port et le pare-feu (§6a).

Le rĂ©flexe gagnant : reproduis la tentative qui Ă©choue pendant que tshark -Y opcua tourne, et lis le code de statut exact du ServiceFault. C'est lui qui tranche entre « problĂšme de certificat », « problĂšme de politique » et « problĂšme de rĂ©seau » — bien plus fiable qu'un message d'erreur gĂ©nĂ©rique cĂŽtĂ© client.

6. Arbre de diagnostic « la session ne s'établit pas »

Face à une connexion qui refuse de s'ouvrir, on remonte la pile dans l'ordre, du plus bas au plus haut. Chaque étape écarte une famille de causes.

a. Le port 4840 est-il seulement ouvert ?

Inutile de parler certificats si rien n'Ă©coute. On confirme d'abord la couche transport avec nmap (ou ss cĂŽtĂ© serveur, cf. chapitre 5) :

$ nmap -p4840 192.168.0.1
PORT     STATE  SERVICE
4840/tcp open   opcua

Si le port est filtered (pare-feu) ou closed (serveur OPC-UA dĂ©sactivĂ© dans le CPU), le problĂšme est lĂ  : aucune poignĂ©e de main ne pourra mĂȘme commencer. C'est cohĂ©rent avec un BadTimeout ou un « connexion refusĂ©e » sans aucune trame de retour.

b. Le serveur publie-t-il une politique compatible ?

Le port est ouvert : on interroge les endpoints avec uadiscover (§3) et on vĂ©rifie que la politique et le mode qu'on veut utiliser figurent bien dans la liste. Un BadSecurityPolicyRejected Ă  la capture confirme qu'on demande quelque chose d'absent ; on aligne son client sur ce qui est publiĂ©.

c. Le certificat client est-il approuvĂ© ?

La politique est bonne mais la capture montre BadCertificateUntrusted Ă  l'OpenSecureChannel : le serveur voit notre certificat mais ne lui fait pas confiance. La correction ne se fait pas au shell mais cĂŽtĂ© serveur : il faut ajouter le certificat client Ă  la liste de confiance du CPU (voir l'encart TIA ci-dessous). AprĂšs approbation, la mĂȘme tentative franchit l'OpenSecureChannel.

d. L'URL d'endpoint est-elle rĂ©soluble par le client ?

Cas plus subtil : la dĂ©couverte rĂ©ussit, mais la connexion Ă©choue juste aprĂšs avec un timeout ou une erreur de rĂ©solution. C'est que le serveur, quand on l'interroge, renvoie ses endpoints avec son propre hostname (ex. opc.tcp://plc1-s71500:4840) au lieu de son IP. Si le client ne rĂ©sout pas ce nom (pas de DNS, pas d'entrĂ©e /etc/hosts), il n'arrive nulle part. La parade : forcer l'IP.

Un serveur OPC-UA renvoie souvent ses endpoints avec son hostname, pas son IP. Le client suit alors cette URL
 et se perd si le nom n'est pas rĂ©soluble sur le rĂ©seau OT (souvent sans DNS). SymptĂŽme : uadiscover marche mais uals/uaread temporise. Deux parades : ajouter le nom dans /etc/hosts (192.168.0.1 plc1-s71500), ou dire au client d'ignorer l'URL renvoyĂ©e et de garder l'IP d'origine — l'option --no-check-hostname des outils asyncua, ou client.check_hostname = False en Python.

Sur le S7-1500, le serveur OPC-UA s'active et se paramĂštre dans TIA Portal (propriĂ©tĂ©s du CPU → OPC UA → Serveur) : activation, port, endpoints, Security Policies proposĂ©es, gestion des utilisateurs. Surtout, c'est lĂ  que se trouve la liste de confiance des certificats (SĂ©curitĂ© de la communication → Certificats des partenaires). Pour lever un BadCertificateUntrusted, on y importe et on approuve le certificat du client, puis on recharge la configuration. Sans cette approbation, aucun canal sĂ©curisĂ© ne s'ouvrira, quoi que fasse le shell.

Récapitulatif

Exercices

Exercice 1 — Cartographier ce que le serveur accepte

Interroge le serveur OPC-UA du CPU (opc.tcp://192.168.0.1:4840) et dresse la liste des endpoints publiĂ©s : pour chacun, quelle Security Policy, quel Security Mode, et quels modes d'authentification. Quel endpoint choisirais-tu pour un premier test rapide, et pourquoi ?

Voir la solution
$ uadiscover opc.tcp://192.168.0.1:4840
Endpoints:
  opc.tcp://192.168.0.1:4840  None / None            (Anonymous, UserName)
  opc.tcp://192.168.0.1:4840  Basic256Sha256 / SignAndEncrypt  (UserName, Certificate)
  opc.tcp://192.168.0.1:4840  Aes128_Sha256_RsaOaep / SignAndEncrypt  (UserName, Certificate)

Pour un premier test, l'endpoint None/None en authentification anonyme est le plus rapide : aucun certificat, aucune politique Ă  nĂ©gocier. S'il rĂ©pond, on a validĂ© le rĂ©seau, le port et le serveur d'un coup. Attention : sur une vraie installation cet endpoint est souvent dĂ©sactivĂ© pour raison de sĂ©curitĂ© — auquel cas il faudra passer directement par un endpoint sĂ©curisĂ© et gĂ©rer les certificats.

Exercice 2 — Diagnostiquer une session qui n'aboutit pas

Un collĂšgue se plaint : « ma connexion OPC-UA sĂ©curisĂ©e ne s'ouvre pas, le client dit juste connection failed ». Le port 4840 rĂ©pond pourtant. Capture sa tentative, identifie le code de statut, et explique la correction.

Voir la solution

On capture pendant qu'il relance sa connexion :

$ sudo tshark -i eth0 -f 'tcp port 4840' -Y opcua
1  0.000  .71 → .1   OpcUa  Hello message
2  0.002  .1  → .71  OpcUa  Acknowledge message
3  0.004  .71 → .1   OpcUa  OpenSecureChannel request
4  0.019  .1  → .71  OpcUa  ServiceFault : BadCertificateUntrusted

Hello/Acknowledge passent : le port est bien ouvert et un serveur OPC-UA rĂ©pond — donc ce n'est pas un problĂšme rĂ©seau. La connexion casse Ă  l'OpenSecureChannel avec BadCertificateUntrusted : le serveur reçoit le certificat du client mais ne lui fait pas confiance. Correction : cĂŽtĂ© CPU, dans TIA Portal (SĂ©curitĂ© de la communication → Certificats des partenaires), importer et approuver le certificat client, puis recharger. Rien Ă  changer cĂŽtĂ© shell : le mĂȘme client repassera ensuite l'OpenSecureChannel. Si le code avait Ă©tĂ© BadSecurityPolicyRejected, la correction aurait Ă©tĂ© tout autre — aligner la politique du client sur celles publiĂ©es par uadiscover.

Exercice 3 — Lire une valeur, proprement

Lis la valeur du nƓud ns=3;i=1001 du CPU. Avant de tenter la lecture, vĂ©rifie que le port 4840 est bien ouvert — pour ne pas confondre « nƓud introuvable » et « serveur injoignable ». Donne les deux commandes, puis l'Ă©quivalent en Python.

Voir la solution

D'abord la couche transport, ensuite l'application :

$ nmap -p4840 192.168.0.1
4840/tcp open opcua
$ uaread -u opc.tcp://192.168.0.1:4840 -n "ns=3;i=1001"
1450.0

Le port open Ă©carte le pare-feu et le serveur dĂ©sactivĂ© ; si uaread Ă©chouait ensuite, on saurait que le problĂšme est applicatif (nƓud, politique, certificat), pas rĂ©seau. L'Ă©quivalent Python, pratique pour scripter une supervision :

import asyncio
from asyncua import Client

async def main():
    async with Client("opc.tcp://192.168.0.1:4840") as c:
        node = c.get_node("ns=3;i=1001")
        print(await node.read_value())

asyncio.run(main())

Si le serveur renvoie ses endpoints avec son hostname et que la lecture temporise malgré un port ouvert, ajoute c.check_hostname = False avant l'entrée dans le with, ou une entrée dans /etc/hosts (cf. le piÚge du §6d).