Encrypting and Authenticating MQTT Traffic With NGINX Plus
In addition to load balancing, NGINX Plus can help offload authentication and encryption duties, particularly along the TLS front.
Join the DZone community and get the full member experience.
Join For FreeIn the first part of this two‑part series of blog posts about NGINX Plus and the Internet of Things (IoT), we showed how NGINX Plus – as a fully featured application delivery controller (ADC) with support for TCP and UDP applications – increases the availability and reliability of IoT applications. In this second post, we discuss two ways to use NGINX Plus to improve IoT security. The two posts cover the following use cases:
- Load balancing MQTT traffic
- High availability with active health checks
- Session persistence based on the MQTT ClientId, with nginScript
- Encrypting and authenticating MQTT traffic (this post)
- Offloading TLS termination
- Processing client certificates for TLS authentication
Offloading TLS Termination From MQTT Servers
In the example use cases in the first post, all of the MQTT traffic is plaintext and unencrypted. To improve IoT security, it is a best practice to use TLS to encrypt the MQTT data passing between clients and upstream servers, whenever TLS encryption is supported by the IoT devices or IoT gateway. Encryption is an effective way to protect data in motion as it crosses public networks, but in production environments with millions of devices, it can put an enormous load on the MQTT servers.
As shown in Figure 1, NGINX Plus can offload the CPU‑intensive workload associated with TLS encryption from your MQTT servers (commonly called SSL offloading). This separation of concerns allows for the load‑balancing tier and MQTT data‑processing tier to scale independently and requires only a simple modification to our MQTT test environment.
(As in the first post, we use the Mosquitto command line tool as the client and HiveMQ instances running inside Docker containers as the MQTT brokers. For installation instructions, see Creating the Test Environment in the first post.)
After furnishing the NGINX Plus instance with a TLS certificate key‑pair, we can use directives from the Stream SSL module to enable TLS termination.
server {
listen 8883 ssl; # MQTT secure port
preread_buffer_size 1k;
js_preread getClientId;
ssl_certificate /etc/nginx/certs/my_cert.crt;
ssl_certificate_key /etc/nginx/certs/my_cert.key;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_session_cache shared:SSL:128m; # 128MB ~= 500k sessions
ssl_session_tickets on;
ssl_session_timeout 8h;
proxy_pass hive_mq;
proxy_connect_timeout 1s;
access_log /var/log/nginx/mqtt_access.log mqtt;
error_log /var/log/nginx/mqtt_error.log info; # nginScript debug logging
}
This server
block is identical to the configuration for session persistence in the previous post, except that line 2 specifies the standard port number for secure MQTT traffic, 8883, and lines 6–11 are added to configure the server to terminate TLS connections from clients. Explaining how to obtain and install a certificate is beyond the scope of this post; a production IoT deployment generally uses its own public key infrastructure (PKI). We also won’t go over the TLS‑related directives. For more information about TLS termination, see the NGINX Plus Admin Guide.
With this configuration in place we can use the Mosquitto MQTT client to publish an encrypted message. Notice that we specify the secure MQTT port (8883) and a file containing the public key of the certificate authority that issued the server certificate on our NGINX instance (cafile.pem).
$ mosquitto_pub -d -h mqtt.example.com -t "topic/test" -m "test123" -i "thing001" -p 8883 --cafile cafile.pem
Client thing001 sending CONNECT
Client thing001 received CONNACK
Client thing001 sending PUBLISH (d0, q0, r0, m1, 'topic/test', ... (7 bytes))
Client thing001 sending DISCONNECT
$ tail --lines=1 /var/log/nginx/mqtt_access.log
192.168.91.1 [23/Feb/2017:11:41:56 +0000] TCP 200 23 4 127.0.0.1:18832 thing001
Using Client Certificates to Authenticate MQTT Clients
Despite the success and widespread adoption of MQTT for IoT use cases, the protocol itself has very limited provision for verifying the identity of clients. Authentication is supported through the use of a username and password field in the MQTT CONNECT
packet but in reality this is difficult to manage.
Although not officially supported by the MQTT specification, X.509 client certificates are commonly used to authenticate clients and are especially useful when combined with TLS encryption so that mutual authentication can take place:
- The client verifies the identity of the server
- The server verifies the identity of the client
NGINX Plus can combine TLS termination with client certificate authentication so that MQTT clients must provide a certificate, and that the common name (CN) of the certificate matches the MQTT ClientId. By linking the ClientId to the CN of the X.509 certificate, the MQTT server can be sure that messages received are from a trusted and genuine device.
NGINX Plus Configuration for MQTT Client Authentication
For this use case, we extend both the NGINX Plus configuration from the previous section (to enable authentication of client certificates) and the nginScript code from the previous post (to match the certificate CN with the ClientId). Adding the following config snippet to the server
block enables authentication of client certificates.
ssl_verify_client on; # Clients must supply certificate
ssl_verify_depth 2; # In case of intermediate CA
ssl_client_certificate /etc/nginx/certs/cafile.pem; # Issuer of client certificates
The ssl_verify_client
on
directive tells NGINX that clients must present certificates. In this example, client certificates are issued by an intermediate certificate authority and so we use the ssl_verify_depth
directive to tell NGINX that there are two levels of issuer certificates to verify. The ssl_client_certificate
directive specifies the location on disk of the public certificates for the certificate authorities (CAs) that issue certificates to clients; NGINX uses public CA certificates as part of the client authentication process.
nginScript Code for MQTT Client Authentication
Finally, we extend the nginScript code (mqtt.js) we created for the session persistence use case discussed in the previous post with additional code to validate that the MQTT ClientId presented in the CONNECT
packet has the same value as the CN in the certificate issued to that same client.
function parseCSKVpairs(cskvpairs, key) {
if ( cskvpairs.length ) {
var kvpairs = cskvpairs.split(',');
for ( var i = 0; i < kvpairs.length; i++ ) {
var kvpair = kvpairs[i].split('=');
if ( kvpair[0].toUpperCase() == key ) {
return kvpair[1];
}
}
}
return ""; // Default condition
}
We add the parseCSKVpairs
function to extract the CN value from the X.509 certificate. It is called by another function (the getClientId
function), and so must appear above it in the file.
var client_messages = 1;
var client_id_str = "-";
function getClientId(s) {
if ( !s.fromUpstream ) {
if ( s.buffer.toString().length == 0 ) { // Initial calls may
s.log("No buffer yet"); // contain no data, so
return s.AGAIN; // ask that we get called again
} else if ( client_messages == 1 ) { // Connect is first packet from the client
// Connect packet is 1, using upper 4 bits (00010000 to 00011111)
var packet_type_flags_byte = s.buffer.charCodeAt(0);
s.log("MQTT packet type+flags = " + packet_type_flags_byte.toString());
if ( packet_type_flags_byte >= 16 && packet_type_flags_byte < 32 ) {
// Calculate remaining length with variable encoding scheme
var multiplier = 1;
var remaining_len_val = 0;
var remaining_len_byte;
for (var remaining_len_pos = 1; remaining_len_pos < 5; remaining_len_pos++ ) {
remaining_len_byte = s.buffer.charCodeAt(remaining_len_pos);
if ( remaining_len_byte == 0 ) break; // Stop decoding on 0
remaining_len_val += (remaining_len_byte & 127) * multiplier;
multiplier *= 128;
}
// Extract ClientId based on length defined by 2-byte encoding
var payload_offset = remaining_len_pos + 12; // Skip fixed header
var client_id_len_msb = s.buffer.charCodeAt(payload_offset).toString(16);
var client_id_len_lsb = s.buffer.charCodeAt(payload_offset + 1).toString(16);
if ( client_id_len_lsb.length < 2 ) client_id_len_lsb = "0" + client_id_len_lsb;
var client_id_len_int = parseInt(client_id_len_msb + client_id_len_lsb, 16);
client_id_str = s.buffer.substr(payload_offset + 2, client_id_len_int);
s.log("ClientId value = " + client_id_str);
The variable declarations and getClientId
function on lines 1–2 are the same as lines 1–32 of the mqtt.js file we created for the session persistence use case.
// If client authentication then check certificate CN matches ClientId
var client_cert_cn = parseCSKVpairs(s.variables.ssl_client_s_dn, "CN");
if ( client_cert_cn.length && client_cert_cn != client_id_str ) {
s.log("Client certificate common name (" + client_cert_cn + ") does not match client ID");
return s.ERROR; // Close the TCP connection (logged as 500)
}
} else {
s.log("Received unexpected MQTT packet type+flags: " + packet_type_flags_byte.toString());
}
}
client_messages++;
}
return s.OK;
}
function setClientId(s) {
return client_id_str;
}
Lines 2 and 3 are concerned with matching the ClientId with the certificate CN. The CN itself is contained within the certificate’s subject distinguished name, the value of which is available in the $ssl_client_s_dn
variable. nginScript has access to all of the NGINX variables through the s.variables
object. This variable contains numerous attributes as a comma‑separated list of key‑value pairs.
The final lines (8–19) are the same as lines 33–44 in the mqtt.js file for the session persistence use case.
Testing MQTT Client Authentication
With this configuration in place we can send an authenticated and encrypted message to our test environment using the Mosquitto client. We can examine the certificate key‑pair that has been issued to thing001 by running this openssl
x509(1)
command.
$ openssl x509 -subject -noout < thing0001.crt
subject= /C=GB/L=Cambridge/O=example.com/OU=Example CA/CN=thing001
We can now supply this certificate key‑pair to our test environment.
$ mosquitto_pub -d -h mqtt.example.com -t "topic/test" -m "test123" -i "thing001" -p 8883 --cafile cafile.pem --cert thing0001.crt --key thing0001.key
Client thing001 sending CONNECT
Client thing001 received CONNACK
Client thing001 sending PUBLISH (d0, q0, r0, m1, 'topic/test', ... (7 bytes))
Client thing001 sending DISCONNECT
$ tail --lines=1 /var/log/nginx/mqtt_access.log
192.168.91.1 [24/Feb/2017:14:37:08 +0000] TCP 200 23 4 127.0.0.1:18832 thing001
If a client trying to establish a connection provides a ClientId that is mismatched with our certificate, or fails to provide a certificate at all, NGINX Plus terminates the connection immediately, so that unauthenticated messages never reach the upstream MQTT servers. Thus NGINX Plus provides additional protection to the MQTT servers from malicious or erroneous clients.
$ mosquitto_pub -d -h mqtt.example.com -t "topic/test" -m "test123" -i "BADTHING" -p 8883 --cafile cafile.pem --cert thing0001.crt --key thing0001.key
Client BADTHING sending CONNECT
Error: The connection was lost.
$ mosquitto_pub -d -h mqtt.example.com -t "topic/test" -m "test123" -i "NOCERT" -p 8883 --cafile cafile.pem
Client NOCERT sending CONNECT
Error: The connection was lost.
$ tail --lines=2 /var/log/nginx/mqtt_access.log
192.168.91.1 [24/Feb/2017:14:37:16 +0000] TCP 500 0 0 - BADTHING
192.168.91.1 [24/Feb/2017:14:42:16 +0000] TCP 500 0 0 - -
Conclusion
Using NGINX Plus to offload both the encryption and authentication workloads from the MQTT servers increases IoT security as well as the overall performance and traffic capacity of IoT deployments. We’d love to hear about the use cases that you come up with for NGINX Plus and nginScript, IoT or otherwise – please tell us about them in the comments section below.
Published at DZone with permission of Liam Crilly, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments