Webhooks
fremforge envoie des webhooks compatibles à la fois avec le schéma de signature natif de Forgejo et avec le schéma compatible GitHub, dans la même livraison. Les récepteurs conçus pour l’un ou l’autre fonctionnent sans modification. Choisissez l’en-tête auquel vous faites confiance et vérifiez-le avec votre secret partagé.
Cette page documente le contrat : ce que fremforge envoie, comment le vérifier, et quelles garanties vous avez sur les nouvelles tentatives et TLS.
Où configurer quoi : les webhooks au niveau de l’organisation (événements auxquels s’abonner, URL cible, secret de signature) se créent dans l’administration fremforge, à /<org>/_admin/webhooks, qui contient aussi l’historique des livraisons et le renvoi. La page des hooks de Forgejo au niveau de l’organisation, /org/<org>/settings/hooks, redirige vers cette page. Les hooks Forgejo au niveau du dépôt (limités à un seul dépôt) se configurent dans l’interface de Forgejo, à /<org>/<repo>/settings/hooks.
Aide-mémoire
POST <your-webhook-url> HTTP/1.1
Host: receiver.example.com
User-Agent: fremforge-webhook/1
Content-Type: application/json
X-Forgejo-Event: push
X-Gitea-Delivery: 7c4e2ecd-79b5-4e1d-8eaa-0d1c2c8e5fbd
X-Forgejo-Signature: 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
X-Hub-Signature-256: sha256=9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
X-Hub-Signature: sha1=2fd4e1c67a2d28fced849ee1bb76e7391b93eb12
{<your event payload as JSON>}| En-tête | Signification |
|---|---|
X-Forgejo-Event | Type d’événement (push, pull_request, issues, release, etc.) |
X-Gitea-Delivery | UUID unique pour chaque tentative de livraison, à utiliser pour la protection contre le rejeu |
X-Forgejo-Signature | HMAC-SHA256 du corps brut, encodé en hexadécimal, natif Forgejo |
X-Hub-Signature-256 | HMAC-SHA256 du corps brut, encodé en hexadécimal avec le préfixe sha256=, compatible GitHub |
X-Hub-Signature | HMAC-SHA1 du corps brut, ancienne compatibilité GitHub, ne vous y fiez pas |
La clé de signature est le secret que vous définissez sur le webhook dans l’interface d’administration de l’organisation. Secret vide = aucune signature émise ; les en-têtes X-*-Signature sont absents. La politique par défaut de la plateforme fremforge refuse la configuration d’un webhook sans secret dans les nouvelles organisations (phase 1.5 et suivantes).
Pourquoi deux en-têtes de signature
Les récepteurs conçus pour GitHub vérifient généralement X-Hub-Signature-256. Les récepteurs conçus nativement pour Forgejo / Gitea vérifient X-Forgejo-Signature. Émettre les deux dans la même livraison permet de migrer depuis GitHub sans toucher au code du récepteur ; les nouvelles intégrations peuvent choisir la forme qui leur semble la plus naturelle.
Les deux en-têtes portent exactement la même valeur HMAC-SHA256, calculée sur le même corps brut. Vérifiez l’un ou l’autre ; si les deux réussissent, la requête est authentique.
Vérifier un webhook : recettes
Toutes les recettes ci-dessous vérifient l’en-tête compatible GitHub X-Hub-Signature-256. Pour les adapter à X-Forgejo-Signature, supprimez l’étape qui retire le préfixe sha256=.
Node.js
import crypto from 'node:crypto';
function verifyFremforgeWebhook(rawBody, signatureHeader, secret) {
if (!signatureHeader || !signatureHeader.startsWith('sha256=')) return false;
const provided = Buffer.from(signatureHeader.slice('sha256='.length), 'hex');
const expected = crypto.createHmac('sha256', secret).update(rawBody).digest();
return provided.length === expected.length &&
crypto.timingSafeEqual(provided, expected);
}
// In an Express / Hono / Fastify route:
// const isAuthentic = verifyFremforgeWebhook(
// req.rawBody, // Buffer or string of the EXACT bytes received
// req.headers['x-hub-signature-256'],
// process.env.FREMFORGE_WEBHOOK_SECRET,
// );
Le rawBody DOIT être exactement les octets reçus, avant toute analyse JSON. Un middleware qui resérialise produira une empreinte différente et un faux négatif.
Python
import hmac
import hashlib
def verify_fremforge_webhook(raw_body: bytes, signature_header: str, secret: str) -> bool:
if not signature_header or not signature_header.startswith("sha256="):
return False
provided = signature_header[len("sha256="):]
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(provided, expected)
# In a Flask / FastAPI handler:
# raw = request.get_data() # Flask: raw bytes; do NOT parse JSON first
# sig = request.headers.get("X-Hub-Signature-256", "")
# if not verify_fremforge_webhook(raw, sig, os.environ["FREMFORGE_WEBHOOK_SECRET"]):
# abort(401)Go
package webhook
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"strings"
)
func VerifyFremforge(rawBody []byte, signatureHeader, secret string) bool {
if !strings.HasPrefix(signatureHeader, "sha256=") {
return false
}
provided, err := hex.DecodeString(strings.TrimPrefix(signatureHeader, "sha256="))
if err != nil {
return false
}
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(rawBody)
expected := mac.Sum(nil)
return hmac.Equal(provided, expected)
}Ruby
require 'openssl'
def verify_fremforge_webhook(raw_body, signature_header, secret)
return false unless signature_header&.start_with?('sha256=')
expected = OpenSSL::HMAC.hexdigest('sha256', secret, raw_body)
Rack::Utils.secure_compare(signature_header.sub('sha256=', ''), expected)
endRust
use hmac::{Hmac, Mac};
use sha2::Sha256;
use subtle::ConstantTimeEq;
type HmacSha256 = Hmac<Sha256>;
pub fn verify_fremforge_webhook(
raw_body: &[u8],
signature_header: &str,
secret: &[u8],
) -> bool {
let Some(hex_sig) = signature_header.strip_prefix("sha256=") else {
return false;
};
let Ok(provided) = hex::decode(hex_sig) else {
return false;
};
let mut mac = HmacSha256::new_from_slice(secret).expect("HMAC key");
mac.update(raw_body);
let expected = mac.finalize().into_bytes();
expected.as_slice().ct_eq(&provided).into()
}Protection contre le rejeu
La signature HMAC prouve l’authenticité (la charge utile provient de fremforge, qui connaît votre secret partagé), mais pas la fraîcheur. Un attaquant qui capture un webhook signé valide peut le rejouer indéfiniment, jusqu’à la rotation du secret.
Utilisez l’UUID X-Gitea-Delivery pour vous défendre contre le rejeu :
- Tenez un petit cache (Redis, mémoire, voire SQLite) des identifiants de livraison récents.
- Rejetez tout webhook entrant dont l’identifiant de livraison figure déjà dans le cache.
- Fixez la durée de vie du cache à 24 heures. Les nouvelles tentatives de fremforge tiennent toutes dans environ 7,5 heures, cette fenêtre les couvre donc ; des rejeux plus anciens sont irréalistes compte tenu de la cadence de rotation des secrets.
Ne vous passez de la défense contre le rejeu que si votre gestionnaire est entièrement idempotent (la même livraison appliquée deux fois produit le même état final). Les gestionnaires idempotents sont la solution la plus propre ; le cache anti-rejeu est la solution de repli quand le gestionnaire ne l’est pas.
Exigence TLS
fremforge ne livre les webhooks qu’en HTTPS. Les URL de récepteur en HTTP sont refusées au moment de la configuration du webhook. Le récepteur doit :
- Présenter un certificat TLS valide pour le nom d’hôte configuré (n’importe quelle autorité de certification publique convient : Let’s Encrypt, ZeroSSL, ACM, commerciale).
- Prendre en charge TLS 1.2 ou supérieur.
- Ne pas dépendre d’astuces SNI qui ne fonctionnent qu’avec les navigateurs : le TLS sortant de fremforge se comporte comme un client HTTP, pas comme un navigateur.
Les certificats auto-signés sont refusés. Si vous testez en local, utilisez plutôt un tunnel (ngrok, Cloudflare Tunnel, Bunny Origin Shield) avec un vrai certificat.
Comportement des nouvelles tentatives
fremforge ne considère une livraison comme réussie que sur une réponse 2xx. Toute autre réponse donne lieu à une nouvelle tentative : un 3xx (les redirections ne sont pas suivies), un 4xx, un 5xx, un dépassement de délai (10 secondes), ou une erreur de connexion ou TLS. Pour arrêter les nouvelles tentatives pour un événement qui ne vous intéresse pas, répondez 200. Réservez les 4xx aux charges utiles mal formées, et attendez-vous à ce qu’elles fassent aussi l’objet de nouvelles tentatives.
Calendrier des nouvelles tentatives : six tentatives en tout. Après le premier essai, les nouvelles tentatives ont lieu 2 minutes, 5 minutes, 15 minutes, 1 heure et 6 heures après la tentative précédente, soit environ 7,5 heures au total. Après le sixième échec, la livraison est marquée dlq dans le journal des livraisons de webhooks de l’interface d’administration de l’organisation, et n’est plus retentée. Après cinq livraisons échouées d’affilée, les contacts de facturation de l’organisation reçoivent un e-mail nommant la destination.
Pour rejouer manuellement une livraison échouée, utilisez le bouton Rejouer du journal des livraisons de webhooks dans l’interface d’administration de l’organisation. Il n’existe pas de point de terminaison REST pour cette opération.
Configurer un webhook
La configuration des webhooks se fait à deux endroits, selon ce que vous voulez faire :
- Diffusion sortante de fremforge vers votre point de terminaison : administration de l’organisation, Intégration, Webhooks (
https://frem.sh/<your-org>/_admin/webhooks). Ajoutez-y la destination, le secret de signature et le filtre d’événements ; consultez-y l’état des livraisons et la file de rebut. - Hooks natifs Forgejo par dépôt : l’interface propre à Forgejo, à
https://frem.sh/<your-org>/<repo>/settings/hooks. La page de Forgejo au niveau de l’organisation,https://frem.sh/org/<your-org>/settings/hooks, redirige vers l’onglet Webhooks ci-dessus. Ce sont les anciens hooks Forgejo. Le hook système de fremforge, valable pour toute l’instance, capture les mêmes événements de façon centralisée : la plupart des clients n’ont donc plus besoin de configurer des hooks par dépôt.
Pour ajouter une destination sortante gérée par fremforge :
- Administration de l’organisation, Intégration, Webhooks, Ajouter une destination.
- URL cible : doit commencer par
https://. - Content type :
application/jsonrecommandé. - Secret : générez une valeur à forte entropie (≥ 32 octets) ; conservez-la dans le gestionnaire de secrets de votre récepteur. Ne réutilisez pas un secret d’un webhook à l’autre. Des récepteurs différents doivent utiliser des secrets différents.
- Événements : choisissez les événements qui déclenchent le webhook.
- Enregistrez.
Le secret du webhook est stocké chiffré au repos dans la base de données de fremforge. fremforge ne le renvoie jamais via l’API après sa création ; pour le faire tourner, modifiez le webhook et saisissez une nouvelle valeur (l’ancienne est remplacée de façon atomique).
Une destination peut aussi recevoir des événements de sécurité : nouveaux résultats, correspondances de malware, pulls bloqués, violations de politique et acceptations de risque arrivées à échéance. Ils s’activent un par un, par leur nom, et peuvent être livrés sous forme d’incidents ServiceNow. Voir Security alert webhooks.
Dépannage
Signatures qui ne correspondent pas
La plupart des signatures qui ne correspondent pas viennent d’une resérialisation du corps. Les coupables habituels :
- Le middleware JSON
body-parserd’Express qui s’exécute AVANT votre vérificateur : celui-ci voit une charge utile resérialisée, et non les octets envoyés par fremforge. Solution : capturez le corps brut avant l’analyse (express.raw(), puis analyse JSON manuelle). - Les frameworks qui, « pour vous rendre service », reformatent ou canonicalisent le JSON avant de l’exposer : même solution.
- Les différences d’encodage : fremforge envoie de l’UTF-8 ; les récepteurs qui l’interprètent comme du Latin-1 échoueront.
Erreurs TLS dans le journal des livraisons
unable to get local issuer certificate: votre chaîne TLS est incomplète ; servez les certificats intermédiaires avec le certificat final.certificate has expired: renouvelez-le, puis cliquez sur Rejouer.tls: handshake failure: incompatibilité de version TLS ou de suite de chiffrement ; le TLS sortant de fremforge ne prend en charge que des suites de chiffrement modernes.
Rejeux / effets de bord en double
Si votre récepteur n’est pas idempotent et ne suit pas X-Gitea-Delivery, de brefs doublons peuvent survenir lors des nouvelles tentatives (par ex. fremforge relivre un webhook parce que votre réponse 200 s’est perdue en route). Corrigez cela en ajoutant le cache anti-rejeu décrit plus haut.
Source
L’implémentation de la signature est celle de Forgejo en amont ; fremforge ne la modifie pas. Consultez la documentation des webhooks de Forgejo pour plus de détails sur la forme des événements. Le rôle de fremforge est de configurer par défaut des secrets à forte entropie et de mettre à disposition les recettes de vérification documentées ici.
Pour la sécurité des webhooks entrants (paiements Mollie → fremforge), consultez le modèle de sécurité. Celui-ci repose sur une autre forme : l’URL elle-même fait office de secret.