Webex pose une bannière jaune en haut de la fenêtre. Offline - No internet connection. Les services téléphoniques apparaissent déconnectés, rien ne se synchronise, et vous ne pouvez pas rejoindre la réunion commencée il y a quatre-vingt-dix secondes.

Ce n’est pas un désagrément quand la chose est votre téléphone de travail. C’est par Webex que mes appels arrivent, et ma ligne VoIP passe par là, donc un client qui refuse de s’authentifier, c’est un poste fixe qui ne sonne pas, une réunion où je ne suis pas, et un collègue qui tombe sur la messagerie. Ça m’a empêché de travailler. Pas ralenti, pas dégradé. Arrêté.

Et rien de tout ça ne dépendait de moi, ni pour le causer ni pour l’éviter. Une journée de travail est partie de travers à cause d’un contrôle qualité qui n’a pas été appliqué, chez un fournisseur qui est payé, sur un produit vendu avec un contrat de support, contre une plateforme que sa propre page d’exigences dit prise en charge. Je n’ai rien mal configuré. J’ai installé le paquet de l’éditeur, depuis le dépôt de l’éditeur, sur une plateforme que l’éditeur liste, et il n’a pas pu ouvrir une connexion TLS. Puis j’ai passé une soirée de mon temps à découvrir pourquoi, c’est-à-dire le temps que les gens qui ont signé le paquet n’ont pas passé.

La machine n’est pas hors ligne. Le navigateur à côté charge des pages. Votre courrier arrive, votre terminal tire depuis un dépôt distant, et si vous demandez au système d’exploitation s’il atteint internet il répond oui. Webex lui-même en convient, dans son propre journal, onze secondes avant de vous dire le contraire.

Ce qui s’est réellement passé, c’est que la copie d’OpenSSL que Cisco livre dans Webex ne trouve pas une seule autorité de certification, parce qu’elle a été compilée pour les chercher dans /workspace/.conan2/p/b/cisco8ee8b59cf93de/p/ssl. C’est un répertoire sur un conteneur de build Cisco. Il n’a jamais existé sur votre ordinateur et il n’existera jamais. Toute connexion TLS de l’application échoue à la vérification du certificat, le jeton de connexion ne peut pas être renouvelé, un minuteur de quinze secondes expire, et l’interface attrape la seule explication pour laquelle elle a une chaîne de caractères.

La bannière est donc fausse d’une manière précise et peu utile. Elle désigne votre réseau. La faute est un chemin dans leur build.

Il s’agit de la version 46.8.0.35631, sur Fedora 44, noyau 7.2.4. C’est une plateforme prise en charge. Cisco publie des exigences système Linux et livre un .rpm signé, webex-46.8.0.35631-1.x86_641. Ce qui suit, c’est comment le prouver en une dizaine de minutes, pourquoi aucun des correctifs évidents ne marche, celui qui marche, et ensuite la partie qui compte plus que tout le reste : ce n’est pas un bug subtil. C’est un build que personne n’a jamais lancé sur une machine qui ne l’avait pas construit.

Ce que la bannière vous dit vraiment

Webex pilote son affichage de connectivité avec un automate composite, sept sous-automates ayant chacun ses minuteurs. Au démarrage ils s’initialisent ainsi :

ConnectivityStateMachine::ConnectivityBanner - Initializing with state: Connected
ConnectivityStateMachine::Network        - Initializing with state: NoNetwork
ConnectivityStateMachine::Services       - Initializing with state: Connected
ConnectivityStateMachine::Mercury        - Initializing with state: Disconnected
ConnectivityStateMachine::Authentication - Initializing with state: UserNotAuthenticated
ConnectivityStateMachine::Syncing        - Initializing with state: Synced
ConnectivityStateMachine::Survivability  - Initializing with state: SurvivabilityHide

Quarante millisecondes plus tard le système d’exploitation répond, et l’application le consigne :

NetworkManagerPowerNetworkWatcher.cpp:93 onConnectivityCheckSuccess::
  The host is connected to a network, that appears to be able to reach the full Internet.

Cette ligne est dans le même fichier, dans la même session, que la bannière affirmant qu’il n’y a pas de connexion internet. L’application savait. Elle avait la réponse en main à 08:25:12.131 et a affiché le contraire à 08:25:27.132.

Entre ces deux moments, ceci :

HeureCe qui s’est passé
08:25:12.131Le système d’exploitation confirme l’accès complet à internet
08:25:12.218Détection de proxy : aucun configuré, connexion directe
08:25:12.241La première requête HTTPS échoue, errorCode: 167772294 Error in SSL handshake
08:25:12.569Le renouvellement du jeton CloudApps échoue, même code
08:25:12.571Le renouvellement du jeton Kms échoue, même code
08:25:15.684Nouvel essai, les deux échouent
08:25:21.745Nouvel essai, les deux échouent
08:25:27.132Le minuteur de quinze secondes expire, la bannière bascule sur NoInternet
08:26:12.091Les minuteurs de soixante secondes expirent, les services tombent en DisconnectedShortTerm

Le sous-automate d’authentification ne quitte jamais UserNotAuthenticated, donc Services se déduit comme déconnecté, donc la bannière se déclenche. Chacune de ces étapes est un comportement correct vu l’entrée. L’entrée est fausse, et l’entrée est un nombre : 167772294, sur chaque requête en échec, de la première à la dernière.

Ce nombre, c’est tout l’article. Gardez-le en tête.

Trois chemins, dont aucun n’existe

Webex n’utilise pas l’OpenSSL du système. Il apporte le sien, avec sa propre libcurl, et cette libcurl se lie à celui qui est livré plutôt qu’au vôtre :

$ ldd /opt/Webex/bin/libcurl.so | grep -E 'ssl|crypto'
    libssl.so.3    => /opt/Webex/bin/../lib/libssl.so.3
    libcrypto.so.3 => /opt/Webex/bin/../lib/libcrypto.so.3

Jusqu’ici pas de problème. Livrer sa propre bibliothèque TLS est un choix défendable et beaucoup d’éditeurs le font. Ce qui compte, c’est ce qui a été cuit dedans, et OpenSSL vous le dira si vous demandez :

$ strings /opt/Webex/lib/libcrypto.so.3 | grep -E 'OPENSSLDIR|ENGINESDIR|MODULESDIR'
OPENSSLDIR: "/workspace/.conan2/p/b/cisco8ee8b59cf93de/p/ssl"
ENGINESDIR: "/workspace/.conan2/p/b/cisco8ee8b59cf93de/p/lib/engines-3"
MODULESDIR: "/workspace/.conan2/p/b/cisco8ee8b59cf93de/p/lib/ossl-modules"

Trois. Pas un réglage qui a glissé. Le préfixe d’installation entier, transporté depuis la machine qui l’a compilé, dans une bibliothèque qui atteint le client sous forme de paquet signé sur une plateforme prise en charge.

OPENSSLDIR se règle une fois, au moment de la configuration, avec --openssldir, et la documentation de build d’OpenSSL est nette sur son rôle : « Directory for OpenSSL configuration files, and also the default certificate and key store. »2 Tout ce qui pend sous la confiance y est accroché. Le fichier CA par défaut est cert.pem à l’intérieur et le répertoire CA par défaut est certs à l’intérieur3. /workspace est un cache de build Conan. Conan est le gestionnaire de paquets C++ avec lequel Cisco construit, et il range chaque paquet sous une empreinte de ses entrées de build4. L’empreinte cisco8ee8b59cf93de est un fait au sujet d’un conteneur probablement supprimé quelques minutes après la fin du build.

Demandez à la bibliothèque ce qu’elle est, et ce n’est même pas de l’OpenSSL d’origine :

VERSION              CiscoSSL 3.5.5.8.5.4 27 Jan 2026
BUILT_ON             built on: Wed Feb 25 06:29:18 2026 UTC
PLATFORM             platform: conan-Release-Linux-x86_64-gcc-13
DIR                  OPENSSLDIR: "/workspace/.conan2/p/b/cisco8ee8b59cf93de/p/ssl"

Un fork maison entretenu, avec son propre schéma de versions, construit en février, livré en août, et transportant toujours le répertoire de travail où il a été fabriqué.

