El build publicó la clave
El informe de incidente ampliado de Beacon CRM atribuye el robo de toda su base de datos de clientes, que cubre más de 1.500 organizaciones benéficas del Reino Unido, a una clave de acceso de AWS que su propio proceso de build horneó dentro de un archivo JavaScript público. Nadie entró en la máquina de un desarrollador. Una herramienta de build copió una variable de entorno en un artefacto, que es lo que hacen las herramientas de build. La respuesta de Bromure Agentic Coding es que la variable no guarda nada que valga la pena copiar.
No hay atacante en la primera mitad de esta historia. Un proceso de build copió un secreto desde una variable de entorno a un archivo JavaScript, y un servidor web sirvió ese archivo a quien lo pidiera. El robo, cuando llegó, fue una petición GET.
Beacon es un CRM que usan las organizaciones benéficas británicas para llevar el registro de donantes, simpatizantes y voluntarios. El 12 de agosto su CTO, David Simpson, publicó un informe de incidente ampliado sobre una brecha que la empresa había revelado por primera vez el 4 de agosto. The Register lo recogió al día siguiente, SecurityWeek siguió el 14 de agosto, y la causa raíz cabe en una línea:
Una clave de acceso de AWS potencialmente expuesta en artefactos de build JavaScript públicos.
A las 01:20:16 UTC del 27 de julio, alguien empezó a usar esa clave y mantuvo el acceso durante una hora y veintisiete minutos. La evaluación de Beacon sobre lo que salió:
Se hizo una copia de la base de datos que contiene todos los datos de clientes de Beacon, incluidos los archivos adjuntos, y el actor de amenazas probablemente la descargó en un formato legible.
Eso cubre más de 1.500 organizaciones: nombres de simpatizantes, teléfonos, direcciones de correo, direcciones postales, historiales de donación y archivos adjuntos. Ningún dato de tarjeta ni bancario, ya que los clientes de Beacon no los guardan ahí. La ICO revisó al menos una organización afectada y determinó que no tiene responsabilidad alguna en la brecha, lo cual es correcto y también un consuelo frío para un equipo de captación de fondos que tiene que explicar a sus simpatizantes adónde fueron a parar sus domicilios.
Beacon cifraba los datos en reposo. Eso no cambió nada. Cybersecurity News señala que AWS descifra en nombre de quienquiera que tenga credenciales válidas. El cifrado en reposo te protege de que alguien se marche con un disco, y no tiene opinión alguna sobre un llamante con la clave.
La parte que conviene mirar fijamente
Lee el informe de incidente buscando la intrusión y no la encontrarás. No hubo
correo de phishing, ni mantenedor comprometido, ni dependencia envenenada ni
inyección de prompt, ni malware en el portátil de un ingeniero. Nada en esta
historia sorteó un control, porque nadie extrajo la credencial de ningún sitio.
El proceso de build de Beacon copió un valor desde una variable de entorno a un
bundle, Beacon desplegó el bundle como recurso estático, y un servidor web se lo
entregó a cada cliente que lo pidió, tal como está diseñado. El canal de
exfiltración fue una etiqueta <script>.
El mecanismo no es exótico. Los builds de front-end insertan variables de entorno a propósito, porque el navegador no tiene entorno alguno que leer en tiempo de ejecución:
- Vite sustituye cualquier variable con el prefijo
VITE_enimport.meta.enven tiempo de build. Next.js hace lo mismo conNEXT_PUBLIC_, y Create React App usabaREACT_APP_. El prefijo es la forma de pedir la sustitución. - El
DefinePluginde webpack y el--definede esbuild reemplazan un token de tu código por una cadena. No tienen noción de qué cadenas son secretas, ni manera de adquirirla. - Un source map es una segunda copia. Los frameworks con renderizado en servidor añaden una tercera vía: un valor leído en código que acaba en un componente de cliente se serializa en el payload.
Cada una de esas copias sale del entorno de quien, o de lo que, arrancó el build. El entorno es la materia prima, y el build es una máquina para copiarlo a un archivo que después publicas.
La estación de trabajo donde empieza esto
La brecha de Beacon es una historia de nube en sus consecuencias. Su causa se sienta en una estación de trabajo de desarrollador, y la estación de trabajo está cambiando.
Un agente que atiende un ticket hace todo esto en una tarde normal. Añade una
variable de entorno para que una funcionalidad pueda alcanzar un servicio. Edita
vite.config.ts o next.config.js. Escribe la línea que lee la variable, y
elige el módulo en el que vive esa línea, que es la decisión que zanja si el
valor se queda en el servidor o cruza al bundle. Ejecuta npm run build.
Comitea dist/ cuando un destino de despliegue quiere el artefacto versionado.
Nada de eso exige que el agente cometa un error, y no obtienes ninguna señal si lo comete. El build pasa, porque una cadena es una cadena. El bundle se publica como una única línea minificada, así que nada en el diff te llama la atención. El valor funciona, así que la funcionalidad funciona y el ticket se cierra.
Mientras tanto, el espacio de trabajo en el que corre un agente está surtido de credenciales, porque las credenciales son lo que lo hace útil: claves de nube para que pueda revisar un bucket, un token de GitHub para que pueda empujar una rama, credenciales de registro y de base de datos, una clave de API de modelo. Cada una de ellas es una cadena posada en un entorno que un proceso de build lee por completo.
El informe de Beacon le hace una pregunta directa a esa disposición. Algo en tu espacio de trabajo copia tu entorno a un archivo, tarde o temprano. Pregúntate qué se lleva.
En un espacio de trabajo Bromure, se lleva una cadena de región
Bromure Agentic Coding ejecuta el agente en una VM Ubuntu desechable sobre el framework Virtualization de Apple, con un proxy MITM del lado del host como única ruta hacia la red. El diseño de credenciales se desprende de eso: los secretos reales se quedan en tu Mac, y la VM recibe valores que parecen correctos y no valen nada.
AWS recibe un trato propio, porque SigV4 nunca envía el secreto. El SDK consume el secreto en el cliente para calcular un HMAC y pone la firma en el cable, así que Bromure no puede cambiar un falso por un valor real en tránsito como sí hace con un token bearer. En su lugar, mueve la firma.
Empieza por lo que encontraría un proceso de build. Bromure exporta
AWS_DEFAULT_REGION y AWS_REGION dentro de la VM, y ninguna clave, secreto ni
token de sesión. El razonamiento está en el código fuente, en SessionDisk.swift:
NO debemos exportar
AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY/AWS_SESSION_TOKENaquí: las variables de entorno le ganan acredential_processen la cadena del SDK, y además echarían abajo la garantía de ningún secreto en disco (las variables de entorno se filtran por/proc,ps -Ey el historial del shell).
Esa lista de canales de fuga describe la inspección de procesos. Un bundler que
lee process.env pertenece a ella, y la misma decisión lo cubre. No existe
ningún AWS_SECRET_ACCESS_KEY en el entorno para que un build lo inserte, así
que DefinePlugin no sustituye nada, import.meta.env no lleva nada, y
NEXT_PUBLIC_AWS_SECRET_ACCESS_KEY, el error de Beacon cometido a propósito,
publica una cadena vacía.
El SDK sigue funcionando. ~/.aws/config apunta a un ayudante
credential_process:
[default]
credential_process = /mnt/bromure-meta/bromure-aws-creds.py
region = eu-west-2
El ayudante lee un documento JSON de un socket. Devuelve el AccessKeyId real,
que te identifica en vez de autenticarte, emparejado con una SecretAccessKey
que Bromure genera para la sesión: cuarenta caracteres tomados del alfabeto que
usa un secreto real de AWS, de modo que boto3, la CLI aws y Terraform los
aceptan y firman como de costumbre. La firma que producen está condenada a
fallar.
Entonces el host la arregla. AWSResigner reconoce cualquier petición
*.amazonaws.com que pase por el proxy, retira la cabecera Authorization del
invitado y recalcula SigV4 con credenciales que solo existen en el espacio de
direcciones del proceso del host, añadiendo el X-Amz-Security-Token real cuando
el perfil lleva material STS. Tu terraform apply funciona, y el secreto nunca
entró en la máquina que lo ejecutó.
Rodea el proxy y AWS responde:
An error occurred (InvalidSignatureException) when calling the
ListBuckets operation: The request signature we calculated does not
match the signature you provided.
La credencial falla en cerrado. Una que solo funciona cuando se enruta por tu Mac no puede ejercerse desde el portátil de un desconocido, que es la propiedad de la que carecía la clave publicada de Beacon.
El resto del panel son señuelos
AWS es el caso especial. Para el resto, la frontera del cable hace el trabajo: cada credencial que configuras aparece dentro de la VM como un falso que preserva la estructura, derivado del valor real más una sal de 32 bytes por instalación a través de HKDF-SHA256. El valor real se queda cifrado en tu Mac, y el proxy lo sustituye en la petición después de que los bytes hayan salido de la VM, y solo cuando la petición se dirige al host para el que se acuñó esa credencial.
Eso resuelve toda la clase de “una herramienta copió el entorno a un sitio al que no debería haber ido” de la misma manera, fuera cual fuera la herramienta:
Claves de API de modelos y proveedores
ANTHROPIC_API_KEY es sk-ant-api03-brm-…. OPENAI_API_KEY es sk-brm-…,
XAI_API_KEY es xai-brm-…. Preservan la estructura, así que las CLI las
aceptan sin rechistar, e inertes en cualquier otro sitio.
Tokens de Git, registro y nube
GH_TOKEN es ghp_ más 36 caracteres, GitLab es glpat- más 20,
DigitalOcean es dop_v1_ más hexadecimal. ~/.git-credentials,
~/.docker/config.json, ~/.kube/config y ~/.config/doctl/config.yaml
están todos presentes, todos poblados, todos falsos.
Un prefijo NEXT_PUBLIC_
Ponle el prefijo a cualquiera de ellos y el bundler hará lo que le dijiste.
El artefacto publicado lleva relleno con forma de brm-docker-… o de
ghp_, y el error te cuesta un redespliegue en lugar de una notificación de
brecha.
Un dist/ o un source map comiteados
La misma respuesta, y no depende de que nadie se dé cuenta. El artefacto puede quedarse en un repositorio público, ser indexado y raspado por todos los rastreadores de secretos en activo, y la cadena que recojan no autentica contra nada.
Bromure deriva los falsos igual cada vez, así que una herramienta que toma la huella de su propia clave, como hace Claude Code cuando cachea un hash de clave, nunca ve cambiar la credencial entre sesiones. Tampoco hay un interruptor que olvidar. El proxy es la única ruta de salida de la VM, así que una petición que lo rodea lleva un marcador de posición y falla aguas arriba.
El señuelo publicado es un hilo de alarma
La clave de Beacon estuvo en un archivo público durante un periodo desconocido antes del 27 de julio, y la primera señal que obtuvo alguien fue un pico en los informes de AWS Cost and Usage del 27 y el 28 de julio, leídos a posteriori.
El detector de compromiso de Bromure vigila tus credenciales en lugar de la reputación del destino, que es lo que le permite dispararse ante un host que nadie ha nombrado jamás. Un autómata de Aho-Corasick construido a partir de los propios falsos acuñados por el espacio de trabajo barre cada petición saliente, cabeceras y cuerpo. Un falso con destino a un host fuera del alcance para el que fue acuñado cuenta como intento de exfiltración, y Bromure responde:
- El proxy rechaza la petición con un HTTP 451, y ni un solo byte llega al destino.
- Bromure pausa la VM en el acto.
- Bromure levanta una alerta nombrando la credencial, el host para el que se acuñó y el host hacia el que se la observó ir.
Bromure marca entonces el espacio de trabajo como comprometido, de modo que el siguiente lanzamiento exige borrar la imagen de disco de la VM y el home persistente. Tus tokens, claves SSH y ajustes sobreviven a eso.
Apunta el mecanismo al escenario de Beacon. Un falso acuñado para un destino que aparece en una petición a otro es la firma, y da igual cómo salió el falso: un bundle filtrado, un dotfile robado, un desconocido curioso con un navegador. La primera persona que pruebe la credencial que encontró se anuncia sola, en un registro que es tuyo, en el momento en que la prueba.
El registro que Beacon dice que nunca tendrá
Simpson cierra el informe de incidente con los límites de lo que Beacon puede establecer:
Hay cosas que quizá nunca lleguemos a averiguar sobre este incidente.
Beacon enuncia el límite sin rodeos. Los objetos concretos, el destino exacto de las descargas y la atribución definitiva de a qué objetos se accedió no se pueden determinar a partir de los registros disponibles. La conclusión de Beacon de que salió la base de datos entera es una inferencia a partir de la forma de un informe de facturación: un volumen de transferencia en los datos de Cost and Usage que coincide con el tamaño aproximado de lo que Beacon almacena. Es buen trabajo forense sin nada con lo que trabajar, y cada organización que ahora escribe a sus simpatizantes se apoya en él.
Un espacio de trabajo Bromure produce ese registro como subproducto de su modo de funcionar. Cada petición cruza el proxy del host, así que el proxy anota lo que pasó:
$ bromure-cli trace ls
HOST METHOD STATUS MS FLAGS
api.anthropic.com POST 200 412 swap×1
s3.eu-west-2.amazonaws.com GET 200 88
registry.npmjs.org GET 200 31
api.github.com POST 201 140 swap×1
trace hostnames lista cada host distinto que contactó una sesión, trace summary agrega el conjunto, y trace leaks muestra las credenciales no
gestionadas. El Trace Inspector (⇧⌘I) da la misma vista con los cuerpos de las
peticiones, y el Registro de Seguridad (Window → Supply Chain Log…) sigue en
directo las decisiones de cadena de suministro y los 451 según ocurren. Bromure
mantiene todo eso cifrado en reposo en tu Mac bajo la clave maestra de la bóveda.
Las preguntas que Beacon solo puede responder por inferencia son una consulta que ejecutas tú mismo: con qué habló este espacio de trabajo, cuándo, llevando qué, y si salió algo con forma de credencial. Contra tus propios datos, en el minuto en que te lo preguntas por primera vez.
La disposición que cierra el hueco
Los ingenieros de Beacon no hicieron nada fuera de lo común. Poner una credencial en una variable de entorno es la práctica recomendada, y leer variables de entorno durante un build es lo que hacen los builds. Una base de datos de clientes entera pasó por el hueco entre esas dos cosas razonables, y ninguna cantidad de cuidado lo cierra, porque el cuidado es una esperanza sobre la atención, no un control.
El control consiste en disponer las cosas de modo que la copia no valga nada. Guarda la credencial real en el host, entrégale al espacio de trabajo un marcador de posición que satisfaga a cada herramienta que lo lea, firma y sustituye fuera de la máquina que ejecuta el código, y lleva tu propio registro de cada petición que sale. Entonces una herramienta de build haciendo su trabajo, un agente haciendo una edición de aspecto razonable y un desconocido leyendo tu bundle con calma llegan todos al mismo sitio: a una cadena que no significa nada fuera de tu Mac.
Fuentes: The Register, “AWS key exposed in JavaScript may have lit way to Beacon's charity data” (13 de agosto de 2026) · SecurityWeek, “Over 1,000 Charities Hit by Beacon CRM Data Breach” (14 de agosto de 2026) · Infosecurity Magazine, “Exposed AWS Access Key Linked to Data Breach Affecting 1500+ UK Charities” · Cybersecurity News, “Beacon CRM Confirms Full Database Theft After AWS Access Key Breach”