Nachts um drei blockiert mein Backup-Skript einen Commit: mögliche Zugangsdaten im Klartext. Der Treffer war ein Fehlalarm – aber beim Nachsehen kam ein echtes Passwort zum Vorschein, das der Scanner seit über zwei Wochen übersehen hatte. Abgelegt hatte es mein KI-Assistent.

Worum geht’s?

Meine Home-Assistant-Konfiguration landet jede Nacht um 03:00 Uhr als Commit in einem privaten Repository auf meinem eigenen Gitea. Das ist praktisch: Ich sehe jede Änderung, kann zurückspringen und habe neben den normalen Backups noch eine lesbare Historie.

Weil in so einer Konfiguration schnell mal ein Passwort landet, hat das Backup-Skript ein kleines Sicherheitsnetz: Bevor es committet, sucht es in den neuen Zeilen nach Dingen, die wie Zugangsdaten aussehen. Findet es etwas, bricht es ab und schickt mir eine Push-Nachricht. So ein Werkzeug nennt man Secret-Scanner – „Secrets“ sind alles, was nicht in fremde Hände gehört: Passwörter, Tokens, API-Schlüssel.

Am 1. November hat es zum ersten Mal ernsthaft angeschlagen. Und die Geschichte dahinter ist lehrreicher, als ich dachte – über meinen Scanner, über Logdateien und über KI-Assistenten im Homelab.

Voraussetzungen

  • Home Assistant OS 18.4 mit Home Assistant Core 2026.10.4
  • Gitea im Homelab, privates Repository für die HA-Konfiguration
  • App „Home Assistant MCP Server“ (ha-mcp) 9.0.0 – damit kann ein KI-Assistent Home Assistant bedienen und konfigurieren
  • Claude als KI-Assistent
  • Beteiligt am eigentlichen Fund: die App Ring-MQTT with Video Streaming und Frigate 0.18.0 als Videoaufzeichnung

Kurz zu MCP: Das Model Context Protocol ist eine Schnittstelle, über die ein KI-Assistent Werkzeuge benutzen kann. Bei mir heißt das: Claude kann über den MCP-Server in Home Assistant Automationen anlegen, Entitäten umbenennen, Apps konfigurieren und vieles mehr. Das spart mir viel Klickerei – und ist der Grund, warum es diesen Artikel gibt.

Das Backup mit Sicherheitsnetz

Die Automation „Konfiguration nach Gitea sichern“ ruft um 03:00 Uhr einen shell_command auf und wertet dessen Rückgabewert aus. Der Eintrag in der configuration.yaml:

shell_command:
  git_autocommit: "sh /config/shell/git_autocommit.sh"

Und das Skript selbst:

#!/bin/sh
# Auto-Commit der HA-Konfiguration nach Gitea – mit Klartext-Secret-Prüfung
LOG=/config/.git_autocommit.log
PATTERN='(password|passwort|token|api_?key|secret|network_key|client_id)[^:]*:'
EXCL=':(exclude)shell/git_autocommit.sh'
{
  echo "=== $(date '+%Y-%m-%d %H:%M:%S')"
  cd /config || exit 1
  git add -A
  if git diff --cached --quiet; then
    echo "Keine Änderungen."
    exit 0
  fi

  # Sicherheitsnetz: neu hinzugefügte Zeilen mit Zugangsdaten im Klartext?
  LEAKS=$(git diff --cached -U0 -- . "$EXCL" \
    | grep -E '^\+' | grep -vE '^\+\+\+' \
    | grep -iE "$PATTERN" \
    | grep -vE '!secret|REDACTED')
  if [ -n "$LEAKS" ]; then
    echo "ABBRUCH: mögliche Zugangsdaten im Klartext gefunden in:"
    git diff --cached --name-only -i -G "$PATTERN" -- . "$EXCL"
    git reset -q
    exit 2
  fi

  git commit -q -m "Auto-Commit $(date '+%Y-%m-%d %H:%M')"
  git push -q && echo "Push OK."
} >> "$LOG" 2>&1

Die Idee in Kurzform:

  • git add -A nimmt alle Änderungen auf.
  • git diff --cached -U0 zeigt nur die geänderten Zeilen, ohne Kontext. Davon interessieren mich nur die neu hinzugefügten (sie beginnen mit +).
  • Darin sucht das Skript nach Schlüsselnamen wie password, token oder api_key, gefolgt von einem Doppelpunkt – egal ob in YAML (password: geheim) oder JSON ("api_key": "…").
  • Zeilen mit !secret sind ausgenommen, denn so verweist Home Assistant korrekt auf die secrets.yaml.
  • Bei einem Treffer: alles zurücksetzen, Exit-Code 2. Die Automation macht daraus eine Push-Nachricht aufs Handy und eine Meldung in Home Assistant.

Seit Anfang Oktober lief das unauffällig jede Nacht durch.

03:00 Uhr: „Commit blockiert“

Am 1. November um 03:00 Uhr kam diese Meldung aufs Handy:

Gitea-Backup fehlgeschlagen – Commit blockiert: mögliche Zugangsdaten im Klartext gefunden.

