Brecha de Vercel: Roté 6 Secretos. Un Comando de Auditoría Filtró 18 Más
Veo que VERCEL hackeado circulando por el feed. Luego esta mañana, un email de VERCEL en mi bandeja. Vale, no había planeado ningún mantenimiento hoy. Aparentemente hay una emergencia.
Abro el dashboard de Vercel de un proyecto en producción. Seis badges naranjas en mis variables de entorno. Need To Rotate. Claves JWT, secretos OAuth, tokens MCP. Arghh.
TLDR: una brecha expone un secreto. Tu respuesta a la brecha expone el resto. Los comandos de auditoría por defecto que ejecutas en pánico post-incidente muestran valores en texto plano por diseño, y tus secretos viven en más lugares de los que piensas. ¿Cuántos exactamente? Esa es la verdadera pregunta.

Antes de los badges, había un cheat de Roblox.
Hace veintidós meses, un empleado de Context.ai, una herramienta de IA a la que nadie fuera de su equipo prestaba atención, descargó un binario de un enlace sospechoso en su laptop de trabajo. Buscando una forma de romper un nivel de Roblox. El binario era Lumma Stealer. Se llevó sus sesiones del navegador, sus tokens OAuth guardados, su acceso a Google Workspace. Luego se quedó en silencio.
Veintidós meses de nada. Sin alertas. Sin anomalías. Sin razón para mirar.
Entonces a principios de este mes, el atacante entra a través de ese acceso OAuth a las cuentas de Google Workspace de los clientes de Context.ai. Incluyendo a un empleado de Vercel. Incluyendo, a través del acceso de ese empleado, los sistemas internos de Vercel. Encuentran las variables de entorno que los clientes habían marcado como no sensibles, lo que significaba que Vercel las almacenaba sin cifrar en reposo, que era la configuración por defecto. Copian. Publican el dump en BreachForums. Precio pedido: dos millones de dólares.
El domingo por la mañana mi teléfono vibra con el email del boletín.
Ahora los badges naranjas.
Domingo Por La Mañana, 6 Badges Naranjas

Los badges no son una suposición. Están basados en los logs de acceso de Vercel durante la ventana del incidente. Cada uno significa que una variable específica fue leída por la app OAuth comprometida durante la brecha. Lo que significa que del otro lado, en ese dump de BreachForums, esos valores están en texto plano.
Seis badges en un proyecto: JWT_PRIVATE_KEY_JWK, OAUTH_SECRET, OAUTH_CLIENT_ID, DASHBOARD_PASSWORD, MCP_AUTH_TOKEN, CAROUSEL_RENDER_SECRET. Tengo nueve proyectos. El conteo realista está entre 25 y 40 variables una vez que factorizas las apps downstream que cachean esos tokens.
Y mis secretos no viven solo en Vercel.
Treinta minutos después termino el primer proyecto. Metódico, satisfecho conmigo mismo. En lugar de parar, decido auditar todo lo demás.
Ahí es donde se pone interesante.
El Comando Que Filtró Más Que La Brecha
Para mapear los secretos en mi instancia self-hosted de Convex, escribo el comando más natural del mundo.
bunx convex env list
Espero nombres. Como aws iam list-users, gh secret list, vercel env ls. Todos esos devuelven nombres. Solo nombres. Los valores se mantienen ocultos detrás de un segundo comando explícito.
Mi terminal se llena. GitHub PAT con scope de escritura en prod. JWT de admin de Beehiiv. Clave de Fal.ai. Clave de OpenRouter. Clave de YouTube Data API. Clave de RapidAPI. Doce más abajo. Todos los valores. Todos en texto plano. Sin flag que pasar. Sin banner de advertencia. Solo el dump.
Cuatro segundos mirando fijamente. Luego cierro los ojos.
Triple persistencia. Historial de Bash. Scrollback del terminal. La transcripción de Claude Code que tenía abierta mientras auditaba, porque por supuesto la tenía abierta, estaba trabajando.
Dieciocho secretos, no relacionados con Vercel, ahora en tres lugares que no controlo completamente. Mi máquina no está comprometida. La probabilidad de uso malicioso es baja. Pero baja probabilidad, alto radio de explosión es la matemática que nos metió en este lío.
Cuatro horas.
Tus Secretos Viven En Más Lugares De Los Que Piensas

