This sample shows how Envoy can be used as a generic forward proxy on Kubernetes. "Generic" means that it will allow proxying any host, not a predefined set of hosts.
Suppose we need a Kubernetes service named forward-proxy
. The service will be used as a forward proxy to an arbitrary host. The service must satisfy the following requirements:
The following request should be proxied to httpbin.org/headers
:
curl forward-proxy/headers -H Host:httpbin.org" -H Foo:bar
The following request should be proxied to https://edition.cnn.com, with TLS origination performed by forward-proxy
:
curl -v forward-proxy:443 -H Host: edition.cnn.com
Note that the request to the forward proxy is sent over HTTP. The forward proxy opens a TLS connection to https://edition.cnn.com .
A nice-to-have feature: use forward-proxy
as HTTP proxy.
http_proxy=forward-proxy:80 curl httpbin.org/headers -H Foo:bar
Another nice-to-have feature, to show Envoy's capabilities as a sidecar proxy. Transparently catch all the traffic inside a pod with the forward-proxy
container and direct the traffic through the proxy. Use iptables
for directing the traffic.
Use Envoy's filters for monitoring, transforming, policing the traffic that goes through the forward proxy.
Add SNI while performing TLS origination.
This sample shows how Envoy together with NGINX can satisfy the requirements above. The requirement 5 is satisfied trivially, by using Envoy. While Envoy can function perfectly as a forward proxy for predefined hosts, it cannot satisfy the requirement 1. NGINX is used for the generic forward proxy functionality.
Envoy can satisfy the requirement 4, using orignal destination clusters. However, even for this requirement there are issues.
First, Envoy forwards the request by the destination IP, not by the host header. This way, policing the requests cannot be performed based on the destination host, since Envoy will send the request by the IP anyway. A malicious application can issue a request to a malicious IP with a valid host name. Envoy will check the host name, but will not be able to verify that the host name matches the IP. NGINX can forward the request by the host header, disregarding the original destination IP.
Second, Envoy will not be able to set SNI correctly for an arbitrary site, based on the Host header, see this comment. NGINX can set SNI based on the Host header, using proxy_ssl_server_name directive. Let's add the additional requirements:
When being used as a sidecar proxy, the forward-proxy
must direct the traffic by the Host header, not by the original IP.
When performing TLS origination, the forward-proxy
must set SNI according to the Host header.
Using Envoy in tandem with NGINX seems to satisfy the requirements cleanly. Envoy will direct all the traffic to NGINX instances running as forward proxies. Most of the features of Envoy, in particular its HTTP Filters, will be available, while NGINX will complement Envoy, providing missing features for proxying to arbitrary sites.
In this sample, I demonstrate two cases:
Perform this step if you want to run your own version of the forward proxy. Alternatively, skip this step and use the version in https://hub.docker.com/u/vadimeisenbergibm .
./build_and_push_docker.sh <your docker hub user name>
.
Edit forward_proxy.yaml
: replace vadimeisenbergibm
with your docker hub username. Alternatively, just use the images from https://hub.docker.com/u/vadimeisenbergibm .
Deploy the forward proxy:
kubectl apply -f forward_proxy.yaml
Deploy a pod to issue curl
commands. I use the sleep
pod from the Istio samples. Any other pod with curl
installed is good enough.
kubectl apply -f https://raw.githubusercontent.com/istio/istio/master/samples/sleep/sleep.yaml
From any container with curl perform:
curl forward-proxy/headers -H Host:httpbin.org -H Foo:bar
or, alternatively:
http_proxy=forward-proxy:80 curl httpbin.org/headers -H Foo:bar
After each call, check the logs to verify that the traffic indeed went through both Envoy and NGINX:
kubectl logs forward-proxy nginx
you should see log lines similar to:
127.0.0.1 - - [02/Mar/2018:06:32:39 +0000] "GET http://httpbin.org/headers HTTP/1.1" 200 191 "-" "curl/7.47.0"
Envoy stats, from any pod with curl:
for HTTP: curl forward-proxy:8001/stats | grep '^http\.forward_http\.downstream_rq_[1-5]xx'
Check the number of http.forward_http.downstream_rq_2xx
- the number of times 2xx code was returned.
for HTTPS: curl forward-proxy:8001/stats | grep '^http\.forward_https\.downstream_rq_[1-5]xx'
Check the number of http.forward_https.downstream_rq_2xx
- the number of times 2xx code was returned.
curl -v forward-proxy:80 -H Host:edition.cnn.com
will return 301 Moved Permanently, location: https://edition.cnn.com/ .
The same result for:
http_proxy=forward-proxy:80 curl -v edition.cnn.com
We need to perform TLS origination for cnn.com:
curl -v forward-proxy:443 -H Host:edition.cnn.com
or
http_proxy=forward-proxy:443 curl -v edition.cnn.com
Note that we performed HTTP call and used an HTTP proxy (http_proxy
) to connect to edition.cnn.com via HTTPS. We send requests by HTTP, and the forward-proxy
performs TLS origination for us.
Edit sidecar_forward_proxy.yaml
: replace vadimeisenbergibm
with your docker hub username. Alternatively, just use the images from https://hub.docker.com/u/vadimeisenbergibm .
Deploy the forward proxy:
kubectl apply -f sidecar_forward_proxy.yaml
Get a shell into the sleep
container of the sidecar-forward-proxy
pod:
kubectl exec -it sidecar-forward-proxy -c sleep bash
Test the Envoy proxy with NGINX proxy. Note that here the traffic is catched by iptables and forwarded to the Envoy proxy.
curl httpbin.org/headers -H Foo:bar
curl edition.cnn.com:443
Note the HTTP call to the port 443. NGINX will perform TLS origination.
Verify in NGINX logs and Envoy stats that the traffic indeed passed thru Envoy and NGINX.
NGINX logs
kubectl logs sidecar-forward-proxy nginx
you should see log lines similar to:
127.0.0.1 - - [02/Mar/2018:06:32:39 +0000] "GET http://httpbin.org/headers HTTP/1.1" 200 191 "-" "curl/7.47.0"
Envoy stats
kubectl exec -it sidecar-forward-proxy -c envoy -- curl localhost:8001/stats | grep '^http\.forward_http\.downstream_rq_[1-5]xx'
Check the number of http.forward_http.downstream_rq_2xx
- the number of times 2xx code was returned.
kubectl exec -it sidecar-forward-proxy -c envoy -- curl localhost:8001/stats | grep '^http\.forward_https\.downstream_rq_[1-5]xx'
Check the number of http.forward_https.downstream_rq_2xx
- the number of times 2xx code was returned.
For performance measurements, let's deploy Envoy forward proxy for two predefined hosts, httpbin.org and edition.cnn.com.
kubectl apply -f forward_proxy_predefined_hosts.yaml
curl
installed, perform:curl forward-proxy-predefined-hosts/headers -H Foo: bar
curl -s forward-proxy-predefined-hosts:443 | grep -o '<title>.*</title>'
kubectl apply -f sidecar_orig_dst_proxy.yaml
kubectl exec -it sidecar-orig-dst-proxy -c fortio -- fortio load -curl -H Foo:bar http://httpbin.org/headers
kubectl apply -f forward_proxy_nginx.yaml
curl
installed, perform:
curl -H Foo:bar -H Host:httpbin.org http://forward-proxy-nginx/headers
Deploy a fortio pod:
kubectl apply -f fortio.yaml
Run performance tests, for example:
kubectl exec -it fortio -- fortio load http://httpbin.org/headers
kubectl exec -it fortio -- fortio load http://forward-proxy-predefined-hosts/headers
kubectl exec -it fortio -- fortio load -H Host:httpbin.org http://forward-proxy/headers
-curl
flag to fortio load
.original_dst
clusters.allow_absolute_urls
directive of http1_settings
of config
of the http_connection_manager
filter is set to true
, in the Envoy's configuration of the forward proxy for the other pods, so the other pods could use forward-proxy
as their http_proxy
.bind_to_port
to false
for ports 80 and 443 for the sidecar proxy, while setting bind_to_port
to true
for a listener on the port 15001 with use_original_dst
set to true
. The outbound traffic in the pod of the sidecar will be directed by iptables to the port 15001, and from there redirected by Envoy to the listeners on the ports 80 and 443.
Compare it with the forward proxy for the other pods. For that proxy there is no need to listen on the port 15001, and bind_to_port
is true
by default for the ports 80 and 443, the Envoy binds to these ports to accept incoming traffic into the forward_proxy
.proxy_ssl_server_name
directive of NGINX to on
, to set SNI for the port for TLS origination.