Zum Hauptinhalt springen

Webhooks

fremforge liefert Webhooks, die in derselben Zustellung sowohl mit dem nativen Signaturschema von Forgejo als auch mit dem GitHub-kompatiblen Schema kompatibel sind. Empfänger, die für eines der beiden gebaut wurden, funktionieren ohne Änderung. Wähle den Header, dem du vertraust, und prüfe ihn gegen dein gemeinsames Secret.

Diese Seite dokumentiert den Vertrag: was fremforge sendet, wie du es prüfst und welche Garantien du bei Wiederholungen und TLS bekommst.

Wo du was konfigurierst: Webhooks auf Organisationsebene (welche Ereignisse abonniert werden, Ziel-URL, Signatur-Secret) legst du in der fremforge-Verwaltung unter /<org>/_admin/webhooks an; dort findest du auch den Zustellverlauf und das erneute Senden. Forgejos Hook-Seite auf Organisationsebene, /org/<org>/settings/hooks, leitet dorthin weiter. Forgejo-Hooks auf Repository-Ebene (auf ein Repository beschränkt) richtest du in der Forgejo-Oberfläche unter /<org>/<repo>/settings/hooks ein.

Kurzreferenz

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>}
HeaderBedeutung
X-Forgejo-EventEreignistyp (push, pull_request, issues, release usw.)
X-Gitea-DeliveryEindeutige UUID pro Zustellversuch, zum Schutz vor Replays
X-Forgejo-SignatureHMAC-SHA256 des Roh-Bodys, hex-kodiert, Forgejo-nativ
X-Hub-Signature-256HMAC-SHA256 des Roh-Bodys, hex-kodiert mit Präfix sha256=, GitHub-kompatibel
X-Hub-SignatureHMAC-SHA1 des Roh-Bodys, Altlast für GitHub-Kompatibilität, verlass dich nicht darauf

Der Signaturschlüssel ist das Secret, das du beim Webhook in der Verwaltungsoberfläche der Organisation festlegst. Leeres Secret = keine Signatur; die Header X-*-Signature fehlen dann. Die plattformweite Standardrichtlinie von fremforge lehnt bei neuen Organisationen die Konfiguration eines Webhooks ohne Secret ab (ab Phase 1.5).

Warum zwei Signatur-Header

Für GitHub gebaute Empfänger prüfen in der Regel X-Hub-Signature-256. Nativ für Forgejo / Gitea gebaute Empfänger prüfen X-Forgejo-Signature. Weil beide in derselben Zustellung gesendet werden, musst du bei einer Migration von GitHub keinen Empfängercode anfassen; neue Integrationen können die Form wählen, die sich nativer anfühlt.

Beide Header tragen exakt denselben HMAC-SHA256-Wert, berechnet über denselben Roh-Body. Prüfe einen von beiden; ist die Prüfung erfolgreich, ist die Anfrage authentisch.

Einen Webhook prüfen: Rezepte

Alle Rezepte unten prüfen den GitHub-kompatiblen Header X-Hub-Signature-256. Für X-Forgejo-Signature lässt du den Schritt weg, der das Präfix sha256= entfernt.

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

rawBody MUSS exakt die empfangenen Bytes vor jedem JSON-Parsing enthalten. Middleware, die neu serialisiert, erzeugt einen anderen Hash und damit ein falsch negatives Ergebnis.

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

Schutz vor Replays

Die HMAC-Signatur beweist die Echtheit (der Payload kam von fremforge, das dein gemeinsames Secret kennt), aber nicht die Aktualität. Wer einen gültig signierten Webhook abfängt, kann ihn beliebig oft wiederholen, bis das Secret rotiert wird.

Nutze die UUID aus X-Gitea-Delivery zur Abwehr von Replays:

  • Halte einen kleinen Cache (Redis, Arbeitsspeicher, selbst SQLite) der letzten Zustell-IDs.
  • Lehne jeden eingehenden Webhook ab, dessen Zustell-ID bereits im Cache steht.
  • Setze die TTL des Caches auf 24 Stunden. Alle Wiederholungen von fremforge liegen innerhalb von etwa 7,5 Stunden, das Fenster deckt sie also ab; ältere Replays sind angesichts des Rotationsrhythmus des Secrets nicht praktikabel.

Verzichte nur dann auf die Replay-Abwehr, wenn dein Handler vollständig idempotent ist (dieselbe Zustellung zweimal angewendet ergibt denselben Endzustand). Idempotente Handler sind die sauberste Lösung; der Replay-Cache ist die Rückfallebene, wenn der Handler es nicht ist.

TLS-Anforderung

fremforge liefert Webhooks nur über HTTPS. HTTP-URLs als Empfänger werden schon bei der Konfiguration des Webhooks abgelehnt. Der Empfänger muss:

  • ein gültiges TLS-Zertifikat für den konfigurierten Hostnamen ausliefern (jede öffentliche CA funktioniert: Let’s Encrypt, ZeroSSL, ACM, kommerziell).
  • TLS 1.2 oder höher unterstützen.
  • sich nicht auf SNI-Tricks verlassen, die nur für Browser funktionieren; das ausgehende TLS von fremforge verhält sich wie ein HTTP-Client, nicht wie ein Browser.

Selbstsignierte Zertifikate werden abgelehnt. Wenn du lokal testest, nutze stattdessen einen Tunnel (ngrok, Cloudflare Tunnel, Bunny Origin Shield) mit einem echten Zertifikat.

Verhalten bei Wiederholungen

