Atlassian On-Prem Anwendungsverknüpfungen zu Cloud: Workarounds für halbherzige Anwendungstunnel

Atlassian On-Prem Anwendungsverknüpfungen zu Cloud: Workarounds für halbherzige Anwendungstunnel

Bamboo, Bitbucket & co. mit Atlassian Cloud-Instanzen verknüpfen? Dank der Tunnel gar kein Problem – zumindest in der Theorie. Praktisch ist die Software bis heute nicht ausgereift, obwohl es 5 nach 12 ist. Ich habe mich bei einer Migration damit beschäftigt und per Reverse Engineering die Fallstricke in einer typischen Unternehmensumgebung ermittelt. Dieser Beitrag zeigt, was schief läuft, welche technischen Versäumnisse dahinter stecken und mit welchen Workarounds man den Nutzern die Funktionalität trotzdem zur Verfügung stellen kann.

Ausgangssituation und Ziel

Bambo DataCenter und Bitbucket (= On-Prem) soll mit einer neuen Jira Cloud Instanz verbunden werden. Alle drei liefen bisher als Server-Lizenz On-Prem. Atlassian hat mit ihrem Cloudzwang die Verantwortlichen davon überzeugt, dass die Migration zu Jira Cloud weniger teurer ist, als DataCenter mit mindestens 500 Nutzern. Und Cloudausfälle treffen eh bloß die anderen.1 Bisher waren Jira und Bamboo On-Prem über Anwendungsverknüpfungen miteinander verbunden.2 Damit kann man Informationen vom einen System in das andere integrieren – beispielsweise Git-Commits von Bitbucket in Jira Tickets. Im gleichen Netzwerk On-Prem lief der Atlassian-Teil stabil.

Jira in der Atlassian Cloud, Bitbucket & Bamboo weiterhin als DataCenter On Prem: So ist die Ziel-Infrastruktur gewünscht

Die internen Systeme kommen nur über einen Proxy-Server ins Internet, der wiederum selbst ein extern betriebener Dienst ist. Der bricht verschlüsselte Verbindungen per MITM auf, dementsprechend muss sein Root-Zertifikat im cacerts Speicher der Java Laufzeitumgebung hinterlegt sein. Dies geschieht über die Java Properties (-D) in den Startskripten (setenv.sh bzw. vergleichbare, das ist nicht einheitlich).

Application Tunnel: Ein Trick zur Umgehung von Firewalls & co.

Mit den Atlassian Cloudsystemen ändert sich das. Hier hat man bei den Anwendungsverknüpfungen grundsätzlich die Wahl zwischen Getunnelt und Direkt.3 Letztere entspricht den Application Links der On-Prem Editionen untereinander, d.H. Server A holt sich die Infos von Server B sowie umgekehrt. Sobald Cloudsysteme im Spiel sind, wird das schwierig. Tendenziell kann ein internes Unternehmenssystem mit entsprechender Freischaltung auf externe Clouddienste zugreifen. Von außen nach innen möchte man oft einen Reverse Proxy in der DMZ haben und generell sind solche Löcher eher unerwünscht, weil aufwändig und potenziell gefährlich.

Also hat Atlassian Getunnelte Anwendungsverknüpfungen (application tunnels) erfunden. Ihre Doku dazu verspricht eine Lösung für genau dieses Problem: Die Cloud soll auf interne Systeme zugreifen können, ohne diese Systeme öffentlich erreichbar zu machen. Klingt wie die Quadratur des Kreises und mir fällt nur ein Trick ein, wie das umsetzbar ist: Das On-Prem System initiiert eine Verbindung mit dem Cloudsystem. Sie wird offen gehalten, damit die Gegenseite dort Anfragen stellen kann. Bei Websockets wird damit das im Kern gleiche Problem gelöst: Im HTTP-Protokoll sind nur Anfragen vom Client an den Server vorgesehen, nicht umgekehrt. Früher hat man dafür eine Ajax-Anfrage gesendet, die der Server offen lässt, bis er seine Daten als Antwort sendet. Oder pauschales Polling von der Gegenseite. Beides technisch schlecht und skaliert auch nicht gut.

