How to Configure Nginx as Layer 4 Load Balancer with SSL

Written by: Bagus Facsi Aginsa
Published at: 18 Nov 2024


You might think that using a layer 4 load balancer prevents us from dividing traffic based on domain, as this is typically done by a layer 7 load balancer. However, this is not the case.

Nginx allows us to read the traffic domain without terminating the SSL, so we can still route traffic based on a domain at layer 4.

How can we do this? Let’s find out!

Prerequisite

  1. Ubuntu 20.04, 22.04

Sudo Privileges

Before starting, we make sure that we will have no permission issues on the configuration.

sudo su

Use Case

To demonstrate the load balancing configuration, will use this use case

                                             ____________
                                            |            |
                                     -----> |    App1    |
                                    |       |____________|
                    ___________     |        ____________
                   |           |    |       |            |
 user -----------> |     LB    |----|-----> |    App2    |
                   |___________|    |       |____________|
                                    |        ____________
                                    |       |            |
                                     -----> |    App3    |
                                            |____________|

There are 3 nodes of application that will be load balanced using Nginx Layer 4 LB (Load Balancer). These are the specifications of the nodes:

  1. LB Node
    • IP: 10.11.12.13
    • Port: 443
  2. App1 Node
    • IP: 10.1.1.10
    • Port: 443
    • URL: https://app1.facsiaginsa.com
  3. App2 Node
    • IP: 10.1.1.20
    • Port: 443
    • URL: https://app2.facsiaginsa.com
  4. App3 Node
    • IP: 10.1.1.30
    • Port: 443
    • URL: https://app3.facsiaginsa.com

Install Dependencies

Run this command to install Nginx dependencies.

apt update -y && apt-get install git build-essential libpcre3 libpcre3-dev zlib1g zlib1g-dev libssl-dev libgd-dev libxml2 libxml2-dev uuid-dev

Download Nginx Source Code

Before you download the Nginx source code, you can visit http://nginx.org/en/download.html to see the Nginx version available now. After that, you can download them by running this command:

wget http://nginx.org/download/nginx-<version>.tar.gz

Now, the latest stable version is 1.26.2, so for me, I will download the nginx-1.26.2 version

wget http://nginx.org/download/nginx-1.26.2.tar.gz

Extract the downloaded file.

tar -zxvf nginx-1.26.2.tar.gz

Build & Install Nginx

After extracting the file, go to the nginx directory.

cd nginx-1.26.2

Now is the time to configure Nginx that suits your needs, this is where you put in the module you want to include in Nginx using the ./configure command. The full documentation is in here: Building Nginx from Sources. For now, I will give you the minimum configure option so you can build a good load balancer, reverse proxy, or web server. Run this command to configure Nginx:

./configure \
 --prefix=/etc/nginx \
 --conf-path=/etc/nginx/nginx.conf \
 --error-log-path=/var/log/nginx/error.log \
 --http-log-path=/var/log/nginx/access.log \
 --pid-path=/run/nginx.pid \
 --sbin-path=/usr/sbin/nginx \
 --with-http_ssl_module \
 --with-http_v2_module \
 --with-http_stub_status_module \
 --with-http_realip_module \
 --with-file-aio \
 --with-threads \
 --with-stream \
 --with-stream_ssl_preread_module

After that, run this command to build & install the Nginx.

make && make install

To verify the installation, you can check the Nginx version.

nginx -V

Also, notice that the Nginx folder will be created at /etc/nginx that provides the default nginx.conf.

Nginx Layer 4 Configuration

First, move our work directory to the Nginx configuration folder.

cd /etc/nginx

Backup the default nginx configuration file

mv nginx.conf nginx.conf.old

Create a new configuration file.

nano nginx.conf

Add this configuration to the configuration file

user www-data;
worker_processes auto;
worker_rlimit_nofile 8192;
pid /run/nginx.pid;

events {
 worker_connections 4096;
}

This will set up the basic global configuration for the Nginx. The important part is the worker_rlimit_nofile and worker_connections directives. You can set the number higher depending on the specification of your server. No one knows the best setup, you must do a load test yourself.

After that, create a stream block under the events block, and create a server block inside it.

stream {
  server {
    listen 443;
    ssl_preread on;
    proxy_pass $app_node;
  }
}

This will tell the nginx to listen to port 3000, and then pass the TCP traffic to upstream called app_node. If you are using UDP traffic, just change the listen directive to:

listen 3000 udp; 

The ssl_preread on directive will enable Nginx to extract information from the ClientHello message, get the SNI (Server Name Indication) information, and save it to $ssl_preread_server_name variable.

Now we must create a map block inside the stream block, and then also add some upstream block to list all of the app nodes behind the load balancer.

stream {
  map $ssl_preread_server_name $app_node {
    app1.facsiaginsa.com app1;
    app2.facsiaginsa.com app2;
    app3.facsiaginsa.com app3;
  }

  upstream app1 {
    server 10.1.1.10:443;
  }

  upstream app2 {
    server 10.1.1.20:443;
  }

  upstream app3 {
    server 10.1.1.30:443;
  }

  server {
    listen 443;
    ssl_preread on;
    proxy_pass $app_node;
  }
}

Using this configuration, the Nginx will map the SNI information and the upstream server accordingly every time a request is coming.

So the full configuration of nginx.conf is like this.

user www-data;
worker_processes auto;
worker_rlimit_nofile 8192;
pid /run/nginx.pid;

events {
  worker_connections 4096;
}

stream {
  map $ssl_preread_server_name $app_node {
    app1.facsiaginsa.com app1;
    app2.facsiaginsa.com app2;
    app3.facsiaginsa.com app3;
  }

  upstream app1 {
    server 10.1.1.10:443;
  }

  upstream app2 {
    server 10.1.1.20:443;
  }

  upstream app3 {
    server 10.1.1.30:443;
  }

  server {
    listen 443;
    ssl_preread on;
    proxy_pass $app_node;
  }
}

Notice that there are no TLS or https configurations in the Layer 4 Load Balancer because Nginx only reads the SSL/TLS information. The TLS termination must be done in the upstream server (the App node).

After the configuration is done, make sure that there is no error in the configuration by running this command.

nginx -t

Create Systemd File

To make Nginx easier to manage, we can build a systemd file. First, create a new file in the systemd folder:

nano /lib/systemd/system/nginx.service

Then copy & paste this config to the file.

[Unit]
Description=Nginx Custom From Source
After=syslog.target network-online.target remote-fs.target nss-lookup.target
Wants=network-online.target

[Service]
Type=forking
PIDFile=/run/nginx.pid
ExecStartPre=/usr/sbin/nginx -t
ExecStart=/usr/sbin/nginx
ExecReload=/usr/sbin/nginx -s reload
ExecStop=/bin/kill -s QUIT $MAINPID
PrivateTmp=true

[Install]
WantedBy=multi-user.target

Reload the systemd

systemctl daemon-reload

Enable Nginx service so it will auto start when the server boot.

systemctl enable nginx

Now you can control Nginx using systemd, just like this:

service nginx start
service nginx stop
service nginx reload
service nginx restart

Create Logrotate File

Logrotate is useful for rotating the Nginx log so it will not write on a single file continuously. First, create a new file on the log rotate folder

nano /etc/logrotate.d/nginx

Copy & Paste this code

/var/log/nginx/*.log {
  daily
  missingok
  rotate 7
  compress
  delaycompress
  notifempty
  create 0640 root root
  sharedscripts
  prerotate
  if [ -d /etc/logrotate.d/httpd-prerotate ]; then \
    run-parts /etc/logrotate.d/httpd-prerotate; \
  fi \
  endscript
  postrotate
  invoke-rc.d nginx rotate >/dev/null 2>&1
  endscript
}

There you go, now you have a layer 4 load balancer that can divide traffic based on the host domain.