Où chaque copie d'OpenSSL de la machine cherche ses ancres de confianceUne machine, deux copies d'OpenSSL, et une seule sait où sont les certificatsAucune ne se le voit dire à l'exécution. Chacune porte la réponse compilée en dur, et cette chaîne est posée par qui lance le build.La copie système : OpenSSL 3.5.8, Fedora 44OPENSSLDIR compilé en dur/etc/pki/tlsle répertoire est làcert.pem → tls-ca-bundle.pemcerts/ → ancres hachéesla chaîne est vérifiée contre de vraies ancresLa poignée de main aboutit. Tout le reste marche.D'où la certitude de l'utilisateur que le réseau va bien.La copie livrée par Webex : CiscoSSL 3.5.5.8.5.4OPENSSLDIR compilé en dur/workspace/.conan2/p/b/cisco8ee8b59cf93de/p/sslaucun répertoire de ce nom chez un clientcert.pem → absentcerts/ → absentle magasin charge zéro ancre de confianceÉchec : erreur 0x0A000086, 167772294 en décimal.La bannière montrée dit que le réseau est coupé.
Deux copies d’OpenSSL sur une machine. Ni l’une ni l’autre ne se voit dire à l’exécution où se trouve la confiance. Chacune porte une chaîne compilée en dur, et l’une de ces chaînes nomme un répertoire qui n’a jamais existé que sur l’hôte de build de quelqu’un d’autre.

Ne prenez pas strings pour une réponse. Demandez à la bibliothèque

strings trouve du texte dans un fichier. Il ne prouve pas que la bibliothèque s’en sert. Alors chargez la bibliothèque livrée et demandez-lui directement, ce qui prend une douzaine de lignes de Python et aucun accès root :

import ctypes
c = ctypes.CDLL("/opt/Webex/lib/libcrypto.so.3")
for f in ("X509_get_default_cert_file", "X509_get_default_cert_dir",
          "X509_get_default_cert_file_env", "X509_get_default_cert_dir_env"):
    getattr(c, f).restype = ctypes.c_char_p
    print(f"{f:34} {getattr(c, f)().decode()}")
X509_get_default_cert_file         /workspace/.conan2/p/b/cisco8ee8b59cf93de/p/ssl/cert.pem
X509_get_default_cert_dir          /workspace/.conan2/p/b/cisco8ee8b59cf93de/p/ssl/certs
X509_get_default_cert_file_env     SSL_CERT_FILE
X509_get_default_cert_dir_env      SSL_CERT_DIR

Voilà, de la bouche même de la bibliothèque. Quand quoi que ce soit dans Webex demande à cette copie d’OpenSSL le magasin de confiance par défaut, on lui tend un fichier et un répertoire qui n’existent pas. C’est ce que fait SSL_CTX_set_default_verify_paths, et c’est ce que fait presque tout client à moins qu’on lui ait dit autre chose.

Les deux dernières lignes méritent d’être notées, car elles sont l’issue de secours : la bibliothèque laisse SSL_CERT_FILE et SSL_CERT_DIR écraser les deux5. Retenez ça aussi. Ça devient important, et pas de la façon que vous croyez.

Reproduire le code d’erreur exact

Une preuve par inspection n’est pas une preuve. Prenez les libssl.so.3 et libcrypto.so.3 livrées, faites une vraie poignée de main vers un vrai hôte avec rien d’autre que les valeurs par défaut de la bibliothèque, et regardez ce qui revient.

L’essai intéressant est celui où les valeurs par défaut ne pointent sur rien. SSL_CERT_FILE et SSL_CERT_DIR écrasent exactement les deux valeurs qu’un OPENSSLDIR absent laisse pendantes, donc les pointer sur un chemin qui n’existe pas reproduit la condition livrée avec précision :

$ SSL_CERT_FILE=/nonexistent/cert.pem SSL_CERT_DIR=/nonexistent/certs python3 tls.py
set_default_verify_paths -> 1
set_fd -> 1
SNI -> 1
SSL_connect -> -1 SSL_get_error -> 1
verify result 20: unable to get local issuer certificate
  err: 0xa000086 error:0A000086:SSL routines::certificate verify failed

0x0A000086 en décimal, c’est 167772294.

C’est le nombre présent dans chaque ligne en échec du journal Webex, et il ne venait pas de Webex. Il venait de la bibliothèque TLS de Cisco elle-même, tournant en dehors de leur application, échouant pour exactement une raison : elle n’avait aucune ancre de confiance contre laquelle vérifier la chaîne. Même bibliothèque, même erreur, aucune application entre les deux.

Pointez les deux mêmes variables sur le vrai paquet Fedora et le même chemin de code aboutit :

$ SSL_CERT_FILE=/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem python3 tls.py
SSL_connect -> 1 SSL_get_error -> 0
verify result 0: ok

Rien du réseau n’a changé entre ces deux essais. Un chemin de fichier, si.

Chaque étape réseau réussit. Celle qui échoue ouvre un fichier local.Quatre étapes traversent le réseau et réussissent. La cinquième lit un fichier et échoue.Exécuté contre les libssl.so.3 et libcrypto.so.3 livrées, hors de Webex, avec les seuls réglages par défaut de la bibliothèque.TCP connect :443okClientHello, SNIokServerHello, chaîneok, chaîne reçuevérifier la chaîne contre la confianceaucune ancre chargéece que renvoie la bibliothèqueSSL_connect -> -1verify result 20: unable to get local issuer certificateerr: 0xa000086 error:0A000086:SSL routines::certificate verify failed0x0A000086 vaut 167772294 en décimal, le nombre de chaque ligne en échec du journal Webex.L'application n'a écrit le mot « certificat » nulle part. Elle a journalisé « Error in SSL handshake » et un entier décimal, et l'interfaceen a fait « Offline - No internet connection », la seule chose que la machine n'était manifestement pas.Rien ici n'est une panne réseau. Il ne manquait qu'un répertoire.
Quatre étapes traversent le réseau et réussissent, y compris la réception de la chaîne de certificats complète du serveur. L’étape qui échoue ouvre un fichier local. On montre à l’utilisateur un message sur sa connexion internet.

Pourquoi rien ne vous a averti

Deux choses se liguent pour rendre ça silencieux, et une seule des deux est la faute de Cisco.

Ce qui ne dit jamais un motConception de quiPourquoi ça reste muet
Charger un magasin de confiance qui n’est pas làCelle d’OpenSSL, délibérémentSSL_CTX_set_default_verify_paths renvoie 1 que les chemins soient réels ou non. « A missing default location is still treated as a success »3
N’avoir aucun repliCelle de Cisco, et correcteCertificats validés, auto-signés refusés, réessai SSL désactivé, donc aucun mode dégradé n’existe pour masquer une faute

La première est raisonnable. Un programme qui livre ses propres ancres séparément ne devrait pas être forcé de s’en soucier, donc l’appel réussit, le magasin est vide, et rien nulle part dans la pile ne dit j’ai chargé zéro autorité de certification. La première chose qui s’en aperçoit est un échec de vérification une demi-seconde plus tard. J’ai lancé cet appel contre la bibliothèque livrée avec les chemins pointés sur /nonexistent et il a renvoyé 1. C’est dans la sortie ci-dessus.

La seconde est une politique qui descend du service, et le journal la consigne :

Wdm.cpp:1162 parseDeviceJson: Adding policy << allowSelfSignedCertificate with value: false
NetworkManager.cpp:1738 onConfigReady: ...httpRequestSSLRetryEnabled: 0
HttpRequestManager.cpp:1883 rawHttpRequest: {"validateCertificates":"true","useClientCertificate":"false"}

Ces trois drapeaux sont la seule raison pour laquelle cette faute est une panne et pas quelque chose de bien pire, et ça vaut la peine de s’y arrêter plutôt que de passer vite.

Déroulez-le. La bibliothèque livrée charge zéro ancre de confiance, et rien sous la couche de politique n’allait jamais le remarquer, parce que l’échec est silencieux par conception jusqu’en bas. Ce qui en a fait une bannière, ce sont trois drapeaux. Retournez-en un seul comme le font quantité de clients et le même build n’échoue pas du tout. Il se connecte, à n’importe quoi tenant n’importe quel certificat, parce qu’il n’a rien pour en vérifier un.