Wenn der On-Prem Server sich mit der Cloud verbindet und die Verbindung offen hält, sollte das grundsätzlich funktionieren. Dazu gibt es mit WebSockets seit längerem ein auf HTTP aufsetzendes Protokoll, dass dafür ausgelegt ist, eine Verbindung für das Senden von beiden Parteien geöffnet zu halten. HTTP(S) wird zudem von den meisten Firewalls & Proxy-Servern erlaubt.

Installation: DAS soll so funktionieren?

Die Tunnel stecken in einer Erweiterung mit einer sympathischen 2/4 Sternen.4 Kostenpflichtig lizenzieren muss man sie nicht, allerdings über den Marketplace von Atlassian installieren. Dadurch erscheint bei den Integrationen ein neuer Menüpunkt Application tunnels. Zusätzlich verlangt Atlassian einen Connector, je nach Anwendung setzt man den im Tomcat (server.xml) oder in der Properties-Datei.5 Dessen Port soll man anschließend als Java-Property übergeben:

-Dsecure.tunnel.upstream.port=$portNumber

Das macht mich stutzig. Beim ersten überfliegen der Doku hatte ich den Verdacht, es sind doch eingehende Verbindungen auf diesem Port erforderlich – wozu öffnen wir sonst auf dem On-Prem Server ein weiterer Port geöffnet werden, mit dem offensichtlich der Anwendungsserver kommuniziert? Allerdings widerspricht der Eintrag und sagt, der Port müsse nur lokal erreichbar sein. Ein Abschnitt zeigt sogar, wie man den Port auf lokale Verbindungen beschränkt. Seltsam. Auch nach einem längeren Gespräch mit dem Dienstleister war mir nicht klar, wie das funktionieren sollte. Insbesondere weil das Schaubild in der Atlassian Erklärung genau das zeigte, was ich befürchtete: Der möchte sich von der Atlassian Cloud ins Firmennetzwerk verbinden, was nicht funktionieren würde.

Über dem Bild schrieb Atlassian jedoch, genau das sei nicht nötig. Ich entschloss, dass wir es zusammen versuchen einzurichten – vielleicht würde es dann klarer. Außerdem war keine Alternative in Sicht. Dem Fachbereich sind die Anwendungsverknüpfungen enorm wichtig. Somit haben wir in der Konfigurationsdatei des integrierten Tomcat in <Service> einen Connector angelegt:

<Connector port="8093" connectionTimeout="20000" maxThreads="200" minSpareThreads="10" 
           enableLookups="false" acceptCount="10" URIEncoding="UTF-8" />

Und in der Variable JVM_SUPPORT_RECOMMEND aus /setenv.sh der JVM das Property für den Port übergeben:

-Dsecure.tunnel.upstream.port=$portNumber

Schlussendlich einen Tunnel im Administrationsbereich von Jira Cloud angelegt sowie dessen Schlüssel in Bamboo DataCenter hinterlegt.6

Erfolgreich nichts abgeschlossen

Nach dem Anlegen passierte jedoch … nichts. Der Bamboo On-Prem Tunnel bleibt grau auf unavailable, während der Status von Jira Cloud auf unvollständig steht. Das Log von Bamboo sagt … nichts. Mit Bitbucket habe ich es zuerst probiert, weil Bamboo ein Upgrade auf 9.3 verlangt und natürlich 9.2 LTS lief. Dort log das Log [sic], man hätte irgendwas erfolgreich an der Verbindung aktualisiert. Da diese Meldung ohne Interaktion ungefähr jede Minute ins Protokoll geschrieben wurde, war das ein Test, Healthcheck oder etwas anderes automatisiertes. Hergestellt werden konnte die Verbindung allerdings trotzdem nicht.

c.a.t.m.TunnelConnectionService Successfully updated tunnel connection for node with ID XYZ

Dagegen war diese Fehlermeldung, welche seltener (wohl beim Anlegen der Verknüpfung) ins Log geschrieben wurde, zwar genau so hilfreich:

Unrecognized error while attempting to retrieve status of Application Link XXX

Allerdings zumindest ehrlich. Auf dem Niveau ist Atlassian angekommen, dass man sich freut, wenn Logs die Wahrheit erzählen… Willkommen in der Welt der Enterprise Software vom Marktführer! Ich habe zusammen mit einem auf Atlassian spezialisierten Beratungsunternehmen viel herum probiert. Vor allem über das was nicht funktioniert hat wurde mir zunehmend klar, wie die Tunnel funktionieren sollten und warum sie das in der Unternehmensumgebung nicht tun.

