Nginx TLS TCP Proxy server for tcp upstream servers

Nginx TLS TCP Proxy server for tcp upstream servers

What is SSL Termination

SSL/TLS termination means that NGINX acts as the server-side SSL/TLS endpoint for connections with clients: it performs the decryption of requests and encryption of responses that backend servers would otherwise have to do. The operation is called termination because NGINX closes the client connection and forwards the client data over a newly created, unencrypted connection to the servers in an upstream tcp servers. In release R6 and later, NGINX performs SSL/TLS termination for TCP connections as well as HTTP connections.

Nginx can be configured to route to a backend, based on the server's domain name, which is included in the SSL/TLS handshake (Server Name Indication, SNI ). This works for http upstream servers, but also for other protocols, that can be secured with TLS.

What is SNI

Server Name Indication (SNI) is an extension to the Transport Layer Security (TLS) computer networking protocol by which a client indicates which hostname it is attempting to connect to at the start of the handshaking process. This allows a server to present one of multiple possible certificates on the same IP address and TCP port number.

Prior to SNI, when making a TLS connection, the client had no way to specify which site it is trying to connect to. Hence, if one physical server hosts multiple services, the server has no way to know which certificate to use in the TLS protocol. In more detail, when making a TLS connection, the client requests a digital certificate from the server. Once the server sends the certificate, the client examines it and compares the name it was trying to connect to with the name(s) included in the certificate. If a match occurs, the connection proceeds as normal. If a match is not found, the user may be warned of the discrepancy and the connection may abort as the mismatch may indicate an attempted man-in-the-middle attack .

SNI addresses this issue by having the client send the name of the virtual domain as part of the TLS negotiation's ClientHello message. This enables the server to select the correct virtual domain early and present the browser with the certificate containing the correct name. Therefore, with clients and servers that implement SNI, a server with a single IP address can serve a group of domain names for which it is impractical to get a common certificate.

Server Name Indication payload is not encrypted, thus the hostname of the server the client tries to connect to is visible to a passive eavesdropper. Encrypted SNI is introduced in TLS 1.3 .

Prerequisites

  • SSL certificates and a private key (obtained or self-generated)
  • A load-balanced upstream group with several TCP servers, or only one TCP server as backend
  • at least nginx 1.15.9 to use variables in ssl_certificate and ssl_certificate_key.
  • check nginx -V for the following:
...
TLS SNI support enabled
...
--with-stream_ssl_module 
--with-stream_ssl_preread_module

Further reading: You can obtain zero cost SSL certificates from Let's Encrypt CA. Howto is here .

NGINX - terminating TLS, forward TCP

Nginx configuration for tcp proxy - Terminate tcp stream in TLS and forward the plain TCP to the upstream server.

stream {  

  server {
    listen 1111 ssl; 
    ssl_protocols       TLSv1.3;
    ssl_certificate     $targetCert_1111;
    ssl_certificate_key $targetCertKey_1111;
        
    proxy_connect_timeout 1s;
    proxy_timeout 3s;
    resolver 1.1.1.1;
      
    proxy_pass $targetBackend_1111;
  }

  map $ssl_server_name $targetBackend_1111 {
    ab.mydomain.com  upstream1.example.com:2222;
    xy.mydomain.com  upstream2.example.com:2222;
  }

  map $ssl_server_name $targetCert_1111 {
    ab.mydomain.com /certs/upstream1.example.com-cert.pem;
    xy.mydomain.com /certs/upstream2.example.com-cert.pem;
  }

  map $ssl_server_name $targetCertKey_1111 {
    ab.mydomain.com /certs/upstream1.example.com-key.pem;
    xy.mydomain.com /certs/upstream2.example.com-key.pem;
  }


  server {
    listen 9999 ssl;
    ssl_protocols       TLSv1.3;
    ssl_certificate     $targetCert_9999;
    ssl_certificate_key $targetCertKey_9999;
  
    proxy_connect_timeout 1s;
    proxy_timeout 3s;
    resolver 1.1.1.1;
  
    proxy_pass $targetBackend_9999;
  }

  map $ssl_server_name $targetBackend_9999 {
    ab.mydomain.com  upstream8.example.com:4444;
    xy.mydomain.com  upstream9.example.com:4444;
  }

  map $ssl_server_name $targetCert_9999 {
    ab.mydomain.com /certs/upstream8.example.com-cert.pem;
    xy.mydomain.com /certs/upstream9.example.com-cert.pem;
  }

  map $ssl_server_name $targetCertKey_9999 {
    ab.mydomain.com /certs/upstream8.example.com-key.pem;
    xy.mydomain.com /certs/upstream9.example.com-key.pem;
  }
 
}

It is configuration for 2 tcp services. First service listen on tcp port 1111 and second service listen on tcp port 9999. Both services use TLS layer. When client connect to ab.mydomain.com on port 1111 with TLS handsake, SNI for such TLS handshake is ab.mydomain.com.

Read the documentation for ngx_stream_map_module module. The ngx_stream_map_module module creates variables whose values depend on values of other variables. It creates a new variable whose value depends on values of one or more of the source variables specified in the first parameter.

Syntax:         map string $variable { ... }
Default:        
Context:        stream

Let's dive into ngx_stream_ssl_module documentation with focus on $ssl_server_name variable:

$ssl_server_name
    returns the server name requested through SNI; 

So, we can map SNI domain name to right upstream tcp server and use a appropriate SSL/TLS certificate and key.

bonus - non terminating, TLS pass through

Pass the TLS stream to an upstream server, based on the domain name from TLS SNI field. This does not terminate TLS.
The upstream server can serve HTTPS or other TLS secured TCP responses.

stream { 

  server {
    listen 443;

    proxy_connect_timeout 1s;
    proxy_timeout 3s;
    resolver 1.1.1.1;

    proxy_pass $targetBackend;      
    ssl_preread on;
  }

  map $ssl_preread_server_name $targetBackend {
    ab.mydomain.com  upstream1.example.com:443;
    xy.mydomain.com  upstream2.example.com:443;
  }
}

Choose upstream based on domain pattern

The domain name can be matched by a regex pattern, and extracted to variables. See regex_names .

The following configuration extracts a subdomain into variables and uses them to create the upstream server name.

stream {  

  map $ssl_preread_server_name $targetBackend {
    ~^(?<app>.+)-(?<namespace>.+).mydomain.com$ $app-public.$namespace.example.com:8080;
  }
  ...
}

Your Nginx should be reachable over the wildcard subdomain *.mydomain.com.
A request to shop-staging.mydomain.com will be forwarded to shop-public.staging.example.com:8080.

K8s service exposing by pattern

In Kubernetes, you can use this to expose all services with a specific name pattern.
This configuration exposes all service which names end with -public.
A request to shop-staging-9999.mydomain.com will be forwarded to shop-public in the namespace staging on port 9999.
You will also need to update the resolver, see below.

stream {  

  map $ssl_preread_server_name $targetBackend {
    ~^(?<service>.+)-(?<namespace>.+)-(?<port>.+).mydomain.com$ $service-public.$namespace.svc.cluster.local:$port;
  }
  
  server {
    ...
    resolver kube-dns.kube-system.svc.cluster.local;
    ...
  }
}

Resources

SUBSCRIBE FOR NEW ARTICLES

@
comments powered by Disqus