Le même magasin de confiance cassé, à un drapeau de politique d'une faute très différenteLe chemin de confiance est également cassé des deux côtés. Seule la politique décide comment vous l'apprenez.Commun aux deux : l'OPENSSLDIR compilé en dur n'existe pas, donc le magasin charge zéro autorité de certificationTel que Webex le livrevalidateCertificates: trueallowSelfSignedCertificate: falsehttpRequestSSLRetryEnabled: 0Rien contre quoi vérifier, donc il refuse.Échec. Bannière. Vous perdez une journée.Bruyant, et inoffensif.L'un d'eux réglé dans l'autre sensvalidateCertificates: falseou auto-signés autorisésou réessai sans vérificationRien contre quoi vérifier, donc il continue.La poignée de main aboutit. Contre n'importe quoi.Silencieux, et pas inoffensif.Ce qui a séparé les deux issues, c'est une valeur de politique posée par une autre équipe, en aval du défaut, pour des raisons sans rapport.Le chemin de confiance ne protégeait personne. Il était cassé de bout en bout, et la seule question ouverte était dans quel sens il échouerait.
La différence entre « Webex est hors ligne aujourd’hui » et « Webex a fait confiance à ce qui a répondu » tient à une valeur de politique posée par une autre équipe, en aval du défaut, pour des raisons qui n’ont rien à voir avec lui.

Ce qui pose problème, c’est la façon dont ça remonte. L’utilisateur reçoit « Offline - No internet connection ». Le journal reçoit « Error in SSL handshake » et un entier décimal. Le mot certificat n’apparaît nulle part où un utilisateur ou un technicien de premier niveau ira jamais regarder, et la seule miette de diagnostic est un nombre qu’il faut convertir en hexadécimal pour qu’il signifie quoi que ce soit.

Les correctifs qui n’ont pas marché

Les deux évidents échouent, et les raisons sont différentes et toutes deux bonnes à connaître.

Modifier l’openssl.cnf livrée

Webex livre un fichier de configuration dans /opt/Webex/lib/openssl.cnf, et tel qu’il est livré il n’active qu’un fournisseur :

[provider_sect]
fips = fips_sect

FIPS seulement. Pas le fournisseur default, là où vivent les algorithmes TLS ordinaires6. Le rajouter est une modification d’une ligne et ça ne change rien du tout, parce que l’OpenSSL livré ne lit jamais ce fichier. Il cherche openssl.cnf à l’intérieur d’OPENSSLDIR7, et OPENSSLDIR est le chemin qui n’existe pas. Le fichier trône dans le répertoire d’installation avec un air officiel. Rien ne le lit.

C’est un second défaut caché derrière le premier. Même si le problème de chemin était corrigé demain en pointant OPENSSLDIR sur /opt/Webex/lib, cette configuration se chargerait alors et n’activerait que FIPS. Et le module FIPS lui-même est chargé depuis MODULESDIR, le troisième chemin mort du même arbre, donc le fips.so livré dans /opt/Webex/lib reste introuvable lui aussi. Trois chemins, un préfixe erroné, et chacun cassé d’une façon qui cache le suivant.

Définir les variables d’environnement

La bibliothèque honore SSL_CERT_FILE et SSL_CERT_DIR. Je l’ai prouvé plus haut. L’essai réussi est juste là. Le geste évident est donc de les définir dans l’entrée de bureau, et c’est ce qui a été tenté :

Exec=env OPENSSL_CONF=/opt/Webex/lib/openssl.cnf \
  SSL_CERT_FILE=/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem \
  SSL_CERT_DIR=/etc/pki/tls/certs/ /opt/Webex/bin/CiscoCollabHost %U

Aucun changement. Et la raison n’est pas que Webex ignore les variables. La raison est que le processus ne les a jamais reçues :

$ tr '\0' '\n' < /proc/20293/environ | grep -E 'SSL|OPENSSL|CURL'
$ tr '\0' '\n' < /proc/20293/environ | wc -l
99

Quatre-vingt-dix-neuf variables dans le processus Webex en cours, et pas une seule des trois qui avaient été définies. Parce qu’il y a deux entrées de bureau du même nom sur cette machine :

FichierCe que sa ligne Exec lanceÉcrit par
/usr/share/applications/webex.desktopla ligne env à trois variables ci-dessus, en entierle .rpm, puis modifié à la main
~/.local/share/applications/webex.desktop/opt/Webex/bin/CiscoCollabHost %U, et aucun environnementle lanceur de Webex lui-même

Et la spécification n’est pas ambiguë sur celle qui gagne : « The base directory defined by $XDG_DATA_HOME is considered more important than any of the base directories defined by $XDG_DATA_DIRS. »8 $XDG_DATA_HOME vaut ~/.local/share. La copie utilisateur masque celle du paquet, à chaque fois, sur tout bureau qui suit la spécification9.

Webex installe donc une seconde copie de son propre lanceur dans votre répertoire personnel, et c’est cette copie que votre bureau exécute. Modifiez le fichier du paquet autant que vous voulez. Vous éditez un document que rien ne lit.

Deux entrées de bureau du même nom, et celle qui gagne est celle que Webex écrit pour lui-mêmeL'environnement a été défini dans le fichier que le bureau ne lit jamaisDeux entrées, un nom. L'ordre de recherche est écrit, et il ne favorise pas la copie du paquet.Modifiée à la main, et surclassée/usr/share/applications/webex.desktopExec=env OPENSSL_CONF=... SSL_CERT_FILE=...jamais consultée tant que l'autre fichier existeÉcrite par Webex, et elle gagne~/.local/share/applications/webex.desktopExec=/opt/Webex/bin/CiscoCollabHost %Uaucun environnement définiécartéelancéeSpécification des répertoires de base XDG : le répertoire de base défini par $XDG_DATA_HOME est considéré comme plusimportant que tous ceux définis par $XDG_DATA_DIRS.Preuve, tirée du processus en cours plutôt que du raisonnement :tr '\0' '\n' < /proc/20293/environ | grep -E 'SSL|OPENSSL' → aucune sortie, 99 variables, aucune de celles-ciLe correctif était réel, le fichier était réel, et le processus visé a été lancé d'ailleurs.
L’environnement a été défini dans le fichier que le bureau ne lit jamais. Webex écrit sa propre entrée sous le répertoire de données de l’utilisateur, la spécification dit que celle-là l’emporte sur celle du paquet, et la preuve est le processus en cours : quatre-vingt-dix-neuf variables d’environnement et aucune des trois.

Le correctif qui marche

Si la bibliothèque insiste sur un chemin, donnez-lui le chemin. Créez le répertoire qu’elle a été compilée pour vouloir et remplissez-le de liens symboliques vers le vrai :

#!/bin/bash
# Point the bundled CiscoSSL at the system trust store by building the
# directory it was compiled to look for. Tested: Fedora 44, Webex 46.8.0.35631.
set -euo pipefail

OPENSSLDIR=$(strings /opt/Webex/lib/libcrypto.so.3 \
  | grep -oP '(?<=OPENSSLDIR: ")[^"]+')
[ -n "$OPENSSLDIR" ] || { echo "no OPENSSLDIR found in the shipped library"; exit 1; }

for p in /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem \
         /etc/ssl/certs/ca-certificates.crt \
         /etc/pki/tls/certs/ca-bundle.crt \
         /etc/ssl/cert.pem; do
  [ -f "$p" ] && { CA_BUNDLE="$p"; break; }
done
[ -n "${CA_BUNDLE:-}" ] || { echo "no system CA bundle found"; exit 1; }

echo "OPENSSLDIR: $OPENSSLDIR"
echo "CA bundle:  $CA_BUNDLE"

sudo mkdir -p "$OPENSSLDIR"
sudo ln -sf "$CA_BUNDLE" "$OPENSSLDIR/cert.pem"
sudo ln -sf "$(dirname "$CA_BUNDLE")" "$OPENSSLDIR/certs"

sudo tee "$OPENSSLDIR/openssl.cnf" > /dev/null <<'CONF'
openssl_conf = openssl_init
[openssl_init]
providers = provider_sect
[provider_sect]
default = default_sect
fips = fips_sect
[default_sect]
activate = 1
CONF

echo "done. now restart Webex"

Relancez-le, et la même séquence de démarrage produit le résultat inverse. Même binaire. Mêmes lignes de journal. Le renouvellement de jeton, qui échouait en moins de soixante millisecondes, se termine maintenant en trois cent trente :

