Webhook ingress for private servers

GitHub.com, Azure DevOps, and other public SaaS webhook providers must call a public HTTPS URL. SSH access, VPN access, bastion hosts, and ProxyJump only prove that an operator can administer the server; they do not prove that a webhook provider can deliver a payload.

Use this page when your sfp server backend is private but repository webhooks must still reach one public payload URL:

https://<public-webhook-host>/sfp/api/repository/webhook

Azure DevOps uses a sibling path: https://<public-webhook-host>/sfp/api/repository/webhook/azuredevops.

The sfp server host should remain blocked from the public internet. Do not open the EC2 instance, VM, or container host directly to 0.0.0.0/0. Only the ingress layer is public, and for webhook-only ingress it should forward only POST /sfp/api/repository/webhook to Caddy. Return 403 or 404 for other public paths unless you intentionally want to publish the full sfp UI/API through that same ingress.

Webhook payload URL

The section is a URL/path reference, so it carries the conceptual marker.

PatternPayload URL shape
Public ALB / customer load balancerhttps://sfp-webhook.example.com/sfp/api/repository/webhook
Public reverse proxy / tunnelhttps://sfp-webhook-tunnel.example.com/sfp/api/repository/webhook
API Gatewayhttps://<api-id>.execute-api.<region>.amazonaws.com/sfp/api/repository/webhook or a custom domain with the same path

The section is a pattern comparison table, so it is marked conceptual.

PatternBest fitPrivate backend control
Public ALB / customer load balancerDefault choice on AWS or when the customer already has a managed load balancerBackend security group allows only LB-to-server traffic; listener rule forwards only the webhook path unless the full app is intentionally public
Public reverse proxy / tunnelServer cannot accept inbound internet trafficTunnel forwards to localhost or a private listener; proxy allows only the webhook path unless the full app is intentionally public
API GatewayYou need API Gateway features such as custom domains, auth layers, quotas, central API management, or private integrationsHTTP API route exposes only the webhook path and forwards through VPC Link to an internal ALB/NLB

Do not make GitHub.com source-IP allowlists the primary control. Provider IP ranges change, and source IP does not prove authenticity. Put a public HTTPS ingress in front of the server, keep the backend private to that ingress, and let sfp-server validate the webhook signature.


Shared requirements

The section is mechanism prose, so it is marked conceptual.

Every ingress pattern must preserve the request exactly enough for provider signature validation:

  • Allow HTTPS POST to the exact webhook payload URL.
  • Keep the sfp backend host private; the provider should never call the backend IP or private DNS name directly.
  • Forward only POST /sfp/api/repository/webhook (and /sfp/api/repository/webhook/azuredevops for Azure DevOps) for webhook-only ingress.
  • Forward the raw request body unchanged.
  • Preserve provider headers such as X-Hub-Signature-256, X-GitHub-Event, and X-GitHub-Delivery.
  • Set or preserve X-Forwarded-Proto: https, X-Forwarded-Host, and X-Forwarded-For.
  • Return a 2xx response quickly. sfp-server validates the signature, dispatches the workflow to Hatchet, and returns without waiting for workflow completion.

Do not parse and re-serialize webhook payloads in the ingress layer. GitHub computes X-Hub-Signature-256 over the original request body, so mapping templates, body rewrites, or missing signature headers can make valid deliveries fail validation.


What Caddy publishes

Caddy is the only container with host ports. On the default path it publishes four ports; the application server and the Supabase gateway are expose-only and are never reachable from the host exterior:

