Volver al blog
SeguridadBunOpenAI

Amenaza interna: el caso de OpenAI y cómo protegerte con Bun

15 de octubre de 2026·8 min de lectura·Diego Horvatti

Detuvieron a un ingeniero de OpenAI, acusado de instalar un micrófono oculto en la sala de reuniones de los directivos de la empresa. La noticia salió en SF Standard y parece guion de película de espías. Pero el tema de fondo es mucho menos cinematográfico y mucho más común: la amenaza interna. Es el riesgo que viene de quien ya tiene credencial, acceso y la contraseña del Wi-Fi.

Antes de seguir, una advertencia honesta. Los detalles del caso todavía están saliendo y no voy a especular sobre la persona ni sobre el motivo. Una detención no es una condena. Lo que me interesa aquí es otra cosa: lo que este tipo de caso muestra sobre la forma en que construimos sistemas.

Qué pasó, en pocas líneas

Según el reportaje, un empleado del área de ingeniería es sospechoso de espiar una sala que usa la dirección de la empresa. No hubo ransomware, ni phishing, ni zero-day. La persona ya estaba dentro.

Y eso en una de las empresas más codiciadas del planeta. Por esas salas pasan pesos de modelos, roadmaps y negociaciones de miles de millones de dólares. Si puede pasar en OpenAI, puede pasar en tu startup de 12 personas.

Por qué la amenaza interna es el riesgo que todos ignoran

Cuando hablamos de seguridad, la cabeza se va directo al atacante externo. Firewall, rate limit, WAF, CAPTCHA en el login. Todo apuntando hacia afuera.

Pero el modelo mental de muchos equipos es este: quien pasó la autenticación es de confianza. Y listo.

Piensa ahora en tu proyecto. ¿Cuántas personas pueden:

  • Correr un SELECT directo en la base de datos de producción?
  • Leer el .env de producción?
  • Hacer deploy sin que nadie lo revise?
  • Borrar un log sin dejar rastro?

En la mayoría de los equipos pequeños que conozco, la respuesta a las cuatro es "todo el mundo". Incluido el becario que entró el mes pasado. Incluido ese token de CI que nadie recuerda quién creó.

No digo que tu compañero te vaya a espiar. Digo que la amenaza interna no siempre es maldad. A veces es un portátil robado, una cuenta comprometida o alguien que corrió un DELETE sin WHERE un viernes a las 18h.

La confianza no es control de acceso.

Qué cambia en el código: un rastro que no se borra

El micrófono físico es un problema de seguridad del edificio, no es tu trabajo. Pero el equivalente digital sí lo es: alguien con acceso legítimo haciendo algo ilegítimo. Y la primera defensa contra eso es saber quién hizo qué, y estar seguro de que ese registro no fue alterado.

Un log normal no lo resuelve. Si la persona tiene acceso a la base de datos, borra la línea del log junto con el destrozo. Por eso me gusta el log de auditoría encadenado por hash. Cada registro lleva el hash del anterior. Si tocas uno, rompes toda la cadena.

Con Bun se puede hacer sin instalar nada, usando bun:sqlite y Bun.CryptoHasher:

import { Database } from "bun:sqlite";

const db = new Database("audit.db");

db.run(`CREATE TABLE IF NOT EXISTS audit (
  id INTEGER PRIMARY KEY,
  at TEXT NOT NULL,
  actor TEXT NOT NULL,
  action TEXT NOT NULL,
  prev TEXT NOT NULL,
  hash TEXT NOT NULL
)`);

// bloquea UPDATE y DELETE directo en la base de datos
db.run(`CREATE TRIGGER IF NOT EXISTS audit_no_update
  BEFORE UPDATE ON audit BEGIN SELECT RAISE(ABORT, 'append-only'); END`);
db.run(`CREATE TRIGGER IF NOT EXISTS audit_no_delete
  BEFORE DELETE ON audit BEGIN SELECT RAISE(ABORT, 'append-only'); END`);

const sha = (s: string) => new Bun.CryptoHasher("sha256").update(s).digest("hex");

export function audit(actor: string, action: string) {
  const last = db.query("SELECT hash FROM audit ORDER BY id DESC LIMIT 1")
    .get() as { hash: string } | null;
  const prev = last?.hash ?? "genesis";
  const at = new Date().toISOString();
  const hash = sha(`${prev}|${at}|${actor}|${action}`);
  db.run(
    "INSERT INTO audit (at, actor, action, prev, hash) VALUES (?, ?, ?, ?, ?)",
    [at, actor, action, prev, hash],
  );
}

export function verify() {
  let prev = "genesis";
  for (const row of db.query("SELECT * FROM audit ORDER BY id").all() as any[]) {
    if (row.prev !== prev) return { ok: false, brokenAt: row.id };
    if (sha(`${row.prev}|${row.at}|${row.actor}|${row.action}`) !== row.hash) {
      return { ok: false, brokenAt: row.id };
    }
    prev = row.hash;
  }
  return { ok: true };
}

Ahora, siendo honesto sobre el límite de esto. Quien tiene acceso de escritura al archivo puede eliminar el trigger y reescribir toda la cadena desde cero. El hash no impide la manipulación, solo la hace detectable. Para cerrar el agujero, envía el último hash a un lugar que esa misma persona no controle: un bucket con retención bloqueada, un canal de Slack, un e-mail diario. Si el hash de ayer no coincide con la cadena de hoy, alguien la tocó.