AuthTokenRequester.cpp:579  Managed to fetch a new Kms access token.
AuthTokenRequester.cpp:579  Managed to fetch a new CloudApps access token.
AuthTokenSupervisor.cpp:310 Auth tokens refreshed. Expires in [64799 secs].
AuthenticationManager.cpp:1860 onUserAuthenticated: User authenticated.
ConnectivityStateMachine::Authentication - UserNotAuthenticated -> UserAuthenticated

Authentifié en environ 750 ms, donc le minuteur de quinze secondes ne se déclenche jamais et la bannière n’apparaît jamais. Les services téléphoniques passent de Disconnected à Connecting, un état que la session cassée n’a jamais atteint en une minute d’essais.

AvantAprès
Test réseauréussitréussit
Première requête HTTPS167772294 Error in SSL handshakeHTTP 200
Jeton CloudAppséchecobtenu
Jeton Kmséchecobtenu
Authentificationbloquée sur UserNotAuthenticatedUserAuthenticated
Bannière à 15 s« Offline - No internet connection »aucune
Services téléphoniquesjamais tentésen connexion
Temps jusqu’à l’authentificationjamais~750 ms

Notez ce que le correctif fait à votre système de fichiers, parce que ça devrait vous gêner. Il crée sur votre machine un répertoire de premier niveau appelé /workspace, un nom auquel la Filesystem Hierarchy Standard ne donne aucune place10, contenant un chemin de cache Conan et une empreinte de build appartenant à une entreprise à qui vous avez acheté un logiciel. Voilà la forme du remède que Cisco vous a laissé : monter l’environnement de build de quelqu’un d’autre à la racine du vôtre.

Il va aussi casser. De trois façons :

Quand ça cassePourquoiCe que vous faites
Une mise à jour de Webexcisco8ee8b59cf93de dérive des entrées de build, donc une dépendance reconstruite signifie un nouveau répertoireRelancer le script ; il lit le chemin dans le nouveau binaire au lieu de supposer l’ancien
Un changement de distributionLa cible du lien symbolique est une décision de distribution, pas une norme11Le repointer ; le script sonde quatre emplacements connus
Une réinstallation/workspace n’est sauvegardé, empaqueté ni possédé par rienLe relancer, à chaque fois, pour toujours

Rien de tout ça n’est de la maintenance. C’est vous, remplaçant une étape de la chaîne de build de quelqu’un d’autre, indéfiniment, gratuitement. Rustiner un défaut à l’exécution, sur chaque machine que vous possédez, parce que l’éditeur n’a pas voulu le rustiner une fois à la compilation.

Ils connaissaient la règle et l’ont appliquée à la moitié du build

Voici ce qui fait passer ça d’un rapport de bug à un argument.

Lisez la section dynamique des binaires livrés :

$ readelf -d /opt/Webex/bin/libcurl.so | grep RUNPATH
 0x1d (RUNPATH)  Library runpath: [$ORIGIN:$ORIGIN/../lib]
$ readelf -d /opt/Webex/bin/CiscoCollabHost | grep RUNPATH
 0x1d (RUNPATH)  Library runpath: [$ORIGIN/../lib]

$ORIGIN se développe au chargement en le répertoire contenant l’objet lui-même12. C’est le bon outil pour un ensemble déplaçable et ils l’ont utilisé correctement. Ils y étaient obligés : Webex livre le même arbre deux fois, une fois dans /opt/Webex et une fois dans ~/.local/share/WebexLauncher/46.8.0.35631_9e6196c9-…/, et un lanceur choisit entre les deux au démarrage. Deux préfixes, un build, et l’éditeur de liens retrouve ses bibliothèques dans les deux.

Les gens qui ont fait ce paquet avaient donc parfaitement compris le problème. Les chemins absolus ne survivent pas à la livraison. Ils l’ont résolu pour le code.

Puis ils ont laissé les chemins de données sous forme de chaînes absolues nommant le conteneur qui les a compilés. Même build. Même après-midi.

Un chemin du bundle se déplace tout seul. L'autre nomme une machine dans un centre de données quelque part.Ils savaient que le bundle devait bouger. Ils ne l'ont appliqué qu'au code.Les deux valeurs sont posées à la compilation par les mêmes gens le même jour. L'une se développe au chargement, l'autre jamais.D'où vient le code : enregistré comme expression relativeRUNPATH dans libcurl.so$ORIGIN:$ORIGIN/../lib/opt/Webex/lib résolu~/.local/share/WebexLauncher/46.8.0.35631_.../lib résolu aussiCorrect, et voulu. Le même arbre est livré deux fois à deux préfixes et l'éditeur de liens le trouve dans les deux.D'où vient la confiance : enregistré comme le répertoire de travail de quelqu'unOPENSSLDIR dans libcrypto.so.3/workspace/.conan2/p/b/cisco8ee.../p/sslaucun chemin de ce nom non résoluENGINESDIR et MODULESDIR : même arbre, même issueTrois chemins absolus vers un conteneur de build, livrés à chaque client, sur un produit vendu avec un contrat de support.Entre les deux moitiés de cette image, il y a une personne qui lance l'artefact empaqueté sur une machine qui ne l'a pas construit.C'est tout le défaut. Pas un bug difficile. Un bug non testé.
Le même build, le même jour, les mêmes ingénieurs. Le chemin de recherche des bibliothèques est enregistré comme une expression qui se résout là où l’arbre atterrit. Le chemin de confiance est enregistré comme le répertoire de travail de quelqu’un.

Ceci ne peut donc pas être classé sous ils ne savaient pas. On ne tombe pas sur $ORIGIN par hasard. On y va parce qu’on a compris qu’un chemin absolu cuit dans un artefact livré est un défaut, et compris assez bien pour aller le corriger dans l’éditeur de liens. Puis le même build écrit trois chemins absolus dans les mêmes bibliothèques, et les livre.

Connaître la règle et l’appliquer à la moitié du build est pire que de ne pas la connaître. Ne pas savoir est un problème de formation et la formation a une solution. Ceci est un paquet qui contenait la bonne idée, par écrit, dans l’en-tête ELF où n’importe qui pouvait la lire, et il est sorti cassé quand même. Ce qui vous dit que rien en aval du compilateur ne regardait le résultat. Rien.

Le conteneur était déjà là

D’où vient /workspace n’est pas un mystère, et vous n’avez pas à deviner. C’est dans l’en-tête du paquet :

$ rpm -qi webex | grep -E 'Build Host|Build Date|Vendor'
Build Date  : Sat 08 Aug 2026 20:47:43 BST
Build Host  : c964ea9239ae
Vendor      : Cisco

c964ea9239ae n’est pas un nom d’hôte que quelqu’un a tapé. Ce sont douze caractères hexadécimaux, ce qu’un conteneur rapporte comme nom d’hôte quand rien n’en définit un. Le paquet a donc été construit dans un conteneur, par une entreprise qui a manifestement les images, le registre et l’orchestration pour le faire, et trois artefacts distincts sur cette machine le disent indépendamment :

Preuve, lue sur le paquet installéValeurCe qu’elle prouve
Build Host dans l’en-tête RPMc964ea9239aeun identifiant de conteneur, pas une machine de build
OPENSSLDIR dans libcrypto.so.3/workspace/.conan2/…un chemin qui n’existe que dans ce conteneur
PLATFORM dans la même bibliothèqueconan-Release-Linux-x86_64-gcc-13une chaîne d’outils Conan conteneurisée
Requires dans le RPMglibc >= 2.28un plancher d’ABI très ancien, choisi délibérément

Ils l’ont ensuite signé. Le build s’est terminé à 20:47:43 et la signature est datée de 21:02:07 le même soir, identifiant de clé 9995e5bbb5ccde3c. Quinze minutes. Il y a donc une porte de publication, quelqu’un ou quelque chose l’actionne, et ce qu’elle atteste, c’est qui a fait le paquet, pas si le paquet fonctionne. Une signature est une déclaration sur la provenance. Elle n’a jamais été une déclaration sur l’aptitude, et un processus qui a l’une et pas l’autre a ses priorités dans le mauvais ordre.

Parce que l’étape manquante est la moins chère. Le conteneur est déjà dans la chaîne. Prenez l’artefact qui vient d’en sortir, démarrez une image propre de chaque distribution que vous prétendez prendre en charge, installez-le, lancez-le, et lisez les cent premières lignes du journal :

