De un WordPress comprometido a una ontología operativa

Un sitio fue comprometido y cerrarlo costó tres días. Las tres semanas de discusión posteriores produjeron algo que la defensa no podía dar: una razón para que el próximo incidente deje el sistema distinto de como lo encontró. Ésta es la derivación, con los giros equivocados dentro.

Jesús Pérez
Aparecieron cuatro mecanismos sin relación entre sí declarando algo que no entregaban — una política de acceso, una rotación de credenciales, dos alertas, un pool de workers. Ninguno había fallado, porque nada había contrastado nunca la declaración contra el sistema en marcha. Este texto sigue lo que salió de ahí: un modelo derivado de un incidente, tres correcciones hechas por quien tenía el conocimiento operativo, y una decisión de protocolo — una comprobación declara qué necesita para poder responder.
De un WordPress comprometido a una ontología operativa

Empezó con correos de restablecimiento de contraseña llegando a cuentas que nadie había pedido restablecer. Ésa es la clase de señal que o no es nada o lo es todo, y no hay forma de saber cuál sin mirar.

Lo era todo. Un sitio WordPress había sido comprometido semanas antes. Se habían creado dos docenas de cuentas de administrador en un bloque contiguo, había web shells en disco y seguían vivas sesiones del intruso.

El inquilino está anonimizado en todo el texto, y también lo está cada coordenada de la defensa. Lo que sigue es la forma de lo que pasó, que es la parte que se transfiere.

Qué era realmente el ataque

La parte ruidosa era relleno de credenciales: 16.615 peticiones en 24 horas contra tres inquilinos, desde 74 anfitriones, todos infraestructura de nube alquilada. Ruidoso, concentrado, fácil de ver.

El ruido era el diseño. La entrada real corría por debajo, a través de un extremo que empaqueta muchas subpeticiones en una sola petición HTTP — con lo que cualquier límite por petición la cuenta una vez. Y el formulario de acceso había sido renombrado por una extensión, lo que lo sacó discretamente fuera del bloque que la autenticación básica protegía. La página de acceso protegida no vio jamás una sola petición. No le hacía falta.

Esa parte es un incidente normal, aunque desagradable. Cuentas purgadas, shells eliminadas, sesiones destruidas, los extremos cerrados en la capa del servidor web, verificado desde fuera. Hoy el tráfico de ataque no ha bajado nada — siguen llegando miles de peticiones al día — y todas mueren antes de alcanzar la aplicación. La métrica que importa no es cuántas llegan. Es el peor código de respuesta observado. Ese número es ahora un 401.

El hallazgo no fue el ataque

Mientras se cerraba, salieron a la luz cuatro cosas sin relación entre sí. Todas tenían la misma forma:

  • La política de acceso de un registro declaraba once espacios de nombres y servía cuatro. Nada falló, porque todos los servidores del clúster en marcha tenían las imágenes en caché. Habría fallado la primera vez que algo necesitara una descarga fresca.
  • Un componente declaraba la rotación de credenciales entre sus responsabilidades. Su ruta de actualización aplicaba manifiestos y reiniciaba el pod — nunca volvía a crear el secreto. Ninguna rotación podía llegar al clúster. Llevaba meses así.
  • Dos alertas estaban declaradas en el bloque de supervisión de un componente. El recolector de registros del servidor que corría esos servicios llevaba días sin poder alcanzar el almacén. El almacén tenía cero series. Las alertas describían una tubería que no entregaba nada.
  • Un pool de PHP declaraba que podía correr veinte workers. El límite de memoria del contenedor daba para unos seis. Seis workers ya estaban en 411Mi de un techo de 512Mi. El contenedor había sido matado cinco veces.

Cuatro mecanismos, un patrón: la declaración era correcta y no significaba nada, porque nada la contrastaba contra el sistema en marcha.

Todos ellos pasaron todas las revisiones. Una declaración es infalsable por inspección. Puedes leerla, estar de acuerdo, y estar equivocado durante meses.

La pregunta que no tenía respuesta

Tres días después el operador hizo la pregunta obvia: ¿sigue pasando? ¿está mitigado? ¿en qué estado están los servicios?

