nginx  reverse proxy server for node apps on Mac OS

nginx reverse proxy server for node apps on Mac OS

11. Februar 2023 Aus Von admin

nginx is an HTTP server and can also be used as TCP/UDP proxy server. I use nginx mostly as reverse proxy to serve node apps from my local host via the reverse proxy. The reverse proxy is listening on port 443 to requests coming from the outside and forward these requests to localhost where the app is listening on a different port.

Installation

So lets install nginx with homebrew. In case you dont know what homebrew is and need to install it on your Mac read my article on Digitaldocblog.

brew install nginx

brew services start nginx

brew services stop nginx

  • nginx has been installed in /usr/local/Cellar/nginx/1.17.7
  • nginx configuration file is /usr/local/etc/nginx/nginx.conf

Configuration

Setup the core nginx configuration file nginx.conf in your environment:

# /usr/local/etc/nginx/nginx.conf 

# Main Context
worker_processes  auto;
error_log  /usr/local/etc/nginx/logs/error.log;
pid /usr/local/etc/nginx/run/nginx.pid;  

# Events Context
events {
    worker_connections  1024;
}

# HTTP Context
http {
    include       mime.types;
    default_type  application/octet-stream;
    sendfile        on;
    keepalive_timeout  65;

    access_log  /usr/local/etc/nginx/logs/access.log;

    include servers/*;
}

The Main Context is the most general context and is the only context that is not surrounded by curly braces. The Main Context is placed at the beginning of the core nginx configuration file. The directives of the Main Context cannot be inherited in any other context and can not be overridden.

The Main Context is used to configure the basic details of an application. Some common details that are configured in the Main Context are:

  • The directive worker_processes defines the number of worker processes. The number depend on the number of CPU cores and the default value is 1. The value auto will autodetect the number of worker processes. The directive tell the virtual servers defined in the servers folder (see below) know how many workers can be created once they are bound to the IP(s) and port(s). It is common practice to run 1 worker process per core. Anything above this won’t hurt your system, but it will leave idle processes usually just lying about
  • The directive error_log define the default error file for the entire application
  • The directive pid define the file to save the main process ID.

The Event Context as also the HTTP-Context are childs of the Main Context and therefore lower contexts. Lower Contexts handle the request, and the directives at this level and control the defined defaults for every virtual server.

The Events Context define global options for connection processing and is contained within the main context. There can be only one Event Context defined within Nginx configuration. Within the Events Context we define the worker_connections to tells our worker processes how many connections can simultaneously be served by Nginx. The default value is 768. Considering that every browser usually opens up at least 2 connections per server, this is why we need to adjust our worker connections to its full potential. Therefore we define 1024 worker connections.

The HTTP Context is used to hold the directives for handling HTTP or HTTPS traffic.

  • with the include directive we includes another file, or files matching the specified mask, into configuration.
  • with the default_type directive we define the default MIME type of a response. When a web server sends HTTP traffic back to the client (response), it usually adds an IANA media type (formerly known as MIME types), or content type, to the packet header that shows what kind of content is in the packet. The HTTP header on the data stream contains this content type. It is added by the web server before the data is sent.
  • using the directive sendfile enables or disables the use of the function sendfile(). By default, NGINX handles file transmission itself and copies the file into the buffer before sending it. Enabling the sendfile directive eliminates the step of copying the data into the buffer and enables direct file transmission.
  • the directive keepalive_timeout sets a timeout during which a keep-alive client connection will stay open on the server side.

Finally the include directive within the HTTP Context tell nginx to load all virtual servers from the files in /usr/local/etc/nginx/servers in the HTTP Context.

Then I create 2 serverfiles in /usr/local/etc/nginx/servers.

  1. A serverfile to run a virtual http server on localhost port 7445
  2. A serverfile to run a virtual http reverse proxy server on localhost port 7444
Patricks-Macbook Pro:servers patrick$ ls -l

total 24
-rw-r--r--@ 1 patrick  admin  188 15 Nov 06:12 patrick_7445
-rw-r--r--@ 1 patrick  admin  452 15 Nov 07:39 reverse_7444

Patricks-Macbook Pro:servers patrick$ 

The Server Context is inside these serverfiles. In general the Server Context is defined within the HTTP Context.

The Server Context define the virtual host settings. There can be multiple Server Context definitions inside the HTTP Context. The directives inside the Server Context handle the processing of requests for resources associated with a particular domain or IP address.

The directives in this Server Context can override many of the directives that may be defined in the HTTP Context, including the document root, logging, compression, etc. In addition to the directives that are taken from the HTTP Context, we can also configure files to try to respond to requests, issue redirects, and rewrites, and set arbitrary variables.

# /usr/local/etc/nginx/servers/reverse_7444

server {

    listen 7444;
    server_name patrick.local;

    location / {
       proxy_pass http://localhost:7445;
       proxy_set_header X-Forwarded-For $remote_addr;
    }
}

To connect to your localhost using patrick.local the dns settings in etc/host must be changed accordingly.

# /private/etc/hosts

192.168.178.20 servtest.rottlaender.lan

# /usr/local/etc/nginx/servers/patrick_7445

server {
     listen       7445;
     server_name  localhost;

     location / {
         root   /Users/patrick/Sites/www/nginx/patrick_7445;
         index  index.html index.htm;
    }
}

TLS/SSL for nginx

SSL/TLS works by using the combination of a public certificate and a private key.

The SSL key (private key) is kept secret on the server. It is used to encrypt content sent to clients.

The SSL certificate is publicly shared with anyone requesting the content. It can be used to decrypt the content signed by the associated SSL key.

create private key

Patricks-MBP:express-security patrick$ cd /usr/local/etc/nginx

Patricks-MBP:nginx patrick$ ls -l
total 144
-rw-r--r--  1 patrick  admin  1077  8 Apr 13:18 fastcgi.conf
-rw-r--r--  1 patrick  admin  1077  8 Apr 13:18 fastcgi.conf.default
-rw-r--r--  1 patrick  admin  1007  8 Apr 13:18 fastcgi_params
-rw-r--r--  1 patrick  admin  1007  8 Apr 13:18 fastcgi_params.default
-rw-r--r--  1 patrick  admin  2837  8 Apr 13:18 koi-utf
-rw-r--r--  1 patrick  admin  2223  8 Apr 13:18 koi-win
-rw-r--r--  1 patrick  admin  5231  8 Apr 13:18 mime.types
-rw-r--r--  1 patrick  admin  5231  8 Apr 13:18 mime.types.default
-rw-r--r--  1 patrick  admin  3106 15 Mai 11:19 nginx.conf
-rw-r--r--  1 patrick  admin  2680  8 Apr 13:18 nginx.conf.default
-rw-r--r--  1 patrick  admin  3091 21 Jan 05:40 nginx.conf.working
-rw-r--r--  1 patrick  admin   636  8 Apr 13:18 scgi_params
-rw-r--r--  1 patrick  admin   636  8 Apr 13:18 scgi_params.default
drwxr-xr-x  3 patrick  admin    96 21 Jan 06:02 servers
-rw-r--r--  1 patrick  admin   664  8 Apr 13:18 uwsgi_params
-rw-r--r--  1 patrick  admin   664  8 Apr 13:18 uwsgi_params.default
-rw-r--r--  1 patrick  admin  3610  8 Apr 13:18 win-utf

Patricks-MBP:nginx patrick$ mkdir ssl

Patricks-MBP:nginx patrick$ ls -l
total 152
-rw-r--r--  1 patrick  admin  1077  8 Apr 13:18 fastcgi.conf
-rw-r--r--  1 patrick  admin  1077  8 Apr 13:18 fastcgi.conf.default
-rw-r--r--  1 patrick  admin  1007  8 Apr 13:18 fastcgi_params
-rw-r--r--  1 patrick  admin  1007  8 Apr 13:18 fastcgi_params.default
-rw-r--r--  1 patrick  admin  2837  8 Apr 13:18 koi-utf
-rw-r--r--  1 patrick  admin  2223  8 Apr 13:18 koi-win
-rw-r--r--  1 patrick  admin  5231  8 Apr 13:18 mime.types
-rw-r--r--  1 patrick  admin  5231  8 Apr 13:18 mime.types.default
-rw-r--r--@ 1 patrick  admin   373 18 Mai 13:38 nginx.conf
-rw-r--r--  1 patrick  admin  2680  8 Apr 13:18 nginx.conf.default
-rw-r--r--  1 patrick  admin  3091 21 Jan 05:40 nginx.conf.working
-rw-r--r--@ 1 patrick  admin  1390 17 Mai 09:19 nginx_old.conf
-rw-r--r--  1 patrick  admin   636  8 Apr 13:18 scgi_params
-rw-r--r--  1 patrick  admin   636  8 Apr 13:18 scgi_params.default
drwxr-xr-x  5 patrick  admin   160 18 Mai 13:20 servers
drwxr-xr-x  4 patrick  admin   128 16 Mai 07:41 ssl
-rw-r--r--  1 patrick  admin   664  8 Apr 13:18 uwsgi_params
-rw-r--r--  1 patrick  admin   664  8 Apr 13:18 uwsgi_params.default
-rw-r--r--  1 patrick  admin  3610  8 Apr 13:18 win-utf

Patricks-MBP:nginx patrick$ cd ssl

Patricks-MBP:ssl patrick$ pwd
/usr/local/etc/nginx/ssl

Patricks-MBP:ssl patrick$ openssl genrsa -out privateKey.pem 4096

Patricks-MBP:ssl patrick$ ls -l
total 16
-rw-r--r--  1 patrick  admin  3247 16 Mai 07:22 privateKey.pem

create certificate signing request (CSR)

Patricks-MBP:ssl patrick$ pwd
/usr/local/etc/nginx/ssl

Patricks-MBP:ssl patrick$ openssl req -new -key privateKey.pem -out csr.pem

Patricks-MBP:ssl patrick$ ls -l
total 16
-rw-r--r--  1 patrick  admin  1740 16 Mai 07:23 csr.pem
-rw-r--r--  1 patrick  admin  3247 16 Mai 07:22 privateKey.pem

In case I would like to request an official certificate I must send this csr to the certificate authority. This authority would then create an authority signed certificate from the CSR and send it back to me.

This step is done by ourselves and this is the reason why we create a self signed certificate. This self signed certificate is not an official certificate and not trusted by any browser. It is not useful to use a self signed certificate in production because it produces error messages in the browsers. But for local development a self signed certificate is ok.

So create the self signed certificate. The csr file can then be removed.

create the self signed certificate

Patricks-MBP:ssl patrick$ pwd
/usr/local/etc/nginx/ssl

Patricks-MBP:ssl patrick$ openssl x509 -in csr.pem -out selfsignedcertificate.pem -req -signkey privateKey.pem -days 365

Patricks-MBP:ssl patrick$ ls -l
total 24
-rw-r--r--  1 patrick  admin  1740 16 Mai 07:23 csr.pem
-rw-r--r--  1 patrick  admin  3247 16 Mai 07:22 privateKey.pem
-rw-r--r--  1 patrick  admin  1980 16 Mai 07:39 selfsignedcertificate.pem

Patricks-MBP:ssl patrick$ rm csr.pem

Patricks-MBP:ssl patrick$ ls -l
total 24
-rw-r--r--  1 patrick  admin  3247 16 Mai 07:22 privateKey.pem
-rw-r--r--  1 patrick  admin  1980 16 Mai 07:39 selfsignedcertificate.pem
 

show certificate details

Patricks-MBP:ssl patrick$ pwd
/usr/local/etc/nginx/ssl

Patricks-MBP:ssl patrick$ openssl x509 -in selfsignedcertificate.pem -text -noout

renew self signed certificate

So let’s assume 1 year has passed by and now we want to renew this certificate selfsignedcertificate.pem. Now you may either generate a new CSR or export the CSR from the existing expired self-signed certificate.

First let’s check the validity of the existing certificate.

patrick@MacBookPro-Patrick ssl % ls -l                                                          
total 32
-rw-r--r--  1 patrick  admin  6279 12 Mär 08:09 new-csr.pem
-rw-r--r--  1 patrick  admin  3247 16 Mai  2020 privateKey.pem
-rw-r--r--  1 patrick  admin  1980 16 Mai  2020 selfsignedcertificate.pem

patrick@MacBookPro-Patrick ssl % openssl x509 -noout -text -in selfsignedcertificate.pem | grep -i -A2 validity
        Validity
            Not Before: May 16 05:39:28 2020 GMT
            Not After : May 16 05:39:28 2021 GMT

patrick@MacBookPro-Patrick ssl %

So our certificate selfsignedcertificate.pem expired on 16 Mai 2021 and today is 12 Mar 2022.

It is recommended to export the CSR as you don’t have to worry about giving the same Distinguished Name information. Although this information can be collected from the certificate itself. For us, we will export the CSR from the certificate itself.

patrick@MacBookPro-Patrick ssl % ls -l
total 16
-rw-r--r--  1 patrick  admin  3247 16 Mai  2020 privateKey.pem
-rw-r--r--  1 patrick  admin  1980 16 Mai  2020 selfsignedcertificate.pem

patrick@MacBookPro-Patrick ssl % openssl x509 -x509toreq -in selfsignedcertificate.pem  -signkey privateKey.pem -out new-csr.pem          
Getting request Private Key
Generating certificate request

patrick@MacBookPro-Patrick ssl % ls -l
total 32
-rw-r--r--  1 patrick  admin  6279 12 Mär 08:09 new-csr.pem
-rw-r--r--  1 patrick  admin  3247 16 Mai  2020 privateKey.pem
-rw-r--r--  1 patrick  admin  1980 16 Mai  2020 selfsignedcertificate.pem

patrick@MacBookPro-Patrick ssl %

Now that we have our private key privateKey.pem and the new CSR new-csr.pem, we can renew our certificate selfsignedcertificate.pem. You should not get confused with the term „renew“, we are not extending the expiry of the certificate, instead we are just creating a new self signed certificate using the CSR new-csr.pem from the existing private key privateKey.pem. The Signature Modules are the same and our application considers the certificate as same instead of new.

patrick@MacBookPro-Patrick ssl % ls -l
total 32
-rw-r--r--  1 patrick  admin  6279 12 Mär 08:09 new-csr.pem
-rw-r--r--  1 patrick  admin  3247 16 Mai  2020 privateKey.pem
-rw-r--r--  1 patrick  admin  1980 16 Mai  2020 selfsignedcertificate.pem

patrick@MacBookPro-Patrick ssl % openssl x509 -req -days 365 -in new-csr.pem -signkey privateKey.pem -out new-selfsignedcertificate.pem
Signature ok
subject=/C=DE/ST=Bayern/L=Munich/O=Digitaldocblog/CN=localhost/emailAddress=p.rottlaender@icloud.com
Getting Private key

patrick@MacBookPro-Patrick ssl % ls -l
total 40
-rw-r--r--  1 patrick  admin  6279 12 Mär 08:09 new-csr.pem
-rw-r--r--  1 patrick  admin  1980 12 Mär 08:30 new-selfsignedcertificate.pem
-rw-r--r--  1 patrick  admin  3247 16 Mai  2020 privateKey.pem
-rw-r--r--  1 patrick  admin  1980 16 Mai  2020 selfsignedcertificate.pem

patrick@MacBookPro-Patrick ssl % 

Lets check the validity of the new certificate new-selfsignedcertificate.pem.


patrick@MacBookPro-Patrick ssl % openssl x509 -noout -text -in selfsignedcertificate.pem | grep -i -A2 validity
        Validity
            Not Before: May 16 05:39:28 2020 GMT
            Not After : May 16 05:39:28 2021 GMT

patrick@MacBookPro-Patrick ssl % openssl x509 -noout -text -in new-selfsignedcertificate.pem | grep -i -A2 validity
        Validity
            Not Before: Mar 12 07:30:52 2022 GMT
            Not After : Mar 12 07:30:52 2023 GMT

patrick@MacBookPro-Patrick ssl % 

While our certificate selfsignedcertificate.pem expired on 16 Mai 2021 the new certificate new-selfsignedcertificate.pem expires on 12 Mar 2023.

The we rename the new-selfsignedcertificate.pem to selfsignedcertificate.pem to ensure

patrick@MacBookPro-Patrick ssl % mv selfsignedcertificate.pem old-selfsignedcertificate.pem

patrick@MacBookPro-Patrick ssl % mv new-selfsignedcertificate.pem selfsignedcertificate.pem
patrick@MacBookPro-Patrick ssl % ls -l
total 40
-rw-r--r--  1 patrick  admin  6279 12 Mär 08:09 new-csr.pem
-rw-r--r--  1 patrick  admin  1980 16 Mai  2020 old-selfsignedcertificate.pem
-rw-r--r--  1 patrick  admin  3247 16 Mai  2020 privateKey.pem
-rw-r--r--  1 patrick  admin  1980 12 Mär 08:30 selfsignedcertificate.pem

patrick@MacBookPro-Patrick ssl %

Finally we restart nginx service.

patrick@MacBookPro-Patrick ssl % brew services list                                        
Name                  Status  User    File
mongodb-community@4.4 started patrick ~/Library/LaunchAgents/homebrew.mxcl.mongodb-community@4.4.plist
nginx                 started patrick ~/Library/LaunchAgents/homebrew.mxcl.nginx.plist
patrick@MacBookPro-Patrick ssl % brew services stop nginx
Stopping `nginx`... (might take a while)
==> Successfully stopped `nginx` (label: homebrew.mxcl.nginx)
patrick@MacBookPro-Patrick ssl % brew services start nginx
==> Successfully started `nginx` (label: homebrew.mxcl.nginx)
patrick@MacBookPro-Patrick ssl % brew services list       
Name                  Status  User    File
mongodb-community@4.4 started patrick ~/Library/LaunchAgents/homebrew.mxcl.mongodb-community@4.4.plist
nginx                 started patrick ~/Library/LaunchAgents/homebrew.mxcl.nginx.plist
patrick@MacBookPro-Patrick ssl %

configure nginx with SSL

Any default configurations are done in nginx.conf. In this configuration we define a local webserver and a reverse proxy server.

The ip 127.0.0.1 is resolved in hosts file to localhost.

In the default configuration we define that any request to localhost on port 80 is redirected to https/ssl (443 ssl). This is a typical production configuration to enforce htps/ssl.

// /usr/local/etc/nginx.conf
// default configuration

worker_processes  1;

events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;
    sendfile        on;
    keepalive_timeout  65;
    
	# default server configuration
    server {
      	listen 80 default_server;
    	listen [::]:80 default_server;
    	
    	# default server your.domain.com
    	server_name localhost;
    	
    	# enforce 301 redirect to ssl port on your default server
    	return 301 https://$host$request_uri;
    }
    
	# In servers directory we define the servers running on this machine
    include servers/*;
}

The local webserver is bound to 127.0.0.1 and this ip is resolved to localhost. localhost is listening on port 443. This local webserver is using https/ssl. Any ssl configuration is done via the ssl_* directives in the server block.

In the location block we define the root directory of the local webserver and the index files. Here the local webserver process https client requests and send responses back to the client. So when a client request the route / then the local webserver render index.htm or index.html and respond to the client.

// /usr/local/etc/servers/localweb

server {
     listen       443 ssl;
     # here you would define in production your.domain.com
     server_name  localhost;

         ssl_certificate      ssl/selfsignedcertificate.pem;
         ssl_certificate_key  ssl/privateKey.pem;
         ssl_session_cache    shared:SSL:1m;
         ssl_session_timeout  5m;
         ssl_ciphers  HIGH:!aNULL:!MD5;
         ssl_prefer_server_ciphers  on;

     location / {
         root   /Users/patrick/Sites/prod/digitaldocblog-V2/public;
         index  index.html index.htm;
    }
}

The reverse proxy server is bound to ip 192.168.178.20. The ip 192.168.178.20 is resolved to servtest.rottlaender.lan in hosts file. This reverse proxy server is using https/ssl. Any ssl configuration is done via the ssl_* directives in the server block.

The node express server is listening on localhost port 3300. This express server is using http.

Any request to 192.168.178.20 or servtest.rottlaender.lan to port 3000 will be passed to application server listening on localhost port 3300.

So any client https request for servtest.rottlaender.lan on route / will be directly handed over to the express server. The express server will then process the request and send the response back to servtest.rottlaender.lan which will then respond 1:1 to the client.

// /usr/local/etc/servers/reverse

server {
    listen      3000 ssl;
    
	# /private/etc/hosts
	# 192.168.178.20	servtest.rottlaender.lan
	
    server_name servtest.rottlaender.lan;

    ssl_certificate      ssl/selfsignedcertificate.pem;
    ssl_certificate_key  ssl/privateKey.pem;
    ssl_session_cache    shared:SSL:1m;
    ssl_session_timeout  5m;
    ssl_ciphers  HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers  on;

    location / {
       proxy_pass http://localhost:3300;
    }
}