docker run --rm fedora:44 sh -c '
  dnf -y install ./webex-46.8.0.35631-1.x86_64.rpm &&
  timeout 25 /opt/Webex/bin/CiscoCollabHost &
  sleep 20
  grep -c "Error in SSL handshake" ~/.local/share/Webex/current_log.txt'

Non nul, à chaque fois, sur ce build. L’installation fait 1,1 Go, comptez donc une minute par cible sur un cache chaud. Six distributions, c’est six minutes d’une machine qui tourne déjà, sur du matériel que Cisco possède déjà, dans une chaîne qui existe déjà. Ça n’a pas été fait. Pas une fois.

Et c’est la réponse à ce que les gens disent encore de Linux, qu’il est difficile de prendre en charge plusieurs distributions. Ça a cessé d’être difficile le jour où cet outillage est arrivé, et l’outillage est celui-là même avec lequel ils compilent. Une image de base par cible. Le même artefact dans chacune. La matrice est une boucle.

Une chaîne qui construit dans un conteneur et n’exécute jamais le résultat dans un conteneur n’est pas une chaîne. C’est un compilateur avec une tâche planifiée devant et une clé de signature derrière, et quoi qu’elle produise, c’est une supposition.

Ils construisent dans un conteneur et n'exécutent jamais le résultat dans un conteneurLe conteneur est déjà dans la chaîne. Il ne sert que pour la moitié qui arrange l'éditeur.Chaque valeur ci-dessous est lue sur le paquet installé, pas déduite.Build, dans un conteneurBuild Host: c964ea9239ae/workspace/.conan2/p/b/...conan-Release-Linux-x86_64-gcc-13trois preuves distinctes d'un conteneurSigner et publierconstruit 20:47:43signé 21:02:07quinze minutes, et une porte de publicationqui atteste qui, jamais siClientdnf install webexl'ouvre"Offline - No internet"L'étape qui n'est nulle part dans la chaînedocker run --rm fedora:44 sh -c 'dnf -y install ./webex.rpm && CiscoCollabHost &sleep 20; grep -c "Error in SSL handshake" ~/.local/share/Webex/current_log.txt'Même infrastructure. Une image par distribution cible. Moins d'une minute chacune, et ça échoue bruyamment sur ce build.Construire pour plusieurs distributions a cessé d'être difficile le jour où cet outillage est arrivé, et c'est le même outillage avec lequel ils compilent.Une chaîne qui construit dans un conteneur et n'y exécute jamais le résultat est un compilateur avec une tâche planifiée devant et une clé de signature derrière.
Trois preuves indépendantes dans le paquet livré que le build a tourné dans un conteneur, une signature appliquée quinze minutes après le build, et la seule étape qui n’apparaît nulle part : lancer le paquet fini dans une image propre de chaque plateforme pour laquelle il est vendu comme pris en charge.

Pourquoi une version de 2026 est-elle bâtie sur une libc de 2018 ?

Le paquet déclare ce dont il a besoin, et la ligne intéressante est la première :

$ rpm -q --requires webex | grep glibc
glibc >= 2.28

glibc 2.28 est sortie le 1er août 201813. C’est la version de Red Hat Enterprise Linux 814, une édition dont le support complet s’est arrêté en 2024. Ceci est un produit de 2026, compilé en 2026, visant la bibliothèque C de 2018. Puis livrant sa propre copie de 2,5 Mo de libstdc++.so.6 dans le répertoire personnel de l’utilisateur, parce que l’environnement d’exécution C++ qui va avec une base aussi ancienne ne peut pas porter le code.

Pourquoi faire encore ça ? Parce qu’ils refusent de lier statiquement.

C’est tout. Dès l’instant où vous vous liez dynamiquement à la bibliothèque C de l’hôte, la plus vieille distribution que vous acceptez de prendre en charge devient une contrainte sur la machine où vous compilez. Vous ne pouvez pas utiliser un symbole dont la cible la plus ancienne n’a jamais entendu parler, donc vous clouez le build sur une image de base antique et vous y restez. Chaque année l’écart se creuse. Chaque nouvelle fonctionnalité de langage ou de bibliothèque arrive avec une discussion sur la possibilité de relever le plancher. Livrez votre propre libstdc++ pour masquer le pire, et vous voilà à entretenir en plus un environnement d’exécution privé.

Ce marché avait du sens quand un hôte de build était une machine physique dans une baie que quelqu’un devait réinstaller. Il n’en a plus depuis dix ans. Si vous devez vous lier à la libc de l’hôte, un conteneur par cible vous donne un vrai build sur une vraie version de chaque plateforme, et aucune ne contraint les autres.

Mais regardez ce que cette discipline de vieille cible a réellement acheté ici. C’est du conservatisme d’ABI à un coût considérable : un plancher vieux de huit ans, un environnement C++ embarqué, une matrice de support figée autour. Et le client ne démarre toujours pas. Parce que ce qui a cassé, c’est un chemin de fichier, et aucun soin apporté aux versions de symboles ne protège un chemin de fichier. Ils ont payé la taxe de compatibilité et la taxe d’empaquetage, et sauté le seul contrôle qui ne coûte rien. Les deux factures. Pas de produit.

Ils ont payé pour un bundle autonome et ne l’ont pas eu

La façon éprouvée de livrer un logiciel commercial sous Linux est de dépendre du moins possible de l’hôte. Statique quand vous pouvez, un arbre autonome quand vous ne pouvez pas, et aucune hypothèse sur la distribution en dessous. Ce n’est pas élégant et personne ne prétend le contraire. Ça existe à cause de l’alternative. Un binaire qui exige une version précise d’une bibliothèque précise à un endroit précis transforme la machine de chaque client en dossier de support.

Cisco a pris la seconde voie et a tout embarqué. Voici l’addition :

Ce que contient le bundleTaille ou nombre
Objets partagés dans /opt/Webex/lib150
Leur propre libcurl, CiscoSSL, zlib-ng, ICU, Kerberos, client CUPS, hunspell et moteur d’inférencetous
Installé sous /opt/Webex1,1 Go
Une seconde copie sous ~/.local/share/WebexLauncher, par utilisateur1,1 Go
Sur le disque pour un client de messagerie et d’appels2,2 Go

Deux gigaoctets de dépendances. C’est le prix complet de l’embarquement : le téléchargement, le disque, la duplication, la charge de sécurité d’être la seule partie capable d’en corriger quoi que ce soit, tout le lot. Payez ça et ce que vous achetez, c’est un programme qui se moque de ce que l’hôte a installé, qui se comporte pareil sur Fedora, Debian, Arch et sur ce qu’un client a standardisé cette année, et qu’une mise à niveau de distribution dont il n’a jamais entendu parler ne peut pas casser.

Sauf que non. Il se soucie énormément d’un répertoire, et c’est un répertoire sur un serveur de build.

Ils ont livré leur propre Kerberos, leur propre ICU et leur propre correcteur orthographique, et ils n’ont pas su livrer un chemin fonctionnel vers les certificats. Toute la raison d’être du bundle est d’être autonome, et il ne l’est pas sur le seul point qui l’empêche de fonctionner. Chacun de ces 1,1 Go est trouvé correctement grâce à $ORIGIN. La quarantaine d’octets qui comptent le plus, non.

Et c’est là que ça me prend. L’embarquement est l’option chère. Ils ont assumé la dépense, ils ont fait l’ingénierie difficile, ils ont réussi la relocalisation de cent cinquante bibliothèques sur deux préfixes d’installation. Puis pointé la seule partie qui décide si tout cela fonctionne vers une machine qu’aucun client n’a jamais possédée.

Personne ne l’a documenté non plus

Une seconde défaillance se tient à côté de la première, et c’est celle qui aurait attrapé la première.

Demandez au paquet quelle documentation il livre :

$ rpm -qd webex | wc -l
0
$ rpm -qc webex | wc -l
0

Aucun fichier de documentation. Et aucun fichier de configuration. Zéro entrée marquée %config dans un paquet qui livre un openssl.cnf. Ce n’est pas cosmétique. Un RPM marque un fichier %config pour que le gestionnaire de paquets préserve ce que l’administrateur a changé, en enregistrant un .rpmsave plutôt qu’en l’écrasant1516. /opt/Webex/lib/openssl.cnf est livré comme un fichier ordinaire, donc la prochaine mise à jour écrase toute modification que vous y avez faite et ne vous dit rien. Le seul fichier qu’un client pourrait légitimement avoir besoin d’ajuster est celui que l’empaquetage traite comme jetable.

