Agent skill

Flux CD Webhooks Skill

Receive and verify Flux CD webhooks. Use when setting up a Flux notification-controller generic-hmac Provider endpoint, debugging X-Signature verification failures, or handling Flux events by severity (info, error) and reason such as ValidationFailed.

Install this skill

npx skills add hookdeck/webhook-skills --skill flux-cd-webhooks


When to Use This Skill

  • How do I receive Flux CD webhooks (notification-controller generic-hmac Provider)?
  • How do I verify the Flux CD X-Signature header?
  • Why does my Flux webhook signature verification fail?
  • How do I handle Flux events (severity info / error, reason like ValidationFailed)?

Setup note

Use a Flux Provider of type generic-hmac and paste the token from its Secret. Generic providers are unsigned and fail verification.

Verification (core)

Flux signs the raw request body only with HMAC-SHA256, keyed with the token from the Provider's Secret (UTF-8 bytes, used as-is). The X-Signature header is sha256=<hex>; accept the bare hex digest too. No timestamp or id is signed, so there is no replay window to check. Validate the digest format (64 hex characters) before decoding, then compare in constant time. Missing, malformed or mismatched signature: 401.

Node:

const crypto = require('crypto');

function verify(rawBody /* Buffer */, header, secret) {
  if (!header || !secret) return false;
  const digest = header.startsWith('sha256=') ? header.slice(7) : header;
  if (!/^[0-9a-fA-F]{64}$/.test(digest)) return false;
  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest();
  return crypto.timingSafeEqual(Buffer.from(digest, 'hex'), expected);
}

Python:

import hmac, hashlib, re

def verify(raw_body: bytes, header: str | None, secret: str) -> bool:
    if not header or not secret:
        return False
    digest = header.removeprefix("sha256=")
    if not re.fullmatch(r"[0-9a-fA-F]{64}", digest):
        return False
    expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).digest()
    return hmac.compare_digest(bytes.fromhex(digest), expected)

Read the raw body as bytes (express.raw({ type: '*/*' }), await request.arrayBuffer(), await request.body()) and parse JSON only after the check.

For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.

Event Types

Flux has no event-name field or header. Every delivery is one Flux Event; branch on severity and reason.

severityMeaningExample reason
errorA reconciliation or validation failedValidationFailed
infoNormal progressReconciliationSucceeded

Other body fields: involvedObject (kind, name, namespace, uid), message, metadata, reportingController, reportingInstance, timestamp. Which events arrive is set by the Alert (eventSeverity, eventSources).

For the Event object, see Flux notification Provider docs

Important Headers

HeaderDescription
X-Signaturesha256=<hex> HMAC-SHA256 of the raw body (generic-hmac Providers only)
Gotk-ComponentController that raised the event, e.g. kustomize-controller
Content-Typeapplication/json

Requests are POST. Hookdeck verifies sha256 only; Flux notes the default hash "may change in the future", so a delivery signed with another hash fails.

Environment Variables

FLUX_CD_WEBHOOK_SECRET=your_provider_secret_token   # `token` key of the Secret in Provider spec.secretRef

Local Development

# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 flux-cd --path /webhooks/flux-cd

Reference Materials


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 12, 2026

View on GitHub →