nginx reverse proxy server for node apps on Mac OS
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.
- A serverfile to run a virtual http server on localhost port 7445
- 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;
}
}