Responderla costó una sesión de trabajo entera — consultas al clúster, SQL contra tres bases de datos, aritmética de registros, sondas externas. Y aquí está la parte que merece detenerse: cada hecho recuperado ese día ya se había establecido tres días antes. No se aprendió nada nuevo. El trabajo fue reconstrucción pura.

Peor: la reconstrucción produjo un falso positivo. Una cuenta de administrador creada durante la respuesta al incidente fue señalada como posible artefacto del atacante. Era una decisión deliberada del operador — una cuenta creada para gestionar el servicio tras eliminar aquella a la que apuntaban los atacantes. La decisión existía. No estaba escrita en ningún sitio consultable.

Los indicadores de respuesta a incidente y los de intrusión son idénticos. Sin registro, cada revisor futuro vuelve a derivar la misma conclusión equivocada. Incluido uno que sea una máquina.

Por qué nada de lo que ya había podía sostenerlo

Al proyecto no le faltaba estructura. Tenía registros de decisión de arquitectura con predicados ejecutables adjuntos. Tenía una lista de trabajo tipada. Tenía un registro de acciones.

Ninguno encaja:

  • Un registro de decisión sostiene el predicado, pero es arquitectónico y casi inmutable. Veintitrés en toda la vida del proyecto, mientras un servicio acumula compromisos cada semana. «Activamos doble factor para dos de tres administradores» no es una decisión de arquitectura.
  • Un elemento de la lista de trabajo sostiene la intención, y se cierra. Un compromiso no se cierra nunca; recurre. Un elemento de este proyecto afirma, en prosa llana, que hay algo que «una estación de trabajo no puede comprobar» — porque el esquema no tenía forma de expresar la comprobación, así que la narró.
  • Una entrada de registro sostiene el momento, no la afirmación. No puede reevaluarse después.

¿Y el sitio donde esos compromisos vivían de verdad? Una lista de cadenas en la definición de un componente. Cuatro nombres. Sin estado, sin evidencia, sin fecha, sin predicado. Uno de esos nombres era la rotación cuyo mecanismo no podía entregarla.

El primer diseño estaba mal

El movimiento obvio es darles un tipo a esas cadenas: promoverlas a un registro con una afirmación, una comprobación ejecutable y una fecha de verificación, ahí mismo, en el componente.

El operador lo rechazó, sobre tres motivos que eran todos correctos:

  1. Las definiciones de componente son propiedad de arriba, del dominio. Añadir un campo ahí propaga un esquema a cada espacio de trabajo derivado de él, lo use o no.
  2. Un servicio tiene contingencias que ningún esquema puede anticipar. Si registrar una contingencia imprevista exige modificar el contrato del dominio, las contingencias no se van a registrar — el coste burocrático supera al beneficio y la gente vuelve a escribir un comentario.
  3. Un registro dentro de un fichero regenerable comparte su suerte. Una reescritura desde el catálogo lo borra en silencio. Un registro que puede evaporarse al actualizar es peor que ningún registro, porque crees que está ahí.

La corrección ya estaba en el código. El cableado entre componentes se había topado con el mismo problema y lo había resuelto poniendo la información en un fichero lateral, descubierto por convención de nombres, referido al componente, validado contra un esquema — y sin añadir absolutamente nada al contrato compartido. Resuelto una vez lateralizando, no extendiendo.

Células y tejidos

Ahí es donde cambió el marco, y vino del operador, no del diseño. El vocabulario que sigue — célula, tejido, núcleo, membrana, linaje, apoptosis — no se define aquí, y es a propósito. Se define en el registro al que apuntan esos enlaces, y la razón de la disciplina es el tema de este texto: en cuanto un artículo define un término que el esquema también impone, el sitio dice una cosa y el mecanismo otra, y la desviación ya es pública.

Hubo que corregir dos colocaciones, y ambas correcciones salieron de empujar la analogía con honestidad en vez de decorativamente:

La memoria no está en la célula. Pertenece al tejido. Una célula que carga con su propia historia no puede exportarse, porque esa historia no significa nada allá donde aterriza. La ausencia de estado es lo que hace portable a una célula — un espacio de trabajo puede entregar una célula a otro, que entonces la diferencia en su propio contexto.

