Webex legt ein gelbes Banner über den oberen Rand des Fensters. Offline - No internet connection. Die Telefondienste stehen auf getrennt, nichts synchronisiert, und du kommst nicht in die Besprechung, die vor neunzig Sekunden angefangen hat.
Das ist keine Unannehmlichkeit, wenn das Ding dein Diensttelefon ist. Über Webex kommen meine Anrufe herein, und meine VoIP-Leitung läuft darüber, also ist ein Client, der sich nicht anmeldet, ein Tischtelefon, das nicht klingelt, eine Besprechung, in der ich nicht bin, und ein Kollege, der auf der Mailbox landet. Es hat mich an der Arbeit gehindert. Nicht verlangsamt, nicht eingeschränkt. Gestoppt.
Und nichts davon lag an mir, weder es zu verursachen noch es zu verhindern. Ein Arbeitstag ging schief wegen einer Qualitätskontrolle, die nicht stattgefunden hat, bei einem Lieferanten, der bezahlt wird, an einem Produkt, das mit Supportvertrag verkauft wird, auf einer Plattform, die dessen eigene Anforderungsseite als unterstützt nennt. Ich habe nichts falsch konfiguriert. Ich habe das Paket des Herstellers installiert, aus dem Repository des Herstellers, auf einer Plattform, die der Hersteller aufführt, und es konnte keine TLS-Verbindung aufbauen. Dann habe ich einen Abend meiner eigenen Zeit damit verbracht herauszufinden, warum, und das ist Zeit, die die Leute, die das Paket signiert haben, nicht aufgewendet haben.
Die Maschine ist nicht offline. Der Browser daneben lädt Seiten. Deine Mail kommt an, dein Terminal zieht von einem Remote, und wenn du das Betriebssystem fragst, ob es ins Internet kommt, sagt es ja. Webex selbst stimmt dem zu, im eigenen Log, elf Sekunden bevor es dir das Gegenteil erzählt.
Was tatsächlich passiert ist: die Kopie von OpenSSL, die Cisco in Webex mitliefert, findet keine einzige Zertifizierungsstelle, weil sie so kompiliert wurde, dass sie in /workspace/.conan2/p/b/cisco8ee8b59cf93de/p/ssl danach sucht. Das ist ein Verzeichnis auf einem Cisco-Build-Container. Es hat auf deinem Rechner nie existiert und wird es nie. Jede TLS-Verbindung der Anwendung scheitert an der Zertifikatsprüfung, das Anmelde-Token kann nicht erneuert werden, ein Fünfzehn-Sekunden-Timer läuft ab, und die Oberfläche greift zur einzigen Erklärung, für die sie eine Zeichenkette hat.
Das Banner ist also auf eine ganz bestimmte und wenig hilfreiche Weise falsch. Es benennt dein Netz. Der Fehler ist ein Pfad in ihrem Build.
Das ist Version 46.8.0.35631, auf Fedora 44, Kernel 7.2.4. Es ist eine unterstützte Plattform. Cisco veröffentlicht Linux-Systemanforderungen und liefert ein signiertes .rpm, webex-46.8.0.35631-1.x86_641. Was folgt, ist, wie du es in etwa zehn Minuten beweist, warum keine der naheliegenden Lösungen greift, die eine, die es tut, und dann der Teil, der wichtiger ist als alles davor: das ist kein subtiler Fehler. Das ist ein Build, den nie jemand auf einer Maschine ausgeführt hat, die ihn nicht gebaut hatte.
Was das Banner dir tatsächlich sagt
Webex betreibt seine Verbindungsanzeige über einen zusammengesetzten Automaten, sieben Teilautomaten mit je eigenen Timern. Beim Start initialisieren sie so:
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
Vierzig Millisekunden später meldet das Betriebssystem zurück, und die Anwendung schreibt es auf:
NetworkManagerPowerNetworkWatcher.cpp:93 onConnectivityCheckSuccess::
The host is connected to a network, that appears to be able to reach the full Internet.
Diese Zeile steht in derselben Datei, in derselben Sitzung, wie das Banner, das behauptet, es gebe keine Internetverbindung. Die Anwendung wusste es. Sie hatte die Antwort um 08:25:12.131 in der Hand und zeigte um 08:25:27.132 das Gegenteil.
Zwischen diesen beiden Momenten passiert das:
| Zeit | Was passiert ist |
|---|---|
| 08:25:12.131 | Betriebssystem bestätigt volle Internet-Erreichbarkeit |
| 08:25:12.218 | Proxy-Erkennung: keiner konfiguriert, direkte Verbindung |
| 08:25:12.241 | Erste HTTPS-Anfrage scheitert, errorCode: 167772294 Error in SSL handshake |
| 08:25:12.569 | Erneuerung des CloudApps-Zugriffstokens scheitert, gleicher Code |
| 08:25:12.571 | Erneuerung des Kms-Zugriffstokens scheitert, gleicher Code |
| 08:25:15.684 | Wiederholung, beide scheitern |
| 08:25:21.745 | Wiederholung, beide scheitern |
| 08:25:27.132 | Fünfzehn-Sekunden-Timer läuft ab, Banner schaltet auf NoInternet |
| 08:26:12.091 | Sechzig-Sekunden-Timer laufen ab, Dienste fallen auf DisconnectedShortTerm |
Der Authentifizierungs-Teilautomat verlässt UserNotAuthenticated nie, also leitet sich Services als getrennt ab, also feuert das Banner. Jede Stufe davon ist bei dieser Eingabe korrektes Verhalten. Die Eingabe ist falsch, und die Eingabe ist eine Zahl: 167772294, bei jeder gescheiterten Anfrage, von der ersten bis zur letzten.
Diese Zahl ist der ganze Beitrag. Halt sie fest.
Drei Pfade, von denen keiner existiert
Webex benutzt nicht das OpenSSL des Systems. Es bringt sein eigenes mit, samt eigenem libcurl, und dieses libcurl linkt gegen das mitgelieferte und nicht gegen deins:
$ 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
So weit in Ordnung. Eine TLS-Bibliothek mitzuliefern ist eine vertretbare Entscheidung und viele Hersteller tun das. Es kommt darauf an, was hineingebacken wurde, und OpenSSL sagt es dir, wenn du fragst:
$ 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"
Drei Stück. Nicht eine verrutschte Einstellung. Das komplette Installationspräfix, durchgeschleppt von der Maschine, die es kompiliert hat, in einer Bibliothek, die den Kunden als signiertes Paket auf einer unterstützten Plattform erreicht.
OPENSSLDIR wird einmal gesetzt, zur Konfigurationszeit, mit --openssldir, und OpenSSLs eigene Build-Dokumentation ist deutlich, wofür es da ist: „Directory for OpenSSL configuration files, and also the default certificate and key store.“2 Alles, was unterhalb von Vertrauen hängt, hängt daran. Die Standard-CA-Datei ist cert.pem darin und das Standard-CA-Verzeichnis ist certs darin3. /workspace ist ein Conan-Build-Cache. Conan ist der C++-Paketmanager, mit dem Cisco baut, und er legt jedes Paket unter einem Hash seiner Build-Eingaben ab4. Der Hash cisco8ee8b59cf93de ist eine Tatsache über einen Container, der vermutlich Minuten nach dem Ende des Builds gelöscht wurde.
Frag die Bibliothek, was sie ist, und es ist nicht einmal Standard-OpenSSL:
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"
Ein gepflegter hauseigener Fork, mit einem eigenen Versionsschema, im Februar gebaut, im August ausgeliefert, und immer noch mit dem Arbeitsverzeichnis darin, in dem er entstanden ist.
Nimm strings nicht als Antwort. Frag die Bibliothek
strings findet Text in einer Datei. Es beweist nicht, dass die Bibliothek ihn benutzt. Also lade die mitgelieferte Bibliothek und frag sie direkt, was etwa zwölf Zeilen Python braucht und kein 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
Da steht es aus dem Mund der Bibliothek selbst. Wenn irgendetwas in Webex diese Kopie von OpenSSL nach dem Standard-Trust-Store fragt, bekommt es eine Datei und ein Verzeichnis, die es nicht gibt. Genau das tut SSL_CTX_set_default_verify_paths, und genau das tut fast jeder Client, sofern ihm nichts anderes gesagt wurde.
Die letzten beiden Zeilen sind erwähnenswert, denn sie sind die Notausgänge: die Bibliothek lässt SSL_CERT_FILE und SSL_CERT_DIR beides überschreiben5. Merk dir auch das. Es wird wichtig, und nicht so, wie du erwarten würdest.
Den exakten Fehlercode reproduzieren
Beweis durch Hinsehen ist kein Beweis. Nimm die mitgelieferte libssl.so.3 und libcrypto.so.3, mach einen echten Handshake zu einem echten Host mit nichts als den Standardwerten der Bibliothek, und sieh, was zurückkommt.
Der interessante Lauf ist der, in dem die Standardwerte auf nichts zeigen. SSL_CERT_FILE und SSL_CERT_DIR überschreiben genau die zwei Werte, die ein fehlendes OPENSSLDIR in der Luft hängen lässt, also reproduziert es den ausgelieferten Zustand exakt, wenn man sie auf einen nicht existierenden Pfad richtet:
$ 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 ist dezimal 167772294.
Das ist die Zahl in jeder gescheiterten Zeile des Webex-Logs, und sie kam nicht von Webex. Sie kam aus Ciscos eigener TLS-Bibliothek, laufend außerhalb ihrer Anwendung, gescheitert aus genau einem Grund: sie hatte keine Vertrauensanker, gegen die sie die Kette prüfen konnte. Gleiche Bibliothek, gleicher Fehler, keine Anwendung dazwischen.
Richte dieselben zwei Variablen auf das echte Fedora-Bündel, und derselbe Codepfad läuft durch:
$ 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
Am Netz hat sich zwischen diesen beiden Läufen nichts geändert. Ein Dateipfad schon.
Warum dich nichts gewarnt hat
Zwei Dinge wirken zusammen, damit das lautlos bleibt, und nur eines davon ist Ciscos Schuld.
| Was nie ein Wort sagt | Wessen Entwurf | Warum es still bleibt |
|---|---|---|
| Einen Trust Store laden, den es nicht gibt | OpenSSLs, absichtlich | SSL_CTX_set_default_verify_paths gibt 1 zurück, ob die Pfade echt sind oder nicht. „A missing default location is still treated as a success“3 |
| Nichts haben, worauf man zurückfällt | Ciscos, und richtig | Zertifikate werden geprüft, selbstsignierte abgelehnt, SSL-Wiederholung aus, es gibt also keinen eingeschränkten Modus, der einen Fehler verdecken könnte |
Das Erste ist vernünftig. Ein Programm, das seine Anker separat mitbringt, sollte nicht gezwungen sein, sich darum zu kümmern, also gelingt der Aufruf, der Store ist leer, und nirgends im Stapel sagt irgendetwas ich habe null Zertifizierungsstellen geladen. Das Erste, was es merkt, ist ein Prüffehler eine halbe Sekunde später. Ich habe diesen Aufruf gegen die mitgelieferte Bibliothek laufen lassen, mit den Pfaden auf /nonexistent, und er gab 1 zurück. Es steht in der Ausgabe oben.
Das Zweite ist eine Richtlinie, die vom Dienst kommt, und das Log hält sie fest:
Wdm.cpp:1162 parseDeviceJson: Adding policy << allowSelfSignedCertificate with value: false
NetworkManager.cpp:1738 onConfigReady: ...httpRequestSSLRetryEnabled: 0
HttpRequestManager.cpp:1883 rawHttpRequest: {"validateCertificates":"true","useClientCertificate":"false"}
Diese drei Schalter sind der einzige Grund, warum dieser Fehler ein Ausfall ist und nicht etwas weit Schlimmeres, und dabei lohnt es sich zu verweilen, statt daran vorbeizuhasten.
Denk es durch. Die mitgelieferte Bibliothek lädt null Vertrauensanker, und unterhalb der Richtlinienebene hätte das nie etwas bemerkt, weil der Fehler über den ganzen Weg nach unten absichtlich still ist. Was daraus ein Banner gemacht hat, waren drei Schalter. Dreh einen davon so, wie ihn reichlich Clients ausliefern, und derselbe Build scheitert überhaupt nicht. Er verbindet sich, mit allem, was irgendein Zertifikat vorhält, weil er nichts hat, wogegen er eines prüfen könnte.
Wie es an die Oberfläche kommt, ist das Problem. Der Benutzer bekommt „Offline - No internet connection“. Das Log bekommt „Error in SSL handshake“ und eine Dezimalzahl. Das Wort Zertifikat taucht nirgends auf, wo ein Benutzer oder ein Supportmitarbeiter der ersten Ebene je hinsehen wird, und die eine diagnostische Brotkrume ist eine Zahl, die du erst nach hex umrechnen musst, damit sie etwas bedeutet.
Die Lösungen, die nicht funktioniert haben
Beide naheliegenden scheitern, und die Gründe sind verschieden und beide wissenswert.
Die mitgelieferte openssl.cnf bearbeiten
Webex liefert eine Konfigurationsdatei unter /opt/Webex/lib/openssl.cnf mit, und im Auslieferungszustand aktiviert sie einen Provider:
[provider_sect]
fips = fips_sect
Nur FIPS. Nicht den default-Provider, in dem die gewöhnlichen TLS-Algorithmen wohnen6. Ihn wieder hinzuzufügen ist eine einzeilige Änderung und ändert überhaupt nichts, weil das mitgelieferte OpenSSL diese Datei nie liest. Es sucht openssl.cnf innerhalb von OPENSSLDIR7, und OPENSSLDIR ist der Pfad, den es nicht gibt. Die Datei liegt im Installationsverzeichnis und sieht maßgeblich aus. Nichts liest sie.
Es ist ein zweiter Defekt, der sich hinter dem ersten versteckt. Selbst wenn das Pfadproblem morgen behoben würde, indem man OPENSSLDIR auf /opt/Webex/lib richtet, würde diese Konfiguration dann geladen und aktivierte nur FIPS. Und das FIPS-Modul selbst wird aus MODULESDIR geladen, dem dritten toten Pfad aus demselben Baum, also kann auch die in /opt/Webex/lib mitgelieferte fips.so nicht gefunden werden. Drei Pfade, ein falsches Präfix, und jeder einzelne so kaputt, dass er den nächsten verdeckt.
Die Umgebungsvariablen setzen
Die Bibliothek beachtet SSL_CERT_FILE und SSL_CERT_DIR. Das habe ich oben bewiesen. Der erfolgreiche Lauf steht genau da. Der naheliegende Schritt ist also, sie im Desktop-Eintrag zu setzen, und das wurde versucht:
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
Keine Änderung. Und der Grund ist nicht, dass Webex die Variablen ignoriert. Der Grund ist, dass der Prozess sie nie bekommen hat:
$ tr '\0' '\n' < /proc/20293/environ | grep -E 'SSL|OPENSSL|CURL'
$ tr '\0' '\n' < /proc/20293/environ | wc -l
99
Neunundneunzig Variablen im laufenden Webex-Prozess, und keine einzige davon eine der drei gesetzten. Weil es auf dieser Maschine zwei Desktop-Einträge mit demselben Namen gibt:
| Datei | Was ihre Exec-Zeile ausführt | Geschrieben von |
|---|---|---|
/usr/share/applications/webex.desktop | die dreifache env-Zeile von oben, vollständig | dem .rpm, danach von Hand bearbeitet |
~/.local/share/applications/webex.desktop | /opt/Webex/bin/CiscoCollabHost %U, und gar keine Umgebung | Webex’ eigenem Launcher |
Und die Spezifikation ist nicht mehrdeutig darin, welche gewinnt: „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 ist ~/.local/share. Die Benutzerkopie überdeckt die paketierte, jedes Mal, auf jedem Desktop, der sich an die Spezifikation hält9.
Webex installiert also eine zweite Kopie seines eigenen Launchers in dein Heimatverzeichnis, und diese Kopie ist die, die dein Desktop ausführt. Ändere die paketierte Datei so viel du willst. Du bearbeitest ein Dokument, das nichts liest.
Die Lösung, die funktioniert
Wenn die Bibliothek auf einem Pfad besteht, gib ihr den Pfad. Erzeuge das Verzeichnis, nach dem sie zu suchen kompiliert wurde, und füll es mit Symlinks auf das Echte:
#!/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"
Starte es neu, und dieselbe Startsequenz erzeugt das gegenteilige Ergebnis. Gleiche Binärdatei. Gleiche Log-Zeilen. Die Token-Erneuerung, die vorher innerhalb von sechzig Millisekunden scheiterte, ist jetzt nach dreihundertdreißig fertig:
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
Nach etwa 750 ms authentifiziert, der Fünfzehn-Sekunden-Timer feuert also nie und das Banner erscheint nie. Die Telefondienste gehen von Disconnected auf Connecting, einen Zustand, den die kaputte Sitzung in einer Minute Versuchen nie erreicht hat.
| Vorher | Nachher | |
|---|---|---|
| Netzprüfung | besteht | besteht |
| Erste HTTPS-Anfrage | 167772294 Error in SSL handshake | HTTP 200 |
| CloudApps-Token | gescheitert | geholt |
| Kms-Token | gescheitert | geholt |
| Authentifizierung | hängt bei UserNotAuthenticated | UserAuthenticated |
| Banner nach 15 s | „Offline - No internet connection“ | keins |
| Telefondienste | nie versucht | verbindet |
| Zeit bis zur Authentifizierung | nie | ~750 ms |
Beachte, was die Lösung mit deinem Dateisystem macht, denn das sollte dich stören. Sie erzeugt auf deiner Maschine ein Verzeichnis /workspace auf oberster Ebene, einen Namen, für den der Filesystem Hierarchy Standard keinen Platz hat10, und darin einen Conan-Cache-Pfad und einen Build-Hash, die einer Firma gehören, von der du Software gekauft hast. Das ist die Form der Abhilfe, die Cisco dir hinterlassen hat: häng die Build-Umgebung eines anderen in die Wurzel deiner eigenen.
Sie wird auch kaputtgehen. Auf drei Arten:
| Wann sie kaputtgeht | Warum | Was du tust |
|---|---|---|
| Ein Webex-Update | cisco8ee8b59cf93de leitet sich aus den Build-Eingaben ab, eine neu gebaute Abhängigkeit bedeutet also ein neues Verzeichnis | Das Skript erneut laufen lassen; es liest den Pfad aus der neuen Binärdatei, statt die alte anzunehmen |
| Eine Distributionsänderung | Das Ziel des Symlinks ist eine Entscheidung der Distribution, kein Standard11 | Neu ausrichten; das Skript probiert vier bekannte Orte |
| Eine Neuinstallation | /workspace wird von nichts gesichert, paketiert oder besessen | Führ es wieder aus, jedes Mal, für immer |
Nichts davon ist Wartung. Das bist du, wie du für einen Schritt in der Build-Pipeline eines anderen einspringst, auf unbestimmte Zeit, unbezahlt. Einen Defekt zur Laufzeit flicken, auf jeder Maschine, die du besitzt, weil der Hersteller ihn nicht einmal zur Build-Zeit flicken wollte.
Sie kannten die Regel und haben sie auf die Hälfte des Builds angewendet
Hier ist, was das von einem Fehlerbericht zu einem Argument macht.
Lies den dynamischen Abschnitt der ausgelieferten Binärdateien:
$ 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 wird zur Ladezeit zu dem Verzeichnis aufgelöst, in dem das Objekt selbst liegt12. Es ist das richtige Werkzeug für ein verschiebbares Bündel, und sie haben es richtig benutzt. Sie mussten: Webex liefert denselben Baum zweimal aus, einmal nach /opt/Webex und einmal nach ~/.local/share/WebexLauncher/46.8.0.35631_9e6196c9-…/, und ein Launcher wählt beim Start zwischen beiden. Zwei Präfixe, ein Build, und der Linker findet seine Bibliotheken in beiden.
Die Leute, die dieses Paket gemacht haben, haben das Problem also genau verstanden. Absolute Pfade überleben das Ausliefern nicht. Für den Code haben sie es gelöst.
Dann haben sie die Datenpfade als absolute Zeichenketten stehen lassen, die den Container benennen, der sie kompiliert hat. Gleicher Build. Gleicher Nachmittag.
Das lässt sich also nicht unter die wussten es nicht ablegen. In $ORIGIN stolpert man nicht hinein. Man greift danach, weil man verstanden hat, dass ein absoluter Pfad, der in ein ausgeliefertes Artefakt gebacken ist, ein Defekt ist, und es gut genug verstanden hat, um es im Linker zu beheben. Dann schreibt derselbe Build drei absolute Pfade in dieselben Bibliotheken und liefert sie aus.
Die Regel zu kennen und sie auf die Hälfte des Builds anzuwenden, ist schlimmer, als sie nicht zu kennen. Nicht zu wissen ist ein Schulungsproblem, und für Schulung gibt es eine Lösung. Das hier ist ein Paket, in dem die richtige Idee steckte, schriftlich, im ELF-Header, wo sie jeder hätte lesen können, und es ging trotzdem kaputt zur Tür hinaus. Was dir sagt, dass nichts unterhalb des Compilers auf das Ergebnis geschaut hat. Nichts hat es.
Der Container war schon da
Woher /workspace kommt, ist kein Rätsel, und raten musst du auch nicht. Es steht im Paket-Header:
$ 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 ist kein Hostname, den jemand getippt hat. Es sind zwölf hexadezimale Zeichen, und genau das meldet ein Container als Hostnamen, wenn niemand einen setzt. Das Paket wurde also in einem Container gebaut, von einer Firma, die offenkundig die Images, die Registry und die Orchestrierung dafür hat, und drei getrennte Artefakte auf dieser Maschine sagen das unabhängig voneinander:
| Belege, abgelesen am installierten Paket | Wert | Was es beweist |
|---|---|---|
Build Host im RPM-Header | c964ea9239ae | eine Container-ID, keine Build-Maschine |
OPENSSLDIR in libcrypto.so.3 | /workspace/.conan2/… | ein Pfad, den es nur in diesem Container gibt |
PLATFORM in derselben Bibliothek | conan-Release-Linux-x86_64-gcc-13 | eine containerisierte Conan-Toolchain |
Requires im RPM | glibc >= 2.28 | eine bewusst gewählte, sehr alte ABI-Untergrenze |
Dann haben sie es signiert. Der Build war um 20:47:43 fertig und die Signatur ist auf 21:02:07 desselben Abends datiert, Schlüssel-ID 9995e5bbb5ccde3c. Fünfzehn Minuten. Es gibt also ein Freigabetor, jemand oder etwas bedient es, und was es bezeugt, ist wer das Paket gemacht hat, nicht ob das Paket funktioniert. Eine Signatur ist eine Aussage über die Herkunft. Sie war nie eine Aussage über die Tauglichkeit, und ein Prozess, der das eine hat und das andere nicht, hat seine Prioritäten in der falschen Reihenfolge.
Denn der fehlende Schritt ist der billige. Der Container ist schon in der Pipeline. Nimm das Artefakt, das gerade herauskam, starte ein sauberes Image jeder Distribution, die du zu unterstützen behauptest, installiere es, starte es und lies die ersten hundert Zeilen des Logs:
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'
Ungleich null, jedes Mal, bei diesem Build. Die Installation ist 1,1 GB groß, rechne also mit einer Minute je Ziel bei warmem Cache. Sechs Distributionen sind sechs Minuten auf einer Maschine, die ohnehin läuft, auf Hardware, die Cisco ohnehin besitzt, in einer Pipeline, die es ohnehin gibt. Es wurde nicht ausgeführt. Kein einziges Mal.
Und das ist die Antwort auf das, was Leute über Linux immer noch sagen, dass mehrere Distributionen zu unterstützen schwer sei. Es hat an dem Tag aufgehört schwer zu sein, an dem dieses Werkzeug kam, und das Werkzeug ist dasselbe, mit dem sie kompilieren. Ein Basis-Image je Ziel. Dasselbe Artefakt in jedes. Die Matrix ist eine Schleife.
Eine Pipeline, die in einem Container baut und das Ergebnis nie in einem ausführt, ist keine Pipeline. Sie ist ein Compiler mit einem Cronjob davor und einem Signaturschlüssel dahinter, und was immer dabei herauskommt, ist geraten.
Warum wird ein Release von 2026 gegen eine Libc von 2018 gebaut?
Das Paket erklärt, was es braucht, und die interessante Zeile ist die erste:
$ rpm -q --requires webex | grep glibc
glibc >= 2.28
glibc 2.28 erschien am 1. August 201813. Es ist die Version in Red Hat Enterprise Linux 814, einem Release, dessen voller Support 2024 endete. Das hier ist ein Produkt von 2026, 2026 kompiliert, das auf die C-Bibliothek von 2018 zielt. Und dann liefert es seine eigene 2,5 MB große Kopie von libstdc++.so.6 ins Heimatverzeichnis des Benutzers, weil die C++-Laufzeit, die zu einer so alten Basis gehört, den Code nicht tragen kann.
Warum macht das überhaupt noch jemand? Weil sie nicht statisch linken wollen.
Das ist die ganze Geschichte. In dem Moment, in dem du dynamisch gegen die C-Bibliothek des Hosts linkst, wird die älteste Distribution, die du unterstützen willst, zu einer Einschränkung für die Maschine, auf der du kompilierst. Du kannst kein Symbol benutzen, von dem das älteste Ziel nie gehört hat, also nagelst du den Build auf ein uraltes Basis-Image und bleibst dort. Jedes Jahr wird die Lücke größer. Jede neue Sprach- oder Bibliotheksfunktion kommt mit einer Diskussion darüber, ob die Untergrenze steigen darf. Liefere dein eigenes libstdc++ mit, um das Schlimmste zu übertünchen, und du pflegst jetzt auch noch eine private Laufzeitumgebung.
Dieser Handel ergab Sinn, als ein Build-Host eine physische Maschine im Rack war, die jemand neu aufsetzen musste. Seit einem Jahrzehnt ergibt er keinen Sinn mehr. Wenn du gegen die libc des Hosts linken musst, gibt dir ein Container je Ziel einen echten Build auf einer echten Version jeder Plattform, und keine davon schränkt die anderen ein.
Aber sieh dir an, was die Disziplin mit dem alten Ziel hier tatsächlich gebracht hat. Es ist ABI-Konservatismus zu erheblichen Kosten: eine acht Jahre alte Untergrenze, eine mitgelieferte C++-Laufzeit, eine darum eingefrorene Supportmatrix. Und der Client startet trotzdem nicht. Weil das, was kaputtging, ein Dateipfad war, und keine noch so große Sorgfalt bei Symbolversionen schützt einen Dateipfad. Sie haben die Kompatibilitätssteuer und die Bündelsteuer bezahlt und die eine Prüfung ausgelassen, die nichts kostet. Beide Rechnungen. Kein Produkt.
Sie haben für ein eigenständiges Bündel bezahlt und keins bekommen
Der althergebrachte Weg, kommerzielle Software unter Linux auszuliefern, ist, von so wenig vom Host abzuhängen wie möglich. Statisch, wo es geht, ein eigenständiger Baum, wo es nicht geht, und keine Annahmen über die Distribution darunter. Es ist nicht elegant und niemand behauptet das. Es gibt das wegen der Alternative. Eine Binärdatei, die eine bestimmte Version einer bestimmten Bibliothek an einer bestimmten Stelle braucht, macht aus der Maschine jedes Kunden einen Supportfall.
Cisco ist den zweiten Weg gegangen und hat gebündelt. Hier ist die Rechnung:
| Was das Bündel enthält | Größe oder Anzahl |
|---|---|
Shared Objects in /opt/Webex/lib | 150 |
| Ihr eigenes libcurl, CiscoSSL, zlib-ng, ICU, Kerberos, CUPS-Client, hunspell und Inferenz-Engine | alle davon |
Installiert unter /opt/Webex | 1,1 GB |
Eine zweite Kopie unter ~/.local/share/WebexLauncher, je Benutzer | 1,1 GB |
| Auf der Platte für einen Chat- und Telefonie-Client | 2,2 GB |
Zwei Gigabyte Abhängigkeiten. Das ist der volle Preis des Bündelns: der Download, die Platte, die Doppelung, die Sicherheitslast, die einzige Partei zu sein, die irgendetwas davon patchen kann, alles zusammen. Zahl das, und was du bekommst, ist ein Programm, dem es egal ist, was auf dem Host installiert ist, das sich auf Fedora und Debian und Arch und was immer ein Kunde dieses Jahr standardisiert hat gleich verhält, und das kein Distributions-Upgrade kaputtmachen kann, von dem es nie gehört hat.
Nur tut es das eben doch. Es kümmert sich sehr um ein Verzeichnis, und das ist ein Verzeichnis auf einem Build-Server.
Sie haben ihr eigenes Kerberos und ihr eigenes ICU und ihre eigene Rechtschreibprüfung mitgeliefert, und einen funktionierenden Pfad zu den Zertifikaten konnten sie nicht mitliefern. Der ganze Zweck des Bündels ist, eigenständig zu sein, und in dem einen Punkt, der es zum Laufen bringt, ist es nicht eigenständig. Jedes einzelne dieser 1,1 GB wird über $ORIGIN korrekt gefunden. Die gut vierzig Bytes, auf die es am meisten ankommt, nicht.
Und das ist der Teil, der mich packt. Bündeln ist die teure Option. Sie haben die Kosten getragen, sie haben die schwere Ingenieursarbeit gemacht, sie haben die Verschiebbarkeit für hundertfünfzig Bibliotheken über zwei Installationspräfixe hinbekommen. Und dann den einen Teil, der entscheidet, ob irgendetwas davon funktioniert, auf eine Maschine gerichtet, die nie ein Kunde besessen hat.
Dokumentiert hat es auch niemand
Neben dem ersten Fehler sitzt ein zweiter, und es ist der, der den ersten gefunden hätte.
Frag das Paket, welche Dokumentation es mitbringt:
$ rpm -qd webex | wc -l
0
$ rpm -qc webex | wc -l
0
Keine Dokumentationsdateien. Und keine Konfigurationsdateien. Null Einträge mit %config in einem Paket, das eine openssl.cnf ausliefert. Das ist nicht kosmetisch. Ein RPM markiert eine Datei als %config, damit der Paketmanager bewahrt, was der Administrator geändert hat, und eine .rpmsave anlegt, statt sie zu überschreiben1516. /opt/Webex/lib/openssl.cnf wird als gewöhnliche Datei ausgeliefert, das nächste Update überschreibt also jede Änderung, die du daran gemacht hast, und sagt dir nichts. Die eine Datei, die ein Kunde berechtigterweise anpassen müsste, ist die, die die Paketierung als Wegwerfware behandelt.
Nichts Veröffentlichtes sagt, welchen Trust Store der Client benutzt, welche Umgebungsvariablen er beachtet oder woher er seine TLS-Konfiguration liest. Es gibt keine Seite zum Nachsehen. Es wurde keine geschrieben. Der einzige Weg, irgendetwas davon festzustellen, waren strings, readelf, ldd und ctypes gegen die ausgelieferten Binärdateien. Ein unterstütztes Produkt zurückentwickeln, um eine Frage zu beantworten, die seine Dokumentation in einem Satz hätte beantworten müssen.
Und hier ist, warum das mehr zählt, als es klingt. Diese Dokumentation zu schreiben ist selbst ein Test. Setz irgendwen beim Hersteller vor ein leeres Blatt mit der Überschrift woher Webex für Linux seine CA-Zertifikate liest, und das Erste, was er tun muss, ist nachsehen. In dem Moment, in dem er nachsieht, findet er /workspace/.conan2/p/b/cisco8ee8b59cf93de/p/ssl, und das Nächste, was aus ihm herauskommt, ist eine Frage. Dokumentierte Konfigurationspfade sind kein Papierkram zugunsten des Kunden. Sie sind die billigste Prüfung, die ein Hersteller an seinem eigenen Build vornehmen kann, und sie auszulassen ist der Weg, auf dem so ein Pfad bis in ein Release überlebt.
Nichts davon ist viel verlangt. Sag, wo deine Konfiguration liegt. Sag, welche Umgebungsvariablen du beachtest. Markiere deine Konfigurationsdateien als Konfiguration, damit ein Update sie nicht auffrisst. Dann hat der Kunde, der auf einen Fehler stößt, einen Ort zum Nachsehen, der kein Hex-Editor ist.
Niemand hat es ausgeführt
Der fehlende Schritt ist oben oft genug benannt worden. Wert zu fragen ist, warum er auf dieser Plattform fehlte und auf den anderen nicht.
Auf Fedora jedenfalls nicht. Auf macOS und dem Fisher-Price OS (Windows) kann diese Fehlerklasse nicht auf dieselbe Weise auftauchen, weil diese Plattformen einen System-TLS-Stapel mit einem Trust Store haben, den das Betriebssystem verwaltet. Linux hat so etwas nicht. OpenSSL ist der Trust Store, und entsprechend gehört dem, der es ausliefert, auch die Frage, wo es sucht. Die eine Plattform also, auf der die mitgelieferte Bibliothek tragend ist, ist die Plattform, die ungetestet ausgeliefert wurde, was eine Entscheidung darüber ist, welche Kunden einen Rauchtest wert sind.
Die ganze Untersuchung hat einen Abend gedauert: das Log lesen, bemerken, dass die Netzerkennung bestand, bevor das Banner das Gegenteil behauptete, den kompilierten Pfad aus der Binärdatei ziehen, den exakten Fehlercode gegen die ausgelieferte Bibliothek reproduzieren. Das alles mit einem KI-Agenten, der die Log-Korrelation und das ctypes-Gerüst gemacht hat, während ich mir überlegt habe, was ich ihn fragen soll. Ich erwähne das aus einem Grund: die Diagnose, die ein Hersteller nie gestellt hat, bevor er dieses Paket signiert und in sein eigenes Repository gelegt hat, liegt jetzt für jeden Kunden mit der Geduld nachzusehen in Reichweite eines Abends. Ciscos Ingenieuren steht Claude genauso zur Verfügung wie allen anderen, und es hätte das ordentlich gebaut. Du kannst es nicht bitten, /workspace/.conan2 in ein auszulieferndes Artefakt zu backen, ohne dass es dir sagt, was passiert, wenn das Artefakt den Workspace verlässt. Das Werkzeug, das das findet, ist nicht mehr knapp, und das Wissen auch nicht. Was fehlt, ist jemand beim Hersteller, dessen Aufgabe es war hinzusehen.
Unterdessen ist der Supportweg für den, der darauf stößt, ein Banner, das sagt, sein Internet sei weg. Er wird den Router neu starten. Er wird seinen Anbieter anrufen. Er wird ein Ticket aufmachen, das ins Leere läuft, weil das Symptom, das Cisco anzuzeigen beschlossen hat, von Cisco wegzeigt.
Das ist die vollständige Liste dessen, was schiefgelaufen ist. Es lohnt sich zu sagen, wie richtig ausgesehen hätte, denn zu jedem Punkt darauf gibt es eine feststehende Antwort, die älter ist als dieses Produkt.
Wie es hätte gebaut werden müssen
Genug davon, was schiefgelaufen ist. Hier ist der Maßstab, und nichts davon ist neu. Es ist das, was das Ausliefern einer Binärdatei an den Rechner eines anderen seit zwanzig Jahren von dir verlangt.
Fang mit der Entscheidung an, die Cisco im Grundsatz richtig und in der Ausführung falsch getroffen hat: wie viel vom Host du bereit bist vorauszusetzen. Statisch zu linken ist die stärkste Antwort, und es lohnt sich, dabei konkret zu werden, denn „link es doch einfach statisch“ wird von Leuten herumgeschwenkt, die es nie mussten, und von Leuten abgetan, die es nie versucht haben.
| Was es beseitigt | Was es kostet |
|---|---|
RUNPATH und jede Art, es falsch zu machen, weil es zur Ladezeit keine Suche gibt | glibc linkt nicht sauber statisch: Namens- und Benutzerauflösung laufen über dlopen, die Binärdatei greift also weiterhin nach den NSS-Modulen des Hosts17 |
| Die glibc-Untergrenze, sodass die älteste Distribution nicht mehr diktiert, worauf du kompilieren darfst | LGPL-Teile bringen eine Relink-Pflicht mit, sie bleiben also dynamisch, oder du lieferst mit, was zum Relinken nötig ist |
Kaputtes durch ein Distributions-Upgrade, eine umbenannte Bibliothek oder einen veralteten ldconfig-Cache | Du besitzt jeden Patch: kein Sicherheitsupdate einer Distribution erreicht deine Kunden |
dlopen einer versionierten .so aus einem Verzeichnis, das es vielleicht nicht gibt | Ein größerer Download, und kein Teilen von Speicherseiten zwischen Prozessen |
Und eine Zeile in der linken Spalte, die die anderen nur stützen: das Artefakt, das deine Pipeline erzeugt hat, ist das Artefakt, das der Kunde ausführt, Byte für Byte. Teste es, und du hast das getestet, was du ausgeliefert hast. Genau um das Fehlen dieser Eigenschaft geht es in diesem ganzen Beitrag.
Über die rechte Spalte gehört Ehrlichkeit. Echte Kosten, und der Grund, warum Leute zu musl greifen oder eine Mischform akzeptieren. Aber sieh dir die dritte Zeile an. Jeden Patch zu besitzen gilt genauso für das, was Cisco bereits getan hat: ein gebündelter Baum aus 150 Bibliotheken ist dieselbe Verpflichtung ohne jede der Garantien zur Ladezeit. Sie haben sich so oder so verpflichtet, jeden Patch zu besitzen, und nichts dafür bekommen.
Die Regel ist also einfach, und es ist die Regel, die sie gebrochen haben: was du nicht hineinlinken kannst, musst du über einen Pfad relativ zur Binärdatei finden. $ORIGIN für den Code, und dieselbe Disziplin, bewusst, für jeden Datenpfad, nach dem die Bibliothek suchen wird. In diesem Fall sind es vier, und für jeden gibt es einen dokumentierten Griff:
| Wonach die Bibliothek sucht | Was ausgeliefert wurde | Was es hätte sein müssen |
|---|---|---|
| Vertrauensanker | OPENSSLDIR/cert.pem, zur Build-Zeit festgelegt | im Baum mitgeliefert, oder SSL_CERT_FILE beim Start gesetzt5 |
| Konfiguration | OPENSSLDIR/openssl.cnf, gleicher Pfad, nie gefunden | OPENSSL_CONF, auf die Kopie im Paket gerichtet5 |
| Provider, einschließlich FIPS | MODULESDIR, absolut, fips.so also unerreichbar | OPENSSL_MODULES, „the directory from which cryptographic providers are loaded“5 |
| Engines | ENGINESDIR, absolut | OPENSSL_ENGINES, oder gar nichts, da OpenSSL 4.0 die Engine-Unterstützung ganz entfernt hat5 |
Vier Pfade, vier Umgebungsvariablen, alle in einer einzigen Handbuchseite, die ihre eigene Bibliothek mitliefert. Wenn irgendeine Zeichenkette in deinem Artefakt mit einem / beginnt und zur Build-Zeit festgelegt wurde, ist sie ein Defekt, der auf einen Kunden wartet, der ihn findet. Es gibt keine dritte Möglichkeit, in der ein absoluter Build-Pfad in Ordnung ist.
Die Lösungen für diesen Fall, und die Prüfung, die ihn findet
Herunter vom Grundsatz zu diesem konkreten Defekt. Sechs Änderungen, keine einzige davon Forschung:
| Lösung | Aufwand | Warum sie richtig ist |
|---|---|---|
--openssldir=/opt/Webex/lib/ssl setzen und den Baum mitliefern | ein Konfigurationsschalter | Der Pfad existiert dann im Paket, und genau dafür ist der Schalter da2 |
SSL_CERT_FILE/SSL_CERT_DIR beim Start in CiscoSSLUtils setzen und die bekannten Distributionspfade abklopfen | ein Dutzend Zeilen | Standard, dokumentiert, von ihrer eigenen Bibliothek bereits unterstützt5 |
| Das CA-Bündel selbst im Paket mitliefern | nur Paketierung | Volle Kontrolle über das Vertrauen, zum Preis, für dessen Aktualität zuständig zu sein |
Die openssl.cnf so korrigieren, dass sie den default-Provider aktiviert | eine Zeile | Ohnehin nötig, und derzeit vom Pfadfehler verdeckt6 |
openssl.cnf als %config markieren | eine Zeile in der Spec | Verhindert, dass ein Update die Änderung eines Administrators still auffrisst15 |
| Die Pfade und die beachteten Variablen veröffentlichen | eine Seite | Die billigste Prüfung, die es gibt, und sie findet diesen Fehler, während sie geschrieben wird |
Die erste ist die Ein-Schalter-Lösung, und sie hätte das Produkt funktionsfähig ausgeliefert. Es ist ein einziger Wert in einem Build-Skript, einmal gesetzt, den sie falsch hatten, weil ihn unterhalb nie etwas geprüft hat.
Die Prüfung ist kleiner als die Lösung:
# 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; }
Eine Zeile. Sie hätte dieses Release laut scheitern lassen, im Februar, auf der Maschine, die es gemacht hat, in dem Container, der es gemacht hat, bevor das Paket signiert wurde. Der Grund, warum sie nicht da ist, ist weder Schwierigkeit noch Kosten. Es ist, dass niemand gebeten wurde, sie zu schreiben, und damit war die Frage, ob sich das paketierte Artefakt wie Software verhält, niemandes Aufgabe.
Was ein schlampiger Build-Prozess alle anderen kostet
Alles bisher ist der Build-Prozess eines Produkts von innen gesehen, und ich werde dich nicht noch einmal hindurchführen. Die Frage, die sich lohnt, ist, ob dieser Sorgfaltsmaßstab wahrscheinlich bei einem Team aufhört.
Also stell den Befund von außen daneben. Die CISA führt einen Katalog von Schwachstellen, die bekanntermaßen in freier Wildbahn ausgenutzt werden. Nicht theoretisch, nicht bewertet. Beobachtet, wie sie gegen Menschen eingesetzt werden. Stand 14. September 2026 enthält er 1.710 Einträge18:
| Hersteller | Einträge im KEV-Katalog |
|---|---|
| Microsoft | 388 |
| Cisco | 98 |
| Apple | 94 |
| Adobe | 81 |
| 74 | |
| Oracle | 46 |
| Fortinet | 30 |
| VMware | 26 |
Zweiter, hinter einem Betriebssystemmonopol und vor allen anderen. Der jüngste Cisco-Eintrag kam am 14. September 2026 dazu, dem Tag, bevor dies geschrieben wurde.
Dieselbe Datei habe ich drei Wochen vorher für /de/random/is-your-msp-lying-to-you-part2/ gezählt: Version 2026.08.27, 1.685 Einträge, Cisco bei 96. Zwei mehr seither, in einundzwanzig Tagen.
Sei fair damit, was diese Tabelle für sich genommen beweist und was nicht. Eine große installierte Basis an hochwertigen Stellen zieht Aufmerksamkeit an, und Aufmerksamkeit findet Fehler, also wird jeder Hersteller dieser Größe eine lange Liste tragen. Die Zahl ist ein Vorzeichen, kein Urteil.
Die Zusammensetzung lässt sich schwerer wegwischen als die Gesamtzahl. Fünfundzwanzig von Ciscos achtundneunzig sitzen auf den Sicherheitslinien: den Firewalls, den Appliances, den VPN-Konzentratoren, den Mail- und Web-Gateways, den Identitätsdiensten. Und die vier jüngsten Einträge gegen sie, hintereinander, sind Secure Firewall Management Center zweimal, Secure Firewall ASA und Secure Email Gateway, letzteres am 14. September 202618. Nicht die Switches. Nicht die Kollaborationstechnik. Die Produkte, die eigens dafür verkauft werden, das zu sein, was alle anderen schützt.
Und das ist der Satz, auf den dieser ganze Beitrag zuläuft, also hier ist er im Klartext. Das ist eine Firma, deren Geschäft der Verkauf von Sicherheits-Appliances ist, und sie bekommt es nicht hin, dass ein Desktop-Client ein Zertifikat prüft. Kein schwerer Fall. Kein neuartiger Angriff. Die allerroutinemäßigste Sicherheitsoperation der Informatik, die jeder Browser bei jedem Seitenaufruf ausführt, in einer Bibliothek, die sie selbst geforkt haben, und sie haben sie auf ein Verzeichnis gerichtet ausgeliefert, das auf keiner Kundenmaschine je existiert hat. Und dann signiert.
Was dieser Beitrag hinzufügt, ist eine Stichprobe des Prozesses dahinter. Keine Schwachstelle. Ein schlichter Paketierungsdefekt, die am wenigsten subtile Fehlerklasse, die es gibt, auf einer unterstützten Plattform, von jedem Rauchtest gefunden, den irgendwer hätte laufen lassen wollen, und trotzdem ausgeliefert. Wenn eine Build-Pipeline das einem zahlenden Kunden vorsetzt, ist nicht offensichtlich, was sie aufhalten würde. Nichts hat es.
Darf ich fragen, warum das signiert wurde?
Darf ich fragen, warum ein Paket, das keinen TLS-Handshake abschließen kann, signiert und gegen eine Plattform veröffentlicht wurde, die eure eigene Anforderungsseite als unterstützt aufführt? Nicht, wer es signiert hat. An einem Namen habe ich kein Interesse, und darum geht es auch nicht. Was hat es zugelassen, denn irgendetwas hat es, fünfzehn Minuten nach dem Ende des Builds, und was immer es ist, es läuft heute noch.
Jetzt die deutliche Fassung. Das ist kein Projekt, das ich mir von einer Plattform gezogen und auf gut Glück ausprobiert habe. Es ist bezahlt. Dahinter stehen ein Vertrag, eine Supportleitung, eine Anforderungsseite, die eine Behauptung über Linux aufstellt, und ein Signaturschlüssel, der versichert, das Paket komme von Cisco und sei zur Installation geeignet. Jedes davon ist eine Aussage an einen Kunden, und bei diesem Release war jede davon nichts wert, weil das Ergebnis nie geöffnet wurde.
Und der Maßstab, der hier verfehlt wird, ist nicht meiner. Es ist ihrer. Die vier Variablen, die diese Pfade getragen hätten, sind in einer Handbuchseite dokumentiert, die ihr im Bündel mitliefert. Euer eigenes Paketformat hat eine %config-Markierung, die ihr nicht benutzt habt, und einen Dokumentationsabschnitt, den ihr leer gelassen habt. Euer eigener Build lief in einem Container, den ihr nie benutzt habt, um die Ausgabe auszuführen. Ich bitte kein Netzwerk- und Kollaborationsunternehmen darum, irgendetwas zu erfinden. Ich frage, warum es seine eigenen Handbücher nicht gelesen hat.
Entsprechend ist die Ausrede, die ich nicht gelten lasse, dass das schwer sei. Es ist für niemanden schwer, und für eine Firma, die das täglich macht, in dieser Größe, für dieses Geld, schon gar nicht. Fachleute, die das jeden Tag tun, haben keine Entschuldigung, und groß zu sein ist keine.
Und darf ich noch eine stellen, denn das ist die, auf die es wirklich ankommt. Ihr verkauft Firewalls. Ihr verkauft ein E-Mail-Gateway, einen VPN-Konzentrator, einen Identitätsdienst, ein Management Center für das alles, und der Verkaufsspruch bei allen ist, dass ihr das besser versteht als euer Kunde. Wie also liefert eine Firma, die sich als Autorität für Netzwerksicherheit positioniert, einen Client aus, der ein Zertifikat nicht prüfen kann? Nicht: ihn nicht raffiniert genug prüft. Ihn nicht nachschlagen kann, weil das Verzeichnis nie da war.
Es gibt keine Fassung dieser Antwort, die ich hören will und die damit anfängt, dass das Desktop-Team vom Appliance-Team getrennt sei. Es ist dieselbe Signatur, dieselbe Pipeline, dieselbe veröffentlichte Behauptung über eine unterstützte Plattform und derselbe fehlende Schritt am Ende, nämlich die Schachtel zu öffnen. Wenn die Zertifikatsprüfung vor der Auslieferung in dem Produkt nicht geprüft wird, in dem ein Defekt laut und harmlos ist, habe ich keinen Grund zu glauben, dass sie in dem Produkt geprüft wird, in dem ein Defekt leise und teuer ist.
Und der ehrliche Teil, im Klartext. Nichts davon hat mich überrascht. Ich habe mir angewöhnt, diesen Maßstab von diesem Hersteller zu erwarten, weshalb ich, wo die Wahl bei mir liegt, seine Technik nicht kaufe und nicht darauf baue. Das ist keine Vorliebe für Logos. Es ist dasselbe Urteil, das ich über jeden Lieferanten fällen würde: ich habe seine Arbeit gemessen, mehr als einmal, und sie kommt immer wieder gleich zurück. Der Katalog oben ist eine Messung. Was die erste Hälfte dieses Beitrags gekostet hat, ist eine zweite.
Der Grund, warum ich überhaupt auf Webex war, ist, dass die Wahl nicht bei mir lag, und das gehört benannt, denn es ist die Lage, in der die meisten sind, die das hier lesen. Du kommst selten darum herum, einen Hersteller zu benutzen, dessen Qualität du schon abgemessen hast. Jemand anders unterschreibt den Vertrag, die Technik kommt an, und der Erste, der herausfindet, was im Build ausgelassen wurde, bist du, an deinem Schreibtisch, mit einer Besprechung, die gerade anfängt.
Etwas ausliefern, das man nie ausgeführt hat
Es wird eine interne Erklärung geben. Ein voller Sprint, eine Pipeline, die den Besitzer gewechselt hat, eine Plattform ohne Zuständigen. Nichts davon ist es wert, gehört zu werden, denn jede beschreibt dasselbe: die Arbeit wurde nicht gemacht, und nichts im Prozess hat sie verlangt.
Es gibt einen alten Maßstab im Handwerk, der es nie in die Software geschafft hat: du gehst nicht von der Baustelle, bevor du das Ding in Betrieb genommen hast. Du füllst die Anlage und prüfst jede Verbindung. Du legst Spannung auf den Verteiler und misst jeden Stromkreis. Nicht, weil du an deiner Arbeit zweifelst. Weil der Kunde sie benutzen wird, und es vor seinen Augen herauszufinden ist kein professionelles Ergebnis. Niemand sieht dir dabei zu. Du machst es trotzdem. Das ist die ganze Bedeutung des Wortes.
Software streitet seit dreißig Jahren dafür, dass sie anders sei, dass der Build das Lieferbare sei und die Installation das Problem eines anderen, dass eine grüne Pipeline dasselbe sei wie ein funktionierendes Produkt, und das ist sie nicht. Ein Build, der nie außerhalb des Containers ausgeführt wurde, der ihn erzeugt hat, ist nicht fertiggestellt, sondern an der Stelle liegen gelassen worden, an der das Fertigstellen langweilig wird. Alles danach ist eine Behauptung über Arbeit, die nicht gemacht wurde.
Die Lösung ist ein Konfigurationsschalter. Die Prüfung ist eine Zeile Shell. Die Kosten von beidem sind nicht das Interessante; die interessante Zahl ist, wie viele Leute ihre Zugangsdaten in ein Webex-Fenster getippt haben, zugesehen haben, wie es sagte, ihr Internet sei weg, und es geglaubt haben, weil Cisco es ihnen so gesagt hat und Cisco eine Netzwerkfirma ist. Das hat der fehlende Test tatsächlich eingekauft: keinen Fehler, sondern eine Lüge, die das Produkt selbstbewusst erzählt, bei jedem Start, Leuten, die keine Möglichkeit haben, es besser zu wissen.
Und das ist die Linie, die es vor dem Schließen des Tabs zu ziehen lohnt, denn die beiden Hälften dieses Beitrags sind nicht zwei Themen. Ein Vertrauenspfad, der auf einen Build-Container zeigt, und eine Authentifizierungsumgehung auf einer Edge-Appliance sind derselbe Fehler bei unterschiedlichem Einsatz. Beides ist ein Wert, den niemand geprüft hat, in einem Artefakt, das niemand ausgeführt hat, signiert von einem Prozess, der Herkunft bezeugt und nicht Tauglichkeit. Der vor mir war die harmlose Sorte. Er ging laut kaputt, auf meinem eigenen Schreibtisch, und ich war der Erste, der es wusste. Die andere Sorte tut dir das nicht.
Niemand bei Cisco hat beschlossen, eine ausnutzbare Firewall auszuliefern, genauso wenig, wie jemand beschlossen hat, einen Client auszuliefern, der nicht ins Internet kommt. Das ist keine Verteidigung. Das ist der Vorwurf. Keines von beidem muss beschlossen werden, und genau das ist das Problem: beides ist das, was hinten herauskommt, wenn eine Pipeline kompiliert, signiert und veröffentlicht, ohne dass irgendwer dafür zuständig gemacht wird, das Ergebnis zu öffnen. Ein Prozess, der ein nicht existierendes Verzeichnis nicht findet, hätte eine nicht geprüfte Länge nie gefunden, und ein Hersteller, der dir die Appliance verkauft, die deinen Perimeter bewacht, hat auf so einen Prozess keinen Anspruch.
Wenn also der Name eines Herstellers immer wieder auf dieser Liste auftaucht, widersteh der bequemen Erklärung, er sei eben groß und stark im Visier. Die Größe erklärt die Menge. Sie erklärt nicht die Art. Sieh dir stattdessen an, was sein Build-Prozess mit den langweiligen Dingen macht, denn die langweiligen Dinge sind von außen messbar, von dir, heute, an Technik, die du schon besitzt.
Prüf deine eigenen Bündel. strings und readelf und zwanzig Minuten sagen dir, welche deiner Hersteller einen Pfad zu einer Maschine ausliefern, die du nie sehen wirst. Was du in Wahrheit misst, ist nicht der Pfad. Es ist, ob dort irgendwer hingesehen hat, und wenn die Antwort bei etwas so billig Auffindbarem nein lautet, weißt du bereits, wie sie bei den Dingen lautet, die es nicht sind.
Cisco — Webex App system requirements — die Liste unterstützter Plattformen, Linux eingeschlossen. ↩︎
OpenSSL — INSTALL.md,
--openssldir— „Directory for OpenSSL configuration files, and also the default certificate and key store.“ ↩︎ ↩︎OpenSSL — SSL_CTX_load_verify_locations(3) — die Standard-CA-Datei ist
cert.pemund das Standard-CA-Verzeichniscerts, beide innerhalb des Standard-OpenSSL-Verzeichnisses; und zu den Rückgabewerten: „A missing default location is still treated as a success.“ ↩︎ ↩︎Conan 2 —
conan cache— Paketbinärdateien liegen unter einem gehashten Pfad im lokalen Cache, und daher kommt/workspace/.conan2/p/b/<hash>/p. ↩︎OpenSSL — openssl-env(7) —
SSL_CERT_DIRundSSL_CERT_FILE„specify the default directory or file containing CA certificates“. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎OpenSSL — provider(7) — der Standard-Provider und was eine Konfiguration, die ihn weglässt, unverfügbar macht. ↩︎ ↩︎
OpenSSL — config(5) — die Konfigurationsdatei, die OpenSSL bei der Initialisierung lädt, und wo danach gesucht wird. ↩︎
freedesktop.org — XDG Base Directory Specification — „The base directory defined by
$XDG_DATA_HOMEis considered more important than any of the base directories defined by$XDG_DATA_DIRS.“ ↩︎freedesktop.org — Desktop Entry Specification — wo nach
.desktop-Dateien gesucht wird und wie eine eine andere gleichen Namens überdeckt. ↩︎Filesystem Hierarchy Standard 3.0 — die Verzeichnisse, die ein Wurzeldateisystem enthalten soll,
/workspacenicht darunter. ↩︎update-ca-trust(8) — wie das konsolidierte Bündel unter
/etc/pki/ca-trust/extractedauf Fedora und Verwandten erzeugt wird. ↩︎ld.so(8) —
$ORIGINwird zu dem Verzeichnis aufgelöst, das das Programm oder Shared Object enthält, und genau das macht einen gebündelten Baum verschiebbar. ↩︎glibc timeline — „2018-08-01 GLIBC 2.28 — The GNU C Library version 2.28 is now available“. ↩︎
glibc — Release wiki — die Tabelle Version zu Distribution; Red Hat Enterprise Linux 8 ist glibc 2.28. ↩︎
RPM — spec file reference —
%configund was der Paketmanager mit einer als Konfiguration markierten Datei macht. ↩︎ ↩︎Fedora packaging guidelines — configuration files — wann eine mitgelieferte Datei als
%configmarkiert sein muss. ↩︎glibc FAQ — warum eine statisch gelinkte glibc-Binärdatei zur Laufzeit trotzdem die NSS-Module des Hosts braucht. ↩︎
CISA — Known Exploited Vulnerabilities Catalog — gezählt aus dem veröffentlichten JSON-Feed, Katalogversion 2026.09.14, 1.710 Einträge. ↩︎ ↩︎