chobits / ngx_http_proxy_connect_module

A forward proxy module for CONNECT request handling
BSD 2-Clause "Simplified" License
1.85k stars 498 forks source link

proxy auth in [client] -> [nginx connect] -> [nginx reverse proxy] -> [backend] -> [real site] #282

Closed DeoLeung closed 1 year ago

DeoLeung commented 1 year ago

I'm having the following config

# a minimum proxy setting example
events {
}

http {
  server {
    listen 80;

    access_log /dev/stdout;
    error_log /dev/stdout;

    # forward proxy for CONNECT requests
    proxy_connect;
    proxy_connect_allow            443;
    proxy_connect_connect_timeout  10s;
    proxy_connect_data_timeout     10s;
    proxy_connect_address 127.0.0.1:443;
    # not working
    proxy_set_header Proxy-Authorization $http_proxy_authorization;

    location / {
        return 403 "Non-CONNECT requests are forbidden";
    }
  }
  server {
    listen 443 ssl;
    server_name ok.com;

    access_log /dev/stdout;
    error_log /dev/stdout;

    # self signed certificate generated via openssl command
    ssl_certificate_key            server.key;
    ssl_certificate                trust.crt;
    ssl_session_cache              shared:SSL:1m;

    location / {
      proxy_set_header Host $http_host;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header X-Forwarded-Proto $scheme;
      proxy_redirect off;
      proxy_buffering off;
      proxy_pass http://uvicorn/dispatch/$host$request_uri;
    }
  }

  upstream uvicorn {
    # mac docker connect to host ip
    server host.docker.internal:3000;
  }

}

using curl

curl --cacert ca.crt -v -x http://a:b@localhost:3001 --location 'https://ok.com/abc' --header 'Content-Type: application/json' --data '{}'

*   Trying 127.0.0.1:3001...
* Connected to (nil) (127.0.0.1) port 3001 (#0)
* allocate connect buffer
* Establish HTTP proxy tunnel to ok.com:443
* Proxy auth using Basic with user 'a'
> CONNECT ok.com:443 HTTP/1.1
> Host: ok.com:443
> Proxy-Authorization: Basic YTpi
> User-Agent: curl/7.86.0
> Proxy-Connection: Keep-Alive
>
< HTTP/1.1 200 Connection Established
< Proxy-agent: nginx
<
* Proxy replied 200 to CONNECT request
* CONNECT phase completed
* ALPN: offers h2
* ALPN: offers http/1.1
*  CAfile: ca.crt
*  CApath: none
* (304) (OUT), TLS handshake, Client hello (1):
* (304) (IN), TLS handshake, Server hello (2):
* (304) (IN), TLS handshake, Unknown (8):
* (304) (IN), TLS handshake, Certificate (11):
* (304) (IN), TLS handshake, CERT verify (15):
* (304) (IN), TLS handshake, Finished (20):
* (304) (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / AEAD-CHACHA20-POLY1305-SHA256
* ALPN: server accepted http/1.1
* Server certificate:
*  subject: C=JP; ST=Tokyo; L=Suginami; O=Example Company, Inc.; OU=IT; CN=example.com
*  start date: Aug  2 05:16:08 2023 GMT
*  expire date: Jul  9 05:16:08 2123 GMT
*  subjectAltName: host "ok.com" matched cert's "ok.com"
*  issuer: C=JP; ST=Tokyo; L=Suginami; O=Example Company, Inc.; OU=IT; CN=example.com
*  SSL certificate verify ok.
> POST /abc HTTP/1.1
> Host: ok.com
> User-Agent: curl/7.86.0
> Accept: */*
> Content-Type: application/json
> Content-Length: 2
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 404 Not Found
< Server: nginx/1.25.1
< Date: Wed, 02 Aug 2023 06:27:25 GMT
< Content-Type: application/json
< Content-Length: 22
< Connection: keep-alive
<
* Connection #0 to host (nil) left intact
{"detail":"Not Found"}

I'm using this route to add some processing of the request before it's sent to the real site. so the client user don't need to modify the request, just add a proxy, we could inject some credential and audit and send it out.

my question is , is it possible to forward the Proxy-Authorization header to backend for every subsequence requests? or it's there any hacky way to carry this information for every tunneled requests?

Also, is there any improvement I can make for this route, currently I need to self-signed a ca and have my client disable verification, something like verify=False.

chobits commented 1 year ago

my question is , is it possible to forward the Proxy-Authorization header to backend for every subsequence requests? or it's there any hacky way to carry this information for every tunneled requests?

Unfortunately, this module does not support this feature. The data sent through the established CONNECT tunnel is transmitted transparently between the client and the endpoint. In theory, it is not possible to determine the specific protocol being used, as the CONNECT tunnel protocol is too simple to allow for explicit detection of the specific protocol being used.

But in practice people usually use this tunnel to proxy SSL data flow, and if you have already known the data flow over the tunnel is SSL protocol, you can unpack it and insert the "Proxy-Authorization" header. You can do it in your listen 443 ssl - server_name ok.com server, or you can add an addtional nginx reverse server to do this between [nginx connect] and [nginx reverse proxy] as long as your intermediate proxy can hold the correct SSL certificate. The key problem is how to decrypt the SSL stream, however, this method may not work for commercial websites, as obtaining their SSL certificates may not be possible.

DeoLeung commented 1 year ago

yes, using a self-signed certificate now I can process the proxied ssl data for known sites. (though I need to set unverified in http client, but it's ok for internal server or cli use)

from my testing, it's now impossible to add the proxy-authorization(notice it's not a fixed user/password, it's dynamic I want it to be checked in backend ), as the information is only visible in connect (server 80), server 443 won't receive it. so I just wonder is it possible to add some extra variable through the tunnel from 80 to 443.

anyway I can let the client explicit carry the information for now. If we could figure out a way to carry this information from connect, will be much better :)

chobits commented 1 year ago

yes, using a self-signed certificate now I can process the proxied ssl data for known sites. (though I need to set unverified in http client, but it's ok for internal server or cli use)

from my testing, it's now impossible to add the proxy-authorization(notice it's not a fixed user/password, it's dynamic I want it to be checked in backend ), as the information is only visible in connect (server 80), server 443 won't receive it. so I just wonder is it possible to add some extra variable through the tunnel from 80 to 443.

anyway I can let the client explicit carry the information for now. If we could figure out a way to carry this information from connect, will be much better :)

If you want to do this in proxy_connect module, it is very complicated to implement, especially developing the logic to unpack data flow and insert additional information, all your need to do is to implemnt a new SSL server. And meanwhile, you cannot get some information from the data flow (like unpacking and reading information from the proxied data flow).

That's why it is easy to add an nginx proxy between [nginx connect] and [nginx reverse proxy], you use the lua-nginx-module to modify the data flow (HTTP requests) dynamically on that intermediate nginx proxy.