Laut Log betraf es eine einzige Datei: .ha_mcp/logs/mcp_usage.jsonl.

Dahinter steckt das Nutzungsprotokoll des MCP-Servers. Bei meiner Installation schreibt er jeden Aufruf eines Werkzeugs als JSON-Zeile in diese Datei – mit Zeitstempel, Werkzeugname und allen Parametern. Die Datei liegt unter /config, also genau in dem Verzeichnis, das mein Backup nach Gitea schiebt. Sie war schon in 16 Commits gelandet und mit nach Gitea gepusht worden.

Die Nachschau: nur die Namen, nicht die Werte

Die naheliegende Reaktion wäre, die Datei zu öffnen und durchzulesen. Ich bin einen anderen Weg gegangen: Wenn darin Zugangsdaten stehen, sollen die nicht auch noch in einem Terminal-Verlauf oder einem Chat landen.

Deshalb habe ich nur die Namen der Parameter auswerten lassen, also welches Werkzeug mit welchem Feld aufgerufen wurde, und dann geschaut, welche Namen nach Zugangsdaten klingen. Ergebnis:

  • passive, rund 80-mal: ein ganz normales Feld bei Zonen. Enthält zufällig „pass“.
  • confirm_token: eine Bestätigungsmarke, die der MCP-Server bei bestimmten Änderungen verlangt. Kein Zugangsdatum.
  • Ein paar Treffer in Beschreibungen und Suchbegriffen, in denen eines der Stichworte als normales Wort vorkam.
  • Und ein echter Fund: ha_manage_app mit dem Feld options.livestream_pass vom 14. Oktober.

Zwei Treffer ließen sich am Namen allein nicht beurteilen: ein Dienstaufruf mit freiem Inhalt (data.payload) und ein roher WebSocket-Befehl (ws_command). Die habe ich mir selbst im Terminal angesehen, ohne sie irgendwo hineinzukopieren. Beide waren harmlos.

Wie das Passwort ins Protokoll kam

Am 14. Oktober habe ich meine Ring-Klingel zusätzlich in Frigate eingebunden, damit bei Bewegung oder Klingeln eine Aufnahme startet. Dafür bietet die App Ring-MQTT ein RTSP-Gateway an: Es stellt den Kamerastream im Heimnetz bereit, geschützt mit Benutzername und Passwort. RTSP ist das Protokoll, mit dem Kameras ihr Bild übertragen.

Eingerichtet hat das Claude über MCP. Das Werkzeug ha_manage_app hat die Optionen der App gesetzt: den Benutzer frigate – und das Passwort im Feld livestream_pass. Funktioniert hat das tadellos. Nur hat der MCP-Server den Aufruf eben vollständig protokolliert, inklusive Passwort im Klartext. Und von dort ist es mit dem nächsten nächtlichen Backup nach Gitea gewandert.

Der MCP-Server hat dabei nichts falsch gemacht. Er protokolliert, was man ihm gibt. Der Fehler lag darin, ein Passwort überhaupt durch den KI-Assistenten zu schleusen.

Der eigentliche Knackpunkt: Mein Scanner hat es nie gesehen

Jetzt wird es interessant. Das Passwort stand seit dem 14. Oktober in der Datei, und die Datei ging regelmäßig durch meinen Scanner. Angeschlagen hat er aber erst am 1. November. Warum?

Ein Blick auf das Suchmuster beantwortet das:

PATTERN='(password|passwort|token|api_?key|secret|network_key|client_id)[^:]*:'

Das Feld hieß livestream_pass. Darin steckt weder password noch token noch secret – nur pass. Die Zeile ist also seit Mitte Oktober jede Nacht anstandslos durchgerutscht.

Angeschlagen hat der Scanner am 1. November wegen einer anderen Zeile, die in dieser Nacht neu dazukam. Welche genau, kann ich nicht mehr sagen – mein Log speichert nur die Dateinamen. Wahrscheinlich war es einer der Fehlalarme wie confirm_token, der auf das Muster token passt.

Mit anderen Worten: Mein Secret-Scanner hat angeschlagen – aber aus dem falschen Grund. Das echte Passwort habe ich nur gefunden, weil ein Fehlalarm mich dazu gebracht hat, genauer hinzuschauen.

Aufräumen

Passwort ändern statt Historie umschreiben

Das Passwort steckt jetzt in der Git-Historie. Dafür gibt es zwei Wege:

  1. Die Historie umschreiben, zum Beispiel mit git filter-repo, und per Force-Push nach Gitea schieben. Das ist aufwendig, fehleranfällig und hilft nur dann wirklich, wenn auch alle anderen Kopien des Repos bereinigt werden.
  2. Das Passwort ändern. Dann steht in der Historie nur noch ein wertloser alter Wert.

Ich habe mich für Weg 2 entschieden. Das Repository ist privat und liegt auf meinem eigenen Gitea. Das läuft nur im Homelab und ist aus dem Internet nicht erreichbar. Und ein geändertes Passwort ist die einzige Maßnahme, die sicher wirkt, egal wo das alte noch herumliegt. Neu gesetzt habe ich es in Ring-MQTT und in der Konfiguration von Frigate, danach Frigate neu gestartet.

Das Protokoll aus dem Backup nehmen

Das MCP-Protokoll hat im Backup nichts verloren. Es ist Laufzeitdatum, keine Konfiguration. Also kommt der Ordner in die .gitignore:

# HA-MCP-Server: Laufzeitdaten und Nutzungsprotokoll (Inhalte der Tool-Aufrufe)
.ha_mcp/

Weil Git die Dateien schon kennt, reicht das allein nicht. Sie müssen zusätzlich aus dem Index entfernt werden – die Dateien selbst bleiben dabei in Home Assistant liegen, Git verfolgt sie nur nicht mehr:

cd /config
git rm -r --cached .ha_mcp

Den Commit dazu macht die Automation beim nächsten Lauf selbst. Ich habe sie von Hand angestoßen.

Prüfen, ob es wirklich durchgelaufen ist

Hier bin ich noch einmal kurz gestolpert. Bei Erfolg meldet sich die Automation nämlich gar nicht – keine Push-Nachricht, nichts. Im Trace der Automation stand Rückgabewert 0 und dass der Fehlerzweig nicht ausgeführt wurde. Das heißt aber nur: Das Skript ist fehlerfrei durchgelaufen. Ob es dabei tatsächlich etwas committet hat, steht da nicht.

Der echte Beleg ist der Commit in Gitea:

Der Commit in Gitea: .gitignore ergänzt, die Dateien unter .ha_mcp entfernt

Drei Dateien: die .gitignore mit dem neuen Eintrag und die beiden Dateien unter .ha_mcp/, die nicht mehr verfolgt werden. Den Diff der Protokolldatei habe ich übrigens nicht aufgeklappt – der hätte den alten Inhalt samt Passwort angezeigt.

Die neue Regel

Die wichtigste Änderung ist keine Konfiguration, sondern eine Regel für mich:

Zugangsdaten gebe ich nicht mehr an den KI-Assistenten weiter. Wenn eine App oder Integration ein Passwort braucht, richtet der Assistent alles andere ein – und das Passwort trage ich selbst in der Oberfläche von Home Assistant ein.

Stolperfallen & Learnings

Eine Liste von Schlüsselwörtern findet nur, woran man vorher gedacht hat. password ja, pass nein. pwd, psk, Zugangsdaten in URLs nach dem Muster benutzer:passwort@host oder ein Token ohne verräterischen Feldnamen rutschen genauso durch. Ein selbstgebauter Scanner ist besser als keiner – aber man sollte ihm nicht mehr zutrauen, als er kann.

Logs speichern oft mehr, als man denkt. Ein Nutzungsprotokoll mit „allen Parametern“ ist für die Fehlersuche Gold wert. Es ist aber auch eine Sammelstelle für alles, was man je an das Werkzeug übergeben hat. Und es lag genau dort, wo mein Backup alles einsammelt.

KI-Assistenten sind Werkzeuge mit Gedächtnis. Was ich einem Assistenten gebe, steht danach mindestens im Chat und je nach Werkzeug noch an weiteren Stellen. Bei Passwörtern sollte die Antwort deshalb immer lauten: Das mache ich selbst.

Ein Fehlalarm ist kein Grund, das Sicherheitsnetz abzuschalten. Ich hätte das Muster auch schnell lockern oder die Datei einfach durchwinken können. Dann wäre das echte Passwort weiter unentdeckt geblieben.

„Exit 0“ ist kein Beweis. Ein grüner Trace heißt nur, dass nichts schiefging. Ob das passiert ist, was passieren sollte, sieht man erst am Ergebnis – hier am Commit in Gitea.

Fazit

Mein Sicherheitsnetz hat funktioniert – nur nicht so, wie ich dachte. Den echten Fund hat es zweieinhalb Wochen übersehen und dann über einen Fehlalarm doch noch ans Licht gebracht. Das Passwort ist geändert, das Protokoll fliegt aus dem Backup, und ich habe eine neue Regel im Umgang mit meinem KI-Assistenten. Ein bisschen Glück war dabei. Beim nächsten Mal möchte ich mich darauf nicht verlassen müssen.

Was als Nächstes kommt

  • Das Suchmuster verbessern: Kürzel wie pass, pwd und psk aufnehmen und Zugangsdaten in URLs erkennen. Oder gleich ein richtiges Werkzeug wie gitleaks einsetzen, das mit vielen Regeln und der Zufälligkeit von Zeichenketten arbeitet. Ob und wie das unter Home Assistant OS sinnvoll läuft, muss ich mir noch ansehen.
  • Die Automation gesprächiger machen: Bei Erfolg soll sie die alte Fehlermeldung von selbst wegräumen. Bisher bleibt die stehen, bis ich sie wegklicke.
  • Eine kleine Lücke im Skript schließen: Scheitert einmal der Push, bleibt der Commit lokal liegen. Gibt es am nächsten Tag keine neuen Änderungen, wird er nicht nachgeschoben.