fremforge wertet nur eine 2xx-Antwort als zugestellt. Jede andere Antwort wird wiederholt: ein 3xx (Weiterleitungen werden nicht verfolgt), ein 4xx, ein 5xx, ein Timeout (10 Sekunden) oder ein Verbindungs- oder TLS-Fehler. Um Wiederholungen für ein Ereignis zu stoppen, das dich nicht interessiert, antworte mit 200. Reserviere 4xx für fehlerhafte Payloads und rechne damit, dass auch diese wiederholt werden.

Wiederholungsplan: insgesamt sechs Versuche. Nach dem ersten Versuch folgen Wiederholungen 2 Minuten, 5 Minuten, 15 Minuten, 1 Stunde und 6 Stunden nach dem jeweils vorherigen Versuch, insgesamt etwa 7,5 Stunden. Nach dem sechsten fehlgeschlagenen Versuch wird die Zustellung im Zustellprotokoll der Webhooks in der Verwaltungsoberfläche der Organisation als dlq markiert und nicht weiter wiederholt. Nach fünf fehlgeschlagenen Zustellungen in Folge erhalten die Abrechnungskontakte der Organisation eine E-Mail, die das Ziel nennt.

Um eine fehlgeschlagene Zustellung manuell erneut zu senden, nutze die Schaltfläche Erneut senden im Zustellprotokoll der Webhooks in der Verwaltungsoberfläche der Organisation. Einen REST-Endpunkt dafür gibt es nicht.

Einen Webhook konfigurieren

Die Konfiguration von Webhooks liegt an zwei Stellen, je nachdem, was du vorhast:

  • Ausgehende Verteilung von fremforge an deinen Endpunkt: Tenant-Verwaltung, Integration, Webhooks (https://frem.sh/<your-org>/_admin/webhooks). Hier legst du Ziel, Signatur-Secret und Ereignisfilter fest und prüfst den Zustand der Zustellungen und die DLQ.
  • Forgejo-native Hooks pro Repository: Forgejos eigene Oberfläche unter https://frem.sh/<your-org>/<repo>/settings/hooks. Forgejos Seite auf Organisationsebene, https://frem.sh/org/<your-org>/settings/hooks, leitet zum Tab Webhooks oben weiter. Das sind die klassischen Forgejo-Hooks. Der instanzweite System-Hook von fremforge erfasst dieselben Ereignisse zentral, daher müssen die meisten Kunden keine Hooks pro Repository mehr einrichten.

So fügst du ein von fremforge verwaltetes ausgehendes Ziel hinzu:

  1. Tenant-Verwaltung, Integration, Webhooks, Add destination.
  2. Ziel-URL: muss mit https:// beginnen.
  3. Content type: application/json empfohlen.
  4. Secret: Erzeuge einen Wert mit hoher Entropie (≥ 32 Byte) und lege ihn im Secret-Manager deines Empfängers ab. Verwende ein Secret nicht für mehrere Webhooks. Unterschiedliche Empfänger sollten unterschiedliche Secrets nutzen.
  5. Ereignisse: Wähle, welche Ereignisse den Webhook auslösen.
  6. Speichern.

Das Webhook-Secret wird verschlüsselt in der Datenbank von fremforge gespeichert. fremforge gibt es nach dem Anlegen nie wieder über die API aus; zum Rotieren bearbeitest du den Webhook und gibst einen neuen Wert ein (der alte Wert wird atomar ersetzt).

Ein Ziel kann auch Sicherheitsereignisse empfangen: neue Befunde, Malware-Treffer, blockierte Pulls, Richtlinienverstöße und abgelaufene Risikoakzeptanzen. Sie werden einzeln per Name abonniert und können als ServiceNow-Incidents zugestellt werden. Siehe Security alert webhooks.

Fehlerbehebung

Abweichende Signaturen

Die meisten abweichenden Signaturen entstehen durch erneute Serialisierung des Bodys. Häufige Ursachen:

  • Die JSON-Middleware body-parser von Express läuft VOR deiner Prüfung; deine Prüfung sieht dann einen neu serialisierten Payload, nicht die Bytes, die fremforge gesendet hat. Lösung: den Roh-Body vor dem Parsen abgreifen (express.raw() und JSON danach selbst parsen).
  • Frameworks, die JSON „hilfreich“ formatieren oder kanonisieren, bevor sie es bereitstellen; gleiche Lösung.
  • Unterschiedliche Kodierungen: fremforge sendet UTF-8; Empfänger, die als Latin-1 interpretieren, scheitern.

TLS-Fehler im Zustellprotokoll

  • unable to get local issuer certificate: Deine TLS-Kette ist unvollständig; liefere die Zwischenzertifikate zusammen mit dem Endzertifikat aus.
  • certificate has expired: Erneuere das Zertifikat und klicke dann auf Erneut senden.
  • tls: handshake failure: Abweichende TLS-Version oder Cipher; das ausgehende TLS von fremforge unterstützt nur moderne Cipher-Suites.

Replays / doppelte Nebeneffekte

Ist dein Empfänger nicht idempotent und verfolgt er X-Gitea-Delivery nicht, können bei Wiederholungen kurzzeitig Duplikate auftreten (z. B. wenn fremforge einen Webhook erneut zustellt, weil deine 200-Antwort unterwegs verloren ging). Abhilfe schafft der oben beschriebene Replay-Cache.

Quelle

Die Signatur-Implementierung ist die des Upstream-Forgejo; fremforge patcht sie nicht. Weitere Details zur Form der Ereignisse findest du in der Webhook-Dokumentation von Forgejo. Die Rolle von fremforge besteht darin, standardmäßig Secrets mit hoher Entropie zu konfigurieren und die hier dokumentierten Prüfrezepte bereitzustellen.

Zur Sicherheit eingehender Webhooks (Mollie-Zahlungen → fremforge) siehe das Sicherheitsmodell. Dort gilt eine andere Form: das Muster „URL als Secret“.