Naar de hoofdinhoud

Webhooks

fremforge levert webhooks die in dezelfde levering compatibel zijn met zowel het eigen handtekeningschema van Forgejo als het GitHub-compatibele schema. Ontvangers die voor een van beide zijn gebouwd, werken zonder aanpassingen. Kies de header die je vertrouwt en verifieer met je gedeelde secret.

Deze pagina beschrijft het contract: wat fremforge verstuurt, hoe je het verifieert en welke garanties je krijgt bij nieuwe pogingen en TLS.

Wat je waar instelt: webhooks op organisatieniveau (op welke events je je abonneert, doel-URL, ondertekeningssecret) maak je aan in het fremforge-beheer op /<org>/_admin/webhooks, waar ook de leveringsgeschiedenis en het opnieuw versturen staan. De hookpagina van Forgejo op organisatieniveau, /org/<org>/settings/hooks, stuurt je daarheen door. Forgejo-hooks op repositoryniveau (beperkt tot één repository) stel je in de interface van Forgejo in op /<org>/<repo>/settings/hooks.

Snelle referentie

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>}
HeaderBetekenis
X-Forgejo-EventType event (push, pull_request, issues, release, enz.)
X-Gitea-DeliveryUnieke UUID per leveringspoging, te gebruiken als bescherming tegen herhaling
X-Forgejo-SignatureHMAC-SHA256 van de ruwe body, hex-gecodeerd, eigen schema van Forgejo
X-Hub-Signature-256HMAC-SHA256 van de ruwe body, hex-gecodeerd met het voorvoegsel sha256=, compatibel met GitHub
X-Hub-SignatureHMAC-SHA1 van de ruwe body, verouderde compatibiliteit met GitHub, vertrouw hier niet op

De ondertekeningssleutel is het secret dat je op de webhook instelt in het organisatiebeheer. Leeg secret = geen handtekening; de X-*-Signature-headers ontbreken dan. Het standaardbeleid van het fremforge-platform weigert bij nieuwe organisaties het instellen van een webhook zonder secret (fase 1.5+).

Waarom twee handtekeningheaders

Ontvangers die voor GitHub zijn gebouwd, controleren meestal X-Hub-Signature-256. Ontvangers die rechtstreeks voor Forgejo / Gitea zijn gebouwd, controleren X-Forgejo-Signature. Door beide in dezelfde levering mee te sturen, hoef je bij een migratie vanaf GitHub de code van je ontvanger niet aan te passen; nieuwe integraties kunnen kiezen welke vorm het meest vanzelfsprekend voelt.

De twee headers bevatten exact dezelfde HMAC-SHA256-waarde, berekend over dezelfde ruwe body. Verifieer een van beide; slagen ze allebei, dan is het verzoek authentiek.

Een webhook verifiëren: recepten

Alle recepten hieronder verifiëren de GitHub-compatibele header X-Hub-Signature-256. Pas ze aan voor X-Forgejo-Signature door de stap weg te laten die het voorvoegsel sha256= verwijdert.

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,
//   );

De rawBody MOET bestaan uit exact de ontvangen bytes, vóór elke JSON-verwerking. Middleware die opnieuw serialiseert, levert een andere hash op en dus een onterechte afwijzing.

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)
end

Rust

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()
}

Bescherming tegen herhaling

De HMAC-handtekening bewijst de authenticiteit (de payload komt van fremforge, met kennis van je gedeelde secret), maar niet de versheid. Een aanvaller die een geldige ondertekende webhook onderschept, kan die onbeperkt opnieuw afspelen totdat het secret wordt geroteerd.

Gebruik de UUID uit X-Gitea-Delivery als verdediging tegen herhaling:

  • Houd een kleine cache bij (Redis, geheugen, zelfs SQLite) van recente leverings-ID’s.
  • Weiger elke binnenkomende webhook waarvan de leverings-ID al in de cache staat.
  • Geef de cache een TTL van 24 uur. Alle nieuwe pogingen van fremforge vallen binnen ongeveer 7,5 uur, dus dat venster dekt ze; oudere herhalingen zijn gezien het rotatieritme van secrets niet haalbaar.

Sla de verdediging tegen herhaling alleen over als je handler volledig idempotent is (dezelfde levering twee keer toepassen geeft dezelfde eindtoestand). Idempotente handlers zijn de netste oplossing; een herhalingscache is de terugvaloptie als de handler dat niet is.

TLS-vereiste

fremforge levert webhooks alleen via HTTPS. HTTP-URL’s voor ontvangers worden al bij het instellen van de webhook geweigerd. De ontvanger moet:

  • Een geldig TLS-certificaat serveren voor de ingestelde hostnaam (elke publieke CA werkt: Let’s Encrypt, ZeroSSL, ACM, commercieel).
  • TLS 1.2 of hoger ondersteunen.
  • Niet leunen op SNI-trucs die alleen in browsers werken; de uitgaande TLS van fremforge gedraagt zich als een HTTP-client, niet als een browser.

Zelfondertekende certificaten worden geweigerd. Test je lokaal, gebruik dan een tunnel (ngrok, Cloudflare Tunnel, Bunny Origin Shield) met een echt certificaat.

Gedrag bij nieuwe pogingen

