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. Useiptables
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:
- Using Envoy with NGINX as a generic forward proxy for other pods (other pods can access arbitrary hosts via the forward proxy)
- Using Envoy with NGINX as a sidecar generic forward proxy (the application in the pod can access arbitrary hosts via the forward proxy)
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
: replacevadimeisenbergibm
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 thesleep
pod from the Istio samples. Any other pod withcurl
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:
-
NGINX logs
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
: replacevadimeisenbergibm
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
-
for HTTP:
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. -
for HTTPS:
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.
- Deploy the forward proxy with predefined hosts:
kubectl apply -f forward_proxy_predefined_hosts.yaml
- From a pod with
curl
installed, perform:
curl forward-proxy-predefined-hosts/headers -H Foo: bar
- Perform:
curl -s forward-proxy-predefined-hosts:443 | grep -o '<title>.*</title>'
- Deploy a sidecar Envoy with original_dst cluster, without NGINX:
kubectl apply -f sidecar_orig_dst_proxy.yaml
- The pod contains a fortio container, for perfomance measurements. Perform:
kubectl exec -it sidecar-orig-dst-proxy -c fortio -- fortio load -curl -H Foo:bar http://httpbin.org/headers
- Deploy:
kubectl apply -f forward_proxy_nginx.yaml
- From a pod with
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
- To check that the hosts are accessed correctly, add
-curl
flag tofortio load
.
- envoy_forward_proxy contains Envoy's configuration and a Dockerfile for the case of the forward proxy for other pods.
- envoy_sidecar_forward_proxy contains Envoy's configuration, a Dockerfile and scripts to direct the traffic inside the pod by iptables for the case of the sidecar forward proxy.
- nginx_forward_proxy contains NGINX's configuration and a Dockerfile for NGINX as a forward proxy.
- sleep contains a Docker file, which extends the Istio sleep sample, by adding a non-root user.
- envoy_predefined_hosts_forward_proxy contains Envoy's configuration and a Dockerfile for the case of the forward proxy for other pods, with two predefined proxied hosts, httpbin.org on the port 80 and edition.cnn.com on the port 443.
- envoy_sidecar_orig_dst_proxy contains Envoy's configuration, a Dockerfile and scripts to direct the traffic inside the pod by iptables, for the case where Envoy is standalone generic forward proxy with
original_dst
clusters. - nginx_forward_proxy_standalone contains NGINX's configuration and a Dockerfile for NGINX as a standalone forward proxy, without Envoy.
- The
allow_absolute_urls
directive ofhttp1_settings
ofconfig
of thehttp_connection_manager
filter is set totrue
, in the Envoy's configuration of the forward proxy for the other pods, so the other pods could useforward-proxy
as theirhttp_proxy
. - I set
bind_to_port
tofalse
for ports 80 and 443 for the sidecar proxy, while settingbind_to_port
totrue
for a listener on the port 15001 withuse_original_dst
set totrue
. 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, andbind_to_port
istrue
by default for the ports 80 and 443, the Envoy binds to these ports to accept incoming traffic into theforward_proxy
. - I set
proxy_ssl_server_name
directive of NGINX toon
, to set SNI for the port for TLS origination. - NGINX listens on the localhost, to reduce the attack surface. It is not possible to connect to NGINX from outside of the pod.
- iptables catch all the traffic, except for the users root, www-data , and for a specially created envoyuser. Excluding www-data from Envoy's traffic control is required since NGINX workers run as www-data. Excluding root from Envoy's traffic control is required since NGINX itself has to run as root. Envoy runs as envoyuser, and its traffic must not be controlled by Envoy as well (otherwise an infinite loop will be created). The app container, sleep runs as sleepuser. Note that for the apps that run as root the traffic will not be handled by the sidecar proxy, since root is excluded by iptables to be redirected to Envoy (the requirement due to NGINX).