El rol no está en la célula. Sensar, validar y regular son ejes de expresión que asigna el tejido. La misma célula es un sensor en un tejido y una política en otro. Lo que significa además que tampoco puede soldarse a un momento: la misma célula tiene que servir en un anzuelo previo a un commit, en la integración continua, en una sesión de agente y en una comprobación de servicio vivo. Responde con lo que tiene, y lo que no tiene tiene un nombre.

No saber es una respuesta de primera clase

Un veredicto es pass, fail o unknown. Una célula que necesita acceso al clúster, invocada desde una comprobación previa al commit, no falla — devuelve unknown, y cada circuito decide qué significa eso para él. Sin eso, hay que excluir cada célula por contexto, y la exclusión vuelve a soldarla a un sitio.

Ésta es la parte que dejó de ser una propuesta mientras se escribía este texto. Es ya una decisión de protocolo ratificada, y llegó con números que defienden el caso mejor que el argumento.

La herramienta ya tenía el tercer valor en uno de sus dos ejecutores, y nunca había terminado de aplicarlo. De 59 restricciones reportadas como violadas en este árbol, al menos 27 eran el arnés no alcanzando su oráculo — 46% de ruido, en rojo, que es precisamente por lo que el número se había ejecutado, leído e ignorado. Extender el tercer valor al otro ejecutor destapó tres comprobaciones que no habían pasado ni una sola vez, una de ellas anunciando «capacidades del manifiesto incompletas» desde una ejecución en la que el módulo que necesitaba no se había cargado nunca. Una afirmación sobre su sujeto, desde una ejecución que jamás miró el sujeto.

Dos de esas tres las encontró el propio mecanismo, el día que les llegó, antes de que nadie fuera a buscar. Ésa es la diferencia entre un mecanismo que informa y uno del que se afirma que funciona.

Y el tercer estado por sí solo resultó no bastar — que es la cuarta corrección de esta historia, y la única hecha contra el modelo en vez de contra el diseño.

Una comprobación declara qué necesita para poder responder, de modo que la ausencia de una capacidad da unknown en vez de un rojo falso. Ahora aplica eso a una puerta que debe confirmar que hay una política de privacidad publicada antes de encaminar datos personales a ninguna parte. Sin red → unknown → permisivo → el flujo procede a recoger datos personales sin haber verificado nada. La ausencia de prueba se convierte en prueba de cumplimiento, y el mecanismo construido para impedir que una comprobación mienta introduce la mentira él mismo.

La severidad dice cuán grave es que una afirmación sea falsa. Nada decía cuán grave es que quede sin responder. Así que una afirmación declara ahora también qué significa su propio silencio — permitir, o rehusar — y esa postura viaja con la afirmación en lugar de con el circuito que la invocó. Una comprobación que cierra ante la duda y que al llegar a otro sitio abre no es la misma comprobación.

Protegerse no es evolucionar

Aquí está la pregunta que replanteó todo, y se hizo como objeción: un arnés sin ontología resuelve un solo problema. Nos protegemos — ¿pero evolucionamos?

No. Un arnés sin ontología es inmunidad innata: rápida, eficaz, y no aprende nada.

Hay un ejemplo funcionando en el mismo clúster — un detector automático que descubre un sistema de ficheros que se ha puesto en sólo lectura y lanza un trabajo de reparación. Funciona. Puede hacer cien reparaciones y el modelo que el sistema tiene de sí mismo es idéntico al de antes de la primera. Protege, y no enseña nada.

La inmunidad adaptativa es más lenta, genera especificidad nueva, recuerda, y deja al organismo distinto tras el encuentro. La diferencia es un arco de retorno:

percepción → acción                          protección
percepción → señal → tensión → decisión      evolución

El proyecto ya tenía ese arco — para las sesiones. Cerrar una sesión de trabajo captura estado, evalúa si una decisión merece registrarse y anota una señal de emergencia. Lo que faltaba era su gemelo del lado de la operación. Un servicio puede fallar cincuenta veces y no llegar nada al modelo.

La evidencia estaba ahí: un patrón — el mecanismo declara y no entrega — apareció cuatro veces en siete días y no tenía existencia ontológica ninguna.