Rien de publié ne dit quel magasin de confiance le client utilise, quelles variables d’environnement il honore, ni d’où il lit sa configuration TLS. Il n’y a aucune page à consulter. Aucune n’a été écrite. La seule façon dont j’ai établi tout cela, c’est strings, readelf, ldd et ctypes contre les binaires livrés. Rétro-ingénierie d’un produit pris en charge pour répondre à une question à laquelle sa documentation aurait dû répondre en une phrase.

Et voici pourquoi ça compte plus qu’il n’y paraît. Écrire cette documentation est en soi un test. Mettez n’importe qui chez l’éditeur devant une page blanche intitulée d’où Webex pour Linux lit ses certificats d’autorité, et la première chose à faire est d’aller voir. Dès qu’ils regardent, ils trouvent /workspace/.conan2/p/b/cisco8ee8b59cf93de/p/ssl, et la chose suivante qui sort d’eux est une question. Des chemins de configuration documentés ne sont pas de la paperasse au bénéfice du client. C’est l’audit le moins cher qu’un éditeur puisse mener sur son propre build, et le sauter est la façon dont un chemin pareil survit jusqu’à une publication.

Rien de tout ça n’est une grande demande. Dites où vit votre configuration. Dites quelles variables d’environnement vous honorez. Marquez vos fichiers de configuration comme tels pour qu’une mise à jour ne les avale pas. Alors le client qui tombe sur une panne a un endroit où regarder qui n’est pas un éditeur hexadécimal.

Personne ne l’a lancé

L’étape manquante a été nommée assez de fois plus haut. Ce qui vaut la peine d’être demandé, c’est pourquoi elle a manqué sur cette plateforme et pas sur les autres.

Pas sur Fedora, en tout cas. Sur macOS et sur le Fisher-Price OS (Windows), cette classe de faute ne peut pas se manifester de la même manière, parce que ces plateformes ont une pile TLS système avec un magasin de confiance géré par le système d’exploitation. Linux n’a rien de tel. OpenSSL est le magasin de confiance, et par conséquent celui qui le livre est propriétaire de l’endroit où il regarde. La seule plateforme où la bibliothèque embarquée est porteuse est donc celle qui a été livrée sans test, ce qui est une décision sur les clients qui méritent un test de fumée.

Toute l’enquête a pris une soirée : lire le journal, remarquer que la détection réseau passait avant que la bannière n’affirme le contraire, extraire le chemin compilé du binaire, reproduire le code d’erreur exact contre la bibliothèque livrée. Tout cela avec un agent d’IA qui faisait la corrélation des journaux et le harnais ctypes pendant que je décidais quoi lui demander. Je le mentionne pour une raison : le diagnostic qu’un éditeur n’a jamais posé avant de signer ce paquet et de le mettre dans son propre dépôt est désormais à portée d’une soirée pour n’importe quel client ayant la patience de regarder. Les ingénieurs de Cisco ont Claude à disposition comme tout le monde, et il aurait construit ça correctement. Vous ne pouvez pas lui demander de cuire /workspace/.conan2 dans un artefact destiné à être livré sans qu’il vous dise ce qui arrivera quand l’artefact quittera le workspace. L’outillage pour attraper ça n’est plus rare, et la connaissance non plus. Ce qui manque, c’est quelqu’un chez l’éditeur dont c’était le travail de regarder.

Pendant ce temps, le chemin de support pour la personne qui tombe là-dessus est une bannière disant que son internet est coupé. Elle va redémarrer sa box. Elle va appeler son opérateur. Elle va ouvrir un ticket qui ne mènera nulle part, parce que le symptôme que Cisco a choisi d’afficher pointe ailleurs que vers Cisco.

Voilà la liste complète de ce qui a mal tourné. Il vaut la peine de dire à quoi bien aurait ressemblé, parce que chaque point de cette liste a une réponse établie qui est antérieure à ce produit.

Comment il aurait fallu le construire

Assez de ce qui a mal tourné. Voici la norme, et rien n’y est nouveau. C’est ce que livrer un binaire sur l’ordinateur de quelqu’un d’autre vous demande depuis vingt ans.

Commencez par la décision que Cisco a eue juste sur le principe et fausse à l’exécution : jusqu’où vous acceptez de dépendre de l’hôte. Lier statiquement est la réponse la plus forte, et il vaut la peine d’être concret, parce que « il n’y a qu’à lier statiquement » est brandi par des gens qui n’ont jamais eu à le faire et balayé par des gens qui n’ont jamais essayé.

Ce que ça supprimeCe que ça coûte
RUNPATH, et toutes les façons de se tromper, parce qu’il n’y a plus de recherche au chargementla glibc ne se lie pas proprement en statique : la résolution des noms et des utilisateurs passe par dlopen, donc le binaire va quand même chercher les modules NSS de l’hôte17
Le plancher glibc, si bien que la plus vieille distribution cesse de dicter sur quoi vous pouvez compilerles parties LGPL portent une obligation de réédition de liens, donc elles restent dynamiques ou vous livrez de quoi relier
La casse due à une mise à niveau de distribution, à une bibliothèque renommée ou à un cache ldconfig périméVous possédez chaque correctif : aucune mise à jour de sécurité de distribution n’atteint vos clients
Le dlopen d’un .so versionné depuis un répertoire qui peut ne pas être làUn téléchargement plus gros, et aucun partage de pages entre processus

Et une ligne dans la colonne de gauche que les autres ne font que soutenir : l’artefact que votre chaîne a produit est l’artefact que le client exécute, octet pour octet. Testez-le et vous avez testé la chose que vous avez livrée. C’est la propriété dont tout cet article raconte l’absence.

La colonne de droite mérite de l’honnêteté. Des coûts réels, et la raison pour laquelle les gens se tournent vers musl ou acceptent un hybride. Mais regardez la troisième ligne. Posséder chaque correctif s’applique à l’identique à ce que Cisco a déjà fait : un arbre embarqué de 150 bibliothèques est le même engagement sans aucune des garanties au chargement. Ils se sont engagés à posséder chaque correctif dans les deux cas et n’ont rien obtenu en retour.

La règle est donc simple, et c’est celle qu’ils ont enfreinte : tout ce que vous ne pouvez pas lier dedans, vous devez le trouver par un chemin relatif au binaire. $ORIGIN pour le code, et la même discipline, délibérément, pour chaque chemin de données que la bibliothèque ira chercher. Ici il y en a quatre et chacun a une poignée documentée :

Ce que la bibliothèque va chercherCe qui a été livréCe que ça aurait dû être
Ancres de confianceOPENSSLDIR/cert.pem, figé à la compilationlivrées dans l’arbre, ou SSL_CERT_FILE défini au démarrage5
ConfigurationOPENSSLDIR/openssl.cnf, même chemin, jamais trouvéOPENSSL_CONF, pointé sur la copie du paquet5
Fournisseurs, dont FIPSMODULESDIR, absolu, donc fips.so inatteignableOPENSSL_MODULES, « the directory from which cryptographic providers are loaded »5
MoteursENGINESDIR, absoluOPENSSL_ENGINES, ou rien, puisque OpenSSL 4.0 a retiré le support des moteurs5

Quatre chemins, quatre variables d’environnement, toutes dans une seule page de manuel que leur propre bibliothèque livre avec elle. Si une chaîne de votre artefact commence par un / et a été décidée à la compilation, c’est un défaut qui attend qu’un client le trouve. Il n’existe pas de troisième option où un chemin de build absolu soit acceptable.

Les correctifs pour celui-ci, et le contrôle qui l’attrape

Descendons du principe à ce défaut précis. Six changements, pas un seul qui relève de la recherche :

