Skip to content

Webhook exposure

A GitHub App has exactly one webhook URL, and GitHub POSTs every subscribed event to it. In patchy that URL points at the integration-controller — the single internet-facing component and the only webhook receiver in the system:

https://<webhook.host>/github/webhooks

Each delivery's HMAC signature is validated against the webhookSecret of your configured Integration resources before anything else happens; a delivery no Integration's secret matches is rejected with 401. There is no routing tier and nothing to fan deliveries out to — scanner events are ingested into Finding resources and human signals (issue close, /approve, PR merge) are applied to them, all inside this one controller.

Two properties follow:

  • Losing a delivery is not losing work. The webhook path carries ingestion and human-in-the-loop signals only; pipeline progress rides on the controllers' watch-driven reconcile loops, which no delivery can announce anyway ("accumulation closed", "older than an hour", "a slot freed"). A rejected delivery (503 on a full queue, downtime) stays in GitHub's 30-day delivery log, and the redelivery sweep replays it.
  • Exposure needs nothing exotic. Any plain Ingress or Gateway API implementation works as-is — no header matching, no rewrites, no mirroring. Only the provider webhook paths need exposing — /github/webhooks, plus /google-cloud/webhooks, /wiz/webhooks, and the /generic/ prefix when those Integrations are configured; the probes stay cluster-internal on port 8081.

The credential story: the integration-controller holds no GitHub credential in its Deployment at all — the Integration/Forge Secrets are read on demand through the Kubernetes API, and the components that exercise write credentials (remediation-controller's push/PR) never face the internet.

Missed deliveries: the redelivery sweep

GitHub does not retry a failed webhook delivery on its own — it records the attempt in the App's 30-day delivery log and moves on. A delivery patchy never got a 2xx to (receiver down, queue full, misconfigured secret) would otherwise sit there until someone redelivers it by hand.

The integration-controller closes that gap when spec.github.redelivery is enabled on the Integration:

spec:
  interval: 10m # the sweep rides the reconcile interval
  github:
    redelivery:
      enabled: true
      lookback: 24h # how far back each sweep scans (GitHub retains 30d)

Each sweep lists the App webhook's recent deliveries (app-level API — this needs App credentials, a PAT cannot see the delivery log) and asks GitHub to redeliver every delivery whose attempts within the lookback all failed, up to three attempts per delivery. Duplicates are harmless: the receiver dedups per delivery GUID and ingestion is idempotent. The outcome lands on status.redelivery (scan size, redeliveries requested, truncation, last error).

The status page's user menu offers a Replay deliveries action (RBAC verb replay) that redelivers everything in the lookback window, including deliveries that already succeeded — paired with Reset all data (verb reset), it re-runs the whole pipeline from ingestion, which is exactly what a demo wants. The button writes spec.replay on the Integration; the controller performs the redelivery on its next reconcile.

Deliveries that never happened — alerts predating the App installation — are invisible to the sweep: there is nothing in the delivery log to redeliver. Those are what the manual backfill exists for: a one-shot list-alerts walk that ingests the provider's open alerts directly. Trigger it from the status page's configuration view, with patchy backfill <integration> [--repo owner/ ...], or by stamping spec.backfill on the Integration directly (RBAC verb backfill on integrations). The walk honours an optional repository prefix filter, is bounded by a page budget (status.backfill.truncated says when to re-run with a narrower prefix), and is idempotent — alerts that already have findings simply fold in. See the GitHub source for the full semantics.

Expose it

Enable one flavour under the chart's webhook value and point the App's webhook URL at https://<host>/github/webhooks.

Plain Ingress (webhook.ingress) — works with any ingress controller:

webhook:
  host: patchy.example.com
  ingress:
    enabled: true
    className: nginx
    tls:
      - secretName: patchy-webhook-tls
        hosts:
          - patchy.example.com

GitHub should always deliver over HTTPS — set tls (cert-manager annotations go in webhook.ingress.annotations) or terminate TLS in front of the Ingress.

Gateway API (webhook.httpRoute) — one HTTPRoute that attaches to a Gateway you bring via parentRefs; TLS and certificates are the Gateway listener's concern:

webhook:
  host: patchy.example.com
  httpRoute:
    enabled: true
    parentRefs:
      - name: my-gateway
        namespace: gateway-system
        sectionName: https

Managed platform notes

Nothing here is patchy-specific — the chart emits standard resources with no implementation-specific annotations of its own (add what your controller needs via webhook.ingress.annotations / webhook.httpRoute.annotations) — but for orientation:

  • GKE — both flavours work out of the box: the built-in GKE Ingress (gce class), or the GKE Gateway controller (enable with --gateway-api=standard; Google manages the CRDs and controller) with a gke-l7-* Gateway.
  • EKS — install the AWS Load Balancer Controller: its alb IngressClass covers the Ingress flavour, and its GA Gateway API support (Gateway API CRDs installed alongside) covers httpRoute, provisioning an ALB with the certificate from ACM.
  • AKSApplication Gateway for Containers implements both the Ingress and Gateway APIs; enable its ALB Controller as an AKS managed add-on (requires workload identity and Azure CNI). The AKS application routing add-on (managed NGINX) also covers the Ingress flavour.

Any other conformant implementation (ingress-nginx, Istio, Envoy Gateway, Cilium, ...) works the same way.

Kustomize

The base ships the integration-controller Deployment and its ClusterIP Service (patchy-integration-controller:8080) but deliberately no Ingress — put your environment's Ingress or Gateway in front of that Service in your own overlay. The dev overlay exposes it two ways at once: NodePort 30079 (where a webhook tunnel — smee.io, ngrok, gh webhook forward — or mise run replay should point) and a host-less, class-less Ingress for the same path that any default ingress controller satisfies. On Colima that Ingress is live out of the box.