Ir al contenido

Webhook firmado

El webhook firmado es el conector para destinos propios: una herramienta de incidentes que acepta fuentes de alerta genéricas, una automatización que opera usted mismo o un script. Los destinos de chat, PagerDuty y Opsgenie tienen conectores propios; esta página trata solo del tipo genérico. Todo lo que sigue describe lo que se entrega hoy.

En la aplicación, en Integraciones, cree un conector del tipo Webhook y asígnele una URL (solo propietario y administrador). Perstat genera un secreto de firma y lo muestra exactamente una vez; después solo aparece enmascarado y únicamente puede rotarse. La rotación sustituye el secreto de inmediato. No hay periodo de solapamiento en el que ambos sean válidos.

El secreto tiene la forma whsec_ seguido de 64 caracteres hexadecimales en minúsculas. Trate la cadena completa como una clave opaca.

Las URL http:// se aceptan para destinos locales; use https:// para todo lo que atraviese una red que no sea suya. El destino debe ser alcanzable públicamente: un host que resuelva a una dirección privada, de bucle local o de enlace local se rechaza en cada envío.

Lo que recibe un punto de entrega depende de si su organización tiene una cadena de escalado activa: un plan con rotación de guardias (a partir de Sentinel, vea planes y límites), la guardia activada y la cadena habilitada.

Sin cadena activa (en cualquier otro plan, o con la guardia o la cadena desactivadas), cada punto de entrega activo recibe los eventos a los que está suscrito en Integraciones:

Evento Cuándo
incident.opened Se abre un incidente, crítico o degradado. Una alerta retenida por una regla de dependencia se entrega en cuanto se libera.
incident.resolved El incidente se cierra.
incident.test Pulsa Probar en el conector. Se entrega de forma síncrona, solo a este punto de entrega.

Con una cadena activa, la cadena dirige los mensajes de incidente salientes. Un punto de entrega se entera de una caída solo si un paso lo nombra como destinatario, en el minuto 0 o después, y solo para incidentes críticos:

Evento Cuándo
incident.escalation Se dispara un paso que nombra el punto de entrega. Es también el primer mensaje para un paso en el minuto 0: el evento dice «este incidente está sin confirmar», no «acaba de abrirse uno nuevo».
incident.resolved El incidente se cierra. Va a cada punto de entrega que un paso alertó, suscrito o no, y a cada punto de entrega activo suscrito a él. Lo que la cadena abre, la cadena lo cierra.
incident.test Como arriba.

Con una cadena activa, la suscripción a incident.opened no tiene efecto. Un paso omite un punto de entrega desactivado, y omite todos los puntos de entrega mientras el incidente está confirmado, mientras su monitor está desactivado o archivado y mientras el monitor se encuentra en una ventana de mantenimiento. Un punto de entrega desactivado durante el incidente recibe igualmente el fin de alerta de un incidente sobre el que fue alertado.

Un POST por evento, con cuerpo JSON:

POST <su URL>
content-type: application/json
user-agent: perstat-webhook/1.0
x-perstat-signature: sha256=<64 caracteres hexadecimales en minúsculas>
{
"event": "incident.escalation",
"incident": {
"id": "inc_9f3c2a7e1b4d48c0a6e5f1d2c3b4a596",
"severity": "critical",
"cause": "HTTP 503",
"summary": "La API no responde",
"monitor": { "id": "mon_4b1e8d2c7a9f4e6b8c0d1e2f3a4b5c6d", "name": "API" },
"started_at_unix": 1757930400,
"resolved_at_unix": null
},
"organization": { "id": "org_2d7a9c4e1f3b4a5c8d6e7f8a9b0c1d2e", "name": "Acme" }
}
Campo Significado
event Uno de los eventos anteriores.
incident.id Estable en todos los eventos de un mismo incidente. Úselo para abrir y cerrar el mismo caso de su lado; PagerDuty lo recibe como dedup_key, Opsgenie como alias.
incident.severity critical o degraded. A través de una cadena activa solo verá critical; la prueba también envía critical.
incident.cause Texto libre: el detalle de la primera comprobación fallida, o la causa introducida en un incidente manual. test en una entrega de prueba.
incident.summary Opcional, null si está vacío.
incident.monitor id y name del monitor vinculado. null en un incidente manual sin monitor y en la prueba.
incident.started_at_unix Segundos Unix.
incident.resolved_at_unix Segundos Unix, null hasta que el incidente se resuelve.
organization id y name de la organización a la que pertenece el punto de entrega.

El cuerpo es estable y solo se amplía de forma aditiva: lea lo que necesite e ignore los campos que no conozca.

La cabecera lleva sha256= seguido del HMAC-SHA256 del cuerpo bruto de la petición, con la cadena del secreto como clave, exactamente como se le mostró: sus bytes ASCII son la clave, no se decodifica. Calcule el MAC sobre los bytes recibidos, antes de cualquier análisis JSON, y compare en tiempo constante.

Node:

import { createHmac, timingSafeEqual } from "node:crypto";
function verify(secret, rawBody, header) {
const expected = "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex");
return expected.length === header.length
&& timingSafeEqual(Buffer.from(expected), Buffer.from(header));
}

Python:

import hashlib
import hmac
def verify(secret: str, raw_body: bytes, header: str) -> bool:
expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, header)

Un cuerpo que no supera la verificación no vino de Perstat, o fue alterado por el camino. Responda 401 y no haga nada más.

La firma no lleva marca de tiempo ni identificador de entrega. Una petición capturada puede por tanto reproducirse y volverá a superar la verificación. Entregue sobre TLS y trate un par (incident.id, event) repetido como ya atendido: para una herramienta de incidentes, cerrar dos veces una alerta es inofensivo, abrirla dos veces es ruido.

  • Una respuesta 2xx cuenta como entregada. Cualquier otra, incluida una 3xx, cuenta como fallida; no se siguen redirecciones.
  • El tiempo de espera es de 10 segundos por entrega. Responda rápido y haga el trabajo después.
  • Un intento por evento. No hay cola de reintentos; una entrega fallida sigue fallida.
  • El resultado de la última entrega, con el texto de error si lo hubo, se muestra en el conector en Integraciones y junto al paso en la cadena.

Como no hay reintento, un receptor brevemente caído pierde ese evento. Si necesita el cuadro completo en lugar del último mensaje, lea el incidente a través de la API usando incident.id.