Using NGINX as HTTPS Forward Proxy Server

Time:2019-8-6

NGINX is mainly designed as a reverse proxy server, but with the development of NGINX, it can also be used as one of the options of forward proxy. Forward proxy itself is not complicated, and how to proxy encrypted HTTPS traffic is the main problem to be solved by forward proxy. This article will introduce two schemes of using NGINX to forward proxy HTTPS traffic, as well as their usage scenarios and main problems.

Classification of HTTP/HTTPS Forward Proxies

The classification of forward agents is briefly introduced as background knowledge for understanding the following:

Classification by client perception or not

  • General AgentThe client needs to manually set the agent’s address and port in the browser or system environment variables. For example, squid specifies the squid server IP and port 3128 on the client side.
  • Transparent AgentThe client does not need to do any proxy settings. The role of proxy is transparent to the client. For example, Web Gateway devices in enterprise network links.

Classification of HTTPS by proxy decryption or not

  • Tunnel AgentThat is, the transfer agent. The proxy server only transmits HTTPS traffic on TCP protocol, and is not aware of the specific content of its proxy traffic. The client and the destination server it visits interact directly with TLS/SSL. The NGINX proxy approach discussed in this article belongs to this pattern.
  • MITM, Man-in-the-Middle AgentProxy server decrypts HTTPS traffic, completes TLS/SSL handshake with self-signed certificate to client, and completes normal TLS interaction to destination server. Two TLS/SSL sessions are established in the client-proxy-server link. For example, Charles, a simple description of the principles can be referred to in the article.
    Note: In this case, the client actually gets the proxy server’s own self-signed certificate during the TLS handshake phase. The certificate chain verification is unsuccessful by default, and the client needs to trust the Root CA certificate of the proxy self-visa. So in the process, the client feels it. If we want to be a senseless transparent agent, we need to push the self-built ROOTCA certificate to the client, which is achievable in the internal environment of the enterprise.

Why does a forward agent need special handling to process HTTPS traffic?

As a reverse proxy, the proxy server usually terminates the HTTPS encrypted traffic and forwards it to the back-end instance. The encryption, decryption and authentication process of HTTPS traffic occurs between the client and the reverse proxy server.

As a forward proxy, HTTP encryption is encapsulated in TLS/SSL when dealing with the traffic from the client. The proxy server can not see the domain name that the client wants to access in the request URL, as shown below. So proxy HTTPS traffic, compared with HTTP, needs to do some special processing.

Using NGINX as HTTPS Forward Proxy Server

NGINX Solution

According to the classification method mentioned above, NGINX’s solution to HTTPS proxy belongs to the transmission (tunnel) mode, that is, it does not decrypt and does not perceive the upper traffic. Specific ways are as follows: 7-tier and 4-tier solutions.

HTTP CONNECT Tunnel (7 Layer Solution)

historical background

As early as 1998, in the SL era when TLS was not formally born, Netscape, which dominates the SSL protocol, proposed INTERNET-DRAFT for tunneling SSL traffic using web agents. The core idea is to establish an HTTP CONNECT Tunnel between the client and the proxy by using HTTP CONNECT requests. In the CONNECT requests, the destination host and port that the client needs to access are specified. The original drawings in Draft are as follows:

Using NGINX as HTTPS Forward Proxy Server

The whole process can refer to the chart in the HTTP authoritative guide:

  1. Client sends HTTP CONNECT request to proxy server.
  2. The proxy server uses the host and port in the HTTP CONNECT request to establish a TCP connection with the destination server.
  3. The proxy server returns the HTTP 200 response to the client.
  4. The client and proxy server set up the HTTP CONNECT tunnel. After HTTPS traffic arrives at the proxy server, it is transmitted directly to the remote destination server through TCP. The proxy server’s role is to pass through HTTPS traffic, and it does not need to decrypt HTTPS.

Using NGINX as HTTPS Forward Proxy Server

NGINX ngx_http_proxy_connect_module module

NGINX serves as a reverse proxy server, and the HTTP CONNECT method has not been officially supported. However, based on the modularization and scalability of NGINX, Ali’s @chobits provides the ngx_http_proxy_connect_module module to support the HTTP CONNECT method, so that NGINX can be extended to forward proxy.

Environmental Construction

Take the environment of CentOS 7 for example.