CorrectifEffortPourquoi c’est juste
Mettre --openssldir=/opt/Webex/lib/ssl et livrer l’arbreune option de configurationLe chemin existe alors dans le paquet, et c’est à ça que sert l’option2
Définir SSL_CERT_FILE/SSL_CERT_DIR dans CiscoSSLUtils au démarrage, en sondant les chemins connus des distributionsune douzaine de lignesStandard, documenté, déjà pris en charge par leur propre bibliothèque5
Livrer eux-mêmes le paquet de CA dans le paquetempaquetage seulementContrôle complet de la confiance, au prix d’en assurer la fraîcheur
Corriger l’openssl.cnf pour activer le fournisseur defaultune ligneNécessaire de toute façon, et actuellement masqué par la faute de chemin6
Marquer openssl.cnf comme %configune ligne de specEmpêche une mise à jour d’avaler en silence la modification d’un administrateur15
Publier les chemins et les variables honoréesune pageL’audit le moins cher qui soit, et il trouve cette faute pendant qu’on l’écrit

Le premier est le correctif d’une option et il aurait livré le produit en état de marche. C’est une seule valeur dans un script de build, définie une fois, qu’ils ont eue fausse parce que rien en aval ne l’a jamais vérifiée.

Le contrôle est plus petit que le correctif :

