Blog ·
Un secreto erróneo suena a silencio
Tres protocolos de monitorización escritos a mano en lugar de importados, y un fallo que el propio protocolo impide distinguir de un servidor muerto.
Por Provibr
Añadir una comprobación de monitorización para un protocolo nuevo parece una cuestión de dependencias. Hay un paquete para MongoDB, otro para BSON, otro que habla el protocolo de red de SQL Server. Tres líneas en un manifiesto, cuarenta líneas de pegamento, listo.
Los tres protocolos se escribieron a mano, y las razones merecen quedar por escrito porque ninguna es «no inventado aquí». Dos son mediciones, y la tercera es una promesa que ya se había hecho a los clientes sobre lo que este agente puede hacer.
Lo que trae de verdad una dependencia
El paquete de BSON arrastra cuarenta y una dependencias. Eso se puede medir y se midió, pero el número no fue lo decisivo.
La comprobación lee cuatro campos de una respuesta del servidor. Un tipo de documento genérico mantiene la respuesta entera en memoria: la lista de hosts, las etiquetas, la marca de la última escritura, la hora del clúster, todo lo que el servidor haya querido enviar. En ese momento, «solo leemos cuatro campos» pasa a ser un acuerdo entre desarrolladores en lugar de una propiedad del código, y los acuerdos los rompe la siguiente ronda que necesita un quinto campo por una buena razón.
Hay por tanto un lector que recorre los elementos, devuelve los cuatro solicitados y salta el resto sin dejarlo en ninguna parte. No puede retener la respuesta, así que la promesa es una forma y no una costumbre. La misma decisión que la lista de permitidos del comando INFO de Redis, y la misma que dejó fuera un controlador de base de datos completo en la comprobación de PostgreSQL: un controlador trae un constructor de consultas, y entonces existe, dentro del binario que corre en la red de un cliente, código capaz de hacer una pregunta arbitraria a su base de datos.
El paquete de SQL Server es la versión más afilada. Compila para nuestro destino de publicación, que era justo lo que se esperaba que lo bloqueara. Lo que trae es una segunda pila de TLS: una línea sin mantenimiento junto a la actual que ya distribuimos. El proveedor criptográfico de todo el proceso se instala para la línea actual y no hace nada por la antigua, de modo que el TLS de una comprobación usaría un proveedor distinto al de cualquier otra conexión de este agente; una vulnerabilidad en esa línea es una que distribuimos y no podemos corregir sin esperar a quien la mantiene; y la política de certificados, que vive en un único módulo precisamente para que no se desvíe, tendría una segunda implementación contra otra interfaz.
Saltar es donde falla un lector escrito a mano
Quien escribe su propio lector para un formato binario puede cometer un error que pesa más que los demás, y no está en el análisis. Está en el salto.
Cada valor de BSON tiene un byte de tipo y una longitud que se deriva de él. Un lector que conoce los tipos que ha visto por casualidad, y que salta un tipo desconocido en cero bytes, no falla: sigue leyendo el resto del documento desde la posición equivocada. Todo lo que viene después queda desplazado. Se obtienen valores verosímiles que no son los que se enviaron.
Por eso la tabla de longitudes está completa, con un rechazo cerrado para todo lo demás. Un byte de tipo desconocido es un error de protocolo y nunca un salto.
El mismo tipo de fallo aparece un nivel más arriba, en cómo se leen los números. Un servidor de MongoDB responde ok: 1 como número de coma flotante y un code de error como entero de 32 bits, en el mismo documento. Un lector que solo entiende 32 bits pierde el primero en silencio, y entonces cada inicio de sesión fallido se lee como correcto. El lector acepta un número BSON en sus tres anchuras y devuelve un entero.
El paso de autenticación que tiene que poder fallar
La comprobación inicia sesión con SCRAM-SHA-256 y no con SCRAM-SHA-1, por dos razones que apuntan en la misma dirección.
El mecanismo antiguo no introduce la contraseña en SCRAM. Introduce un MD5 hexadecimal del nombre de usuario, una cadena fija y la contraseña. Admitirlo obligaría a escribir código que genera un MD5 de credenciales, para un mecanismo que ningún servidor actual necesita ofrecer.
La razón silenciosa es peor. Un servidor que ofrece ambos acabaría, al replegarse, en el más débil sin que nada se pusiera en rojo, y entonces hay una medición verde encima de un inicio de sesión más débil de lo que el cliente cree. Si el servidor no ofrece SHA-256 para ese usuario, la comprobación se niega con un código propio y lo dice.
Escribir SCRAM a mano se rechazó en una ronda anterior para la comprobación de PostgreSQL, y la objeción era real: el intercambio tiene una firma del servidor que el cliente debe verificar, y una implementación que se salta ese paso está verde frente a un servidor auténtico y verde igual frente a uno falsificado. Aquí la objeción no se esquivó, se respondió. La verificación de la firma es una función propia con un código de error propio, para que «falta el paso» no sea lo mismo que «el paso salió bien», y hay una prueba que ofrece una firma falsificada y exige que el inicio de sesión falle. Esa prueba se demostró en rojo quitando la verificación.
El paquete que usa la comprobación de PostgreSQL no servía aquí, y eso está medido y no supuesto. Su implementación de SCRAM escribe el primer mensaje del cliente con el nombre de usuario vacío, porque PostgreSQL envía el nombre en su paquete de arranque. MongoDB no tiene ese paquete y lee el nombre precisamente de ese campo. Insertarlo después cambiaría los mismos bytes sobre los que ya se firmó, y el servidor rechazaría nuestra prueba. No hay interfaz para pasarlo.
Un fallo que el protocolo no deja nombrar
Ahora la parte que ningún simulacro habría encontrado.
RADIUS se comprueba con una petición Status-Server, que pregunta al servidor si está vivo y no pregunta nada más. La alternativa evidente, una petición de acceso con una cuenta de prueba, se descartó: eso es un intento de inicio de sesión real en la red del cliente, con una cuenta real que alguien podría usar, y escribe una línea en su registro de autenticación entre sus usuarios reales. Con un intervalo de un minuto son mil cuatrocientas cuarenta líneas al día que no dicen nada sobre su servicio, y en muchas instalaciones una contraseña equivocada es motivo para bloquear una cuenta, con lo que la monitorización provoca justo el incidente que debía haber visto venir.
Después se midió, contra un servidor real, qué hace un secreto compartido equivocado.
Nada. Un servidor que no puede verificar el autenticador de mensaje de una petición descarta el paquete en silencio, sin respuesta de error. Está en la especificación, y significa que el aspecto de un secreto equivocado no se distingue de una regla de cortafuegos, un puerto erróneo o una máquina muerta.
Eso no se arregla en un adaptador, porque es una propiedad del protocolo. Lo que sí se puede hacer es dejar de fingir. El mensaje de «sin respuesta» menciona un secreto compartido equivocado como causa posible, en el texto que lee el cliente, para que un administrador no pase una tarde en su cortafuegos buscando algo que está en un fichero de configuración. Y si el servidor sí responde, se verifica el autenticador de respuesta, y una discrepancia ahí se informa como hecho de autenticación en lugar de error de protocolo, porque en ese montaje realmente lo es.
| Situación | Lo que ve el cliente | Por qué |
|---|---|---|
| Secreto erróneo, sin respuesta | Sin respuesta, con el secreto como causa posible | El servidor descartó el paquete; no volvió nada que examinar |
| Secreto erróneo, llega respuesta | Un secreto erróneo, como resultado de autenticación | El autenticador de respuesta no cuadraba |
| Secreto correcto, sin respuesta | Sin respuesta | El mismo silencio, y merece las mismas palabras |
Esa fila central es el único caso en que la diferencia se ve, y no es el habitual.
Un servidor que no se puede medir, dicho en voz alta
La última medición es la que menos gustó.
SQL Server en Linux genera un certificado en el primer arranque. Ese certificado es X.509 versión 1, una forma sin extensiones que hace mucho que no está pensada para TLS, y el analizador de certificados que hay debajo la rechaza. El modo cifrado falla por tanto con una versión de certificado no admitida.
Conviene precisar por qué ese modo sigue verificando algo. La identidad no se pesa ahí: el nombre del certificado no se comprueba. La firma sí, porque sin ella no queda un saludo inicial sino un intercambio de bytes con quien haya respondido. Para pesar una firma hay que leer la clave pública del certificado, y ese es justo el paso que se niega.
El resultado es por tanto: un SQL Server con su propio certificado generado no se puede medir con esta comprobación. El cliente lee el error real del analizador como detalle, citado en lugar de parafraseado, y la solución es un certificado de verdad en la configuración del servidor, algo que un administrador que hace su servidor monitorizable hace de todos modos.
Dos cosas se dejaron de hacer a propósito. La verificación de la firma no se salta en ese modo, porque rompería la promesa justo en el ajuste que más gente elige y convertiría un rechazo visible en una debilidad invisible. Y todavía no hay un código de error propio para este caso, porque la única forma de detectarlo sería comparar con el texto legible de una biblioteca, y eso es una suposición sobre código ajeno que puede desplazarse en silencio. Eso merece una ronda propia, con su propia prueba.
Tampoco hay un modo sin cifrado alguno, por una razón propia de este protocolo: la contraseña del paquete de inicio de sesión se ofusca con un o exclusivo fijo y un intercambio de cuartetos. Eso es una codificación, no un cifrado. Quien ve los bytes lee la contraseña, así que un modo sin cifrar enviaría la contraseña de un cliente en texto claro por su red, bajo un nombre que sugiere otra cosa.
Lo que encontró el canario
Todo se ejecutó contra servidores reales en lugar de simulacros: catorce comprobaciones contra MongoDB desnudo y con TLS, un servidor RADIUS real y un SQL Server real. El resultado más útil es aburrido. Una contraseña equivocada es en todas partes un fallo de autenticación y nunca un fallo de protocolo. Esa es la distinción que le dice a un cliente si mirar sus credenciales o su red, y una ronda anterior la tenía mal en la comprobación de PostgreSQL: un error durante el paso de autenticación se traducía como «esto no es un servidor PostgreSQL».
Lo que dejaríamos por escrito
- Cuente lo que retiene una dependencia, no solo lo que cuesta. Un tipo de documento genérico convierte «leemos cuatro campos» en un acuerdo en lugar de una propiedad.
- Un controlador trae un constructor de consultas. Si la promesa es que no se puede enviar ninguna consulta arbitraria, el código para enviarla no debería existir en el binario.
- Dos pilas de TLS en un binario son un problema de política, no de tamaño. La que no eligió es la que no puede parchear.
- En un lector escrito a mano, la tabla de saltos es la parte peligrosa. Un tipo desconocido debe ser un error; saltarlo en cero bytes desplaza todo lo que viene después.
- Acepte un número en cada anchura que permita el protocolo. Un campo que llega como coma flotante basta para leer cada fallo como un éxito.
- Un repliegue a un mecanismo más débil debe ser un rechazo, no un repliegue. Verde encima de un inicio de sesión más débil es peor que rojo.
- Si el protocolo hace indistinguible un fallo, dígalo en el mensaje. La redacción es el único lugar donde aún cabe esa distinción.
- Algunos servidores no se pueden medir, y eso va en la documentación y no en un rodeo. El rodeo aquí habría sido dejar de comprobar firmas.
Para que quede claro qué es esto: las tres comprobaciones están construidas, probadas contra servidores reales y detrás de un interruptor de despliegue. Queda sin demostrar el viaje de las tres por la red hasta un agente real de cliente, la contabilidad de RADIUS más allá de la petición de estado, y SQL Server en montajes distintos de una instancia única sobre Linux.
Del trabajo de plataforma en Provibr. Cómo el mismo enfoque de canario destapó dos defectos en las comprobaciones de base de datos anteriores está en un canario que corre contra servidores reales, y cómo llegan al agente las credenciales que usan estas comprobaciones está en un secreto que viaja una sola vez.