Moritz Marquardt IT-Projekte zwischen Embedded & Web

Webhooks mit Docker empfangen - der simple Weg

Docker-Images, die einen HTTP-Server für eingehende Webhooks aufmachen, gibt es viele, auch ich habe mich vor einiger Zeit einmal daran versucht, doch wenn man bedenkt, dass die Webhooks meist recht ausgeweiteten Zugriff aufs System haben, wird man hier schnell skeptisch - schließlich kann ein bösartiges Image also katastrophale Folgen haben, die simpelste/offizielle Lösung ist also stark zu bevorzugen.

In den meisten Fällen läuft auf meinen Servern mit Docker ein Reverse-Proxy wie Caddy oder Traefik mit, der HTTPS und Authentifizierung regeln kann. Den meisten Webhook-Sendern kann zudem vertraut werden, keine DoS-Angriffe ausführen zu wollen, sondern nur Webhooks abzusetzen wenn dies auch tatsächlich notwendig ist. In einem solchen System sind die meisten Features solcher fertigen Container überflüssig.

Die simpelste Lösung liegt dann im httpd von BusyBox: dieser kann CGI-Skripte ausführen, also im Endeffekt simple ausführbare Dateien wie Shell-Skripte oder Python-Programme.

Alles was wir brauchen ist einen Ordner wie beispielsweise /etc/webhooks, in dem unser Webhook-Skript liegt - beispielsweise update-containers.sh:

#!/bin/sh
set -eu # strict mode
if [ "$token" != "helloworld" ]; then
  # Require ?token=helloworld
  echo "Invalid token"
  exit 1
fi
cd /srv/my-project
docker compose pull
docker compose up -d

Nun können wir die Datei ausführbar machen und den Docker-Container starten:

chmod +x /etc/webhooks/update-containers.sh
docker run \
  -d --name webhooks \
  -p 8080:80 \
  -v "/srv/my-project:/srv/my-project" \
  -v "/etc/webhooks:/var/www/cgi-bin" \
  -v "/var/run/docker.sock:/var/run/docker.sock" \
  -w "/var/www" \
  alpine \
  sh -c 'apk add --no-cache docker-cli docker-compose busybox-extras && exec httpd -fvv'

Dies startet einen Container im Hintergrund (-d) mit dem Namen "webhooks" auf Basis vom "alpine"-Image. Port 80 aus dem Container wird als Port 8080 auf dem Host gemappt, und notwendige Verzeichnisse sowie der Docker-Socket (Achtung: dieser hat Root-Zugriff aufs Host-System!) in den Container gemounted (-v). Mit -w wird noch angegeben welches Verzeichnis httpd ausliefern soll - in diesem muss das Unterverzeichnis cgi-bin existieren.
Der angegebene Befehl installiert schließlich die Docker-CLI (damit wir auf den Docker-Daemon zugreifen können) sowie die busybox-extras (die in Alpine Linux httpd enthalten), und führt httpd -fvv aus, also im Vordergrund (-f) mit sehr ausführlichen Logs (-vv).
Wird ausschließlich BusyBox benötigt, kann auch direkt das busybox-Image genutzt werden - hier ist httpd schon mit drin, man kann aber keine weiteren Pakete wie Docker installieren.

Nun können wir auf http://localhost:8080/cgi-bin/update-containers.sh?token=helloworld zugreifen, und das Skript wird ausgeführt.

Wie schaut es mit der Sicherheit aus? Beim offiziellen Alpine-Image selbst ist es natürlich erst einmal mehr oder weniger ausgeschlossen dass sich absichtlicher Schadcode einschleichen sollte.
Was Bugs betrifft, sind CGI-Skripte allerdings naturgemäß unter Umständen etwas anfälliger als eigens für die Webentwicklung optimierte Sprachen - so gab es beispielsweise die Sicherheitslücke "Shellshock", mit welcher man wie hier bei einem Skript, das auf den Docker-Daemon zugreifen kann, volle Root-Rechte auf das Hostsystem erlangen könnte. Solche fatalen Sicherheitslücken sind natürlich selten und werden recht groß kommuniziert. Ein Reverse Proxy mit Basic-Auth kann selbst bei solchen gravierenden Lücken allerdings recht gut Abhilfe schaffen.

Problematischer sind vermutlich eher Bugs, wie der Fall dass ein Webhook zweimal fast gleichzeitig gestartet wird. Dies muss im Zweifelsfall vom Skript selbst gehandelt werden (z.B. sehr vereinfacht mit einer Lockfile: while [ -f .lock ]; do sleep 0.1; done; trap "rm -f .lock" EXIT; touch .lock) - bei solch komplexen Szenarien lohnt sich dann jedoch eventuell doch wieder ein fertiges Skript.

Vorheriger Beitrag