When our engineering team showed me the initial code for TLS support a few weeks ago, I blocked off a few hours to get through the configuration and set‑up steps. It turned out to be so easy, I finished in about half an hour.
On September 20, 2018, we released NGINX Unit 1.4, adding TLS support, certificate storage, and the new /config URL path for configuration objects. In NGINX Unit 1.5, released on October 25, we add support for Node.js applications. Take a look at the complete change list at https://unit.nginx.org/CHANGES.txt.
In this post we cover the configuration of TLS certificates and Node.js applications in detail.
Dynamic Configuration of SSL/TLS Certificates
Security and encryption is a must for every production application. Application connectivity today requires proper and rapid configuration and reconfiguration. For applications of a more dynamic nature, constant changes to their connectivity and security certificates are common.
NGINX Unit is configured on the fly, without relying on static configuration files and requiring process reloads. Now we extend this philosophy to TLS certificates as well. With NGINX Unit, you can upload certificates, apply them to or remove them from listeners, and replace old certificates – all dynamically, with zero downtime and no changes to the application processes.
Besides quick and easy configuration, use of TLS with NGINX Unit relieves Go and Node.js engineers from the burden of implementing encryption in the application code itself. Now all apps, including Go and Node.js, can be deployed into a larger production environment faster, more safely, and with no downtime.
With NGINX Unit, you can add, change, and remove certificates without reloading application processes
The following example of the initial control API response from NGINX Unit 1.5 shows that there is now a certificates
object in addition to the existing configuration structure with listener and application objects:
$ curl --unix-socket /run/control.unit.sock http://localhost/
{
"certificates": {},
"config": {
"listeners": {},
"applications": {}
}
}
Certificates and keys are represented in a different format from other configuration objects in the API: we upload them to the certificate store in PEM format as the body of a PUT
request.
You upload certificates and keys to the certificate store in PEM format as the body of a PUT request
Generating a Certificate
To try this out with a simple self‑signed certificate, we first run this command to generate one:
$ openssl req -x509 -subj /CN=localhost -days 365 -set_serial 2 -newkey rsa:4096 -keyout cert.key -nodes -out cert.pem
Notice that we placed the certificate and the key in separate files. Let’s combine them:
$ cat cert.key cert.pem > chain.pem
For a production environment, it’s a best practice to obtain a full certificate chain from a provider such as Let’s Encrypt. The resulting file looks similar to this:
-----BEGIN CERTIFICATE-----
MIIDHDCCAgQCCQCVgasbCyT1BjANBgkqhkiG9w0BAQsFADBQMQswCQYDVQQGEwJV
...
vyYJp0qUWRWeYm1CTZaudpR74cZVhz/RKGFfXkK2bD6Vv8uHI0hbK0p3KlKZ333j
kMSJ9yD+dF9quUbvS9yJUEHcF+96ZcmFVGRnwvnwXUE=
-----END CERTIFICATE-----
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAqb8VkMnAKqqIERHwNklelCE6gzfMd99yX+YoeerB9QVKZkx/
...
qhtqYRlezYQOUbMGAsREtKqZCn+urL8Bql2IbPQ9vwSfioxNUNyHod+2E/JDoEF8
OzsBSkR8H7NcSaHCbZCMKtIvGZGM4ODRqP484/KiXpqjhq8BOtTKJA==
-----END RSA PRIVATE KEY-----
If we have intermediate certificates or a CA certificate (or both), we concatenate them with the certificate and key as well.
Uploading a Certificate Chain
Then we upload the entire chain via the NGINX Unit API. You can use any HTTP tool to upload certificates and update the configuration. Here, we use curl
:
$ curl -X PUT --data-binary @path/to/chain.pem --unix-socket /run/control.unit.sock 'http://localhost/certificates/mychain'
This creates a new chain that we can check out at the /certificates/mychain URL path. In our self‑signed certificate example, the configuration looks like this:
{
"key": "RSA (4096 bits)",
"chain": [
{
"subject": {
"common_name": "localhost"
},
"issuer": {
"common_name": "localhost"
},
"validity": {
"since": "Oct 25 18:59:18 2018 GMT",
"until": "Oct 25 18:59:18 2019 GMT"
}
}
]
}
Applying the Certificate to a Listener
Now we apply the newly created certificate to a listener – either an existing one or one created by the command – by including a tls
object with the certificate
parameter in the listener’s configuration:
$ curl -X PUT --data-binary '{"application":"myapp","tls":{"certificate":"mychain"}}' --unix-socket /run/control.unit.sock 'http://localhost/config/listeners/*:8043'
A listener configuration with the new tls
object results:
"*:8043": {
"application": "myapp",
"tls": {
"certificate":"mychain"
}
}
To summarize, the full configuration of a TLS‑enabled application is as follows:
{
"certificates": {
"mychain": {
"key": "RSA (4096 bits)",
"chain": [
{
"subject": {
"common_name": "localhost"
},
"issuer": {
"common_name": "localhost"
},
"validity": {
"since": "Oct 25 18:59:18 2018 GMT",
"until": "Oct 25 18:59:18 2019 GMT"
}
}
]
}
},
"config": {
"settings": {},
"listeners": {
"*:8043": {
"application": "myapp",
"tls": {
"certificate":"mychain"
}
}
},
"applications": {
"myapp": {
"type": "php",
"root": "/path/to/php",
"script": "index.php"
}
}
}
}
Learn more about configuring TLS with NGINX Unit at https://unit.nginx.org/configuration/#ssl-tls-and-certificates.
Support for Node.js Applications
NGINX Unit 1.5 adds Node.js (JavaScript) to the set of languages we support.
Node.js and Go apps are similar in that you essentially create your own server when you build an application. The application is launched as a separate process – either directly or through a process manager.
In many environments, NGINX (or another load balancer) is placed in front of Node.js and Go apps. This additional software often complicates the environment. With NGINX Unit, you can configure this functionality using just one software server through one consistent and easy‑to‑use API.
Here are two examples, in Go and Node.js respectively:
http.ListenAndServe(":8000", nil)
var http = require('http');
http.createServer(function (req, res) {
// ...
}).listen(8080);
Installing the unit‑http
Package
To run a Node.js application in NGINX Unit, we add a special package (module) so the external runtime can communicate with NGINX Unit’s router process instead of listening on a port directly. NGINX Unit launches the app and communicates with it through shared memory. To reflect this difference from other app types, we have elevated our concept of these apps to a higher level, referring to them as "type":
"external"
.
For Node.js, the relevant package is called unit-http, and it’s now available on NPM at https://www.npmjs.com/package/unit-http.
After installing NGINX Unit, we first install the prerequisite unit-dev package (the command shown is for Debian and Ubuntu systems), and then use NPM to install the unit-http package:
$ apt-get install unit-dev
$ npm install unit-http
We then make two changes to the code for our Node.js app:
- We make it executable by adding a line at the top of the file with the shebang (
#!
) followed by the path to the launcher. (We also run thechmod
+x
shell command on the file.) - We change the required package from
http
tounit-http
.
Here’s a simple “Hello World” application with the two changes:
#!/usr/bin/env node
var unit = require('unit-http');
var server = unit.createServer(function(request, response) {
response.writeHead(200, {"Content-Type": "text/html"});
response.end("Read more at unit.nginx.org");
});
server.listen();
Configuring the App in NGINX Unit
We add the app to NGINX Unit via the control API, in the same way as for a Go application. A minimal application configuration looks like this:
"mynodeapp": {
"type": "external",
"executable": "/path/to/app.js"
}
To add this configuration code with the NGINX Unit API, we run:
$ curl -X PUT --data-binary '{"type":"external", "executable":"/path/to/app.js"}' --unix-socket /run/control.unit.sock 'http://localhost/config/applications/mynodeapp'
We can now reference this application in a listener object – either an existing one, or a new one created by the command:
$ curl -X PUT --data-binary '{"application":"mynodeapp"}' --unix-socket /run/control.unit.sock 'http://localhost/config/listeners/*:8071'
When we have the Node.js app configured, we can scale it up with NGINX Unit’s dynamic process management to increase performance:
$ curl -X PUT --data-binary '{"max":10, "spare":5}' --unix-socket /run/control.unit.sock 'http://localhost/config/applications/mynodeapp/processes'
The complete configuration looks like the following:
{
"settings": {},
"listeners": {
"*:8071": {
"application": "mynodeapp"
}
},
"applications": {
"mynodeapp": {
"type": "external",
"executable": "/path/to/app.js",
"processes": {
"max": 10,
"spare": 5
}
}
}
}
For more information on configuring Node.js with NGINX Unit, see https://unit.nginx.org/configuration#go-node-js-applications.
Conclusion
In this blog post, we reviewed the latest features of NGINX Unit 1.4 and 1.5, configured encryption with SSL/TLS for an existing application, and showed how to add a Node.js app to the NGINX Unit configuration.
Learn more about configuring NGINX Unit at unit.nginx.org.