1) Installation
For the newly installed environment, refer to the normal installation steps and the steps of installing this module (https://github.com/chobits/ngx_http_proxy_connect_module), type the corresponding version of the patch and add the parameter – add-module=/path/to/ngx_http_proxy_connect_module when configuring. The example is as follows:

./configure \
--user=www \
--group=www \
--prefix=/usr/local/nginx \
--with-http_ssl_module \
--with-http_stub_status_module \
--with-http_realip_module \
--with-threads \
--add-module=/root/src/ngx_http_proxy_connect_module

For the environment that has been installed, compiled and installed, the above modules need to be added. The steps are as follows:

# Stop NGINX Service
# systemctl stop nginx
# Back up the original execution file
# cp /usr/local/nginx/sbin/nginx /usr/local/nginx/sbin/nginx.bak
# recompile in source path
# cd /usr/local/src/nginx-1.16.0
./configure \
--user=www \
--group=www \
--prefix=/usr/local/nginx \
--with-http_ssl_module \
--with-http_stub_status_module \
--with-http_realip_module \
--with-threads \
--add-module=/root/src/ngx_http_proxy_connect_module
# make
# Don't make install
# Overwrite the original nginx executable with a copy of the newly generated executable
# cp objs/nginx /usr/local/nginx/sbin/nginx
# /usr/bin/nginx -V
nginx version: nginx/1.16.0
built by gcc 4.8.5 20150623 (Red Hat 4.8.5-36) (GCC)
built with OpenSSL 1.0.2k-fips  26 Jan 2017
TLS SNI support enabled
configure arguments: --user=www --group=www --prefix=/usr/local/nginx --with-http_ssl_module --with-http_stub_status_module --with-http_realip_module --with-threads --add-module=/root/src/ngx_http_proxy_connect_module

2) nginx.conf file configuration

server {
     listen  443;

     # dns resolver used by forward proxying
     resolver  114.114.114.114;

     # forward proxy for CONNECT request
     proxy_connect;
     proxy_connect_allow            443;
     proxy_connect_connect_timeout  10s;
     proxy_connect_read_timeout     10s;
     proxy_connect_send_timeout     10s;

     # forward proxy for non-CONNECT request
     location / {
         proxy_pass http://$host;
         proxy_set_header Host $host;
     }
 }

Use scenarios

Layer 7 needs to build tunnels through HTTP CONNECT, which belongs to the common proxy mode that the client perceives. It needs to manually configure the IP and port of HTTP (S) proxy server on the client side. On the client side, access with curl plus-x parameter is as follows:

