NGINX was initially designed as a reverse proxy server. However, with continuous development, NGINX also serves as one of the options to implement the forward proxy. The forward proxy itself is not complex, the key issue it addresses is how to encrypt HTTPS traffic. This article describes two methods for using NGINX as the forward proxy for HTTPS traffic, as well as their application scenarios and principal problems.
To begin with, let's take a closer look at the classification of the forward proxy.
Note: In this case, the client actually obtains the self-signed certificate of the proxy server in the TLS handshake process, and verification of the certificate chain is unsuccessful by default. The Root CA certificate among the proxy self-signed certificates must be trusted on the client. Therefore, the client is aware of the proxy in this process. A transparent proxy is achieved if the self-signed Root CA certificate is pushed to the client, which is implemented in the internal environment of an enterprise.
While serving as a reverse proxy, the proxy server usually terminates HTTPS encrypted traffic and forwards it to the backend instance. Encryption, decryption, and authentication of HTTPS traffic occur between the client and the reverse proxy server.
On the other hand, when acting as a forward proxy and processing the traffic sent by the client, the proxy server doesn't see the target domain name in the URL requested by the client since the HTTP traffic is encrypted and encapsulated in TLS/SSL, as shown in the following figure. Therefore, unlike HTTP traffic, HTTPS traffic requires some special processing during proxy implementation.
According to the classification in the preceding sections, when NGINX is used as the HTTPS proxy, the proxy is a transparent transmission (tunnel) proxy, which neither decrypts nor perceives the upper layer traffic. Specifically, two NGINX solutions are available: Layer 7 (L7) and Layer 4 (L4). The following sections describes these solutions in detail.
As early as 1998 when TLS was still not formally available, Netscape, which promoted the SSL protocol, proposed using the Web proxy for the tunneling of SSL traffic. The core idea is to use the HTTP CONNECT request to establish an HTTP CONNECT tunnel between the client and the proxy. The CONNECT request must specify the target host and port that the client needs to access. The original diagram in INTERNET-DRAFT is as follows:
For more information about the entire process, refer to the diagram in the HTTP: The Definitive Guide. The following steps briefly outlines the process.
1) The client sends an HTTP CONNECT request to the proxy server.
2) The proxy server uses the host and port information in the HTTP CONNECT request to establish a TCP connection with the target server.
3) The proxy server returns an HTTP 200 response to the client.
4) The client establishes an HTTP CONNECT tunnel with the proxy server. After HTTPS traffic arrives at the proxy server, the proxy server transparently transmits HTTPS traffic to the remote target server through the TCP connection. The proxy server only transparently transmits HTTPS traffic and does not decrypt HTTPS traffic.
As a reverse proxy server, NGINX does not officially support the HTTP CONNECT method. However, thanks to the modular and scalable features of NGINX, Alibaba @chobits provides the ngx_http_proxy_connect_module
connect module (content in Chinese) to support the HTTP CONNECT method, to extend NGINX as a forward proxy.
Considering CentOS 7 environment as an example, let's take a look at the process in detail.
1) Environment Installation
For new environment installation, refer to the common installation steps (content in Chinese) for installing the ngx_http_proxy_connect_module
connect module.
Install the patch for the corresponding version, and add the --add-module=/path/to/ngx_http_proxy_connect_module
parameter under the "configure" command, as shown in the following example.
./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
Also, add ngx_http_proxy_connect_module
for the existing environments as shown below.
# 停止NGINX服务
# systemctl stop nginx
# 备份原执行文件
# cp /usr/local/nginx/sbin/nginx /usr/local/nginx/sbin/nginx.bak
# 在源代码路径重新编译
# 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
# 不要make install
# 将新生成的可执行文件拷贝覆盖原来的nginx执行文件
# 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) Configure nginx.conf File
Execute the following commands to configure the nginx.conf
file.
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;
}
}
In the L7 solution, the HTTP CONNECT request must establish a tunnel, and therefore, the proxy server is a common proxy that the client must perceive. Manually configure the IP address and port of the HTTP(S) proxy server on the client. Access the client using the "-x" parameter of cURL as shown below.
# 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]
The preceding details printed by the "-v" parameter indicate that the client first establishes an HTTP CONNECT tunnel with the proxy server 39.105.196.164. Once the proxy replies with "HTTP/1.1 200 Connection Established", the client initiates a TLS/SSL handshake and sends traffic to the server.
Since the upper-layer traffic is transparently transmitted, the critical question that arises here, is whether NGINX should serve as an "L4 proxy" to implement completely transparent transmission of protocols above TCP/UDP. The answer is yes. NGINX 1.9.0 or later supports ngx_stream_core_module
. This module is not built by default. Add the -- with-stream
option under the configure
command to enable this module.
Using NGINX stream as a proxy of the HTTPS traffic at the TCP layer, leads to the same problem mentioned at the beginning of this article: the proxy server does not obtain the target domain name that the client wants to access. This happens because the information obtained at the TCP layer is limited to the IP address and port, without obtaining the domain name. To obtain the target domain name, the proxy must be able to extract the domain name from the upper-layer packets. Therefore, NGINX stream is not an L4 proxy in a strict sense, and it must seek help from the upper layer to extract the domain name.
In order to obtain the target domain name of HTTPS traffic without decrypting HTTPS traffic, the only method is to use the SNI field contained in the first ClientHello packet during the TLS/SSL handshake. Starting from the version 1.11.5, NGINX supports ngx_stream_ssl_preread_module
. This module helps to obtain SNI and ALPN from the ClientHello packet. For a L4 forward proxy, the ability to extract SNI from the ClientHello packet is crucial, otherwise the NGINX stream solution will not be implemented. This, however, also brings a restriction that all clients must include the SNI field in the ClientHello packets during the TLS/SSL handshake. Otherwise, the NGINX stream proxy wouldn't know the target domain name that the client needs to access.
1) Environment Installation
For the newly installed environment, refer to the common installation steps (content in Chinese), and directly add the --with-stream
, --with-stream_ssl_preread_module
, and --with-stream_ssl_module
options under the "configure" command. Consider the example below for better understanding.
./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
Add the preceding three stream-related modules for already installed and compiled environments as shown below.
# 停止NGINX服务
# systemctl stop nginx
# 备份原执行文件
# cp /usr/local/nginx/sbin/nginx /usr/local/nginx/sbin/nginx.bak
# 在源代码路径重新编译
# 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
# 不要make install
# 将新生成的可执行文件拷贝覆盖原来的nginx执行文件
# 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) Configure nginx.conf File
Unlike HTTP, configure NGINX stream in the stream block. Though, the command parameters are similar to those of the HTTP block. The following snippet shows the main configuration.
stream {
resolver 114.114.114.114;
server {
listen 443;
ssl_preread on;
proxy_connect_timeout 5s;
proxy_pass $ssl_preread_server_name:$server_port;
}
}
As L4 forward proxy, NGINX basically transparently transmits traffic to the upper layer, and does not require HTTP CONNECT to establish a tunnel. Therefore, the L4 solution is suitable for the transparent proxy mode. For example, when the target domain name is directed to the proxy server by means of DNS resolution, it require simulating the transparent proxy mode by binding /etc/hosts
to the client.
The following snippet shows the commands on the client:
cat /etc/hosts
...
# 把域名www.baidu.com绑定到正向代理服务器39.105.196.164
39.105.196.164 www.baidu.com
# 正常利用curl来访问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: 5月 09 01:22:02 2019 GMT
* expire date: 6月 25 05: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
Now, let's take a quick look at the key problems concerning the L4 solution.
1) Access attempt failure due to the manual proxy settings on the client.
The L4 forward proxy transparently transmits the upper-layer HTTPS traffic and does not require HTTP CONNECT to establish a tunnel. Thus, it is unnecessary to set the HTTP(S) proxy on the client. Critical question is whether manually setting the HTTP(S) proxy on the client, ensures a successful access attempt. Use the "-x" parameter of cURL to set the forward proxy server and test the access to this server. The following snippet shows the result.
# 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
The result indicates that the client tries to establish an HTTP CONNECT tunnel before NGINX. However, as NGINX transparently transmits the traffic, the CONNECT request is directly forwarded to the target server. The target server does not accept the CONNECT method. Therefore, "Proxy CONNECT aborted" reflects in the above snippet, resulting in an access failure.
2) Access attempt failure as the client does not include SNI in the ClientHello packet.
As mentioned earlier, when NGINX stream is used as a forward proxy, it is crucial to use ngx_stream_ssl_preread_module
to extract the SNI field from ClientHello. If the client does not include SNI in the ClientHello packet, the proxy server wouldn't know the target domain name, resulting in an access failure.
In transparent proxy mode (simulated by manually binding hosts), use OpenSSL for simulation on the client.
# 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
⋯
OpenSSL s_client
does not include SNI by default. As the snippet shows, the preceding request terminates in the TLS/SSL handshake phase after ClientHello is sent. This occurs because the proxy server does not know the target domain name where ClientHello should be forwarded.
Using OpenSSL with the "servername" parameter to specify SNI, results in successful access.
# openssl s_client -connect www.baidu.com:443 -servername www.baidu.com
This article describes two methods for using NGINX as the forward proxy for HTTPS traffic. It summarizes the principles, environment building requirements, application scenarios, and key problems of the solutions where NGINX acts as the HTTPS forward proxy using the HTTP CONNECT tunnel and NGINX stream. This article serves as a reference while you use NGINX as a forward proxy in various scenarios.
TCP Connection Analysis Why the Socket Remains in the FIN_WAIT_1 State Post Killing the Process
Analysis of TLS/SSL Handshake Failure Scenarios on Alibaba Cloud
7 posts | 5 followers
FollowAlibaba Clouder - August 27, 2020
Alex - June 21, 2019
Haemi Kim - June 14, 2021
Alibaba Clouder - June 20, 2018
Alibaba Developer - May 31, 2021
Alibaba Clouder - January 11, 2018
Very good article and very helpful..we have installed Ngnix and used modules (proxy_set_header and ssl_preread - Stream L4 Solution) to act ngnix as a transparent mode even for ssl encrypted traffic.. thx
7 posts | 5 followers
FollowExplore Web Hosting solutions that can power your personal website or empower your online business.
Learn MoreElastic and secure virtual cloud servers to cater all your cloud hosting needs.
Learn MoreAlibaba Cloud is committed to safeguarding the cloud security for every business.
Learn MoreHTTPDNS is a domain name resolution service for mobile clients. It features anti-hijacking, high accuracy, and low latency.
Learn MoreMore Posts by William Pan
Techhelpdesk October 10, 2020 at 5:24 pm
good one