BookinglyTech News
Software

crt.sh, reintentos y el bug que falla en silencio: lecciones de un monitor de certificados

Un desarrollador cuenta los cuatro fallos que le costó hacer fiable un actor de Apify que vigila los logs de Certificate Transparency contra crt.sh, un servicio comunitario y gratuito

3 min de lecturaDev.to0 vistas

Un desarrollador ha montado un actor de Apify que vigila los logs de Certificate Transparency de un dominio —para detectar a tiempo certificados emitidos a subdominios inesperados o a dominios que imitan al propio— y ha contado lo que le costó convertirlo en algo fiable. La consulta en sí le llevó diez minutos. Lo demás, bastante más, y el resumen son cuatro problemas que cualquiera que escriba scrapers contra servicios ajenos reconocerá.

404 no significa "no hay resultados"

El servicio es crt.sh, una búsqueda comunitaria y gratuita sobre los logs de CT. Bajo carga devuelve páginas HTML de error desnudas, no JSON. El autor interpretó al principio un 404 como "ningún certificado encontrado", que es una lectura razonable. El problema es que lanzaba la misma consulta dos veces seguidas y obtenía un 404 una vez y el resultado real la siguiente. Con 502, 503 y 504 pasa lo mismo: ninguno de esos códigos es una señal fiable de cero resultados en este servicio, así que todos hay que reintentarlos en lugar de darlos por buenos.

Presupuesto de reintentos y timeout

Empezó con 4 intentos y backoff exponencial, y le pareció generoso. Durante una mala racha vio encadenar 6 respuestas 502 consecutivas antes de un éxito, y la tasa de fallo del actor en 30 días móviles se le fue a cerca del 46%. Lo subió a 8 intentos y puso un tope al backoff en vez de dejar que creciera sin límite, porque una respuesta correcta puede tardar entre 10 y 20 segundos con crt.sh cargado: el bucle necesita tiempo real, no más intentos disparados unos contra otros.

El tercer fallo es de manual: fetch() no lleva timeout. Una ejecución de prueba se quedó colgada sin 502, sin respuesta lenta y sin nada, de forma indefinida. Si la petición nunca resuelve, nunca llega al punto donde entra la lógica de reintento. La solución fue un AbortController con timeout por intento, de modo que una conexión muerta cuenta como fallo y se reintenta como cualquier otro.

El campo que se esfumó

El cuarto es el más traicionero. El actor filtra por fecha y ordena de más nuevo a más antiguo usando el campo entry_timestamp de crt.sh. En algún momento, la salida JSON dejó de incluir ese campo para esa consulta concreta. Sin error y sin aviso: undefined. Eso hacía que new Date(undefined) >= startDate se evaluara como falso en cada entrada, así que el actor devolvía cero resultados siempre, hubiera datos o no. Un fallo que se manifiesta callándose en vez de lanzar una excepción, que es lo peor que le puede pasar a una herramienta de monitorización. Lo cambió por not_before, que siempre está presente.

El código de reintentos está en el repositorio. El actor en sí corre en Apify y se puede apuntar a cualquier dominio.

Ninguno de los cuatro fallos es exótico: páginas de error bajo carga, un presupuesto demasiado corto, un fetch sin timeout y un campo que se desvanece sin decir nada. Apilados son la diferencia entre algo que funciona cuando lo pruebas y algo en lo que puedes confiar cuando su única misión es avisarte de certificados que no esperabas. La parte incómoda es que el fallo silencioso hay que ir a buscarlo; no se presenta solo.