Con una línea dura: el arnés propone, el humano ratifica. Detectar un patrón repetido es mecánico. Nombrarlo es una decisión. Un arnés que declara sus propias tensiones no es evolución, es desviación.

Qué le pide esto a un protocolo

El marco no es vocabulario nuevo atornillado encima, y la prueba de eso no es si las palabras suenan atinadas. Es si aterrizan sobre fronteras que la arquitectura ya había trazado por razones sin relación con ellas.

Lo hicieron, y la instancia más fuerte no es léxica. La separación célula/tejido — lo que viaja, y lo que no significa nada fuera del contexto que lo sostiene — resultó ser la misma línea que el protocolo ya había ratificado entre un dominio y sus instancias: las declaraciones convergen en el dominio, la ejecución y la memoria divergen por instancia. Nadie apuntaba ahí. El modelo lo derivó de un incidente alguien que no estaba mirando esa decisión.

Que un término aterrice sobre un término puede despacharse como ir de compras por el vocabulario. Que un modelo aterrice sobre una frontera que ya estaba ahí, no; y ésa es la respuesta honesta al lector que sospecha que la biología se eligió primero y la mecánica se ajustó después.

Esa convergencia se ha decidido desde entonces por derecho propio: lo que cruza entre tejidos es protocolo — un sobre de linaje que nombra a su padre por resumen de contenido, de modo que un padre mutado deja a los descendientes rancios en vez de silenciosamente equivocados — y el tejido mismo explícitamente no se estandariza. La memoria, la cadencia y el rol se quedan locales, porque no significan nada en un tejido receptor. Decidir que algo se queda local es una decisión, no un hueco, y está escrita como tal para que el siguiente autor no la cierre servicialmente.

También había un término de frontera ya en uso como campo del mecanismo de puertas y ausente del léxico. El mecanismo lo tenía; el vocabulario no. Se ha definido desde entonces, sobre la garantía que ya llevaba — que es la instancia más pequeña posible de todo este texto, y la única que costó diez minutos arreglar.

Un término de esta derivación sigue marcado como provisional en el registro: marcar diferencia, el criterio de que una célula está viva sólo si su veredicto puede cambiar al menos una decisión en al menos un circuito. Todavía no tiene garantía ratificada. Se enlaza aquí en el estado en el que está de verdad, que es justamente el asunto.

Volvió a pasar, a quienes lo escribían

Mientras se escribían estas notas, se le preguntó a la propia superficie de consulta del protocolo por uno de estos términos desde otro repositorio. Respondió «No glossary found» — con total confianza, sobre un término que existe.

El vocabulario se ensambla por raíz de proyecto. Aquella raíz no declaraba glosario. Así que la herramienta informó de la ausencia como un hecho, donde la respuesta honesta era unknown: exactamente el caso a favor del tercer valor, hecho por la superficie que carecía de él, días después de que el caso quedara escrito, a quienes lo escribieron.

La derivación es el producto

Tres días de respuesta al incidente produjeron una defensa que funciona. Las tres semanas de discusión posteriores produjeron algo que la defensa no podía dar: una razón para que el próximo incidente deje el sistema distinto de como lo encontró.

Todo lo de arriba pasó en el orden en que está escrito, correcciones incluidas. Eso no es modestia. En un protocolo cuya afirmación central es que un enunciado sobre un sistema debería poder ser contradicho por una máquina y no sólo por un lector, mostrar los enunciados que fueron contradichos es la única forma honesta de presentarlo.

La pregunta que merece llevarse no va de nuestro clúster. Es: ¿qué aprendió tu infraestructura de su último incidente? Si la respuesta es «se reparó», eso es protección. No es poco. Pero tampoco es evolución, y la diferencia está en si algo en tu sistema es distinto ahora porque aquello ocurrió.

¿Te ha resultado útil? Valóralo
¿Tienes algo que aportar? Cuéntame qué opinas, qué sugieres, o si seguimos explorando este tema.
· lecturas

Usamos cookies para que este sitio funcione, entender el uso del servicio y apoyar acciones de marketing. Política de cookies para más información.