# curl https://www.baidu.com -svo /dev/null -x 39.105.196.164:443
* About to connect() to proxy 39.105.196.164 port 443 (#0)
*   Trying 39.105.196.164...
* Connected to 39.105.196.164 (39.105.196.164) port 443 (#0)
* Establish HTTP proxy tunnel to www.baidu.com:443
> CONNECT www.baidu.com:443 HTTP/1.1
> Host: www.baidu.com:443
> User-Agent: curl/7.29.0
> Proxy-Connection: Keep-Alive
>
< HTTP/1.1 200 Connection Established
< Proxy-agent: nginx
<
* Proxy replied OK to CONNECT request
* Initializing NSS with certpath: sql:/etc/pki/nssdb
*   CAfile: /etc/pki/tls/certs/ca-bundle.crt
  CApath: none
* SSL connection using TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
* Server certificate:
*     subject: CN=baidu.com,O="Beijing Baidu Netcom Science Technology Co., Ltd",OU=service operation department,L=beijing,ST=beijing,C=CN
...
> GET / HTTP/1.1
> User-Agent: curl/7.29.0
> Host: www.baidu.com
> Accept: */*
>
< HTTP/1.1 200 OK
...
{ [data not shown]

From the details printed out by the above-v parameter, we can see that the client first established the HTTP CONNECT tunnel to the proxy server 39.105.196.164, and the proxy responded to the HTTP/1.1 200 Connection Establishment and began to interact with TLS/SSL handshake and traffic.

NGINX stream (4-tier solution)

Since we are using the method of passing through the upper layer traffic, can we not be a “four-layer proxy” to thoroughly transmit the protocol over TCP/UDP? The answer is yes. NGINX has officially supported the ngx_stream_core_module module since version 1.9.0. The module is not built by default. When configure is needed, the – with-stream option is added to turn it on.

problem

Using NGINX stream to proxy HTTPS traffic at the TCP level is bound to encounter the problem mentioned at the beginning of this article: the proxy server cannot obtain the destination domain name that the client wants to access. Because the information acquired at TCP level is limited to IP and port level, there is no chance to get domain name information. In order to get the destination domain name, we must have the ability to disassemble the upper message to get the domain name information, so NGINX stream is not a four-tier agent in the strict sense, or it needs a little help from the upper ability.

Ngx_stream_ssl_preread_module module

To get the domain name accessed by HTTPS traffic without decryption, only the extended address SNI (Server Name Indication) in the first Client Hello message of the TLS/SSL handshake is used. NGINX has officially supported the use of the ngx_stream_ssl_preread_module module, which is mainly used to obtain SNI and ALPN information in Client Hello messages, since version 1.11.5. For a four-tier forward agent, the ability to extract SNI from Client Hello messages is critical, otherwise the NGINX stream solution cannot be established. At the same time, this also brings a limitation, requiring all clients to bring SNI fields in the TLS/SSL handshake, otherwise the NGINX stream proxy can not know the destination domain name the client needs to access.

Environmental Construction

1) Installation
For newly installed environments, refer to the normal installation steps and add the options of — with – stream, — with – stream_ssl_preread_module and — with-stream_ssl_module directly when configuring. Examples are as follows:

./configure \
--user=www \
--group=www \
--prefix=/usr/local/nginx \
--with-http_ssl_module \
--with-http_stub_status_module \
--with-http_realip_module \
--with-threads \
--with-stream \
--with-stream_ssl_preread_module \
--with-stream_ssl_module

For the environment that has been installed, compiled and installed, we need to add the above three stream-related modules, the steps are as follows:

# Stop NGINX Service
# systemctl stop nginx
# Back up the original execution file
# cp /usr/local/nginx/sbin/nginx /usr/local/nginx/sbin/nginx.bak
# recompile in source path
# cd /usr/local/src/nginx-1.16.0
# ./configure \
--user=www \
--group=www \
--prefix=/usr/local/nginx \
--with-http_ssl_module \
--with-http_stub_status_module \
--with-http_realip_module \
--with-threads \
--with-stream \
--with-stream_ssl_preread_module \
--with-stream_ssl_module
# make
# Don't make install
# Overwrite the original nginx executable with a copy of the newly generated executable
# cp objs/nginx /usr/local/nginx/sbin/nginx
# nginx -V
nginx version: nginx/1.16.0
built by gcc 4.8.5 20150623 (Red Hat 4.8.5-36) (GCC)
built with OpenSSL 1.0.2k-fips  26 Jan 2017
TLS SNI support enabled
configure arguments: --user=www --group=www --prefix=/usr/local/nginx --with-http_ssl_module --with-http_stub_status_module --with-http_realip_module --with-threads --with-stream --with-stream_ssl_preread_module --with-stream_ssl_module

2) nginx.conf file configuration
NGINX stream, unlike HTTP, needs to be configured in stream blocks, but the instruction parameters are similar to HTTP blocks. The main configurations are as follows:

stream {
    resolver 114.114.114.114;
    server {
        listen 443;
        ssl_preread on;
        proxy_connect_timeout 5s;
        proxy_pass $ssl_preread_server_name:$server_port;
    }
}

Use scenarios

For 4-tier forward proxy, NGINX basically passes through the upper traffic, and does not need HTTP CONNECT to build tunnels. A model suitable for transparent proxy, such as using DNS to de-direct the domain name to the proxy server. We can simulate it by binding / etc / hosts on the client side.

On the client side:

cat /etc/hosts
...
# Bind the domain name www.baidu.com to the forward proxy server 39.105.196.164
39.105.196.164 www.baidu.com

# Use curl to visit www.baidu.com.
# curl https://www.baidu.com -svo /dev/null
* About to connect() to www.baidu.com port 443 (#0)
*   Trying 39.105.196.164...
* Connected to www.baidu.com (39.105.196.164) port 443 (#0)
* Initializing NSS with certpath: sql:/etc/pki/nssdb
*   CAfile: /etc/pki/tls/certs/ca-bundle.crt
  CApath: none
* SSL connection using TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
* Server certificate:
*     subject: CN=baidu.com,O="Beijing Baidu Netcom Science Technology Co., Ltd",OU=service operation department,L=beijing,ST=beijing,C=CN
* Start date: May 09 01:22:02 2019 GMT
* expire date: June 2505:31:02 2020 GMT
*     common name: baidu.com
*     issuer: CN=GlobalSign Organization Validation CA - SHA256 - G2,O=GlobalSign nv-sa,C=BE
> GET / HTTP/1.1
> User-Agent: curl/7.29.0
> Host: www.baidu.com
> Accept: */*
>
< HTTP/1.1 200 OK
< Accept-Ranges: bytes
< Cache-Control: private, no-cache, no-store, proxy-revalidate, no-transform
< Connection: Keep-Alive
< Content-Length: 2443
< Content-Type: text/html
< Date: Fri, 21 Jun 2019 05:46:07 GMT
< Etag: "5886041d-98b"
< Last-Modified: Mon, 23 Jan 2017 13:24:45 GMT
< Pragma: no-cache
< Server: bfe/1.0.8.18
< Set-Cookie: BDORZ=27315; max-age=86400; domain=.baidu.com; path=/
<
{ [data not shown]
* Connection #0 to host www.baidu.com left intact

Common problem

1) The client manually sets up the proxy causing the access to be unsuccessful
The four-tier forward proxy is to pass through the upper HTTPS traffic, and it does not need HTTP CONNECT to establish the tunnel, that is to say, it does not need the client to set up HTTP (S) proxy. If we set up HTTP (s) proxy manually on the client side, can we use curl-x to set up proxy for this forward server access test and see the results:

# curl https://www.baidu.com -svo /dev/null -x 39.105.196.164:443
* About to connect() to proxy 39.105.196.164 port 443 (#0)
*   Trying 39.105.196.164...
* Connected to 39.105.196.164 (39.105.196.164) port 443 (#0)
* Establish HTTP proxy tunnel to www.baidu.com:443
> CONNECT www.baidu.com:443 HTTP/1.1
> Host: www.baidu.com:443
> User-Agent: curl/7.29.0
> Proxy-Connection: Keep-Alive
>
* Proxy CONNECT aborted
* Connection #0 to host 39.105.196.164 left intact

You can see that the client attempted to establish HTTP CONNECT tunnel before forward NGINX, but because NGINX is passed through, the CONNECT request is forwarded directly to the destination server. The destination server does not accept the CONNECT method, so the “Proxy CONNECT aborted” finally appears, resulting in unsuccessful access.

2) Failure of access due to client’s lack of SNI
One of the key factors mentioned above is to extract SNI fields from Client Hello using ngx_stream_ssl_preread_module. If the client does not carry SNI fields, the proxy server will not be able to know the destination domain name, resulting in unsuccessful access.

In transparent proxy mode (simulated by manually binding hosts), we can use OpenSSL to simulate on the client side:

# openssl s_client -connect www.baidu.com:443 -msg
CONNECTED(00000003)
>>> TLS 1.2  [length 0005]
    16 03 01 01 1c
>>> TLS 1.2 Handshake [length 011c], ClientHello
    01 00 01 18 03 03 6b 2e 75 86 52 6c d5 a5 80 d7
    a4 61 65 6d 72 53 33 fb 33 f0 43 a3 aa c2 4a e3
    47 84 9f 69 8b d6 00 00 ac c0 30 c0 2c c0 28 c0
    24 c0 14 c0 0a 00 a5 00 a3 00 a1 00 9f 00 6b 00
    6a 00 69 00 68 00 39 00 38 00 37 00 36 00 88 00
    87 00 86 00 85 c0 32 c0 2e c0 2a c0 26 c0 0f c0
    05 00 9d 00 3d 00 35 00 84 c0 2f c0 2b c0 27 c0
    23 c0 13 c0 09 00 a4 00 a2 00 a0 00 9e 00 67 00
    40 00 3f 00 3e 00 33 00 32 00 31 00 30 00 9a 00
    99 00 98 00 97 00 45 00 44 00 43 00 42 c0 31 c0
    2d c0 29 c0 25 c0 0e c0 04 00 9c 00 3c 00 2f 00
    96 00 41 c0 12 c0 08 00 16 00 13 00 10 00 0d c0
    0d c0 03 00 0a 00 07 c0 11 c0 07 c0 0c c0 02 00
    05 00 04 00 ff 01 00 00 43 00 0b 00 04 03 00 01
    02 00 0a 00 0a 00 08 00 17 00 19 00 18 00 16 00
    23 00 00 00 0d 00 20 00 1e 06 01 06 02 06 03 05
    01 05 02 05 03 04 01 04 02 04 03 03 01 03 02 03
    03 02 01 02 02 02 03 00 0f 00 01 01
140285606590352:error:140790E5:SSL routines:ssl23_write:ssl handshake failure:s23_lib.c:177:
---
no peer certificate available
---
No client certificate CA names sent
---
SSL handshake has read 0 bytes and written 289 bytes
...

By default, OpenSSL s_client does not have SNI. You can see that the above request ends at the TLS/SSL handshake stage when Client Hello is issued. Because the proxy server does not know which destination domain to forward Client Hello to.

If the SNI is specified by OpenSSL with servername parameters, the normal access can be successful. The command is as follows:

# openssl s_client -connect www.baidu.com:443 -servername www.baidu.com

summary

This paper summarizes the principle, environment, scenarios and main problems of NGINX using HTTP CONNECT tunnel and NGINX stream to do HTTPS forward proxy, hoping to provide reference for you when doing various scenarios forward proxy.


Author of this article: Huaizhi

Read the original text

This article is the original content of Yunqi Community, which can not be reproduced without permission.