fremforge telt alleen een 2xx-antwoord als geleverd. Elk ander antwoord leidt tot een nieuwe poging: een 3xx (redirects worden niet gevolgd), een 4xx, een 5xx, een time-out (10 seconden), of een verbindings- of TLS-fout. Wil je geen nieuwe pogingen voor een event dat je niet interesseert, antwoord dan met 200. Bewaar 4xx voor misvormde payloads, en houd er rekening mee dat ook die opnieuw worden geprobeerd.

Schema voor nieuwe pogingen: in totaal zes pogingen. Na de eerste poging volgen nieuwe pogingen 2 minuten, 5 minuten, 15 minuten, 1 uur en 6 uur na de vorige poging, in totaal ongeveer 7,5 uur. Na de zesde mislukte poging wordt de levering in het leveringslog van de webhook in het organisatiebeheer gemarkeerd als dlq en niet meer opnieuw geprobeerd. Na vijf mislukte leveringen op rij krijgen de factuurcontacten van de organisatie een e-mail waarin de bestemming wordt genoemd.

Wil je een mislukte levering handmatig opnieuw versturen, gebruik dan de knop Opnieuw versturen in het leveringslog van de webhook in het organisatiebeheer. Er is geen REST-endpoint voor.

Een webhook instellen

Webhooks stel je op twee plekken in, afhankelijk van wat je wilt doen:

  • Uitgaande verspreiding van fremforge naar je endpoint: organisatiebeheer, Integratie, Webhooks (https://frem.sh/<your-org>/_admin/webhooks). Voeg hier de bestemming, het ondertekeningssecret en het eventfilter toe; bekijk hier ook de gezondheid van leveringen en de DLQ.
  • Eigen Forgejo-hooks per repository: de eigen interface van Forgejo op https://frem.sh/<your-org>/<repo>/settings/hooks. De pagina van Forgejo op organisatieniveau, https://frem.sh/org/<your-org>/settings/hooks, stuurt door naar het tabblad Webhooks hierboven. Dit zijn de oudere Forgejo-hooks. De systeemhook van fremforge voor de hele instantie vangt dezelfde events centraal op, dus de meeste klanten hoeven geen hooks per repository meer in te stellen.

Zo voeg je een uitgaande bestemming toe die fremforge beheert:

  1. Organisatiebeheer, Integratie, Webhooks, Bestemming toevoegen.
  2. Doel-URL: moet met https:// beginnen.
  3. Inhoudstype: application/json wordt aanbevolen.
  4. Secret: genereer een waarde met hoge entropie (≥ 32 bytes); bewaar die in de secretbeheerder van je ontvanger. Gebruik een secret niet opnieuw voor meerdere webhooks. Verschillende ontvangers horen verschillende secrets te hebben.
  5. Events: kies welke events de webhook laten afgaan.
  6. Opslaan.

Het secret van de webhook wordt at rest versleuteld opgeslagen in de database van fremforge. fremforge geeft het na het aanmaken nooit meer terug via de API; roteer het door de webhook te bewerken en een nieuwe waarde in te vullen (de oude waarde wordt in één keer vervangen).

Een bestemming kan ook beveiligingsgebeurtenissen ontvangen: nieuwe bevindingen, malwaretreffers, geblokkeerde pulls, beleidsovertredingen en verlopen risicoacceptaties. Je kiest ze expliciet bij naam, en ze kunnen als ServiceNow-incidenten worden geleverd. Zie Webhooks voor beveiligingsmeldingen.

Problemen oplossen

Handtekeningen die niet overeenkomen

De meeste niet-overeenkomende handtekeningen komen door het opnieuw serialiseren van de body. Veelvoorkomende boosdoeners:

  • De JSON-middleware body-parser van Express die VÓÓR je verificatie draait: je verificatie ziet dan een opnieuw geserialiseerde payload, niet de bytes die fremforge stuurde. Oplossing: vang de ruwe body op vóór het parsen (express.raw() en daarna zelf de JSON parsen).
  • Frameworks die JSON “behulpzaam” mooi opmaken of normaliseren voordat ze het doorgeven: dezelfde oplossing.
  • Verschillen in codering: fremforge stuurt UTF-8; ontvangers die het als Latin-1 lezen, falen.

TLS-fouten in het leveringslog

  • unable to get local issuer certificate: je TLS-keten is onvolledig; serveer de tussencertificaten samen met het eindcertificaat.
  • certificate has expired: vernieuw het certificaat en klik daarna op Opnieuw versturen.
  • tls: handshake failure: TLS-versie of cipher komt niet overeen; de uitgaande verbindingen van fremforge ondersteunen alleen moderne ciphersuites.

Herhalingen / dubbele bijwerkingen

Is je ontvanger niet idempotent en houdt hij X-Gitea-Delivery niet bij, dan kunnen tijdens nieuwe pogingen kort dubbele leveringen voorkomen (bijv. fremforge levert een webhook opnieuw omdat je 200-antwoord onderweg verloren ging). Los dat op met de herhalingscache die hierboven is beschreven.

Bron

De ondertekening is die van upstream Forgejo; fremforge past haar niet aan. Zie de webhookdocumentatie van Forgejo voor meer details over de vorm van events. De rol van fremforge is om standaard secrets met hoge entropie in te stellen en de verificatierecepten op deze pagina beschikbaar te maken.

Voor de beveiliging van inkomende webhooks (Mollie-betalingen → fremforge), zie het beveiligingsmodel. Dat gebruikt een andere vorm: de URL zelf als secret.