Artikel

Ich habe meinen Bot-Token auf GitHub geschoben

Discord hat den Token schneller gefunden als ich meinen Fehler. Was danach passierte, wie ich es gelöst habe und warum „einfach den Commit löschen“ nicht funktioniert.

BlackZackBlackzack

1097 Wörter · 5 Min. Lesezeit

  • sicherheit
  • discord
  • fehler

Ich hatte gerade angefangen, Git zu benutzen. Nicht, weil ich verstanden hätte, wofür es gut ist, sondern weil in jedem Tutorial stand, dass man das so macht. Also: git init, git add ., git commit -m "erster commit", Repository auf GitHub angelegt, hochgeschoben. Öffentlich, weil privat damals nach etwas klang, das man bezahlt.

Zwei Stunden später kam eine E-Mail von Discord. Sinngemäß: Wir haben festgestellt, dass der Token deiner Anwendung öffentlich einsehbar ist. Wir haben ihn deshalb ungültig gemacht. Bitte erstelle einen neuen.

Mein erster Gedanke war nicht Scham, sondern Verwirrung: Woher wissen die das?

Wie Discord das merkt

Die Antwort ist unspektakulär und ziemlich klug. Discord-Tokens haben ein erkennbares Format, und GitHub durchsucht öffentliche Repositories nach Mustern, die nach Zugangsdaten aussehen. Findet der Scan etwas, geht eine Meldung an den Anbieter, dem das Muster gehört. Der Anbieter erklärt den Schlüssel für ungültig, bevor jemand anderes ihn benutzen kann.

Das nennt sich Secret Scanning, und es gilt für viele Dienste, nicht nur für Discord. Für mich war das damals die erste Begegnung mit einer Idee, die mich seitdem begleitet: Ein Geheimnis, das einmal öffentlich war, ist für immer verbrannt. Nicht „wahrscheinlich verbrannt", nicht „vermutlich hat es keiner gesehen". Verbrannt.

Warum „ich lösche den Commit einfach" nicht reicht

Mein zweiter Gedanke war der naheliegende: Ich lösche die Datei, mache einen neuen Commit, fertig. Das ist falsch, und es ist wichtig, zu verstehen warum.

Git löscht nichts. Ein Commit, der eine Datei entfernt, ist ein neuer Zustand — der alte bleibt in der Historie und lässt sich mit einem einzigen Befehl wieder ansehen:

git log --all --full-history -- config.json
git show <commit>:config.json

Solange der alte Commit im Repository liegt, liegt der Token darin. Und wenn jemand das Repository geklont hat, liegt er auch in dessen Kopie, unabhängig davon, was ich hinterher auf GitHub tue.

Es gibt Werkzeuge, die die Historie umschreiben — damals der BFG Repo-Cleaner, heute eher git filter-repo. Sie funktionieren, aber sie sind eine Reparatur, keine Lösung. Die Lösung heißt: neuen Token erzeugen, alten für ungültig erklären, und den neuen nie wieder an einen Ort legen, den Git anfasst.

Warnung

Die Reihenfolge ist entscheidend. Zuerst den Schlüssel beim Anbieter zurückziehen, danach die Historie aufräumen. Wer erst putzt, arbeitet minutenlang an der Kosmetik, während der Schlüssel weiter gültig ist. In meinem Fall hat Discord mir diese Entscheidung abgenommen — was ein Glücksfall war, kein Verdienst.

Was ich danach geändert habe

Die Umstellung war kleiner, als ich befürchtet hatte. Statt einer config.json, die im Repository liegt, gibt es zwei Dinge:

Eine .gitignore, die dafür sorgt, dass bestimmte Dateien nie hochgeladen werden:

node_modules/
.env
config.json
*.log

Und eine .env-Datei, aus der das Programm die Werte zur Laufzeit liest:

require("dotenv").config();
client.login(process.env.DISCORD_TOKEN);

Dazu eine .env.example, die mit ins Repository darf. Darin stehen die Namen aller Variablen, aber keine Werte:

DISCORD_TOKEN=
CLIENT_ID=

Das klingt trivial. Es ist aber die erste Trennung, die ich verstanden habe: Code ist öffentlich, Konfiguration ist es nicht. Der Code beschreibt, was passiert. Die Konfiguration beschreibt, wo und mit welchen Rechten. Beides gehört nicht in dieselbe Datei, und schon gar nicht in dieselbe Versionsverwaltung.

Der Teil, den ich damals noch nicht verstanden habe