Bevor wir zu diesem eher frustrierenden Teil kommen, ein guter Witz von Atlassian. Zur zweiten Fehlermeldung gibt es einen Forumsbeitrag von 2018.7 Der bezieht sich auf einen Bug in Jira Server, wie ein Atlassian-Mitarbeiter in den Antworten kommunizierte. Dazu schrieb er: Man solle im Ticket abstimmen, damit Atlassian weiß, wie viele das betrifft. Ist ja nicht so, als ob ihr mit dem Kauf von Lizenzen und Wartung (Ohne aktive Wartungslizenz keine Updates!) dafür bezahlt, damit die ihre kaputte Software in den Griff bekommen. Das muss sich schon lohnen – wenn es zu wenige betrifft, bleibt die halt kaputt. Wo kommen wir denn da hin, wenn man für Geld eine funktionierende proprietäre Software bekommt? Der Fehler wurde übrigens bis heute nicht behoben. 6 Monate später hat man ihn in ein anderes Ticket verlagert8. Wer betroffen ist, hat wohl Pech. Das ist Enterprise Software Level? Ahja…

Wo ist das Problem?

Quelle: turnoff.us

Bitbucket machte den Anfang – ein Fehler, wie sich herausstellen sollte: Die Logs zeigen außer einer Erfolgsmeldung nichts an. In der Admin-Oberfläche lassen sich Debug-Meldungen aktivieren – natürlich nur global, wer möchte schon nicht mit zig Meldungen eines produktiv genutzten Systems geflutet werden? Vor allem, wenn sich darin exakt 0,0 Informationen zum Problem finden. Der einzige Eintrag zu den Tunneln bleibt, dass er sie angeblich regelmäßig aktualisiert.

Wir haben also keine Ahnung, wo das Problem sein soll. Da der Atlassian Support bisher auch nicht hilfreich war, wurde blind nach der Nadel im Heuhaufen gesucht. Das ging bis zu einem Jira-Artikel, in dem sich der Tunnel versucht, zum Standardport statt dem übergebenen zu verbinden. Ursache laut Atlassian: Man müsste bei diesem JVM-Property die Parameter mit Komma statt Leerzeichen trennen9 – WTF? Habt ihr schon mal -Da=1=b=2 gesehen? Ich habe meine Zweifel. Außerdem ist in der Installation der Standardport verwendet worden, auf dem nach dem Neustart auch ein Server lauscht. Allerdings greift man blind nach jedem Strohhalm, gebracht hat es nichts.

Die Katakomben der Tunnel

Nachdem sich Bitbucket als Sackgasse erwies, wurde es mit Bamboo probiert. Intern sind die Atlassian-Anwendungen keineswegs so unterschiedlich, wie es Atlassian mit der Oberfläche glaubhaft machen möchte. Auf diese Inkonsistenz kann man sich verlassen: Im Administrationsbereich unter System > Log settings hat jemand mitgedacht und Log-Level für verschiedene Namensräume der Atlassian-Komponenten eingebaut10 – statt nur eines globalen Schalters, wie bei Bitbucket. Erinnert etwas an WebSphere. Nachdem der Namensraum com.atlassian.tunnel auf ALL gesetzt wurde,11 siehe da, wir sehen etwas passieren:

