Webhooks explicados: cargas firmadas y estrategias de reintento
Has construido una API y ahora necesitas notificar a otros servicios cuando ocurren eventos. El polling es ineficiente, así que implementas webhooks. Pero pronto te enfrentas a dos problemas comunes: cómo asegurar que la carga no ha sido manipulada y qué hacer cuando el endpoint receptor está caído. Este artículo explica cómo usar cargas firmadas y estrategias de reintento para hacer tus webhooks seguros y confiables.
Por qué los webhooks necesitan firmas y reintentos
Los webhooks son callbacks HTTP activados por eventos. Son simples de implementar pero introducen desafíos de seguridad y confiabilidad. Sin firmas, un atacante podría falsificar solicitudes a tu endpoint, llevando a corrupción de datos o acciones no autorizadas. Sin reintentos, fallos transitorios (como cortes de red o errores temporales del servidor) causan eventos perdidos.
Las cargas firmadas resuelven la autenticidad: el emisor firma la carga con un secreto y el receptor la verifica. Los reintentos resuelven la confiabilidad: el emisor reintenta entregas fallidas con retroceso exponencial.
Cómo funcionan las cargas firmadas
Una carga firmada incluye una firma criptográfica generada a partir del cuerpo de la solicitud y un secreto compartido. El método más común es HMAC (Hash-based Message Authentication Code) con SHA-256.
Generando la firma
El emisor calcula el HMAC del cuerpo crudo de la solicitud usando la clave secreta. La firma se envía en un encabezado, a menudo junto con una marca de tiempo para prevenir ataques de repetición.
// Ejemplo en Node.js: generando firma HMAC
const crypto = require('crypto');
function signPayload(payload, secret) {
const timestamp = Math.floor(Date.now() / 1000);
const signedPayload = `${timestamp}.${JSON.stringify(payload)}`;
const signature = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
return { timestamp, signature };
}
Incluye la marca de tiempo y la firma en los encabezados, por ejemplo, X-Webhook-Timestamp y X-Webhook-Signature.
Verificando la firma
El receptor debe reconstruir la carga firmada usando la marca de tiempo recibida y el cuerpo crudo, luego calcular el HMAC y compararlo de forma segura.
// Ejemplo en Node.js: verificando firma
function verifySignature(rawBody, timestamp, receivedSignature, secret) {
const signedPayload = `${timestamp}.${rawBody}`;
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
// Usar comparación timing-safe
return crypto.timingSafeEqual(
Buffer.from(expectedSignature, 'hex'),
Buffer.from(receivedSignature, 'hex')
);
}
Siempre usa una comparación timing-safe para prevenir ataques de temporización. Además, verifica que la marca de tiempo sea reciente (por ejemplo, dentro de 5 minutos) para mitigar ataques de repetición.
Diseñando una estrategia de reintento
Los reintentos son esenciales para una entrega confiable de webhooks. Pero los reintentos ingenuos pueden abrumar al receptor o causar procesamiento duplicado. Una buena estrategia de reintento incluye:
- Retroceso exponencial: Espera más tiempo entre cada reintento (por ejemplo, 1s, 2s, 4s, 8s...).
- Máximo de reintentos: Limita el número de intentos (por ejemplo, 5) para evitar bucles infinitos.
- Cola de mensajes muertos: Después del máximo de reintentos, almacena el evento para inspección manual.
- Idempotencia: Incluye un ID de evento único para que el receptor pueda deduplicar.
Aquí hay un bucle de reintento simple en pseudocódigo:
function deliverWithRetry(url, payload, maxRetries = 5) {
let attempt = 0;
while (attempt < maxRetries) {
try {
const response = await fetch(url, { method: 'POST', body: payload });
if (response.ok) return;
} catch (error) {
// Registrar error
}
attempt++;
const delay = Math.pow(2, attempt) * 1000; // retroceso exponencial
await new Promise(resolve => setTimeout(resolve, delay));
}
// Mover a cola de mensajes muertos
}
Considera añadir jitter al retroceso para evitar problemas de estampida.
Comparación: Polling vs Webhooks con reintentos
| Aspecto | Polling | Webhooks con reintentos |
|---|---|---|
| Latencia | Alta (depende del intervalo) | Baja (casi en tiempo real) |
| Carga del servidor | Constante (incluso sin eventos) | Basada en eventos |
| Confiabilidad | Alta (controlada por el receptor) | Depende de la estrategia de reintento |
| Seguridad | Autenticación de API estándar | Requiere cargas firmadas |
Paso a paso: Implementando webhooks con firmas y reintentos
- Genera una clave secreta para cada endpoint de webhook y compártela de forma segura.
- Al ocurrir un evento, construye la carga (JSON) y calcula la firma usando HMAC-SHA256 con el secreto y una marca de tiempo.
- Envía la solicitud POST al endpoint con los encabezados:
Content-Type: application/json,X-Webhook-TimestampyX-Webhook-Signature. - En el lado del receptor, verifica la firma usando el cuerpo crudo y la marca de tiempo. Rechaza si es inválida o la marca de tiempo es demasiado antigua.
- Si la entrega falla (respuesta no 2xx o error de red), reintenta con retroceso exponencial hasta un número máximo de intentos.
- Después del máximo de reintentos, registra el fallo y opcionalmente alerta; almacena el evento en una cola de mensajes muertos para reproducción manual.
- Asegura la idempotencia en el receptor usando un ID de evento para ignorar duplicados.
Errores comunes y mejores prácticas
- No usar el cuerpo crudo: Muchos frameworks analizan JSON y alteran el cuerpo. Usa el cuerpo crudo para la verificación de firma.
- Ignorar las marcas de tiempo: Sin validación de marca de tiempo, los ataques de repetición son posibles.
- Sin idempotencia: Los reintentos pueden causar procesamiento duplicado. Usa claves de idempotencia.
- Reintentos infinitos: Siempre establece un límite máximo de reintentos y una cola de mensajes muertos.
- No registrar: Registra los intentos de entrega y fallos para depuración.
Preguntas frecuentes
¿Qué es una carga de webhook firmada?
Una carga de webhook firmada incluye una firma criptográfica (por ejemplo, HMAC) que permite al receptor verificar que la solicitud se originó del emisor esperado y no fue manipulada.
¿Cuántas veces debo reintentar un webhook fallido?
Una práctica común es de 3 a 5 reintentos con retroceso exponencial. El número exacto depende de tu SLA y la criticidad del evento. Siempre usa una cola de mensajes muertos para eventos que excedan el límite de reintentos.
¿Puedo usar webhooks sin firmas?
No se recomienda. Sin firmas, tu endpoint es vulnerable a solicitudes falsificadas. Siempre usa firmas (como HMAC) y valida las marcas de tiempo para prevenir ataques de repetición.
Al depurar entregas de webhooks, inspeccionar los registros del servidor es crucial. Si estás usando Nginx, el Analizador de registros de Nginx puede ayudarte a analizar los registros de acceso para rastrear solicitudes de webhooks e identificar fallos.