Skip to content

Instantly share code, notes, and snippets.

@nginx-gists
Last active March 21, 2023 12:53
Show Gist options
  • Save nginx-gists/85be3fdc8c9a55c446988bbbfdcf0a1d to your computer and use it in GitHub Desktop.
Save nginx-gists/85be3fdc8c9a55c446988bbbfdcf0a1d to your computer and use it in GitHub Desktop.
NGINX Plus for the IoT: Encrypting and Authenticating MQTT Traffic
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
}
var client_messages = 1;
var client_id_str = "-";
function getClientId(s) {
s.on('upload', function (data, flags) {
if ( data.length == 0 ) { // Initial calls may contain no data, so
s.log("No buffer yet"); // ask that we get called again
//s.done(1); // (supposing that code=1 means that)
return;
} 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 = data.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 = data.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 = data.charCodeAt(payload_offset).toString(16);
var client_id_len_lsb = data.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 = data.substr(payload_offset + 2, client_id_len_int);
s.log("ClientId value = " + client_id_str);
// 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");
s.deny(); // Close the TCP connection (logged as 500)
}
} else {
s.log("Received unexpected MQTT packet type+flags: " + packet_type_flags_byte.toString());
}
}
client_messages++;
s.allow();
});
}
function setClientId(s) {
return client_id_str;
}
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
# vim: syntax=nginx
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
}
# vim: syntax=nginx
@iRevive
Copy link

iRevive commented Mar 21, 2023

To anyone wondering why it does not work in some scenarios: MQTT 5 has a different structure of the Connect packet and some fields are dynamically populated.

This one works with MQTT 5: https://gist.github.com/iRevive/a441c7f880bba10ca9a3bb7ee686d222

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment