Last active
March 21, 2023 12:53
-
-
Save nginx-gists/85be3fdc8c9a55c446988bbbfdcf0a1d to your computer and use it in GitHub Desktop.
NGINX Plus for the IoT: Encrypting and Authenticating MQTT Traffic
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
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
Maybe I am doing something wrong, but this does not appear to work. When I connect with an MQTT client ID that does not match the certificate common name, I see this error message confirming the mismatched was detected:
...which also means
s.deny()
is called as well. However,return
is not called, so flow continues to eventually also calls.allow()
before returning (and afters.deny()
) is called, and this means the connection succeeds with the client ID / CN mismatch. If I am a return afters.deny()
, then it works becauses.allow()
is not called.Am I doing something wrong? Seems like this has worked for others, but it clearly does not for me.