Vercel-Datenpanne: 6 Geheimnisse rotiert. Ein Audit-Befehl verriet 18 weitere.
Ich sehe VERCEL gehackt in meinem Feed. Dann heute Morgen eine E-Mail von VERCEL in meinem Postfach. Okay, ich hatte heute keine Wartung geplant. Offenbar gibt es einen Notfall.
Ich öffne das Vercel-Dashboard eines Projekts, das in Produktion läuft. Sechs orangefarbene Badges bei meinen Umgebungsvariablen. Need To Rotate. JWT-Schlüssel, OAuth-Secrets, MCP-Token. Arghh.
TLDR: Ein Breach legt ein Secret offen. Deine Reaktion auf den Breach legt den Rest offen. Die Standard-Audit-Befehle, die du in der Post-Incident-Panik ausführst, zeigen Werte standardmäßig im Klartext an, und deine Secrets leben an mehr Orten, als du denkst. Wie viele genau? Das ist die eigentliche Frage.

Vor den Badges gab es einen Roblox-Cheat.
Vor zweiundzwanzig Monaten lud ein Mitarbeiter von Context.ai, einem KI-Tool, das außerhalb ihres Teams niemand beachtete, eine Binärdatei von einem dubiosen Link auf seinen Arbeitsrechner. Auf der Suche nach einem Weg, ein Roblox-Level zu knacken. Die Binärdatei war Lumma Stealer. Sie schnappte sich ihre Browser-Sessions, ihre gespeicherten OAuth-Token, ihren Google Workspace-Zugang. Dann wurde es still.
Zweiundzwanzig Monate nichts. Keine Alerts. Keine Anomalien. Kein Grund hinzuschauen.
Dann Anfang dieses Monats spaziert der Angreifer durch diesen OAuth-Grant in die Google Workspace-Konten von Context.ai-Kunden. Darunter ein Vercel-Mitarbeiter. Darunter, durch den Zugang dieses Mitarbeiters, Vercels interne Systeme. Sie finden die Umgebungsvariablen, die Kunden als nicht-sensibel markiert hatten, was bedeutete, dass Vercel sie unverschlüsselt gespeichert hatte, was der Standard war. Sie kopieren. Sie listen den Dump auf BreachForums. Angebotspreis: zwei Millionen Dollar.
Sonntagmorgen vibriert mein Handy mit der Bulletin-E-Mail.
Jetzt die orangefarbenen Badges.
Sonntagmorgen, 6 orangefarbene Badges

Die Badges sind keine Vermutung. Sie basieren auf Vercels Access Logs während des Incident-Zeitfensters. Jeder einzelne bedeutet, dass eine bestimmte Variable während des Breaches von der kompromittierten OAuth-App gelesen wurde. Was bedeutet, dass auf der anderen Seite, in diesem BreachForums-Dump, diese Werte im Klartext liegen.
Sechs Badges bei einem Projekt: JWT_PRIVATE_KEY_JWK, OAUTH_SECRET, OAUTH_CLIENT_ID, DASHBOARD_PASSWORD, MCP_AUTH_TOKEN, CAROUSEL_RENDER_SECRET. Ich habe neun Projekte. Realistische Anzahl liegt zwischen 25 und 40 Variablen, wenn man die nachgelagerten Apps einrechnet, die diese Token cachen.
Und meine Secrets leben nicht nur auf Vercel.
Dreißig Minuten später beende ich das erste Projekt. Methodisch, zufrieden mit mir. Anstatt aufzuhören, beschließe ich, alles andere zu auditieren.
Da wird es interessant.
Der Befehl, der mehr geleakt hat als der Breach
Um die Secrets auf meiner Convex Self-Hosted-Instanz zu mappen, tippe ich den natürlichsten Befehl der Welt.
bunx convex env list
Ich erwarte Namen. Wie aws iam list-users, gh secret list, vercel env ls. Die geben alle Namen zurück. Nur Namen. Werte bleiben hinter einem zweiten expliziten Befehl versteckt.
Mein Terminal füllt sich. GitHub PAT mit Prod-Write-Scope. Beehiiv Admin JWT. Fal.ai-Schlüssel. OpenRouter-Schlüssel. YouTube Data API-Schlüssel. RapidAPI-Schlüssel. Zwölf weitere darunter. Alle Werte. Alle im Klartext. Kein Flag zu übergeben. Keine Warnmeldung. Nur der Dump.
Vier Sekunden starren. Dann schließe ich meine Augen.
Dreifache Persistierung. Bash-History. Terminal-Scrollback. Das Claude Code-Transkript, das ich während des Audits offen hatte, weil ich natürlich eins offen hatte, ich arbeitete ja.
Achtzehn Secrets, nicht mit Vercel verwandt, liegen jetzt an drei Orten, die ich nicht vollständig kontrolliere. Meine Maschine ist nicht kompromittiert. Wahrscheinlichkeit für böswillige Nutzung ist gering. Aber geringe Wahrscheinlichkeit, hoher Blast Radius ist die Mathematik, die uns in dieses Schlamassel gebracht hat.
Vier Stunden.
Deine Secrets leben an mehr Orten, als du denkst