# in CI, on the packaged artefact, in a clean container
test -d "$(strings lib/libcrypto.so.3 | grep -oP '(?<=OPENSSLDIR: ")[^"]+')" \
  || { echo "shipping a trust store path that does not exist"; exit 1; }

Une ligne. Elle aurait fait échouer cette publication bruyamment, en février, sur la machine qui l’a faite, dans le conteneur qui l’a faite, avant que le paquet ne soit signé. Si elle n’est pas là, ce n’est ni par difficulté ni par coût. C’est que personne n’a été chargé de l’écrire, et donc que savoir si l’artefact empaqueté se comportait comme un logiciel n’était le travail de personne.

Ce qu’un processus de build bâclé coûte à tout le monde

Tout ce qui précède est le processus de build d’un produit vu de l’intérieur, et je ne vais pas vous y repromener. La question qui vaut la peine, c’est de savoir si ce niveau de soin a des chances de s’arrêter à une seule équipe.

Alors mettez le dossier extérieur à côté. La CISA tient un catalogue de vulnérabilités connues pour être exploitées dans la nature. Pas théoriques, pas notées. Observées en train de servir contre des gens. Au 14 septembre 2026 il compte 1 710 entrées18 :

ÉditeurEntrées au catalogue KEV
Microsoft388
Cisco98
Apple94
Adobe81
Google74
Oracle46
Fortinet30
VMware26

Deuxième, derrière un monopole de système d’exploitation et devant tous les autres. La plus récente entrée Cisco est tombée le 14 septembre 2026, la veille de la rédaction de ceci.

J’ai compté le même fichier trois semaines plus tôt pour /fr/random/is-your-msp-lying-to-you-part2/ : version 2026.08.27, 1 685 entrées, Cisco à 96. Deux de plus depuis, en vingt et un jours.

Soyez juste sur ce que ce tableau prouve et ne prouve pas à lui seul. Une grande base installée dans des endroits à forte valeur attire l’attention, et l’attention trouve des bugs, donc tout éditeur de cette taille portera une longue liste. Le nombre est un a priori, pas un verdict.

La composition est plus difficile à écarter que le total. Vingt-cinq des quatre-vingt-dix-huit de Cisco sont sur les lignes de sécurité : les pare-feux, les appliances, les concentrateurs VPN, les passerelles de messagerie et web, les services d’identité. Et les quatre entrées les plus récentes contre eux, à la suite, sont Secure Firewall Management Center deux fois, Secure Firewall ASA, et Secure Email Gateway, cette dernière le 14 septembre 202618. Pas les commutateurs. Pas le matériel de collaboration. Les produits vendus spécifiquement pour être ce qui protège tous les autres.

Ce qui est la phrase vers laquelle tout cet article marchait, alors la voici clairement. C’est une entreprise dont le métier est de vendre des appliances de sécurité, et elle n’arrive pas à faire vérifier un certificat par un client de bureau. Pas un cas difficile. Pas une attaque inédite. L’opération de sécurité la plus routinière de l’informatique, exécutée par tous les navigateurs à chaque chargement de page, dans une bibliothèque qu’ils ont eux-mêmes forkée, et ils l’ont livrée pointée sur un répertoire qui n’a jamais existé sur une machine cliente. Puis signée.

Ce que cet article ajoute, c’est un échantillon du processus derrière. Pas une vulnérabilité. Un simple défaut d’empaquetage, la classe de faute la moins subtile qui soit, sur une plateforme prise en charge, attrapable par n’importe quel test de fumée que quelqu’un aurait pris la peine de lancer, et livré quand même. Si une chaîne de build met ça devant un client payant, on ne voit pas bien ce qu’elle arrêterait. Rien ne l’a fait.

Puis-je demander pourquoi ceci a été signé ?

Puis-je demander pourquoi un paquet incapable de mener à bien une poignée de main TLS a été signé et publié contre une plateforme que votre propre page d’exigences liste comme prise en charge ? Pas qui l’a signé. Je n’ai aucun intérêt pour un nom et il ne s’agit pas d’une personne. Qu’est-ce qui l’a permis, car quelque chose l’a permis, quinze minutes après la fin du build, et quoi que ce soit, ça tourne encore aujourd’hui.

Maintenant la version franche. Ce n’est pas un projet que j’ai tiré d’une forge en prenant mes risques. C’est payé. Il y a derrière un contrat, une ligne de support, une page d’exigences affirmant quelque chose sur Linux, et une clé de signature attestant que le paquet vient de Cisco et est apte à être installé. Chacun de ces éléments est une déclaration faite à un client, et sur cette version chacun valait zéro, parce que le résultat n’a jamais été ouvert.

Et la norme manquée ici n’est pas la mienne. C’est la leur. Les quatre variables qui auraient porté ces chemins sont documentées dans une page de manuel que vous livrez dans le bundle. Votre propre format de paquet a un marqueur %config que vous n’avez pas utilisé et une section documentation que vous avez laissée vide. Votre propre build a tourné dans un conteneur que vous n’avez jamais utilisé pour lancer la sortie. Je ne demande pas à une entreprise de réseau et de collaboration d’inventer quoi que ce soit. Je demande pourquoi elle n’a pas lu ses propres manuels.

Aussi l’excuse que je n’accepterai pas, c’est que ce serait difficile. Ce n’est difficile pour personne, et ce n’est certainement pas difficile pour une entreprise qui fait ça tous les jours, à cette échelle, pour cet argent. Des professionnels qui font ça quotidiennement n’ont aucune excuse, et être grand n’en est pas une.

Et puis-je en poser une de plus, parce que c’est celle qui compte vraiment. Vous vendez des pare-feux. Vous vendez une passerelle de messagerie, un concentrateur VPN, un service d’identité, un centre de gestion pour tout ça, et l’argumentaire sur chacun est que vous comprenez ça mieux que votre client. Alors comment une entreprise qui se présente comme une autorité en sécurité réseau livre-t-elle un client incapable de valider un certificat ? Pas qui échoue à le valider avec finesse. Incapable d’en chercher un, parce que le répertoire n’a jamais été là.

Il n’existe aucune version de cette réponse que j’aie envie d’entendre qui commence par le fait que l’équipe bureau serait distincte de l’équipe appliance. C’est la même signature, la même chaîne, la même affirmation publiée sur une plateforme prise en charge, et la même étape manquante à la fin, qui est d’ouvrir la boîte. Si la validation des certificats n’est pas vérifiée avant livraison dans le produit où la casse est bruyante et inoffensive, je n’ai aucune raison de croire qu’elle l’est dans le produit où la casse est silencieuse et coûteuse.

Et la partie honnête, dite simplement. Rien de tout cela ne m’a surpris. J’en suis venu à attendre ce niveau de cet éditeur, et c’est pourquoi, là où le choix est le mien, je n’achète pas son matériel et je ne bâtis pas dessus. Ce n’est pas une préférence de logo. C’est le jugement que je porterais sur n’importe quel fournisseur : j’ai mesuré son travail, plus d’une fois, et il revient toujours pareil. Le catalogue ci-dessus est une mesure. Ce qu’a coûté la première moitié de cet article en est une autre.

La raison pour laquelle j’étais sur Webex, c’est que le choix n’était pas le mien, et ça mérite d’être nommé, parce que c’est la position de la plupart des gens qui lisent ceci. Vous pouvez rarement éviter un éditeur dont vous avez déjà pris la mesure. Quelqu’un d’autre signe le contrat, le matériel arrive, et le premier à découvrir ce qui a été sauté dans le build, c’est vous, à votre bureau, avec une réunion qui commence.

Livrer une chose qu’on n’a jamais lancée

Il y aura une explication interne. Un sprint chargé, une chaîne qui a changé de mains, une plateforme sans propriétaire. Aucune ne vaut d’être entendue, parce que chacune décrit la même chose : le travail n’a pas été fait et rien dans le processus ne l’exigeait.

Il existe dans les métiers manuels une vieille norme qui n’est jamais passée dans le logiciel : vous ne quittez pas le chantier avant d’avoir fait tourner la chose. Vous remplissez le réseau et vous vérifiez chaque raccord. Vous mettez le tableau sous tension et vous testez chaque circuit. Pas parce que vous doutez de votre travail. Parce que le client va s’en servir, et le découvrir devant lui n’est pas un résultat professionnel. Personne ne vous regarde le faire. Vous le faites quand même. C’est tout le sens du mot.

Le logiciel passe trente ans à soutenir qu’il est différent, que le build est le livrable et que l’installation est le problème de quelqu’un d’autre, qu’une chaîne verte équivaut à un produit qui marche, et non. Un build qui n’a jamais été exécuté hors du conteneur qui l’a produit n’a pas été terminé, il a été abandonné au moment où finir devient ennuyeux. Tout ce qui vient après est une affirmation à propos d’un travail qui n’a pas été fait.

Le correctif est une option de configuration. Le contrôle est une ligne de shell. Le coût ni de l’un ni de l’autre n’est ce qui est intéressant ; le nombre intéressant, c’est combien de gens ont tapé leurs identifiants dans une fenêtre Webex, l’ont regardée dire que leur internet était coupé, et l’ont cru, parce que Cisco le leur disait et que Cisco est une entreprise de réseau. Voilà ce que le test manquant a réellement acheté : pas un bug, un mensonge que le produit raconte avec assurance, à chaque lancement, à des gens qui n’ont aucun moyen de savoir.

Et c’est la ligne à tracer avant de fermer l’onglet, parce que les deux moitiés de cet article ne sont pas deux sujets. Un chemin de confiance qui pointe sur un conteneur de build et un contournement d’authentification sur une appliance de périmètre sont la même faute à des enjeux différents. Les deux sont une valeur que personne n’a vérifiée, dans un artefact que personne n’a lancé, signé par un processus qui atteste la provenance et non l’aptitude. Celle que j’ai eue devant moi était de l’espèce inoffensive. Elle a cassé bruyamment, sur mon propre bureau, et j’ai été le premier au courant. L’autre espèce ne vous fait pas ce cadeau.

La même vérification manquante à deux niveaux d'enjeu, et une seule vous le ditUne vérification manquante, deux enjeux. Un seul vous dit qu'il est là.Même faute : une valeur que personne n'a vérifiée, dans un artefact que personne n'a lancé, signé pour la provenance, pas l'aptitudeCelle qui casse bruyammentce que c'étaitun chemin de magasin de confiance inexistantqui l'a trouvéele client, au premier lancementce qu'elle a coûtéune soirée, un bureauqui l'a sumoi, tout de suiteFinit écrite en public.Celle qui ne casse rience que c'étaitune longueur que personne n'a bornéequi l'a trouvéecelui qui la cherchaitce qu'elle a coûtéun incidentqui l'a sule client, par le rapportFinit en 1 des 98 de Cisco au catalogue.Un processus qui n'attrape pas la colonne de gauche n'allait jamais attraper celle de droite. La différence est la chance, pas la rigueur.
Le défaut de cet article et les entrées de ce catalogue sont la même faute sous d’autres habits. L’un s’est annoncé sur mon bureau au premier lancement. L’autre espèce s’annonce d’abord à quelqu’un d’autre.

Personne chez Cisco n’a décidé de livrer un pare-feu exploitable, pas plus qu’ils n’ont décidé de livrer un client incapable d’atteindre internet. Ce n’est pas une défense. C’est l’accusation. Ni l’un ni l’autre n’a besoin d’être décidé, et c’est précisément le problème : les deux sont ce qui sort à l’autre bout quand une chaîne compile, signe et publie sans que personne soit rendu responsable d’ouvrir le résultat. Un processus qui n’attrapera pas un répertoire qui n’existe pas n’allait jamais attraper une longueur qui n’est pas vérifiée, et un éditeur qui vous vend l’appliance gardant votre périmètre n’a pas droit à ce processus.

Alors quand le nom d’un éditeur revient sans cesse sur cette liste, résistez à l’explication confortable qu’il est simplement gros et fortement visé. L’échelle explique le volume. Elle n’explique pas le genre. Regardez plutôt ce que son processus de build fait des choses ennuyeuses, parce que les choses ennuyeuses sont mesurables de l’extérieur, par vous, aujourd’hui, sur du matériel que vous possédez déjà.

Vérifiez vos propres bundles. strings, readelf et vingt minutes vous diront lesquels de vos éditeurs livrent un chemin vers une machine que vous ne verrez jamais. Ce que vous mesurez en réalité, ce n’est pas le chemin. C’est si quelqu’un là-bas regardait, et si la réponse est non sur quelque chose d’aussi bon marché à attraper, vous savez déjà ce qu’elle est sur ce qui ne l’est pas.


  1. Cisco — Webex App system requirements — la liste des plateformes prises en charge, Linux compris. ↩︎

  2. OpenSSL — INSTALL.md, --openssldir — « Directory for OpenSSL configuration files, and also the default certificate and key store. » ↩︎ ↩︎

  3. OpenSSL — SSL_CTX_load_verify_locations(3) — le fichier CA par défaut est cert.pem et le répertoire CA par défaut certs, tous deux à l’intérieur du répertoire OpenSSL par défaut ; et sur les valeurs de retour, « A missing default location is still treated as a success. » ↩︎ ↩︎

  4. Conan 2 — conan cache — les binaires de paquets vivent sous un chemin haché dans le cache local, d’où vient /workspace/.conan2/p/b/<hash>/p. ↩︎

  5. OpenSSL — openssl-env(7) — SSL_CERT_DIR et SSL_CERT_FILE « specify the default directory or file containing CA certificates ». ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  6. OpenSSL — provider(7) — le fournisseur par défaut et ce qu’une configuration qui l’omet rend indisponible. ↩︎ ↩︎

  7. OpenSSL — config(5) — le fichier de configuration qu’OpenSSL charge à l’initialisation, et où il est cherché. ↩︎

  8. freedesktop.org — XDG Base Directory Specification — « The base directory defined by $XDG_DATA_HOME is considered more important than any of the base directories defined by $XDG_DATA_DIRS. » ↩︎

  9. freedesktop.org — Desktop Entry Specification — où les fichiers .desktop sont cherchés et comment l’un masque un autre du même nom. ↩︎

  10. Filesystem Hierarchy Standard 3.0 — les répertoires qu’un système de fichiers racine est censé contenir, /workspace n’en faisant pas partie. ↩︎

  11. update-ca-trust(8) — comment le paquet consolidé sous /etc/pki/ca-trust/extracted est produit sur Fedora et ses dérivés. ↩︎

  12. ld.so(8) — $ORIGIN se développe en le répertoire contenant le programme ou l’objet partagé, ce qui rend un arbre embarqué déplaçable. ↩︎

  13. glibc timeline — « 2018-08-01 GLIBC 2.28 — The GNU C Library version 2.28 is now available ». ↩︎

  14. glibc — Release wiki — le tableau version/distribution ; Red Hat Enterprise Linux 8, c’est glibc 2.28. ↩︎

  15. RPM — spec file reference — %config et ce que le gestionnaire de paquets fait d’un fichier marqué comme configuration. ↩︎ ↩︎

  16. Fedora packaging guidelines — configuration files — quand un fichier livré doit être marqué %config. ↩︎

  17. glibc FAQ — pourquoi un binaire glibc lié statiquement a quand même besoin des modules NSS de l’hôte à l’exécution. ↩︎

  18. CISA — Known Exploited Vulnerabilities Catalog — compté depuis le flux JSON publié, version de catalogue 2026.09.14, 1 710 entrées. ↩︎ ↩︎