Cuéntalos. Realmente cuéntalos.
Un solo secreto en un stack moderno se extiende a través de cuatro almacenes, no uno. Solo te das cuenta el día que tienes que rotarlo, y para entonces es tarde.
El runtime de hosting viene primero. Vercel, Netlify, Railway, Render, Fly.io. Ese es el almacén que golpeó la brecha.
Si has construido algo no trivial, también tienes un backend self-hosted. Convex self-hosted, un VPS personalizado, una máquina Fly ejecutando la capa de aplicación. Segunda copia, desplegada la semana pasada, probablemente ligeramente desincronizada con la primera.
Luego la bóveda externa. Infisical, 1Password, Doppler, HashiCorp Vault. Si eres lo suficientemente disciplinado para usar una. Supuestamente la fuente de verdad. Nunca realmente lo es.
Y .env.local en la máquina de desarrollo. El equivalente digital de un Post-It bajo tu teclado. Siempre ahí. Incluso cuando juraste que lo limpiaste el viernes pasado.
Te dices que la bóveda es la fuente de verdad. Realísticamente, Vercel tiene su propia copia sincronizada el mes pasado, tu instancia de Convex tiene otra copia de la semana pasada, y tu .env.local es una instantánea de hace tres meses con dos valores rotados aún en texto plano al final.
Una brecha en un proveedor expone un almacén. Justo. Ese es el radio de explosión por el que te apuntaste.
Los comandos de auditoría que ejecutas en los próximos treinta minutos, en pánico y con cafeína, exponen los otros tres. Ese es el radio de explosión del que nadie te advirtió.
Regla: trata la auditoría con el mismo cuidado que la rotación. Cada comando de listado es un candidato a release. Cada comando de listado tiene un modo de falla. Cada comando de listado merece la verificación de flag que te saltas.
La auditoría es el incidente.
Comandos Que Vuelcan Todo (Y Qué Ejecutar En Su Lugar)
bunx convex env list no es un caso aislado. Es una familia.
Cualquier CLI que incluya un comando env list o secrets list debe asumirse culpable hasta que la página del manual demuestre lo contrario. Lee el comportamiento de los flags antes de escribir. Una vez que los valores lleguen a tu pantalla, están en tu historial de bash, tu scrollback del terminal, y cualquier herramienta de IA que esté leyendo tu shell ahora mismo.
Los culpables que conozco. gh secret list --show-value imprime cada secreto del repo de GitHub en texto plano. aws ssm get-parameter --with-decryption hace el equivalente de AWS. vercel env pull vuelca cada variable de Vercel en un archivo .env local que se queda en disco hasta que recuerdas borrarlo. Y por supuesto bunx convex env list y npx convex env list, ambos equivalentes.
Qué ejecutar en su lugar.
Para Convex self-hosted, ya que env list es una trampa:
bunx convex env get STRIPE_SECRET_KEY 2>/dev/null | wc -c
Devuelve la longitud en bytes, no el valor. Existe si > 0, falta si 0.
Para Infisical, no uses la UI más allá de veinte secretos, usa la API:
curl -s "https://app.infisical.com/api/v3/secrets/raw/STRIPE_SECRET_KEY" \
-H "Authorization: Bearer $INFISICAL_TOKEN" \
| jq '.secret.updatedAt'
Devuelve un timestamp. Nunca imprime el valor.
Para Vercel, la UI tiene el nuevo dashboard de resumen. Para CLI, vercel env ls imprime nombres, timestamps, entornos. Sin valores. Mantente alejado de vercel env pull hasta que la fase de auditoría termine.
Para .env.local, grep el nombre, no hagas cat del archivo:
grep -l "STRIPE_SECRET_KEY" .env.local
Para GitHub, gh secret list solo (sin --show-value) devuelve nombres. Suficiente para una auditoría.
Construye una pequeña matriz de texto mientras avanzas. Para cada secreto, anota qué almacenes lo contienen. Algo como STRIPE_SECRET_KEY = Vercel + Convex + Infisical + .env.local. Ese es tu plan de rotación. Ahora sabes exactamente cuántos lugares necesitan ser re-sincronizados, en qué orden, y qué apps downstream necesitan tokens frescos.
La página del manual es más barata que la rotación.
El Manual de Procedimientos
El incidente produjo tres reglas, no tres conjuntos de reglas. Son agnósticas del stack. Funcionan ya sea que estés en Vercel, Netlify, Railway, o una máquina Fly que nadie más en la empresa recuerda que existe.
Mapear. Reducir. Rotar.
Mapear
La primera regla es cartografía. Cada secreto en cada proyecto tiene una fuente de verdad y N cachés, y el mapa de dónde viven esos cachés pertenece al repo, no a tu cabeza.
Una sola fuente de verdad por secreto. Infisical, 1Password, Doppler, lo que hayas elegido. Cada otro almacén (Vercel, Convex, .env.local) es un caché de esa fuente. Sincroniza unidireccionalmente donde tu plan lo soporte, documenta la ruta de sincronización donde no.
Un CLAUDE.md o SECURITY.md en cada repo, con cuatro cosas: los secretos que usa el repo, dónde vive cada uno a través de los cuatro almacenes, los dashboards de proveedores para rotarlos, y los comandos que nadie tiene permitido ejecutar contra este repo. Por escrito. Para que la versión de ti a las 11 pm en pánico por una nueva brecha no reinvente el mapa en la oscuridad.
El flag Sensitive al momento de creación, nunca después. Vercel lanzó esto en una respuesta de pánico y vi a la mitad del internet malentenderlo. Activar Sensitive en una variable que ya está comprometida no rebobina nada. El valor está afuera. Solo funciona prospectivamente, lo que significa que la disciplina es en la creación, no después del hecho.
Cero .env commiteados. .gitignore sistemático. .env.example solo con placeholders vacíos. Regla vieja, aún violada semanalmente en algún lugar de tu org, cuenta con ello.
Reducir
La segunda regla es reducción de alcance. La clave que puede hacer menos es la clave que menos vale la pena robar, y la clave que puede hacer más es la clave que el atacante realmente quiere.
Claves con scope donde el proveedor las soporte. Claves restringidas de Stripe con scope por característica, no la clave secreta genérica que puede reembolsar, eliminar clientes y emitir pagos. Personal Access Tokens de grano fino de GitHub, uno por caso de uso, un repo, un scope. Clerk con claves distintas por entorno, para que una clave de dev filtrada se quede en dev. Supabase con políticas de row-level-security y la clave anon para todo del lado del cliente, service_role restringida a un trabajo estrecho del lado del servidor. Convex con una clave de deploy de solo lectura para builds, clave admin solo para scripts admin explícitos.
Límites duros de gasto en cada proveedor medido por consumo. OpenAI, Anthropic, OpenRouter, Fal, Replicate. Una clave comprometida generando a $500 por hora es la diferencia entre una mala tarde y un mal trimestre. O entre un mal trimestre y un post de Hacker News con tu nombre en el título.
MFA de hardware en las cuentas pivote. Google Workspace, registrador de dominio, el runtime de hosting, el host de código. YubiKey o passkey. No SMS. Estas cuatro cuentas son las que desbloquearían todo lo demás si se comprometieran, y son exactamente las que las herramientas estilo Context.ai piden permisos OAuth amplios.
Rotar
La tercera regla es disciplina de rotación. Si solo rotas después de brechas, solo rotas durante el pánico. Y el pánico es cuando los comandos de auditoría filtran cosas.
Una cadencia. Cada 90 días para claves críticas: auth, pagos, LLM de alto volumen. Anual para el resto. Proactivo vence a reactivo siempre.
Un procedimiento. Genera el nuevo valor en el proveedor. Mantén el viejo activo si dual-key es soportado, de lo contrario mantén la ventana de intercambio corta. Actualiza los cuatro almacenes en orden, desde la fuente de verdad hacia afuera. Smoke test end-to-end, no solo el build. Revoca el valor viejo solo después de validación. Marca Sensitive al momento de rotación, misma pantalla, mismo flujo de trabajo, nunca en un pase separado.
Una regla de higiene. Una sesión de shell por proyecto durante la rotación. Terminal fresco, conversación fresca de Claude Code, sin contaminación entre proyectos. Si el historial de una sesión se compromete después, el radio de explosión se detiene en un proyecto.
Una limpieza trimestral de OAuth. GitHub, Workspace, el runtime de hosting, Linear, Notion. Cualquier cosa no usada en 90 días se revoca. Sin sentimentalismo. He reconstruido mi stack desde cero antes, cuando una plataforma me quitó la alfombra. Toma un fin de semana, no una carrera. El costo de mantener un permiso OAuth obsoleto siempre es mayor que el costo de reconstruir la integración el día que realmente la necesites de nuevo.
Mapear, reducir, rotar. Todo lo demás es optimismo.
Entonces Llegó El Segundo Email
Lunes por la mañana. Veinticuatro horas después del mail de Vercel. Notificación de Gmail. La aplicación Claude en tu cuenta de GitHub está solicitando permisos adicionales.
Parpadeo. Misma familia de vector. Una herramienta adyacente a IA ya instalada en mi cuenta, pidiendo scopes expandidos, con un botón Allow All y una captura de pantalla tranquilizadora del producto debajo. Hago clic en Review, no en Allow. Los nuevos scopes incluyen acceso de escritura a todos mis repositorios, privados incluidos. Cierro la pestaña y lo dejo para después.
Dos emails, 24 horas de diferencia, ambos de la misma superficie de amenaza.
El CEO de Vercel Guillermo Rauch declaró públicamente que el ataque fue significativamente acelerado por IA. Lo cual cuadra. Cuando tu amenaza upstream es una herramienta de IA que tiene permisos en Workspace, Supabase, Datadog y Authkit al mismo tiempo, una credencial comprometida se convierte en movimiento lateral a una docena de apps downstream.
Según el Reporte de Seguridad SaaS 2026 de AppOmni, citado por el artículo de Medium del 21 de abril de Before The Curve, 76% de los empleados usan apps SaaS no aprobadas, promediando 25 apps por persona, y 31% de las brechas SaaS ahora explotan conexiones OAuth o API. Esa es la principal superficie de ataque de 2026.
Un clic Allow All es la superficie de ataque más grande en software empresarial hoy, y las herramientas de IA son los generadores más grandes de esos prompts de un clic. Cada agente de IA que se integra con tu stack pide scopes amplios porque pedir estrechos significa más fricción de configuración, menos pegajosidad del producto, peores métricas de onboarding. Así que piden todo. Haces clic en sí. Un año y medio después la herramienta se compromete y se lleva tu Workspace con ella.
En algún lugar por ahí, Karen de Contabilidad vio el mismo email que vi yo y hizo clic en Allow All sin hacer scroll. Ella no es el problema. El botón lo es. Ninguna cantidad de entrenamiento de seguridad arreglará una UX que hace que revisar tome cuatro clics y permitir tome uno.
Este no es un argumento de prohibir herramientas de IA. Uso Claude Code ocho horas al día. Es un argumento de la higiene OAuth ahora es requisito básico. Una táctica específica que ayuda: preferir herramientas CLI de scope estrecho sobre servidores MCP de OAuth amplio cuando tengas la opción. Un CLI con un token de corta duración en tu máquina tiene un radio de explosión mucho menor que un servidor MCP con un permiso OAuth persistente a nivel de proveedor. No siempre es posible. Cuando lo es, tómalo.
La factura se cobra dieciocho meses después.
Lo Que La Brecha Realmente Me Enseñó
La brecha me costó treinta minutos en esos seis badges. La auditoría me costó cuatro horas y dieciocho rotaciones más. El artículo que acabas de leer costó otras dos.
La lección duradera no es el procedimiento. Es el patrón.
Hace veintidós meses alguien hizo clic en un enlace de cheat de Roblox. Ese clic, enrutado a través de un permiso OAuth con scopes expansivos, eventualmente se convirtió en mi email de boletín del domingo por la mañana y un fin de semana de rotaciones. No porque el ataque fuera sofisticado (no lo era). Porque el stack entre esa laptop y mis variables de entorno era una larga cadena de clics Allow All, cada uno una trampa retardada, cada uno esperando a un atacante paciente sin nada mejor que hacer por veintidós meses.
Cada comando de listado por defecto en tu stack es un amplificador para esas trampas. Cada clave sin scope, cada secreto compartido, cada .env commiteado, cada MFA basado en SMS es otro día que el próximo atacante pasa adentro antes de que alguien se dé cuenta.
Una brecha expone un secreto. Tu respuesta expone el resto. A menos que hayas mapeado, reducido scope y rotado antes de que se convirtiera en la única opción.
El próximo Vercel viene en camino. Si serás golpeado no es la pregunta.
Es cuántos secretos más filtrará tu auditoría encima 😬
Fuentes
Boletín del incidente de seguridad de Vercel de abril 2026: https://vercel.com/kb/bulletin/vercel-april-2026-security-incident
BleepingComputer sobre el listado de BreachForums y la cita de Rauch: https://www.bleepingcomputer.com/news/security/vercel-confirms-breach-as-hackers-claim-to-be-selling-stolen-data/
Investigación de Trend Micro sobre el ángulo de cadena de suministro OAuth y tiempo de permanencia de 22 meses: https://www.trendmicro.com/en_us/research/26/d/vercel-breach-oauth-supply-chain.html
CyberScoop sobre el vector de entrada de Lumma Stealer: https://www.cyberscoop.com/vercel-security-breach-third-party-attack-context-ai-lumma-stealer/
Estadísticas del Reporte de Seguridad SaaS 2026 de AppOmni, citado por el artículo de Medium del 21 de abril de Before The Curve: https://medium.com/@beforethecurve/how-a-roblox-cheat-script-led-to-a-2m-ransom-against-vercel-079707c21f0b
Borrador con Claude Code abierto en la pestaña de al lado (sí, la misma pestaña que acaba de filtrar 18 secretos a su transcripción, que es exactamente de lo que trata este artículo). Reescrito, verificado y editado por mí.