En PostgreSQL la idea es la misma: una tabla audit con REVOKE UPDATE, DELETE para el usuario de la aplicación y el hash calculado en la app o en un trigger.

Cómo conectar esto a una API con Bun

Generar el registro tiene que ser automático. Si depende de que alguien se acuerde de llamar a audit(), nadie lo llama. Un wrapper en las rutas sensibles lo resuelve:

import { audit } from "./audit";

type Handler = (req: Request, user: string) => Response | Promise<Response>;

const sensitive = (action: string, handler: Handler) => async (req: Request) => {
  const user = req.headers.get("x-user-id"); // cámbialo por tu auth de verdad
  if (!user) return new Response("unauthorized", { status: 401 });
  audit(user, `${action} ${new URL(req.url).pathname}`);
  return handler(req, user);
};

Bun.serve({
  routes: {
    "/admin/export-users": sensitive("export", () => Response.json({ ok: true })),
    "/admin/rotate-keys": sensitive("rotate", () => Response.json({ ok: true })),
  },
  fetch: () => new Response("not found", { status: 404 }),
});

Fíjate en que el registro ocurre antes de la acción. Si la acción falla a mitad de camino, igual sabes que alguien lo intentó. Un intento de exportar la base de usuarios a las 3 de la mañana es información valiosa, haya salido bien o no.

Y sí, ese header x-user-id es solo para que el ejemplo quepa en pantalla. Si confías en un header que manda el cliente, el problema de amenaza interna se convierte en un problema de amenaza de cualquiera.

Lo que cambiaría mañana en el flujo de trabajo

El código es la mitad de la historia. La otra mitad es el proceso, y normalmente es la parte más barata.

Mínimo privilegio de verdad. Nadie necesita acceso de escritura en producción para trabajar en el día a día. Crea un usuario de solo lectura en Postgres para depurar y deja el de escritura solo para la aplicación y las migraciones.

Secretos fuera del repositorio y fuera de Slack. Si el .env de producción ya pasó por un mensaje directo, dalo por filtrado. Usa el gestor de secretos de tu plataforma y rota lo que esté viejo.

Tokens con dueño y con fecha de caducidad. Todo token de CI o de API necesita un responsable y una fecha de expiración. Un token eterno y sin dueño es una puerta trasera esperando a que alguien la encuentre.

Revisión obligatoria en el deploy. Proteger la rama main cuesta cinco minutos de configuración. Eso impide que una sola persona, de mala fe o simplemente cansada, ponga cualquier cosa en producción.

Offboarding el mismo día. Cuando alguien deja el equipo, su acceso se va con él. No la semana siguiente. Es impresionante cuántas empresas descubren meses después que un exempleado todavía tenía acceso al panel de admin.

Ninguno de estos puntos es paranoia. Es higiene. El mismo tipo de cuidado que te hace escribir un test antes de tocar el cálculo del envío.

¿Esto no es tratar al equipo como sospechoso?

Es la objeción que más escucho, y la entiendo. Nadie quiere trabajar en un lugar donde todos están vigilados.

Pero un log de auditoría no es vigilancia de personas. Es el registro de acciones en un sistema crítico. Protege incluso a los inocentes. Cuando algo desaparece de la base de datos, el log es lo que prueba que no fuiste tú. Sin él, solo quedan la desconfianza y los dedos señalando en la reunión.

Y hay un detalle un poco irónico en el caso de OpenAI. La acusación es justamente la de alguien vigilando a otros sin autorización. El antídoto no es más vigilancia escondida, es control transparente: todos saben que las acciones sensibles quedan registradas, y las reglas valen igual para el becario y para el CTO.

Mi opinión

Este caso va a dar titulares durante semanas por el nombre involucrado. Pero la lección para quien escribe código es aburrida y práctica: el perímetro no es la puerta del edificio, es cada permiso que repartes. La mayoría de los sistemas que veo están listos para el hacker ruso y completamente abiertos para quien ya tiene login.

Si solo vas a hacer una cosa después de leer esto, haz un inventario de quién puede escribir hoy en producción. Probablemente la lista te asuste más que cualquier micrófono en la sala de reuniones.

Si quieres ver cómo aplico este tipo de cuidado en sistemas reales, échale un vistazo a los proyectos que ya he entregado.

Resumen para LinkedIn

Detuvieron a un ingeniero de OpenAI, acusado de poner micrófonos en la sala de reuniones de los directivos.

No hubo ransomware, ni phishing, ni zero-day. La persona ya estaba dentro.

La mayoría de los sistemas que veo están listos para el hacker ruso y completamente abiertos para quien ya tiene login.

¿Quién en tu equipo puede correr un SELECT en producción, leer el .env o hacer deploy sin revisión? En la mayoría de los equipos pequeños, la respuesta es "todo el mundo".

La confianza no es control de acceso. Un log de auditoría que no se puede borrar, el mínimo privilegio y el offboarding el mismo día son higiene, no paranoia.

Escribí cómo montar un log a prueba de manipulaciones con Bun en pocas líneas. El link está en los comentarios.

#SeguridadDeLaInformación #Bun #DesarrolloDeSoftware #DevSecOps #OpenAI