102人参与 • 2024-08-06 • 设计模式
nginx 唯一中文官方社区 ,尽在 nginx.org.cn
在关于 nginx plus 和物联网(iot)的系列博文(共两篇)的第一篇中,我们介绍了 nginx plus 作为一款支持 tcp 和 udp 应用的全功能应用交付控制器(adc),如何提高物联网应用的可用性和可靠性。在第二篇(本文)中,我们将探讨使用 nginx plus 来提高物联网安全性的两种方法。
这两篇文章涉及以下用例:
对 mqtt 流量进行加密和身份验证(本文)
在第一篇文章中的所有用例示例中,所有 mqtt 流量都是明文且未加密。只要物联网设备或物联网网关支持 tls 加密,提升物联网安全性的最佳方法就是使用 tls 加密客户端和上游服务器之间传输的 mqtt 数据。加密可在数据穿过公共网络时提供保护,但在拥有数百万台设备的生产环境中,加密会给 mqtt 服务器带来巨大负担。
如图 1 所示,nginx plus 可从 mqtt 服务器上卸载与 tls 加密相关的 cpu 密集型工作负载(通常被称为 ssl 卸载)。这一关注点分离做法允许负载均衡层和 mqtt 数据处理层独立扩展,而且只需对 mqtt 测试环境进行简单修改。
(与在第一篇文章中一样,我们使用 mosquitto 命令行工具作为客户端,并使用在 docker 容器内运行的 hivemq 作为 mqtt 代理。 有关安装说明,请参阅第一篇文章中的“创建测试环境”一节。)
图 1.nginx plus 为 mqtt 客户端执行 tls 卸载
为 nginx plus 实例提供 tls 证书密钥对后,我们可以使用来自流式 ssl 模块的指令来启用 tls 卸载。
server {
listen 8883 ssl; # mqtt 安全端口
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 约等于 50 万个会话
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; # nginx javascript 调试日志记录
}
除了第 2 行指定了安全 mqtt 流量的标准端口号为 8883,以及添加了第 6 到 11 行以配置服务器来终止客户端的 tls 连接以外,该 server 代码块与上一篇博文中的会话保持配置完全相同。如何获取和安装证书不在本文讨论范围之内,因为生产物联网部署通常使用自己的公钥基础设施(pki)。本文也不会详述与 tls 相关的指令。有关 tls 卸载的更多信息,请参阅《nginx plus 管理指南》。
完成此配置后,我们就可以使用 mosquitto mqtt 客户端发布加密信息。请注意,我们指定了安全 mqtt 端口(8883)和一个包含证书颁发机构公钥的文件——证书颁发机构在我们的 nginx 实例 (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
[编者按 – nginx javascript 模块的用例有很多,以下用例只是其中之一。如欲获取完整列表,请访问 nginx javascript 模块的用例。本文已更新,为 nginx javascript 0.2.4 中引入的 stream 模块使用了重构的会话对象。]
尽管 mqtt 在物联网用例中大获成功并被广泛采用,但该协议本身在验证客户端身份方面的功能非常有限。我们通过在 mqtt connect
数据包中使用用户名和密码字段来支持身份验证,但实际上这很难管理。
虽然 mqtt 规范并不正式支持 x.509 客户端证书,但 x.509 客户端证书常常被用于客户端身份验证,而且与 tls 加密结合使用时尤其有用,支持双向身份验证:
客户端验证服务器身份
服务器验证客户端身份
nginx plus 可以组合使用 tls 卸载与客户端证书身份验证,在这种情况下,mqtt 客户端就必须提供证书,而且证书通用名(cn)必须与 mqtt clientid 匹配。通过将 clientid 链接至 x.509 证书,mqtt 服务器可确保接收到的消息来自真实可信的设备。
图 2.提供 x.509 证书和私钥进行双向身份验证
针对此用例,我们扩展了上一节中的 nginx plus 配置(以启用客户端证书身份验证)和上一篇文章中的 nginx javascript 代码(以匹配证书 cn 与 clientid)。在 server
代码块中添加以下配置片段即可启用客户端证书身份验证。
ssl_verify_client on; # 客户端必须提供证书
ssl_verify_depth 2; # 如果存在中间证书颁发机构
ssl_client_certificate /etc/nginx/certs/cafile.pem; # 客户端证书的颁发机构
ssl_verify_client 指令指示 nginx 客户端必须出示证书。在本例中,客户端证书由中间证书颁发机构颁发,因此我们使用了 ssl_verify_depth 指令来指示 nginx 验证两级颁发机构证书。ssl_client_certificate 指令指定了向客户端颁发证书的证书颁发机构(ca)的公共证书在磁盘上的位置;nginx 在客户端身份验证过程中使用公共 ca 证书。
最后,我们扩展了 nginx javascript 代码(mqtt.js),该代码是我们在上一篇文章中专为讨论会话保持用例而创建的。新增代码可验证 connect
数据包中显示的 mqtt clientid 是否与发送到同一客户端的证书中的 cn 具有相同的值。
1 function parsecskvpairs(cskvpairs, key) {
2 if ( cskvpairs.length ) {
3 var kvpairs = cskvpairs.split(',');
4 for ( var i = 0; i < kvpairs.length; i++ ) {
5 var kvpair = kvpairs[i].split('=');
6 if ( kvpair[0].touppercase() == key ) {
7 return kvpair[1];
8 }
9 }
10 }
11 return ""; // default condition
12 }
我们添加了 parsecskvpairs
函数以从 x.509 证书中提取 cn 值。它由另一个函数(getclientid
函数)调用,因此在文件中必须位于后者的前面。
14 var client_messages = 1;
15 var client_id_str = "-";
16
17 function getclientid(s) {
18 s.on('upload', function (data, flags) {
19 if ( data.length == 0 ) { // initial calls may contain no data, so
20 s.log("no buffer yet"); // ask that we get called again
21 //s.done(1); // (supposing that code=1 means that)
22 return;
23 } else if ( client_messages == 1 ) { // connect is first packet from the client
24 // connect packet is 1, using upper 4 bits (00010000 to 00011111)
25 var packet_type_flags_byte = data.charcodeat(0);
26 s.log("mqtt packet type+flags = " + packet_type_flags_byte.tostring());
27 if ( packet_type_flags_byte >= 16 && packet_type_flags_byte < 32 ) {
28 // calculate remaining length with variable encoding scheme
29 var multiplier = 1;
30 var remaining_len_val = 0;
31 var remaining_len_byte;
32 for (var remaining_len_pos = 1; remaining_len_pos < 5; remaining_len_pos++ ) {
33 remaining_len_byte = data.charcodeat(remaining_len_pos);
34 if ( remaining_len_byte == 0 ) break; // stop decoding on 0
35 remaining_len_val += (remaining_len_byte & 127) * multiplier;
36 multiplier *= 128;
37 }
38
39 // extract clientid based on length defined by 2-byte encoding
40 var payload_offset = remaining_len_pos + 12; // skip fixed header
41 var client_id_len_msb = data.charcodeat(payload_offset).tostring(16);
42 var client_id_len_lsb = data.charcodeat(payload_offset + 1).tostring(16);
43 if ( client_id_len_lsb.length < 2 ) client_id_len_lsb = "0" + client_id_len_lsb;
44 var client_id_len_int = parseint(client_id_len_msb + client_id_len_lsb, 16);
45 client_id_str = data.substr(payload_offset + 2, client_id_len_int);
46 s.log("clientid value = " + client_id_str);
第 14 到 45 行中的 getclientid
函数与我们专为会话保持用例而创建的 mqtt.js 文件中的第 1 到 32 行相同。
47 // if client authentication then check certificate cn matches clientid
48 var client_cert_cn = parsecskvpairs(s.variables.ssl_client_s_dn, "cn");
49 if ( client_cert_cn.length && client_cert_cn != client_id_str ) {
50 s.log("client certificate common name (" + client_cert_cn + ") does not match client id");
51 return s.error; // close the tcp connection (logged as 500)
52 }
53 } else {
54 s.log("received unexpected mqtt packet type+flags: " + packet_type_flags_byte.tostring());
55 }
56 }
57 client_messages++;
58 }
59 return s.ok;
60 }
61
62 function setclientid(s) {
63 return client_id_str;
64 }
第 48 到 49 行涉及 clientid 与证书 cn 的匹配。cn 本身包含在证书的 subject distinguished name 中,其值可从 $ssl_client_s_dn 变量中获取。 nginx javascript 可通过 s.variables
对象访问所有 nginx 变量。该变量包含大量属性,显示为用逗号分隔的键值对列表。
最后几行(53 到 64 行)与用于会话保持用例的 mqtt.js 文件中的第 34 到 45 行相同。
完成此配置后,我们即可使用 mosquitto 客户端向测试环境发送经过身份验证和加密的消息。我们可通过运行此 openssl x509(1) 命令检查发送给 thing001 的证书密钥对。
$ openssl x509 -subject -noout < thing0001.crt
subject= /c=gb/l=cambridge/o=example.com/ou=example ca/cn=thing001
现在,我们可以将此证书密钥对提供给测试环境。
$ 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
如果试图建立连接的客户端提供的 clientid 与我们的证书不匹配,或者根本无法提供证书,nginx plus 将立即终止连接,这样未经身份验证的消息永远都不会到达上游 mqtt 服务器。因此,nginx plus 能够为 mqtt 服务器提供额外的保护,拒绝来自恶意或错误客户端的连接。
$ 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 - -
使用 nginx plus 从 mqtt 服务器卸载加密和身份验证工作负载,可提升物联网安全性,并改善物联网部署的整体性能和流量容量。欢迎大家在下方评论区与我们分享有关 nginx 开源版、nginx plus、nginx javascript、iot 或其他方面的用例。
nginx 唯一中文官方社区 ,尽在 nginx.org.cn
您想发表意见!!点此发布评论
版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。
发表评论