NginX (Host) as transparent proxy for NginX-Proxy (Docker Container)

nginxreverse-proxytransparent-proxy

Problem

I am setting up Nginx-Proxy within docker upon my server. NginX-Proxy is meant to be directly accessible from the web and I would like to place a proxy in front of it.

Desired flow of connections

Ideally NginX(Host) should be the web facing server and reverse proxy back to the NginX-Proxy(Docker) which is served from a loopback address (127.0.0.2).

Essentially I require a proxy configuration for unrecognized sub-domains that passes these requests on to the NginX-Docker proxy, which hosts these sub-domains, any one of which may or may not have SSL enabled.

Homework

NginX supports proxying by various means, but none of them seem to quite fit my use case.

  1. I was hoping that NginX, on the host, might transparently proxy the docker instance using a configuration similar to that of Daniel James. This configuration performs SSL termination at the host which does not seem especially DRY, given that the docker containers, routed via the Nginx-Proxy, are themselves (possibly) doing SSL termination. Commonly this seems to be done with a 301 redirect (Due to limitations of older versions of Nginx) to force all incoming client traffic to use https and forcing the back end/upstream to use, exclusively, either HTTP or HTTPS. I would like to avoid termination at NginX(Host) and let the Nginx-Proxy handle it. The nice feature here is that one may filter domain name e.g. .domain.tld and *.domain.tld patterns. I have seen this referenced as a man in the middle proxy.

Man in the Middle Proxy

  1. NginX discusses a sort of compliments to this in it's Proxy-Protocol docs. I tried setting up a stream as shown but it seems one cannot filter the stream by domain name within the server block. Although it seems to forward both HTTP and HTTPS traffic without alteration. I understand this to be a sort of pass through proxy (Used more for load balancing/shaping).

Pass through proxy

  1. IP transparency seems to offer a solution that is more inline with my requirements. It has the down side that the NginX (Host) runners/thread must be run with root privileges and I'm not sure what the security implications are here (Not that I'm hosting top secret stuff but a hole is a hole). It also requires that one modify the docker containers routing/IP tables which I'm not sure is possible.

  2. The IP transparency doc also mentions DSR as an alternative means of proxying another server but then stats that NginX does not support this. I do not know what this is.

MWE

In my initial efforts I had hashed together the following which returns the site via HTTP alright but fails to return the site over HTTPS. This takes the MitM/reverse proxy strategy. I have left this in for consistency with the comments.

server {
 server_name .domain.tld;
 # note : .domain.tld matches both *.domain.tld and domain.tld
 listen X.X.X.X:80;
 listen [X:X:X:X:X:X:X]:80;
 listen X.X.X.X:443 ssl;
 listen [X:X:X:X:X:X:X]:443 ssl;

 server_tokens off;

 # Logging
 access_log /var/log/nginx/docker_proxy-access.log main;
 error_log /var/log/nginx/docker_proxy-error.log info;

 location / {
  proxy_pass $scheme://127.X.X.X; # Local service IP Address 
  # Alternatively : proxy_pass $scheme://$http_host$uri$is_args$args;
  proxy_set_header X-Real-IP $remote_addr;
  proxy_set_header Host $host;
 }
}

How should I pass through the requested domain ? Using 127.X.X.X seems to confuse the upstream service which needs the domain name to resolve the docker container that provides a service (This was resolved in the comments). I'm not sure if I should configure an upstream service or use proxy_bind here ? Should I be setting the proxy_redirect attribute too ? Should I have separate server blocks for HTTP and HTTPS ?

Best Answer

Since I first asked this I tried the suggestions of @c4f4t0r to use Traefik directly. This largely worked but Traefik is a level 7 proxy, that is it operates in the application layer and not on the transport layer. It cannot redirect any HTTPS traffic to a local webserver alongside it on the host. So SSL-Pass through (SNI) is not an option here. It seems one may perform SSL-Bridging, that is terminate the SSL connection at Traefik and create a new one to the local server.

What I missed from @Ernest Kiwele's advice was to check whether the upstream server handled the Proxy protocol, it does but I didn't know it did so nor had I enabled it at the time.

There is also a subtlety here, in the above configuration I essentially terminate the HTTP connection, modify the Header and establish a new one. This is done via the proxy protocol to the upstream service. As it's HTTP it is quite common to bridge the connection in this way. HTTPS additionally requires re-encryption at each termination/initiation stage. I was hoping to pass it through without decrypting/re-encrypting the traffic. As it turns out this is possible with NginX streams as shown in this answer.

To solve my problem I had to create both a http{server{}} server block and a stream{server{}} block. The former bridging and wrapping the HTTP connection within the proxy protocol at NginX. The latter wraps the HTTPS stream within a proxy protcol wrapper, determined by SNI, and passes it directly to the relevant backend, effectively by passing NginX and the need to bridge and de/encrypt.