DEBUG [tunnel-executor:thread-1] [InletsClientProcessFactory] Creating process for executable com.atlassian.tunnel.file.LinuxInletsExecutable@5b7dd3ee with command [./inlets, client, --url=******, --upstream=******=http://127.0.0.1:8093, --token-from=/bamboo-data/xyz_token, --strict-forwarding]
TRACE [tunnel-executor:thread-1] [InletsClientManager] Tunnel Process Monitor status on Before Start: RESTARTING
DEBUG [tunnel-executor:thread-2] [InletsClientManager] [INLETS CLIENT] Upstream: ****** => http://127.0.0.1:8093
DEBUG [tunnel-executor:thread-2] [InletsClientManager] [INLETS CLIENT] level=info msg="Connecting to proxy" url="******/tunnel"
DEBUG [tunnel-executor:thread-2] [InletsClientManager] [INLETS CLIENT] level=error msg="Failed to connect to proxy. Empty dialer response" error="dial tcp 185.166.143.26:443: connect: connection refused"
DEBUG [tunnel-executor:thread-2] [InletsClientManager] [INLETS CLIENT] level=error msg="Remotedialer proxy error" error="dial tcp 185.166.143.26:443: connect: connection refused"

Fangen wir unten an. Der Tunnel versucht eine Verbindung zu 185.166.143.26:443. Das ist eine öffentliche IP-Adresse, die zu AWS gehört. Das weist auf Atlassian hin, da sie keine Rechenzentren selbst betreiben. Sondern sich für die Atlassian Cloud bloß bei Amazon etwas gemietet haben. Ruft man die IP-Adresse per HTTP im Browser auf, bekommt man mit ungültigem Zertifikat eine Fehlerseite von Atlassian.

Warum kommt der nicht raus? Per -Dhttps.proxy* Property ist der Proxyserver gesetzt (https.proxyHost, https.proxyPort, …) und dort wurden die AWS-Adressen zur Atlassian Cloud bereits freigeschaltet. Per curl auf dem Server funktioniert die Verbindung zum in der Doku angegebenen Host tunnel.services.atlassian.com problemlos. Warum das so ist, verrät die erste Zeile direkt: Da wird eine ausführbare Datei namens inlets aus der JVM heraus gestartet. Sieht … abenteuerlich aus. Schauen wir uns die mal genauer an. Im Anwendungsverzeichnis liegen drei Jars mit tunnel im Name:

find . -name '*tunnel*'
atlassian-bamboo/WEB-INF/lib/atlassian-tunnel-1.8.8.jar
licenses/com.atlassian.tunnel--atlassian-tunnel--1.8.8.txt
temp/plugin.13183305074016480489.tunnel-client-plugin-1.2.0.jar

Jars sind technisch ZIP-Archive, daher kann man sie mit unzip auspacken und hinein schauen:

unzip temp/plugin.13183305074016480489.tunnel-client-plugin-1.2.0.jar -d /tmp/tunnel-plugin/

In der ersten Debug-Meldung steht, wie die Klasse heißt. Damit werden wir fündig:

find /tmp/tunnel-plugin/ -name '*LinuxInletsExecutable*'
/tmp/tunnel-plugin/com/atlassian/tunnel/file/LinuxInletsExecutable.class

Nachdem ich die inlets Binärdatei nicht im Dateisystem des Anwendungsservers finden konnte, liegt sie bestimmt in der Jar. Tatsächlich liegt in inlets-binaries eine 13 MB große Datei – vom Juni 2023, hat also wohl schon länger keiner mehr angefasst. Das Pflegen von Abhängigkeiten gehört bekannterweise nicht zu den Stärken des Konzerns.

l /tmp/tunnel-plugin/inlets-binaries/inlets -lh
-rw-r--r-- 1 user user 13M Jun  1  2023 /tmp/tunnel-plugin/inlets-binaries/inlets

Es handelt sich dabei um inlets.dev. Die Version wurde für Atlassian gebaut. Entweder hat Atlassian den Dienst lizenziert, oder das Unternehmen gekauft. Machen sie ja gerne mal, einige ihrer Software wurde dazu gekauft.

 ./inlets
 _       _      _            _
(_)_ __ | | ___| |_ ___   __| | _____   __
| | '_ \| |/ _ \ __/ __| / _` |/ _ \ \ / /
| | | | | |  __/ |_\__ \| (_| |  __/\ V /
|_|_| |_|_|\___|\__|___(_)__,_|\___| \_/
Expose your local endpoints to the Internet by creating a tunnel between
your local machine and an exit-server with its own public IP address.
Usage:
  inlets [flags]
  inlets [command]Available Commands:
  client      Start the tunnel client.
  completion  generate the autocompletion script for the specified shell
  help        Help about any command
  server      Start the tunnel server.
  version     Display the clients version information.Flags:
  -h, --help   help for inletsUse "inlets [command] --help" for more information about a command.

./inlets version
 _       _      _            _
(_)_ __ | | ___| |_ ___   __| | _____   __
| | '_ \| |/ _ \ __/ __| / _` |/ _ \ \ / /
| | | | | |  __/ |_\__ \| (_| |  __/\ V /
|_|_| |_|_|\___|\__|___(_)__,_|\___| \_/
Version: 2.1.0-atlassian1-31-g33b2f95
Git Commit: 33b2f95625ada3d1346652aeb5ec8e73edcf7388

Diese Software macht genau das, was ich oben vermutet hatte: Eine WebSocket-Verbindung zu einem fremden Server aufbauen und über diese Verbindung Anfragen in umgekehrter Reihenfolge ermöglichen – ohne eben dafür eingehende Ports zu öffnen. Nun wird auch klar, wieso ein Port als Property mitgegeben wird, denn Inlets agiert als Mittelsmann. Bamboo (oder andere On-Prem Software) verbindet sich darüber mit der Atlassian Cloud, welche wiederum mit der lokalen Instanz kommunizieren kann.

Warum funktioniert das nicht?

Wie anfangs erwähnt, ist der Proxy für die Bamboo JVM konfiguriert gewesen. Sie hat auch einen trustStore bekommen, der das MITM Root-Zertifikat vom Proxy enthält. Dieses Konstrukt funktioniert nachweislich im Atlassian Marketplace der Instanz. Allerdings bekommt nur die Bamboo JVM diese Parameter. An einen daraus gestarteten Unterprozess werden sie nicht vererbt. Um sie zu setzen, müsste ich den Start des Prozesses verändern – bei proprietärer Software nicht in vernünftigem Ausmaß möglich. Wer sehr viel Zeit hat, kann die Klassen dekompilieren und mit Javassist manipulieren.

Halte ich für keinen brauchbaren Workaround, daher ein anderer Ansatz: Im Systemd-Dienst http(s)_proxy setzen. Zur Sicherheit auch immer in Großbuchstaben für Windows. Ist theoretisch zwar egal, da wir auf einem GNU/Linux Server unterwegs sind, aber bei Atlassian weiß man ja nie. Das Problem wird damit zwar nicht gelöst, doch wir erhalten eine neue Fehlermeldung:

level=error msg="Failed to connect to proxy. Empty dialer response" error="x509: certificate signed by unknown authority"

Die Anwendung unterstützt also diese Variablen und verwendet den Proxy. Da ihr der angepasste Java Truststore fehlt, vertraut sie dem MITM Zertifikat vom Proxy nicht. Nachdem Atlassian alles in Java macht, hatte ich darauf spekuliert, dass es auch eine Java-Anwendung sein könnte. Natürlich ist es das nicht, sondern Go. Sonst könnte man den globalen lib/security/cacerts Store um das Root-Zertifikat erweitern und wir müssten gar keinen eigenen angeben, weil der standardmäßig geladen wird. Wie funktioniert das bei Go? Er sucht in den Zertifikatsdateien der GNU/Linux-Distributionen.12 Somit können wir das Root-Zertifikat im Betriebssystem hinterlegen und mit update-ca-certificates13 dort hin verteilen – überraschend vernünftig. Anschließend konnte der Tunnel sich mit den Cloudservern verbinden, sodass Anwendungsverknüpfungen darüber eingerichtet werden können.

Diese Qualität vom Marktführer hat Tradition!

Mich wundert das kaum. Die haben nicht mal ein Konzept für regelmäßige Aktualisierungen ihrer Drittanbieter-Abhängigkeiten, die sie reichlich einsetzen. Warum soll ihr eigener Code mehr taugen? Path Traversal z.B. bekommen sie auch selbst hin – Der Fisch stinkt vom Kopf her. An einen Totalschaden (Remote Code Execution!) alle paar Wochen in einer ihrer Software hat man sich inzwischen gewöhnt. Hier liefert Atlassian: Im Mär 2024 alleine in Jira über 20 (!) Sicherheitslücken. Viele in Drittanbieter-Bibliotheken von 2022/2023. Highlight: RCEs von Oktober 2022 (!!).14 Atlassian hat die fast 1,5 Jahre (!!!) bei sich einstauben lassen, bis sie sich zu einem Update erbarmten. Tja, da haben wohl leider nicht genug zahlende Kunden abgestimmt. Werden sie halt gehackt, selbst schuld… Ja ne, ist klar.

Hier bei den Tunneln finde ich ja das geilste: Die Erweiterung dafür gibt es seit 2 Jahren. Atlassians Plan zum Cloudzwang ist seit 2020 bekannt und seit 15.02.2024 sind alle Server-Lizenzen von Updates & Support abgeschnitten.15 Theoretisch müsste also seit über einem Monat alles migriert sein. Wie zur Hölle kann es sein, dass dieses Ding bis heute zusammen gemurkst ist, wie von einem IT Azubi für den eigenen privaten Heimserver? Da wäre so was okay. Aber an größere Unternehmen, wo Proxys, Endpoint Protection und anderes Zeugs eher Regel als Ausnahme ist? Geht gar nicht. Für den Agenten haben sie die Java Standard-Properties sogar dokumentiert.16 Bei den Tunneln wird das a) völlig über den Haufen geworfen und b) hat man das Error-Handling vergessen? Was ist mit Exit-Code auswerfen und wenigstens bei ungleich 0 einen Fehler ins Log schreiben? Oder der andere, der immer erfolgreich ins Log schreibt?

Wir drücken es wem anders aufs Auge

Da kann man sich nur wieder mal den Kopf fassen. Wir reden hier schließlich nicht von einer Rakete zum Neptun, die ein wenig ihr Ziel verfehlt hat. Sondern von grundlegenden Dingen. Es ist seit Ewigkeiten bekannt: Man ruft kein externes Programm auf, ignoriert sämtliche Rückmeldungen von dem, prüft auch sonst nicht ob irgendwas davon funktioniert hat und schreibt am Ende pauschal immer success ins Log. Genau so was würde man auf eine Liste von „mach das bloß nicht“ Dingen als überspitztes Negativbeispiel setzen.

Bei einem FiAE Azubi würde man überlegen, ob man den für solchen Murks durchfallen lassen sollte. Ein Konzern wie Atlassian zeigt seinen Kunden den Mittelfinger und lagert das einfach an die aus. Über 2 Tage Fehleranalyse zahlt schließlich der, während Atlassian sich das Geld für vernünftige Qualität und Qualitätssicherung seit offensichtlich längerem spart. Als Kunde von denen käme ich mir nun gaaaaanz dezent über den Tisch gezogen vor. Aber hey, ist in seiner Branche der Marktführer. Wenn der das nicht kann, dann doch schließlich keiner, oder?

Quellen und weiterführende Informationen

  1. https://www.golem.de/news/atlassian-ausfall-von-jira-und-confluence-dauert-noch-zwei-wochen-an-2204-164566.html ↩︎
  2. https://confluence.atlassian.com/bamboo/linking-to-another-application-360677713.html ↩︎
  3. https://support.atlassian.com/jira-cloud-administration/docs/use-applinks-to-link-to-atlassian-products/ ↩︎
  4. https://support.atlassian.com/organization-administration/docs/install-application-tunnels-from-atlassian-marketplace/ ↩︎
  5. https://support.atlassian.com/organization-administration/docs/configure-required-connections-and-upstream-ports/ ↩︎
  6. https://support.atlassian.com/organization-administration/docs/create-an-application-tunnel-to-your-self-managed-instance/ ↩︎
  7. https://community.atlassian.com/t5/Jira-Software-questions/Jira-Application-Link-Health-Check-fails-for-no-reason/qaq-p/851176 ↩︎
  8. https://ecosystem.atlassian.net/browse/ATST-923 ↩︎
  9. https://confluence.atlassian.com/jirakb/tunnelled-application-link-error-no-response-was-received-from-the-url-you-entered-it-may-not-be-valid-please-fix-the-url-below-if-needed-and-click-continue-1282249432.html ↩︎
  10. https://confluence.atlassian.com/bamboo/logging-in-bamboo-289277239.html ↩︎
  11. https://support.atlassian.com/organization-administration/docs/troubleshoot-application-tunnels/ ↩︎
  12. https://stackoverflow.com/a/40051432 ↩︎
  13. https://manpages.ubuntu.com/manpages/xenial/man8/update-ca-certificates.8.html ↩︎
  14. https://confluence.atlassian.com/security/security-bulletin-march-19-2024-1369444862.html ↩︎
  15. https://www.atlassian.com/migration/assess/journey-to-cloud ↩︎
  16. https://confluence.atlassian.com/bamkb/how-to-configure-an-outbound-proxy-for-a-bamboo-remote-agent-1044107769.html ↩︎

Leave a Reply