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/webhookAzure 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.
| Pattern | Payload URL shape |
|---|---|
| Public ALB / customer load balancer | https://sfp-webhook.example.com/sfp/api/repository/webhook |
| Public reverse proxy / tunnel | https://sfp-webhook-tunnel.example.com/sfp/api/repository/webhook |
| API Gateway | https://<api-id>.execute-api.<region>.amazonaws.com/sfp/api/repository/webhook or a custom domain with the same path |
Recommended order
The section is a pattern comparison table, so it is marked conceptual.
| Pattern | Best fit | Private backend control |
|---|---|---|
| Public ALB / customer load balancer | Default choice on AWS or when the customer already has a managed load balancer | Backend 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 / tunnel | Server cannot accept inbound internet traffic | Tunnel forwards to localhost or a private listener; proxy allows only the webhook path unless the full app is intentionally public |
| API Gateway | You need API Gateway features such as custom domains, auth layers, quotas, central API management, or private integrations | HTTP 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
POSTto 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/azuredevopsfor Azure DevOps) for webhook-only ingress. - Forward the raw request body unchanged.
- Preserve provider headers such as
X-Hub-Signature-256,X-GitHub-Event, andX-GitHub-Delivery. - Set or preserve
X-Forwarded-Proto: https,X-Forwarded-Host, andX-Forwarded-For. - Return a
2xxresponse 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) | Purpose | Reachable from where |
|---|---|---|
443 (HTTP_PORT) | The app — including /sfp/api/* and the webhook payload URL | Public (TLS-terminated by Caddy) |
8080 (HATCHET_DASHBOARD_PORT) | Hatchet dashboard | IP-restricted (admin) |
4873 (VERDACCIO_PORT) | Verdaccio npm registry | IP-restricted (admin) |
3100 (STUDIO_PORT) | Supabase Studio | IP-restricted + basic auth (admin) |
3029 (server) | sfp-server HTTP API | Internal 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
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-serverRequired 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-serverRequired 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-serverRequired checks:
- VPC Link status is
AVAILABLEbefore 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.

See Troubleshooting → Webhooks for common symptoms.