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:
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 (
503on 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 (
gceclass), or the GKE Gateway controller (enable with--gateway-api=standard; Google manages the CRDs and controller) with agke-l7-*Gateway. - EKS — install the AWS Load Balancer Controller:
its
albIngressClass covers the Ingress flavour, and its GA Gateway API support (Gateway API CRDs installed alongside) covershttpRoute, provisioning an ALB with the certificate from ACM. - AKS — Application 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.