最近使用Mqtt
协议为公司的分布式系统搭建了一个“简陋”的原型通信框架,实际使用下来效果还不错,因此打算对通信进行TLS
加密,使其能够真正用于生产中。本文主要记录使用Mosquitto
作为服务端及测试客户端,Paho MQTT(Python)
作为客户端的使用配置下,用openssl
对通信进行加密的过程,以兹备忘。本文主要参考了此文 的大致步骤,但完全遵照此文描述的步骤无法正确完成TLS
配置,具体有问题的步骤及解决方法会在下文叙述。
使用Mqtt
协议的初衷如下:
Mqtt
协议自带的QoS
(Quality of Service
)功能适用于极不可靠链路的实际业务场景Broker
等方式灵活扩展Mqtt
消息网络Web
友好,可通过Websocket
传输,便于实现基于Web
的客户端目前普遍认为Mqtt
存在的缺点有:
TCP
协议,面向连接带来的多次握手的额外开销使其对于功耗要求苛刻的物联网设备并不友好RESTful
风格的Broker
,所有通信节点都必须能够连接到Broker
,这带来额外的通信开销并且限制了Mqtt
网络拓扑的灵活性Mqtt
协议本身缺乏安全机制,需要在传输层使用TLS
或用户在应用层构建相应的安全机制参考文献:
原型系统由服务端(Broker
)和客户端构成。服务端使用Mosquitto
,并且使用Mosquitto
自带的命令行工具Mosquitto_pub
和Mosquitto_sub
作为客户端测试手段。远程的传感器节点客户端使用Python Paho Mqtt
库编写,参考文档
Mqtt
协议版本为v3.1.1
。Docker
宿主环境为CentOS 7.5
Docker
版本为Docker version 1.13.1, build dded712/1.13.1
Broker
)为官方容器(docker.io/eclipse-mosquitto
)版本v1.6.7
OpenSSL
版本为OpenSSL 1.0.2k-fips 26 Jan 2017
Python
版本为3.7.3
Paho-mqtt
版本为1.4.0
OpenSSL
版本为OpenSSL 1.1.1c 28 May 2019
我们的原型系统采用自签名证书,更好的做法是向CA
申请正式的证书或者使用自动证书生成机制例如Let’s Encrypt来制作证书。
一套自签名证书由三个部分组成:
Mosquitto
服务程序使用Mosquitto
客户端以及Paho-mqtt
客户端使用以及生成证书所需的密钥和证书请求(Certificate Request
)。
(以下操作均以root
用户身份完成)
mkdir ca && cd ca
openssl genrsa -out ca.key 2048
openssl req -new -x509 -days 1800 -key ca.key -out ca.crt
填入申请证书所需的各项请求信息。由于是自签名证书,我们自己扮演了CA
所以这里的请求信息可以任意填,但要特别注意的是使用genrsa
生成密钥的时候要求输入Passphrase
,这时不要填入口令对密钥进行加密,这会导致Paho-mqtt
无法使用密钥。直接回车将密码留空即可。完成后在目录ca
下生成两个文件:ca.key
和ca.crt
。
cd ../
mkdir server && cd server
# step 1
openssl genrsa -out server.key 2048 # 这里也一样,不要用口令加密密钥
# step 2
openssl req -new -out server.csr -key server.key # 用上一步得到的密钥生成一个签名请求(.csr)
# step 3
openssl x509 -req -in server.csr -CA ../ca/ca.crt -CAkey ../ca/ca.key \
-CAcreatserial -out server.crt -days 1200 # 自签名,得到服务端证书
注意:
第二步填入请求信息的时候,有两点很重要:
SSL
验证过程认为这是单证书方案并比较CA
证书和服务端证书,但二者的SHA1
指纹又不一致,从而导致验证失败;Common Name (e.g. server FQDN or YOUR name)
一项特别重要,必须和运行服务端的主机域名(FQDN
)完全一致,否则会导致客户端尝试进行连接时SSL
验证过程失败。cd ../
mkdir client && cd client
# step 1
openssl genrsa -out client.key 2048 # 这里也一样,不要用口令加密密钥
# step 2
openssl req -new -out client.csr -key client.key # 用上一步得到的密钥生成一个签名请求(.csr)
# step 3
openssl x509 -req -in client.csr -CA ../ca/ca.crt -CAkey ../ca/ca.key \
-CAcreatserial -out client.crt -days 1200 # 自签名,得到客户端证书
首先拉取mosquitto
的官方docker
镜像:
sudo docker pull eclipse-mosquitto:1.6.7
进入config
目录修改mosquitto.conf
配置:
port 8884 # 使用8884作为TLS加密传输使用的端口
protocol mqtt # 8884 端口上的通信使用MQTT协议,使用websocket可以另外开一个listener,默认端口是9001
allow_anonymous false # 不允许匿名连接,客户端需提供口令才能接入
password_file /mosquitto/config/msq_passwd 口令密文存储在文件内
require_certificate true # 验证客户端证书
#require_certificate false
tls_version tlsv1.2 #指定TLS协议版本为1.2
cafile /mosquitto/config/ca/ca.crt # 将前面生成的根证书文件(ca.crt)拷至该路径下
keyfile /mosquitto/config/certs/server.key # 将前面生成的服务端证书和密钥拷至该路径下
certfile /mosquitto/config/certs/server.crt
use_identity_as_username false
log_dest file /mosquitto/log/mosquitto.log
配置好后使用docker-compose
启动容器:
sudo docker-compose up
容器运行正常则可以看到:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
c33f4e895e4d eclipse-mosquitto:1.6.7 "/docker-entrypoint.…" 3 weeks ago Up 3 weeks 0.0.0.0:8884->8884/tcp,1883/tcp,0.0.0.0:9001->9001/tcp mosquitto_mqtt_1
在客户端主机上安装Mosquitto
客户端工具:
sudo apt-get install -y mosquitto-clients
然后编写一个发布脚本(mosquitto_lpub.sh
)和订阅脚本(mosquitto_lsub.sh
)来测试连接:
#! /bin/bash
# publish a message to topic ${client_id}
client_id="$1"
shift
host="${$(hostname):-mqtt.server.com}"
mosquitto_pub -h "${host}" -t "${client_id}" -p 8884 \
--cafile "/ca/ca.crt" \
--cert "/client/client.crt" \
--key "/client/client.key" \
-u "test" -P "123456" \
--tls-version "tlsv1.2" \
-m "$@"
#! /bin/bash
# subscribe a topic ${client_id}
client_id="$1"
host="${$(hostname):-mqtt.server.com}"
mosquitto_pub -h "${host}" -t "${client_id}" -p 8884 \
--cafile "/ca/ca.crt" \
--cert "/client/client.crt" \
--key "/client/client.key" \
-u "test" -P "123456" \
--tls-version "tlsv1.2"
sudo chmod +x mosquitto_lpub.sh mosquitto_lsub.sh
./mosquitto_lsub.sh 'echo-server'
# 另起一个终端
./mosquitto_lpub.sh 'echo-server' 'hello mqtt'
如果一切正常,能够在订阅端看到这条消息。
在Python Paho-mqtt
中使用TLS
只需要提供一个SSL
上下文即可:
...
import ssl
mqttc = paho.mqtt.client.Client('my_client')
...
# 构建一个SSL上下文
SSL_CTX = {
'ssl_port': 8884,
'ca': /ca.crt,
'client_cert': /client/client.crt,
'client_key': /client/client.key,
'cert_reqs': ssl.CERT_REQUIRED,
'tls_version': ssl.PROTOCOL_TLSv1_2,
'ciphers': None,
'insecure': False # 关闭insecure选项
}
# 设置TLS参数
mqttc.tls_set( SSL_CTX['ca'],
certfile=SSL_CTX['client_cert'],
keyfile=SSL_CTX['client_key'],
cert_reqs=SSL_CTX['cert_reqs'],
tls_version=SSL_CTX['tls_version'],
ciphers=SSL_CTX['ciphers'])
# 要求验证服务端证书中域名与mqtt连接创建时输入的broker域名一致
mqttc.tls_insecure_set(SSL_CTX['insecure'])
tls_set() API
的详细说明可以参考:tls_set()
在使能TLS
连接时最常见的问题是SSL
握手失败。引发握手失败的原因通常都要根据具体情况分析。这里仅列举几种常见的情况:
openssl
工具来创建CA
证书、服务端证书及客户端证书,应该使用同一个环境来生成这三类证书和相关的密钥及签名请求等。NTP
服务来进行互联网授时。同时要保证证书的有效期足够长,当前系统时间是在证书有效期内。根证书的有效期应该长于用其签发的任何证书的有效期。CA
证书的签名请求内容与生成服务端证书的签名请求填入的内容完全一致。这也会导致SSL
握手失败。Mosquitto
无法读密钥内容测试时发现使用本文第2节所列的原型系统环境,SSL
协议版本必须使用v1.2
,使用其它版本会报错。
Broker
域名不一致自签名证书的制作过程不仅适用于MQTT
通信,对于其它类型的Socket
通信也是适用的。
Mosquitto SSL Configuration -MQTT TLS Security