Zähl sie. Zähl sie wirklich.
Ein einzelnes Secret in einem modernen Stack verteilt sich über vier Speicher, nicht einen. Du merkst es erst an dem Tag, an dem du rotieren musst, und dann ist es zu spät.
Die Hosting-Runtime kommt zuerst. Vercel, Netlify, Railway, Render, Fly.io. Das ist der Speicher, den der Breach getroffen hat.
Wenn du etwas Nicht-Triviales gebaut hast, hast du auch ein Self-Hosted Backend. Convex Self-Hosted, einen Custom VPS, eine Fly-Maschine, die die App-Layer läuft. Zweite Kopie, letzte Woche deployed, wahrscheinlich leicht out of sync mit der ersten.
Dann der externe Vault. Infisical, 1Password, Doppler, HashiCorp Vault. Falls du diszipliniert genug bist, einen zu verwenden. Angeblich die Source of Truth. Ist sie nie wirklich.
Und .env.local auf der Dev-Maschine. Das digitale Äquivalent eines Post-Its unter deiner Tastatur. Immer da. Auch wenn du geschworen hattest, es letzten Freitag aufgeräumt zu haben.
Du sagst dir, der Vault ist die Source of Truth. Realistisch hat Vercel seine eigene Kopie, die letzten Monat synchronisiert wurde, deine Convex-Instanz hat eine andere Kopie von letzter Woche, und deine .env.local ist ein Snapshot von vor drei Monaten mit zwei rotierten Werten, die immer noch im Klartext unten stehen.
Ein Breach bei einem Provider legt einen Speicher offen. Fair genug. Das ist der Blast Radius, für den du dich angemeldet hast.
Die Audit-Befehle, die du in den nächsten dreißig Minuten ausführst, panisch und koffeiniert, legen die anderen drei offen. Das ist der Blast Radius, vor dem dich niemand gewarnt hat.
Regel: behandle das Audit mit der gleichen Sorgfalt wie die Rotation. Jeder Listing-Befehl ist ein Release Candidate. Jeder Listing-Befehl hat einen Failure Mode. Jeder Listing-Befehl verdient den Flag-Check, den du überspringst.
Das Audit ist der Incident.
Befehle, die alles dumpen (und was du stattdessen ausführen solltest)
bunx convex env list ist kein Einzelfall. Es ist eine Familie.
Jede CLI, die einen env list- oder secrets list-Befehl mitliefert, sollte als schuldig angenommen werden, bis die Man Page das Gegenteil beweist. Lies das Flag-Verhalten, bevor du tippst. Sobald die Werte auf deinem Bildschirm sind, sind sie in deiner Bash-History, deinem Terminal-Scrollback und welchem KI-Tool auch immer gerade deine Shell liest.
Die Schuldigen, die ich kenne. gh secret list --show-value druckt jedes GitHub-Repo-Secret im Klartext. aws ssm get-parameter --with-decryption macht das AWS-Äquivalent. vercel env pull dumpt jede Vercel-Variable in eine lokale .env-Datei, die auf der Festplatte liegt, bis du dich erinnerst, sie zu löschen. Und natürlich bunx convex env list und npx convex env list, beide äquivalent.
Was du stattdessen ausführen solltest.
Für Convex Self-Hosted, da env list eine Falle ist:
bunx convex env get STRIPE_SECRET_KEY 2>/dev/null | wc -c
Gibt die Byte-Länge zurück, nicht den Wert. Existiert wenn > 0, fehlt wenn 0.
Für Infisical, verwende nicht die UI bei mehr als zwanzig Secrets, verwende die API:
curl -s "https://app.infisical.com/api/v3/secrets/raw/STRIPE_SECRET_KEY" \
-H "Authorization: Bearer $INFISICAL_TOKEN" \
| jq '.secret.updatedAt'
Gibt einen Timestamp zurück. Druckt nie den Wert.
Für Vercel hat die UI das neue Overview-Dashboard. Für CLI druckt vercel env ls Namen, Timestamps, Environments. Keine Werte. Halte dich von vercel env pull fern, bis die Audit-Phase vorbei ist.
Für .env.local, grepe den Namen, catte nicht die Datei:
grep -l "STRIPE_SECRET_KEY" .env.local
Für GitHub gibt gh secret list allein (ohne --show-value) Namen zurück. Reicht für ein Audit.
Baue eine kleine Text-Matrix, während du gehst. Für jedes Secret schreibe auf, welche Speicher es halten. Etwas wie STRIPE_SECRET_KEY = Vercel + Convex + Infisical + .env.local. Das ist dein Rotationsplan. Du weißt jetzt genau, wie viele Orte neu synchronisiert werden müssen, in welcher Reihenfolge und welche nachgelagerten Apps frische Token brauchen.
Die Man Page ist billiger als die Rotation.
Das Playbook
Der Incident produzierte drei Regeln, nicht drei Regelsets. Sie sind stack-agnostisch. Sie funktionieren, egal ob du auf Vercel, Netlify, Railway oder einer Fly-Maschine bist, an die sich sonst niemand in der Firma erinnert.
Map. Reduce. Rotate.
Map
Die erste Regel ist Kartografie. Jedes Secret in jedem Projekt hat eine Source of Truth und N Caches, und die Karte, wo diese Caches leben, gehört ins Repo, nicht in deinen Kopf.
Eine einzige Source of Truth pro Secret. Infisical, 1Password, Doppler, was auch immer du gewählt hast. Jeder andere Speicher (Vercel, Convex, .env.local) ist ein Cache dieser Source. Synchronisiere einseitig, wo dein Plan es unterstützt, dokumentiere den Sync-Pfad, wo nicht.
Eine CLAUDE.md oder SECURITY.md in jedem Repo, mit vier Dingen: die Secrets, die das Repo verwendet, wo jedes einzelne über die vier Speicher lebt, die Provider-Dashboards, um sie zu rotieren, und die Befehle, die niemand gegen dieses Repo ausführen darf. Aufgeschrieben. Damit die Version von dir um 23 Uhr, die wegen eines neuen Breaches panikt, die Karte nicht im Dunkeln neu erfinden muss.
Das Sensitive-Flag zur Erstellungszeit, nie danach. Vercel hat das in einer Panik-Reaktion ausgeliefert und ich habe zugeschaut, wie die halbe Internet es missverstanden hat. Das Sensitive-Flag bei einer bereits kompromittierten Variable zu setzen spult nichts zurück. Der Wert ist draußen. Es funktioniert nur prospektiv, was bedeutet, dass die Disziplin bei der Erstellung liegt, nicht nach der Tatsache.
Null .env committed. .gitignore systematisch. .env.example nur mit leeren Platzhaltern. Alte Regel, wird immer noch wöchentlich irgendwo in deiner Org verletzt, darauf kannst du zählen.
Reduce
Die zweite Regel ist Scope-Reduktion. Der Schlüssel, der am wenigsten kann, ist der Schlüssel, der am wenigsten wert ist zu stehlen, und der Schlüssel, der am meisten kann, ist der Schlüssel, den der Angreifer tatsächlich will.
Scoped Keys überall, wo der Provider sie unterstützt. Stripe Restricted Keys pro Feature gescoped, nicht der generische Secret Key, der refunden, Kunden löschen und Auszahlungen veranlassen kann. GitHub Fine-Grained Personal Access Tokens, einer pro Use Case, ein Repo, ein Scope. Clerk mit unterschiedlichen Schlüsseln pro Environment, damit ein geleakter Dev-Key in Dev bleibt. Supabase mit Row-Level-Security-Policies und dem Anon-Key für alles Client-seitige, service_role beschränkt auf einen engen Server-seitigen Job. Convex mit einem Read-Only Deploy Key für Builds, Admin Key nur für explizite Admin-Scripts.
Spending Hard Caps bei jedem consumption-metered Provider. OpenAI, Anthropic, OpenRouter, Fal, Replicate. Ein kompromittierter Schlüssel, der mit 500 Dollar pro Stunde generiert, ist der Unterschied zwischen einem schlechten Nachmittag und einem schlechten Quartal. Oder zwischen einem schlechten Quartal und einem Hacker News-Post mit deinem Namen im Titel.
Hardware MFA bei den Pivot-Accounts. Google Workspace, Domain-Registrar, die Hosting-Runtime, der Code-Host. YubiKey oder Passkey. Nicht SMS. Diese vier Accounts sind die, die alles andere entsperren würden, wenn sie kompromittiert wären, und sie sind genau die, für die Context.ai-Style-Tools breite OAuth-Grants anfragen.
Rotate
Die dritte Regel ist Rotationsdisziplin. Wenn du nur nach Breaches rotierst, rotierst du nur während Panik. Und Panik ist, wenn die Audit-Befehle Dinge leaken.
Eine Kadenz. Alle 90 Tage für kritische Schlüssel: Auth, Payments, High-Volume LLM. Jährlich für den Rest. Proaktiv schlägt reaktiv jedes Mal.
Eine Prozedur. Generiere den neuen Wert beim Provider. Halte den alten aktiv, wenn Dual-Key unterstützt wird, sonst halte das Swap-Fenster kurz. Update die vier Speicher in Reihenfolge, von Source of Truth nach außen. Smoke Test End-to-End, nicht nur den Build. Widerrufe den alten Wert erst nach Validierung. Flagge Sensitive zur Rotationszeit, gleicher Screen, gleicher Workflow, nie in einem separaten Pass.
Eine Hygiene-Regel. Eine Shell-Session pro Projekt während der Rotation. Frisches Terminal, frische Claude Code-Conversation, keine projektübergreifende Verschmutzung. Wenn die History einer Session später kompromittiert wird, stoppt der Blast Radius bei einem Projekt.
Ein vierteljährlicher OAuth-Sweep. GitHub, Workspace, die Hosting-Runtime, Linear, Notion. Alles, was 90 Tage ungenutzt ist, wird widerrufen. Keine Sentimentalität. Ich habe meinen Stack schon mal von Grund auf neu aufgebaut, als eine Plattform mir den Teppich unter den Füßen weggezogen hat. Dauert ein Wochenende, keine Karriere. Die Kosten, einen veralteten OAuth-Grant herumliegen zu lassen, sind immer höher als die Kosten, die Integration an dem Tag neu aufzubauen, an dem du sie tatsächlich wieder brauchst.
Map, reduce, rotate. Alles andere ist Optimismus.
Dann kam die zweite E-Mail
Montagmorgen. Vierundzwanzig Stunden nach der Vercel-Mail. Gmail-Benachrichtigung. The Claude application on your GitHub account is requesting additional permissions.
Ich blinzle. Gleiche Vektorfamilie. Ein KI-nahes Tool, das bereits auf meinem Account installiert ist, fragt nach erweiterten Scopes, mit einem Allow All-Button und einem beruhigenden Produkt-Screenshot darunter. Ich klicke Review, nicht Allow. Die neuen Scopes beinhalten Schreibzugriff auf alle meine Repositories, private eingeschlossen. Ich schließe den Tab und lasse es für später.
Zwei E-Mails, 24 Stunden auseinander, beide von der gleichen Threat Surface.
Vercels CEO Guillermo Rauch ging öffentlich auf Rekord und sagte, der Angriff wurde erheblich durch KI beschleunigt. Was passt. Wenn deine Upstream-Bedrohung ein KI-Tool ist, das Grants in Workspace, Supabase, Datadog und Authkit gleichzeitig hält, wird eine geknackte Credential zu Lateral Movement in ein Dutzend nachgelagerter Apps.
Laut AppOmnis 2026 SaaS Security Report, zitiert von Before The Curves Medium-Artikel vom 21. April, verwenden 76% der Mitarbeiter nicht genehmigte SaaS-Apps, durchschnittlich 25 Apps pro Person, und 31% der SaaS-Breaches nutzen jetzt OAuth- oder API-Verbindungen aus. Das ist die Hauptangriffsfläche von 2026.
Ein-Klick Allow All ist die größte Angriffsfläche in Unternehmenssoftware heute, und KI-Tools sind die größten Generatoren dieser Ein-Klick-Prompts. Jeder KI-Agent, der sich in deinen Stack integriert, fragt nach breiten Scopes, weil das Fragen nach engen mehr Setup-Friction bedeutet, weniger Produkt-Stickiness, schlechtere Onboarding-Metriken. Also fragen sie nach allem. Du klickst ja. Anderthalb Jahre später wird das Tool geknackt und nimmt dein Workspace mit.
Irgendwo da draußen sah Karen aus der Buchhaltung die gleiche E-Mail, die ich sah, und klickte Allow All, ohne zu scrollen. Sie ist nicht das Problem. Der Button ist es. Keine Menge an Sicherheitstraining wird eine UX reparieren, die Review vier Klicks und Allow einen Klick kostet.
Das ist kein KI-Tools verbieten-Argument. Ich verwende Claude Code acht Stunden am Tag. Es ist ein OAuth-Hygiene ist jetzt Tischpflicht-Argument. Eine spezifische Taktik, die hilft: bevorzuge Narrow-Scope CLI-Tools gegenüber Broad-OAuth MCP-Servern, wann immer du die Wahl hast. Eine CLI mit einem kurzlebigen Token auf deiner Maschine hat einen viel kleineren Blast Radius als ein MCP-Server mit einem persistenten OAuth-Grant auf Provider-Level. Nicht immer möglich. Wenn es geht, nimm es.
Die Rechnung kommt achtzehn Monate später.
Was der Breach mir wirklich beigebracht hat
Der Breach kostete mich dreißig Minuten bei diesen sechs Badges. Das Audit kostete mich vier Stunden und achtzehn weitere Rotationen. Der Artikel, den du gerade gelesen hast, kostete weitere zwei.
Die dauerhafte Lektion ist nicht die Prozedur. Es ist das Muster.
Vor zweiundzwanzig Monaten klickte jemand auf einen Roblox-Cheat-Link. Dieser eine Klick, geroutet durch einen OAuth-Grant mit expansiven Scopes, wurde schließlich zu meiner Sonntagmorgen-Bulletin-E-Mail und einem Wochenende voller Rotationen. Nicht weil der Angriff sophistiziert war (war er nicht). Weil der Stack zwischen diesem Laptop und meinen Umgebungsvariablen eine lange Kette von Allow All-Klicks war, jeder eine verzögerte Falle, jeder wartend auf einen geduldigen Angreifer, der zweiundzwanzig Monate lang nichts Besseres zu tun hatte.
Jeder Standard-Listing-Befehl in deinem Stack ist ein Verstärker für diese Fallen. Jeder unscoped Key, jedes geteilte Secret, jede committete .env, jede SMS-basierte MFA ist ein weiterer Tag, den der nächste Angreifer drinnen verbringt, bevor jemand es bemerkt.
Ein Breach legt ein Secret offen. Deine Reaktion legt den Rest offen. Es sei denn, du hast gemappt, gescoped und rotiert, bevor es zur einzigen Option wurde.
Das nächste Vercel kommt. Ob du getroffen wirst, ist nicht die Frage.
Es ist, wie viele weitere Secrets dein Audit obendrauf leaken wird 😬
Quellen
Vercels April 2026 Sicherheits-Incident-Bulletin: https://vercel.com/kb/bulletin/vercel-april-2026-security-incident
BleepingComputer zum BreachForums-Listing und Rauch-Zitat: https://www.bleepingcomputer.com/news/security/vercel-confirms-breach-as-hackers-claim-to-be-selling-stolen-data/
Trend Micro-Forschung zum OAuth-Supply-Chain-Winkel und 22-Monats-Verweildauer: https://www.trendmicro.com/en_us/research/26/d/vercel-breach-oauth-supply-chain.html
CyberScoop zum Lumma Stealer-Eingangsvektor: https://www.cyberscoop.com/vercel-security-breach-third-party-attack-context-ai-lumma-stealer/
AppOmni 2026 SaaS Security Report-Statistiken, zitiert von Before The Curves Medium-Artikel vom 21. April: https://medium.com/@beforethecurve/how-a-roblox-cheat-script-led-to-a-2m-ransom-against-vercel-079707c21f0b
Entworfen mit Claude Code im nächsten Tab (ja, der gleiche Tab, der gerade 18 Secrets an sein Transkript geleakt hat, was genau das ist, worum es in diesem Artikel geht). Umgeschrieben, fact-gecheckt und von mir editiert.