Vigilancia del ICE: la lección de datos para quien usa Bun
Wired publicó un reportaje que dice que el ICE, la agencia de inmigración de Estados Unidos, está llevando a cabo una de las mayores operaciones de vigilancia contra movimientos de izquierda de la historia del país. Este caso de vigilancia del ICE parece un tema de política, no de código. Pero toda operación de ese tamaño depende de software. Alguien escribió el endpoint que guarda la IP. Alguien decidió que el log iba a durar para siempre. Y muy probablemente esa persona ni sabía para qué se usaría eso después.
El artículo completo está aquí: ICE Is Running One of the Largest Anti-Left Surveillance Operations in US History. Vale la pena leerlo. Aquí quiero hablar de la parte que nos toca a nosotros, que escribimos backend todos los días.
Lo que muestra el reportaje
No voy a contar el artículo entero, y deberías leer la fuente antes de sacar conclusiones. El punto central es simple: una agencia del gobierno reunió información sobre personas y grupos por su posición política. Esa información sale de algún lugar. Redes sociales, bases comerciales, registros públicos, cruce de datos.
Ninguna de esas bases nació para vigilar activistas. Nacieron para vender anuncios, prevenir fraude, medir engagement. Se volvieron herramientas de vigilancia porque estaban ahí, llenas, listas para consultar.
Ese es el patrón que se repite en casi todo escándalo de datos. El villano rara vez es un sistema diseñado para el mal. Es un sistema común que guardó demasiado y durante demasiado tiempo.
Por qué esto importa a quien escribe código
Porque el dato que no recolectas no lo puede pedir nadie. Ni un gobierno, ni un atacante, ni un becario curioso con acceso a la base de producción.
Piensa en el último proyecto que pusiste en producción. Probablemente tiene:
- Log de request con IP, user agent y cuerpo de la petición
- Tabla de usuarios con teléfono, dirección y fecha de nacimiento "por si acaso"
- Analytics guardando eventos por usuario desde el primer día
- Backup de la base sin fecha de caducidad
Nada de eso es maldad. Es pereza mal dirigida. Y lo digo como alguien que ya hizo todo eso.
Un dato guardado sin motivo es un pasivo esperando dueño.
El punto que poca gente enfrenta: la decisión de retener es técnica, pero la consecuencia es política. No controlas quién va a pedir esos datos dentro de cinco años. Solo controlas lo que existe para ser pedido.
Cómo reducir la recolección en un backend Bun
Ahora la parte práctica. Uso Bun en la mayoría de mis proyectos de API, así que los ejemplos van con él. La lógica vale para Node, Deno o lo que sea.
El primer paso es el más aburrido: mirar lo que logueas. Un logger de request ingenuo suele verse así:
Bun.serve({
async fetch(req, server) {
const body = await req.clone().text();
console.log({
ip: server.requestIP(req)?.address,
ua: req.headers.get("user-agent"),
url: req.url,
body,
});
return handle(req);
},
});
Esto guarda IP, navegador, URL con query string (que a veces lleva un e-mail o un token) y el cuerpo entero. Si alguien manda una contraseña en un POST, termina en el log. Lo he visto en producción más de una vez.
Una versión más honesta loguea solo lo que ayuda a depurar:
Bun.serve({
async fetch(req) {
const { pathname } = new URL(req.url);
const start = performance.now();
const res = await handle(req);
console.log({
method: req.method,
path: pathname,
status: res.status,
ms: Math.round(performance.now() - start),
});
return res;
},
});
Sin IP, sin cuerpo, sin query string. En la práctica, casi nunca lo eché de menos. Cuando un bug necesita más contexto, activo un log detallado temporal y lo apago después.
¿Y cuando necesito identificar al usuario?
A veces hace falta de verdad. El rate limit por IP es el ejemplo clásico. La salida es guardar algo que sirva para comparar, pero no para identificar.
Bun tiene hash nativo, sin ninguna dependencia:
const salt = Bun.env.IP_SALT!; // rotar periódicamente
function ipKey(ip: string) {
return new Bun.CryptoHasher("sha256").update(salt + ip).digest("hex");
}
Con un salt que cambia, por ejemplo, todos los días, puedes limitar peticiones sin armar un historial de quién accedió a qué. Cuando el salt cambia, el hash viejo se vuelve basura inútil. Un aviso honesto: el espacio de IPv4 es pequeño, así que un hash con salt fijo y filtrado se puede romper por fuerza bruta. Es la rotación la que hace el trabajo de verdad.
Para contraseñas, ni hay que pensarlo. Usa Bun.password.hash, que ya viene con argon2 por defecto. Si todavía guardas contraseñas con SHA puro en 2026, este artículo es para ti por partida doble.
¿Cuánto tiempo debo guardar los datos?
La respuesta corta: lo mínimo que exige la funcionalidad, con un plazo escrito en el código. La LGPD brasileña ya lo pide, con el principio de necesidad. El RGPD europeo también, con la minimización de datos. La mayoría de los proyectos lo ignora porque nadie lo exige.
Me gusta dejar la retención como regla de la base de datos, no como promesa en el README. Con bun:sqlite queda así:
import { Database } from "bun:sqlite";
const db = new Database("app.db");
// los eventos de analytics viven 30 días
db.run(`DELETE FROM events WHERE created_at < datetime('now', '-30 days')`);
En PostgreSQL es la misma idea, corriendo en un job programado. Lo importante es que la limpieza sea automática. Una retención que depende de que alguien se acuerde no es retención, es intención.
Algunas reglas que adopté y recomiendo:
- Logs de aplicación: 7 a 14 días
- Eventos de analytics por usuario: agrega y borra los datos en bruto a los 30 días
- Cuenta eliminada: bórrala de verdad, no solo
deleted_at - Backups: con plazo, y probados (un backup eterno es solo una base paralela sin control de acceso)
El soft delete merece un comentario. Es genial para el botón "deshacer". Es pésimo como política de privacidad. Si el usuario pidió irse, el registro con deleted_at lleno sigue respondiendo a cualquier consulta que ignore esa columna. Incluida una consulta hecha por orden judicial.
La objeción de siempre: "¿y si lo necesito después?"
La escucho en cada reunión de producto. Y la entiendo. Los datos parecen gratis, y tirarlos parece un desperdicio.
Pero haz la cuenta completa. Cada campo guardado tiene costo de almacenamiento, de backup, de cumplimiento, de respuesta a incidentes. Y tiene el costo que no aparece en la factura de AWS: el riesgo de que se use en contra de la persona que confió en tu sistema.
En mi experiencia, el "por si acaso" casi nunca llega. Cuando llega, empiezas a recolectar desde ahí y listo. Perder tres meses de historial rara vez rompe un producto. Filtrar tres años de historial puede romper la empresa.
Y sí, sé que mi app de lista de tareas no es objetivo del gobierno estadounidense. La tuya probablemente tampoco. Pero las bases que aparecen en este tipo de reportajes tampoco se creían objetivo. Eran solo startups con un SELECT * generoso.
Mi opinión
No creo que el dev sea culpable de cada abuso hecho con datos. La responsabilidad mayor es de quien decide usarlos. Pero creo que subestimamos el poder que tenemos al escribir el schema. Una columna menos, un log más corto, un job de limpieza corriendo cada madrugada. Son decisiones pequeñas, que caben en un PR de 20 líneas, y que cambian lo que existe para ser vigilado.
Bun lo facilita porque ya trae hash, contraseñas y SQLite sin paquetes extra. Ya no hay excusa de "da trabajo configurarlo". Lo que falta es el hábito de preguntar, antes de cada campo nuevo, si de verdad necesita existir.
Si quieres ver cómo aplico esto en mis propios sistemas, échale un vistazo a los proyectos que mantengo.
Resumen para LinkedIn
El dato que no recolectas nadie lo puede pedir. Ni un gobierno, ni un atacante, ni un becario curioso. Wired mostró que el ICE montó una de las mayores operaciones de vigilancia política de la historia de EE. UU. Ninguna de las bases usadas nació para vigilar a nadie. Nacieron para vender anuncios y medir engagement. El villano casi nunca es un sistema hecho para el mal. Es un sistema común que guardó demasiado, durante demasiado tiempo. Logs con IP y cuerpo de la petición, teléfono "por si acaso", backups sin fecha de caducidad. Yo ya hice todo eso. En Bun se resuelve con poco: logs sin datos personales, hash de IP con salt rotativo, Bun.password para contraseñas y un DELETE programado para la retención. Un PR de 20 líneas cambia lo que existe para ser vigilado. Escribí el paso a paso con código en el blog. Antes de crear el próximo campo, vale la pena preguntar si necesita existir. #Privacidad #ProteccionDeDatos #Bun #Backend #DesarrolloDeSoftware