Aller au contenu

Webhook signé

Le webhook signé est le connecteur pour vos propres destinations : un outil d’incidents qui accepte des sources d’alerte génériques, une automatisation que vous exploitez vous-même, ou un script. Les destinations de chat, PagerDuty et Opsgenie ont leurs propres connecteurs ; cette page ne traite que du type générique. Tout ce qui suit décrit ce qui est livré aujourd’hui.

Dans l’application, sous Intégrations, créez un connecteur de type Webhook et donnez-lui une URL (propriétaire et administrateur seulement). Perstat génère un secret de signature et l’affiche exactement une fois ; ensuite il n’apparaît que masqué et ne peut plus qu’être renouvelé. Le renouvellement remplace le secret immédiatement. Il n’y a pas de période de recouvrement pendant laquelle les deux seraient valables.

Le secret a la forme whsec_ suivi de 64 caractères hexadécimaux en minuscules. Traitez la chaîne entière comme une clé opaque.

Les URL en http:// sont acceptées pour des destinations locales ; utilisez https:// pour tout ce qui traverse un réseau qui n’est pas le vôtre. La cible doit être joignable publiquement : un hôte qui se résout en adresse privée, de bouclage ou de lien local est refusé à chaque envoi.

Ce que reçoit un point d’arrivée dépend de l’existence d’une chaîne d’escalade active dans votre organisation : un forfait avec le planning d’astreinte (à partir de Sentinel, voir forfaits et limites), une astreinte activée et une chaîne active.

Sans chaîne active (dans tout autre forfait, ou si l’astreinte ou la chaîne sont désactivées), chaque point d’arrivée actif reçoit les événements auxquels il est abonné sous Intégrations :

Événement Quand
incident.opened Un incident s’ouvre, critique ou dégradé. Une alerte retenue par une règle de dépendance est livrée dès qu’elle est libérée.
incident.resolved L’incident se ferme.
incident.test Vous appuyez sur Test sur le connecteur. Livré de façon synchrone, à ce seul point d’arrivée.

Avec une chaîne active, c’est la chaîne qui pilote les messages d’incident sortants. Un point d’arrivée n’apprend une panne que si une étape le nomme comme cible, à la minute 0 ou plus tard, et seulement pour les incidents critiques :

Événement Quand
incident.escalation Une étape qui nomme le point d’arrivée se déclenche. C’est aussi le premier message pour une étape à la minute 0 : l’événement dit « cet incident n’est pas acquitté », pas « un nouvel incident vient de s’ouvrir ».
incident.resolved L’incident se ferme. Il part vers chaque point d’arrivée qu’une étape a alerté, abonné ou non, et vers chaque point d’arrivée actif qui y est abonné. Ce que la chaîne ouvre, la chaîne le ferme.
incident.test Comme ci-dessus.

Avec une chaîne active, l’abonnement à incident.opened reste sans effet. Une étape ignore un point d’arrivée désactivé, et elle ignore tous les points d’arrivée tant que l’incident est acquitté, tant que son moniteur est désactivé ou archivé, et tant que le moniteur se trouve dans une fenêtre de maintenance. Un point d’arrivée désactivé en cours d’incident reçoit malgré tout la fin d’alerte d’un incident pour lequel il a été alerté.

Un POST par événement, avec un corps JSON :

POST <votre URL>
content-type: application/json
user-agent: perstat-webhook/1.0
x-perstat-signature: sha256=<64 caractères hexadécimaux en minuscules>
{
"event": "incident.escalation",
"incident": {
"id": "inc_9f3c2a7e1b4d48c0a6e5f1d2c3b4a596",
"severity": "critical",
"cause": "HTTP 503",
"summary": "L'API ne répond pas",
"monitor": { "id": "mon_4b1e8d2c7a9f4e6b8c0d1e2f3a4b5c6d", "name": "API" },
"started_at_unix": 1757930400,
"resolved_at_unix": null
},
"organization": { "id": "org_2d7a9c4e1f3b4a5c8d6e7f8a9b0c1d2e", "name": "Acme" }
}
Champ Signification
event L’un des événements ci-dessus.
incident.id Stable sur tous les événements d’un même incident. Servez-vous-en pour ouvrir et fermer le même cas de votre côté ; PagerDuty le reçoit comme dedup_key, Opsgenie comme alias.
incident.severity critical ou degraded. Par une chaîne active vous ne voyez que critical ; le test envoie aussi critical.
incident.cause Texte libre : le détail de la première vérification échouée, ou la cause saisie sur un incident manuel. test pour une livraison de test.
incident.summary Facultatif, null si vide.
incident.monitor id et name du moniteur lié. null pour un incident manuel sans moniteur et pour le test.
incident.started_at_unix Secondes Unix.
incident.resolved_at_unix Secondes Unix, null tant que l’incident n’est pas résolu.
organization id et name de l’organisation à laquelle appartient le point d’arrivée.

Le corps est stable et n’est étendu que par ajout : lisez ce dont vous avez besoin et ignorez les champs que vous ne connaissez pas.

L’en-tête porte sha256= suivi du HMAC-SHA256 du corps brut de la requête, avec pour clé la chaîne du secret exactement telle qu’elle vous a été montrée : ses octets ASCII sont la clé, elle n’est pas décodée. Calculez le MAC sur les octets reçus, avant toute analyse JSON, et comparez en temps constant.

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 corps qui échoue à la vérification ne vient pas de Perstat, ou a été modifié en chemin. Répondez 401 et ne faites rien d’autre.

La signature ne porte ni horodatage ni identifiant de livraison. Une requête capturée peut donc être rejouée et passera de nouveau la vérification. Livrez sur TLS et traitez un couple (incident.id, event) répété comme déjà traité : pour un outil d’incidents, fermer deux fois une alerte est sans conséquence, l’ouvrir deux fois est du bruit.

  • Une réponse 2xx vaut livraison. Tout le reste, y compris un 3xx, vaut échec ; les redirections ne sont pas suivies.
  • Le délai est de 10 secondes par livraison. Répondez vite et faites le travail ensuite.
  • Une tentative par événement. Il n’y a pas de file de reprise ; une livraison échouée reste échouée.
  • Le résultat de la dernière livraison, avec le texte d’erreur s’il y en a eu un, est affiché sur le connecteur sous Intégrations et à côté de l’étape dans la chaîne.

Comme il n’y a pas de reprise, un récepteur brièvement indisponible manque cet événement. S’il vous faut le tableau complet plutôt que le dernier message, lisez l’incident via l’API à partir de incident.id.