Port (default)PurposeReachable from where
443 (HTTP_PORT)The app — including /sfp/api/* and the webhook payload URLPublic (TLS-terminated by Caddy)
8080 (HATCHET_DASHBOARD_PORT)Hatchet dashboardIP-restricted (admin)
4873 (VERDACCIO_PORT)Verdaccio npm registryIP-restricted (admin)
3100 (STUDIO_PORT)Supabase StudioIP-restricted + basic auth (admin)
3029 (server)sfp-server HTTP APIInternal network only (expose, no host port)
8000 (gateway)Supabase API gateway (supabase-kong alias)Internal network only (expose, no host port)

The webhook path lands on Caddy's catch-all route, which proxies to the server at server:3029 with the raw path.

When --tls-mode none (TLS terminated upstream), Caddy still runs — it is required for auth routing — but serves plain HTTP, and its HTTP_PORT becomes 3029 instead of 443. The ingress target group / tunnel / API Gateway backend must then forward to host port 3029, not 443:

provider HTTPS
  -> public ingress (TLS termination)
  -> Caddy HTTP listener on host port 3029 (tls-mode none) or 443 (otherwise)
  -> server:3029
  -> Hatchet workflow
Diagram of a provider webhook arriving at a public ingress (ALB, tunnel, or API Gateway), then forwarding to Caddy's published port and on to the private server container over the internal network
Webhook ingress: public TLS at the edge, private backend, Caddy proxying to the internal server.

Public ALB / load balancer

The section is mechanism prose, so it is marked conceptual.

This is the recommended default for AWS deployments and for customer environments that already operate a public load balancer.

Expected flow:

GitHub.com HTTPS webhook
  -> public ALB / customer LB TLS listener
  -> path rule for POST /sfp/api/repository/webhook
  -> private HTTP target group
  -> Caddy HTTP listener (host port 3029 under --tls-mode none, 443 otherwise)
  -> sfp-server

Required checks:

  • Public DNS resolves to the load balancer.
  • TLS certificate is valid for the webhook hostname.
  • Target group health check passes against the Caddy listener.
  • Backend security group allows traffic from the load balancer security group, not from the whole internet.
  • Listener rules forward the webhook path and reject other public paths unless the full app is intentionally public.
  • Forwarded headers are set or preserved.

When using --tls-mode none, Caddy still runs because it handles auth routing. It serves HTTP behind the load balancer instead of terminating TLS itself.


Public reverse proxy / tunnel

The section is mechanism prose, so it is marked conceptual.

Use a tunnel or managed reverse proxy when the server cannot accept inbound traffic from the internet.

Expected flow:

GitHub.com HTTPS webhook
  -> public proxy hostname
  -> path rule for POST /sfp/api/repository/webhook
  -> outbound tunnel or reverse proxy
  -> Caddy HTTP listener (host port 3029 under --tls-mode none, 443 otherwise)
  -> sfp-server

Required checks:

  • Proxy hostname is stable and reachable over HTTPS.
  • Proxy process is always running and supervised.
  • Proxy allows the webhook path and rejects other public paths unless the full app is intentionally public.
  • Proxy forwards the raw request body and provider signature headers unchanged.
  • Proxy forwards original host/protocol headers or sets equivalent X-Forwarded-* headers.

Quick tunnels are acceptable for smoke testing, but do not use them as production webhook URLs. Production tunnels should be named, supervised, and bound to a stable hostname.


API Gateway

The section is mechanism prose, so it is marked conceptual.

Use API Gateway only when you need its API-management features. For plain webhook ingress, an ALB or managed tunnel is simpler to operate.

Expected flow:

GitHub.com HTTPS webhook
  -> API Gateway HTTP API
  -> route POST /sfp/api/repository/webhook
  -> VPC Link
  -> internal ALB/NLB listener
  -> Caddy HTTP listener (host port 3029 under --tls-mode none, 443 otherwise)
  -> sfp-server

Required checks:

  • VPC Link status is AVAILABLE before testing deliveries.
  • Private integration points to an internal ALB/NLB listener.
  • VPC Link security group can reach the internal load balancer.
  • Internal load balancer target group is healthy.
  • API Gateway exposes a route for POST /sfp/api/repository/webhook.
  • API Gateway stage/base path is stripped or mapped so Caddy receives /sfp/api/repository/webhook.
  • Raw request body and provider signature headers are forwarded unchanged.

If API Gateway forwards the stage name to the backend, use the $default stage, a custom domain/base path mapping, or a request path override. The path received by Caddy must match the webhook payload URL configured in the provider.


Verify delivery

Do not test only the root domain. Test the exact payload URL configured in the provider.

For GitHub, use Recent deliveries and Redeliver from the webhook settings page. A successful route should return 2xx, and sfp-server logs should show that the webhook signature was verified and the event was received — the middleware rejects deliveries missing or failing X-Hub-Signature-256.

If a manual POST works but the provider delivery fails, compare:

  • Public URL and path.
  • HTTP method.
  • TLS certificate validity.
  • Provider signature header preservation.
  • Raw body preservation.
  • Backend path received by Caddy.
The Settings → Webhooks screen in codev (or GitHub Recent deliveries) showing an active webhook at the sfp payload URL with a successful delivery
Verifying the registered webhook's payload URL and a successful delivery.

See Troubleshooting → Webhooks for common symptoms.

On this page