Ich dachte, das Thema sei damit erledigt. War es nicht, und zwar aus einem Grund, den ich erst Jahre später begriffen habe.

Eine .env-Datei löst das Problem „Geheimnis im Code". Sie löst nicht das Problem „Geheimnis auf dem Server". Sie liegt im Klartext auf einer Festplatte, jeder mit Zugriff auf die Maschine kann sie lesen, und wenn ein Backup davon irgendwo landet, liegt sie auch dort. Bei einem Bot mit sechs Nutzern ist das egal. Bei einem System, das später Zugangsdaten für mehrere Dienste hält, ist es das nicht mehr.

Wie ich das gelöst habe — verschlüsselte Werte in einer Datenbank, die .env nur noch als Startkonfiguration für den allerersten Hochlauf — ist ein Thema für einen viel späteren Eintrag. Hier ist nur wichtig: Diese eine peinliche E-Mail von Discord war der Anfang einer Denkweise, nicht ihr Ende.

Was ich mir danach angewöhnt habe

Aus dieser Sache sind über die Jahre ein paar feste Gewohnheiten geworden, die alle in dieselbe Richtung zielen: Es soll gar nicht erst möglich sein, ein Geheimnis zu veröffentlichen.

Eine Prüfung vor dem Übertragen. Ein kleiner Haken, der vor jedem Hochladen die Änderungen nach typischen Mustern durchsucht — lange zufällige Zeichenketten, Schlüsselformate bekannter Dienste, Dateinamen wie .env. Das ist keine Sicherheitsmaßnahme im engeren Sinn, sondern eine Bremse gegen Unachtsamkeit, und genau dagegen braucht man sie.

Geheimnisse nie in Beispielen. In Dokumentationen und Beispielkonfigurationen steht bei mir nie ein echter Wert, auch kein abgelaufener. Der Grund ist banal: Ein Beispiel wird kopiert, und irgendwann kopiert es jemand mit dem echten Wert an eine Stelle, an die es nicht gehört.

Keine Geheimnisse in Bildschirmfotos. Klingt selbstverständlich und ist es nicht — Zugangsdaten stehen erstaunlich oft in Konsolenausgaben, die man beim Erklären eines Problems weitergibt.

Ein Ort, an dem steht, welche Geheimnisse es gibt. Nicht die Werte, sondern die Liste: Welcher Schlüssel gehört wozu, wann wurde er zuletzt gewechselt, wo liegt er. Ohne diese Liste kann man im Ernstfall nicht sagen, was betroffen ist — und diese Frage ist die erste, die man beantworten muss.

Die Sache mit dem „ist ja nur ein Testschlüssel"

Ein Nachtrag, weil mir dieser Satz seitdem oft begegnet ist, meistens von mir selbst.

Es gibt eine Verlockung, bei Schlüsseln für Testumgebungen weniger vorsichtig zu sein. Der Schaden wäre ja gering, es ist ja nur die Spielwiese.

Zwei Gründe, warum das nicht trägt. Erstens haben Testumgebungen die unangenehme Eigenschaft, echte Daten zu enthalten — spätestens, wenn jemand für einen Fehler eine Kopie der Produktionsdatenbank einspielt. Zweitens sind Testzugänge oft mit denselben Diensten verbunden wie die echten, nur mit anderen Kennungen. Wer den Testschlüssel hat, kennt die Struktur.

Ich behandle deshalb alle Zugangsdaten gleich, egal aus welcher Umgebung. Das kostet keinen Aufwand, wenn der Ablauf ohnehin steht, und es erspart die Diskussion darüber, welcher Schlüssel wichtig genug für Sorgfalt ist.

Was ich seitdem anders mache

  • Vor dem allerersten git add . einmal git status lesen. Zehn Sekunden, die genau diesen Fehler verhindern.
  • .gitignore als Erstes anlegen, nicht als Nachtrag. Eine Datei, die nie im Index war, muss man auch nicht aus der Historie schneiden.
  • Bei jedem verbrannten Schlüssel davon ausgehen, dass er benutzt wurde. Nicht, weil das wahrscheinlich ist, sondern weil die Annahme billiger ist als der Irrtum.

Und einen Satz habe ich mir gemerkt, den mir damals jemand in einem Forum geschrieben hat: Jeder, der lange genug programmiert, hat einmal ein Geheimnis veröffentlicht. Der Unterschied liegt darin, was danach passiert. Bei mir: eine automatische E-Mail, ein neuer Token, zwanzig Minuten Arbeit. Es hätte auch teurer ausgehen können.