写点什么

Pipy MQTT 代理之(四)安全性

作者:Flomesh
  • 2022 年 4 月 26 日
  • 本文字数:1989 字

    阅读完需:约 7 分钟

Pipy MQTT 代理之(四)安全性

Pipy MQTT 代理系列:



MQTT 的安全性包括三个方面:身份识别、认证和权限。


  • 身份识别:通过标识来识别客户端,比如客户端 ID

  • 认证:用户名密码、客户端证书对客户端进行认证,决定客户端是否可以连接到 broker

  • 权限:权限并不在 MQTT 协议的范畴,而是由各家厂商在应用层提供


拿消息发布为例,客户端证书认证会在建立连接阶段进行,决定是否建立连接;在发送 CONNECT 请求时,可以通过客户端身份标识或者用户名密码对请求进行验证;最后在消息发送的阶段,通过权限的检查判断客户端是否有权执行操作。



前面我们已经为 MQTT 代理添加了多项功能,对代理来说安全性是不可忽视的功能。限于篇幅和复杂性,我们会从前两个方面,来实现安全性。


权限方面,就如上面提到的其并不在 MQTT 的协议规范中,属于应用层的实现,需要在第二个请求中执行验证。当权限校验失败,还需要根据不同 qos 对返回不同的结果。对结果也没有标准的处理方式:断开连接,或者在 PUBACK 响应中指定 reasonCode仅MQTT 5.0 支持)。


示例代码都可以从 Github 下载,文中展示的是部分核心代码。

身份识别

每个 MQTT 客户端都有一个唯一的客户端标识(clientID),每次向 broker 发送请求时都会携带该 ID。此 ID 可用作客户端的身份识别,在认证时也可对此 ID 进行身份验证。当然,这个验证方式相对较弱,但是在封闭系统中可能就足够了。


identify.json 配置中,允许 client-1client-2 访问 broker,这种做法类似白名单。


{  "ids": [    "client-1",    "client-2"  ]}
复制代码


identify.js 模块中,从 CONNECT 请求中获取 clientID 并检查否在白名单列表中。


.pipeline('request')  .handleMessageStart(    msg => msg?.head?.type == 'CONNECT' && (      __turnDown = !Boolean(config.ids.find(el => el == msg?.head?.clientID))    )  )  .link(    'deny', () => __turnDown,    'bypass'    )

.pipeline('bypass')
.pipeline('deny') .replaceMessage( () => new Message({type: 'CONNACK', reasonCode: 133, sessionPresent: false}, '') )
复制代码


假如客户端 ID 不在白名单中,直接返回 CONNACK 响应,并设置 reasonCode133(MQTT 5 中是 133,在 MQTT 3 中则是 2)。


参考 MQTT 5.0 CONNACK 协议中 reasonCode 的定义


133(0x85)表示 Client Identifier not valid,意思是 The Client Identifier is a valid string but is not allowed by the Server

思考

在设备规模比较大的场景下,一般都会使用类似 UUID、网络设备 MAC 地址作为客户端 ID。这种情况并不建议使用,而且黑白名单还是一种粗粒度的控制方式。


除了客户端 ID,还可以使用客户端证书的验证方式。这种方式类似 TLS 双向认证(mTLS),客户端会检查 broker 的证书,broker 也同样会检查客户端的证书。这种验证是在 TCP 层完成,相比在 7 层的 MQTT 协议层验证效率会更高;在 CONNECT 请求前就可以拒绝无效客户端的连接,有更高的安全性。


但证书的验证方式也有弊端,就是证书的生命周期管理:证书颁发、吊销。如何将证书颁发给客户端、证书的更新和吊销,都带来了挑战。

认证

MQTT 协议在 CONNECT 请求中提供了用户名和密码的认证方式,客户端在发送请求时可以在消息头中通过 usernamepassword 字段发送用户名和密码。


用户名是 UTF-8 编码的字符串;密码是二进制的数据。


credential.json 中,配置有效的用户名和密码。


{  "creds": {    "flomesh": "pipy"  }}
复制代码


credential.js 模块中,对 CONNECT 请求中的 usernamepassword 进行检查。


.pipeline('request')  .handleMessageStart(    msg => msg?.head?.type == 'CONNECT' && (      __turnDown = !msg?.head?.username || !Boolean(config.creds[msg?.head?.username] == msg?.head?.password)    )  )  .link(    'deny', () => __turnDown,    'bypass'    )
.pipeline('bypass')
.pipeline('deny') .replaceMessage( () => new Message({type: 'CONNACK', reasonCode: 134, sessionPresent: false}, '') )
复制代码


当请求用户名与密码无效时,返回 CONNACK 响应,并将 reaseCode 设置为 134(MQTT 5 中是 134,在 MQTT 3 中是 4)。


134(0x86)表示 Bad User Name or Password,意思为 The Server does not accept the User Name or Password specified by the Client

思考

示例中为了方便是将用户名密码配置在了 JSON 文件中。当设备规模较大时,可以使用其他的存储方案,比如目前 Pipy 支持 LevelDB 作为存储。


实际使用中,还可以将客户端 ID 与用户名密码结合,使用客户端 ID、用户名和密码的组合进行认证。


用户名和密码在消息传递以及配置中使用的是明文,存在安全隐患。针对这种情况,可以考虑 OAuth 2.0 或者 JWT 认证,参考使用 Pipy 和 OpenID Connect 提升服务安全Pipy 教程系列之(十二)JWT

发布于: 刚刚阅读数: 5
用户头像

Flomesh

关注

微信订阅号:flomesh 2022.04.07 加入

一站式云原生应用流量管理供应商 官网:https://flomesh.io

评论

发布
暂无评论
Pipy MQTT 代理之(四)安全性_mqtt_Flomesh_InfoQ写作社区