https://github.com/juggernaut/webhook-sentry
Egress proxy for webhooks
https://github.com/juggernaut/webhook-sentry
Last synced: 9 months ago
JSON representation
Egress proxy for webhooks
- Host: GitHub
- URL: https://github.com/juggernaut/webhook-sentry
- Owner: juggernaut
- License: apache-2.0
- Created: 2020-04-24T00:25:45.000Z (about 6 years ago)
- Default Branch: master
- Last Pushed: 2023-05-06T05:26:40.000Z (about 3 years ago)
- Last Synced: 2024-06-20T09:18:24.824Z (about 2 years ago)
- Language: Go
- Size: 271 KB
- Stars: 51
- Watchers: 6
- Forks: 5
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# Webhook Sentry [](https://github.com/juggernaut/egress-proxy/actions) 
Webhook Sentry is a proxy that helps you send [webhooks](https://en.wikipedia.org/wiki/Webhook) to your customers securely.
## Why?
### Security
Sending webhooks appears simple on the surface -- they're just HTTP requests after all. But sending them _securely_ is hard. If your application sends webhooks, does your implementation
1. Prevent [SSRF](https://portswigger.net/web-security/ssrf) attacks?
2. Protect against [DNS rebinding](https://en.wikipedia.org/wiki/DNS_rebinding) attacks?
3. Support mutual TLS?
4. Validate SSL certificate chains correctly?
5. Use an updated CA certificate bundle?
6. Specify reasonable idle socket and connection timeouts?
By proxying webhooks through Webhook Sentry, you get all of these for free.
### Auditability
Sending webhooks involves making connections to untrusted and possibly malicious servers on the public internet. Maintaining an audit trail is essential for forensics and compliance.
Limiting the set of instances that send such requests to a single proxy layer makes auditing simpler and more manageable.
### Static Egress IPs
Many customers require webhook requests to be sent from a list or range of static IPs in order to configure their firewalls. In a cloud environment with autoscaling, you
may not want to allocate static IPs to your application instances. In other situations, like serverless applications, it may be impossible to assign static IPs. With a centralized
egress proxy layer, you only need to assign static IPs to your proxy instances.
## Getting Started
Webhook Sentry runs on port 9090 by default. You can configure the address and port in the `listeners` section of the [config](#Configuration).
The simplest way to run Webhook Sentry is to use the latest binary:
1. Download the [latest release](https://github.com/juggernaut/webhook-sentry/releases/latest) for your platform
2. Run the downloaded binary:
```
whsentry
```
We also have a docker image:
```
docker run -p 9090:9090 juggernaut/webhook-sentry:latest
```
You can also pin a [tagged release](https://github.com/juggernaut/webhook-sentry/releases):
```
docker run -p 9090:9090 juggernaut/webhook-sentry:v1.0.8
```
If you need to override settings, you can mount a configuration file, pass in command line flags or set environment variables. See [configuration](#Configuration) for details.
If you need prometheus metrics for the service, allow access on port 2112 with something like `-p 2112:2112`.
## Usage
### HTTP target
```
curl -x http://localhost:9090 http://www.google.com
```
### HTTPS target
HTTP clients create a `CONNECT` tunnel when a proxy is configured and the target is a `https` URL. This does not give us the benefits of initiating TLS from the proxy. To get around this behavior, Webhook Sentry supports a unique way of proxying to HTTPS targets. Pass a `X-WhSentry-TLS` header and change the protocol to `http`:
```
curl -v -x http://localhost:9090 --header 'X-WhSentry-TLS: true' http://www.google.com
```
Although `CONNECT` is supported, I strongly recommend using the header approach to take advantage of the TLS capabilities of Webhook Sentry, like mutual TLS and robust certificate validation.
### Mutual TLS
Specify `clientCertFile` and `clientKeyFile` in the configuration to enable mutual TLS:
```
clientCertFile: /path/to/client.pem
clientKeyFile: /path/to/key.pem
```
### Prometheus Metrics
Point your collector to :2112 for metrics.
E.g if the proxy is running on localhost, to verify metrics are correctly exposed:
```
curl http://localhost:2112/metrics
```
## AWS EKS Configuration for Static IP egress
To deploy Webhook Sentry with a static egress IP addresses in AWS EKS, you'll need a node group with an Elastic IP address:
- Create a new NAT Gateway.
- Create an Elastic IP address and assign it to your NAT Gateway. This will be your egress IP.
- Create a subnet dividing your network space.
- Create a route table linking the subnet to the NAT Gateway.
- Create a custom node group for the Webhook Sentry pods and assign it to the new subnet.
- Finally, [assign](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/) your Webhook Sentry pods to that node group, and ensure your existing pods do not get assigned to it.
For redundancy, you may want to create multiple NAT Gatways, subnets, and egress IP addressses.
## Protections
### SSRF attack protection
Webhook Sentry blocks access to private/internal IPs to prevent SSRF attacks:
```
$ curl -i -x http://localhost:9090 http://127.0.0.1:3000
HTTP/1.1 403 Forbidden
Content-Type: text/plain; charset=utf-8
X-Content-Type-Options: nosniff
X-Whsentry-Reason: IP 127.0.0.1 is blocked
X-Whsentry-Reasoncode: 1000
Date: Fri, 18 Sep 2020 07:15:20 GMT
Content-Length: 24
IP 127.0.0.1 is blocked
```
Unlike naive implementations, it also correctly checks the IP after DNS resolution. This example makes use of the [1u.ms](http://1u.ms/) service which can serve up DNS records using any IP we want:
```
$ curl -i -x http://localhost:9090 http://make-127-0-0-1-rr.1u.ms
HTTP/1.1 403 Forbidden
Content-Type: text/plain; charset=utf-8
X-Content-Type-Options: nosniff
X-Whsentry-Reason: IP 127.0.0.1 is blocked
X-Whsentry-Reasoncode: 1000
Date: Fri, 18 Sep 2020 07:21:58 GMT
Content-Length: 24
IP 127.0.0.1 is blocked
```
### DNS rebinding attack prevention
A malicious attacker can set up their DNS such that it first resolves to a valid public IP adddress, but subsequent resolutions point to private/internal IP addresses. This can be used to exploit webhook implementations that validate the resolved IP using `getaddrinfo()` or equivalent, then pass the original URL to a HTTP client library which resolves the host a second time. Again, let's use 1u.ms to first return a valid public IP and then the loopback IP:
```
$ curl -i -x http://localhost:9090 http://make-3-221-81-55-rebind-127-0-0-1-rr.1u.ms/get
HTTP/1.1 200 OK
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: *
Content-Length: 324
Content-Type: application/json
Date: Wed, 30 Sep 2020 07:38:47 GMT
Server: gunicorn/19.9.0
{
"args": {},
"headers": {
"Accept": "*/*",
"Host": "make-3-221-81-55-rebind-127-0-0-1-rr.1u.ms",
"User-Agent": "Webhook Sentry/0.1",
"X-Amzn-Trace-Id": "Root=1-5f743607-afdf257ca619f90a14fc92b8"
},
"origin": "73.189.176.226",
"url": "http://make-3-221-81-55-rebind-127-0-0-1-rr.1u.ms/get"
}
```
### Mozilla CA certificate bundle
Webhook Sentry uses the latest [Mozilla CA certificate bundle](https://www.mozilla.org/en-US/about/governance/policies/security-group/certs/) instead of relying on CA certificates bundled with the OS. This avoids the problem of out-of-date root CA certificates on older OS versions. See [this blog post](https://www.agwa.name/blog/post/fixing_the_addtrust_root_expiration) for why this is important. Notably, Stripe's webhooks were affected by this issue and took hours to fix.
On startup, Webhook Sentry checks if there is a newer version of the Mozilla CA certificate bundle than on disk, and if so, downloads it.
Additionally, by virtue of being written in Go, Webhook Sentry does not rely on OpenSSL or GnuTLS for certificate validation.
## Configuration
webhook-sentry uses [viper](https://github.com/spf13/viper) for configuration. You can use a yaml file, environment variables or command line flags to provide configuration parameters.
By default, webhook-sentry looks for a file named `config.yaml` in the current working directory. You can specify a different file using the `--config