Blog ·
Un secreto que viaja una sola vez
Una comprobación de monitorización necesita una contraseña al otro lado. Así llega a una sonda, para un intento, sin caer nunca en una cola ni en un registro.
Por Provibr
Algunas comprobaciones de monitorización necesitan credenciales. No se le puede preguntar a un servidor PostgreSQL si está sano sin iniciar sesión, y una instancia de Redis con contraseña no responde nada útil hasta que se le envía. Así que una regla de monitorización tiene que llevar un secreto desde aquí hasta la sonda o el agente que por casualidad haga el siguiente intento.
La versión barata de esto es una columna. Poner la contraseña en la regla, leerla al enviar, mandarla. Esa versión es incorrecta de una forma que solo se hace visible trece meses después, y esta es la forma que se construyó en su lugar.
Las dos vías que ya existían, y por qué ninguna sirve
Una orden de monitorización llega a una fuente por dos sitios, y ambos son inadecuados.
La tabla de revisiones de la regla es inmutable por diseño, lo cual es exactamente correcto para una revisión y exactamente incorrecto para una credencial: nadie puede rotar una contraseña que vive en una fila que no puede cambiar sin producir una revisión nueva de cada regla que la usaba.
La fila de la orden es peor, porque es una fila de auditoría y viaja por el bus de mensajes camino de la salida. Nuestras propias notas de proyecto llevan un año diciendo que una fila de auditoría de órdenes contiene nombres de campo y nunca valores. Una contraseña que aterrice ahí una vez se queda durante todo el periodo de retención, legible para cualquiera con acceso a la base de datos y copiada a la copia de seguridad que corriera esa noche.
Existe además un canal lateral transitorio para exactamente este tipo de cosas, usado cuando alguien pide una máquina y una contraseña de administración tiene que llegar al agente sin quedar almacenada. Tampoco encaja aquí. Ese canal existe para secretos que el creador de una orden ya conoce, y aquí el creador es un planificador que corre minutos antes y todavía no sabe qué fuente tomará el trabajo.
Por eso los cuatro mensajes de protocolo añadidos en esta ronda no tienen campo de usuario ni campo de contraseña. No es que los campos estén sin usar: es que no existen.
Sellado en el momento del envío
El sobre se crea en el proceso de sesión, justo antes de construir el mensaje, una vez por intento. Eso no es pulcritud. Es el único punto del sistema donde se conocen a la vez las cuatro cosas que tiene que ligar: qué fuente está al otro extremo de esta sesión, qué intento es este, a qué revisión de la regla pertenece y cuándo caduca.
La consecuencia es que no puede estar en ningún otro sitio, y eso es comprobable en lugar de afirmable. Una prueba planta una contraseña conocida, recorre el camino real y la busca en la carga de la orden, en las filas de eventos, en la configuración de comprobación almacenada, en el bus de mensajes, en los bytes crudos del protocolo tanto en UTF-8 como en base64, y en el registro de la pasarela. Exige cero apariciones en los seis. Una lista de afirmaciones sobre dónde no llega un secreto no vale nada; una búsqueda sí vale algo.
El texto en claro existe lo que dura una expresión. Una función lo produce, la siguiente lo consume, y después ya no hay ninguna referencia. JavaScript no puede borrar un búfer, así que lo único disponible es no conservar ninguno. Al otro lado, en Rust, va a un tipo que se pone a cero al liberarse.
La construcción
Un par de claves X25519 efímero por intento, un Diffie-Hellman contra la clave pública de exactamente una fuente, HKDF-SHA256 para derivar la clave de cifrado y AES-256-GCM sobre el texto en claro. Por el cable van la clave pública efímera más un número de un solo uso, el criptograma y la etiqueta.
Las dos claves públicas entran en la sal de la HKDF, de modo que la clave derivada queda ligada a ese par y no a un secreto compartido que en principio podría surgir de otro par. La misma idea que el plan de claves de un esquema híbrido habitual, con la simplificación que aquí es honesta: hay una clave efímera nueva por intento, así que ninguna clave se usa dos veces.
Cuatro campos entran en los datos autenticados adicionales:
| Campo | Qué impide |
|---|---|
| Identificador de intento | Que la misma fuente reutilice un sobre para una medición posterior |
| Identificador de revisión | Que un sobre se pegue bajo otra comprobación |
| Plazo | Que un sobre recuperado de una captura o un volcado sirva más tarde |
| Propósito | Que una credencial de PostgreSQL se dé de comer a una comprobación de Redis |
La identidad de la fuente no está en esa lista porque no hace falta: el sellado va contra una única clave pública, así que otra fuente no lo abre de ninguna manera.
Prefijos de longitud, porque un separador es ambiguo
Los datos adicionales se construyen con la longitud de cada campo delante en vez de con un separador entre campos. Suena a puntillismo hasta que se escribe la alternativa.
Con a:b como formato, el par ("x", "y:z") y el par ("x:y", "z") producen bytes idénticos. El vínculo que estos datos deben establecer deja entonces de distinguir dos situaciones distintas, y quien controle parte de un campo puede desplazar la frontera. Cuatro campos con cuatro bytes de longitud delante no tienen ese problema. Ambos lados lo construyen byte a byte igual, con un vector de prueba compartido para que una divergencia sea una prueba en rojo y no un sobre que ya nadie abre.
Por qué no el cifrador que ya usamos
El almacén de credenciales del agente usa XChaCha20-Poly1305, y la misma construcción aquí daría una sola respuesta a la pregunta de cómo ciframos un secreto en vez de dos. Se midió, y no puede ser:
node v24.20.0 crypto.getCiphers() incluye "chacha20-poly1305"
bun 1.4.0 crypto.getCiphers() devuelve 28 nombres, ninguno chacha
createCipheriv("chacha20-poly1305", ...) -> "Unknown cipher"Producción corre sobre Node, así que allí funcionaría. Pero el banco de pruebas de este repositorio es bun, y un cifrador autenticado que el banco no puede ejecutar es uno cuyo ciclo de ida y vuelta nunca se demuestra. El único control sobre el sobre viviría entonces en una prueba que se salta, y ese es justo el sabor de medición verde en vacío que tanto esfuerzo cuesta evitar.
La alternativa era escribir a mano la pieza que falta. Se hizo de verdad, y funcionó contra el vector de prueba publicado, y después se borró. El código criptográfico que solo existe para esquivar un banco de pruebas no pertenece a un repositorio de producto.
Así que AES-256-GCM, el mismo cifrador autenticado que ya protege este secreto en reposo, presente en ambos entornos de ejecución y ya en el árbol de dependencias del agente. El número de un solo uso de 96 bits por el que se suele acudir a XChaCha no es un problema aquí, porque la razón de ser de XChaCha es poder elegir esos números al azar bajo una clave de vida larga. No hay ninguna clave de vida larga. Cada sobre tiene una efímera nueva, así que cada clave se usa exactamente una vez y una repetición no sería un segundo mensaje bajo la misma clave.
Dónde guarda la fuente su mitad
La clave privada se sitúa junto a la identidad del agente o de la sonda, en un fichero de treinta y dos bytes crudos con modo 0600, al lado de la clave privada del certificado de cliente. No en el almacén de credenciales, y las tres razones merecen escribirse porque fueron la decisión real.
Revocar la borra gratis: la revocación ya elimina ese directorio, y una clave guardada en cualquier otro sitio necesitaría su propia regla de limpieza, que la primera ronda que la olvide dejará atrás en una máquina revocada. Pertenece a un alta concreta, y la plataforma guarda la mitad pública junto al certificado, así que un alta nueva debería producir un par nuevo, cosa que hace porque el directorio está vacío. Y el almacén no siempre está disponible: se desbloquea con material derivado del fichero de licencia, y una sonda de monitorización no tiene licencia y por tanto tampoco almacén. Poner ahí la clave obligaría a un segundo mecanismo para sondas, y entonces habría dos respuestas a una pregunta.
Tampoco se mantiene en memoria, y es deliberado, aunque resulte tentador para una sonda que no almacena nada. La plataforma guarda la mitad pública, así que tras un reinicio dejaría de coincidir y todos los sobres quedarían cerrados hasta que alguien volviera a dar de alta la sonda. Un punto de monitorización que tras un reinicio deja de hacer comprobaciones autenticadas en silencio es exactamente el tipo de avería que nadie nota.
Nunca un plan de reserva
Si una fuente no tiene clave, la comprobación no se ejecuta. Informa de un error de configuración que nombra al intermediario como no disponible, y ese es todo el comportamiento. No hay reserva en texto claro y, más importante, no hay un estado de caída engañoso para el destino, porque una comprobación que no se puede medir jamás debe parecerse a un servicio roto. Esa es una alerta que manda a alguien a la máquina equivocada a las tres de la madrugada.
Lo que anotaríamos
- Una fila de auditoría no es un canal de entrega. Lo que se escriba ahí estará todo el periodo de retención, en cada copia de seguridad.
- No añada el campo. Un mensaje de protocolo sin campo de contraseña no puede filtrar ninguna por un camino de código que alguien olvide revisar.
- Selle donde se conocen los vínculos. Si quien crea un trabajo aún no sabe quién lo ejecutará, el secreto no puede crearse con el trabajo.
- Demuestre la ausencia buscando, no afirmando. Plante un valor y cácelo en cada serialización, base64 incluida.
- Ponga prefijos de longitud en sus datos autenticados. Un separador hace que dos tuplas distintas produzcan los mismos bytes.
- Elija la primitiva que su banco de pruebas puede ejecutar. Una construcción más fuerte que nunca se ejercita es, en la práctica, más débil que una buena que sí.
- Una clave que es una identidad pertenece a la identidad. Así la revocación la limpia y el alta nueva la sustituye, sin una regla que alguien deba recordar.
Para dejar claro qué es esto: la monitorización está detrás de un interruptor de despliegue apagado por omisión, así que nada de esto se puede comprar hoy. Dos cosas están sin demostrar. El intercambio de autenticación basado en contraseña solo se midió contra la implementación de referencia de la biblioteca del protocolo y no contra un servidor real. Y el binario de la sonda escrito en Rust no se ejercitó por el cable en esta ronda.
Del trabajo de plataforma de Provibr. Por qué una sonda trata como no fiable todo mensaje que recibe está en una sonda a la que no se puede convencer de nada, y por qué las credenciales de los paneles propios de un cliente no llegan nunca hasta nosotros está en dónde deben vivir las credenciales de panel.