Mosquitto配置TLS记录

最近有个项目需要用到MQTT协议,且客户端需要连接到公网部署的Broker,直接裸奔肯定是不可取的了,至少需要做个单向认证,折腾了将近半个工作日,期间也遇到几个坑,这里做个记录。

环境

先来说说环境。
使用的操作系统是CentOS7,因为上线生产环境也是选的CentOS7,所以直接在CentOS7进行测试配置有问题也好处理。
除了CentOS7之外,要配置MQTT Broker肯定需要对应的服务器了,服务器我这边选的是EPEL源里面自带的mosquitto,可以通过如下命令安装:

yum install mosquitto

安装成功之后,其配置文件目录在/etc/mosquitto/,主配置文件是mosquitto.conf

为mosquitto生成自签名证书

生成所需要的证书文件步骤,请移步这里:openssl ssl证书相关命令,参考其中的利用OpenSSL生成自签名证书部分。
生成过程很简单,只说几句需要注意的:

  • 由于是物联网设备使用,建议生成的证书有效期适当长久一些。
  • 签发server.crt这一步尤其要注意Common Name (eg, your name or your server’s hostname) []这一栏,我就是因为之前想为IP直接申请证书,导致后续客户端连不上Broker。
  • 虽然稍加折腾就能为IP签发证书,但是考虑后期服务器端运维、负载均衡等,建议客户端使用域名解析IP的方式连接。

如果过程没问题,应该会有如下几个文件:ca.crt ca.key ca.srl server.crt server.csr server.key

配置Mosquitto单向认证

生成相关证书之后,下一步就是配置mosquitto启用TLS了。
mosquitto默认监听在1883端口,无加密无认证,只需要IP及端口号就能随意连接,如果不需要可以删除默认的配置,我这边默认不改变这部分内容配置。
编辑/etc/mosquitto/mosquitto.conf,在文件末尾增加如下内容:

port 8883
cafile /etc/mosquitto/certs/ca.crt
keyfile /etc/mosquitto/certs/server.key
certfile /etc/mosquitto/certs/server.crt
tls_version tlsv1

单向认证配置很简单,就是指定端口、证书文件及TLS版本,实际配置需要注意证书文件的存放位置。

测试配置文件

更改配置文件之后,最好先测试一下配置文件,确保配置指令都正确再重启服务端,相关命令如下:

[root@cloudbool ~]# mosquitto -c /etc/mosquitto/mosquitto.conf
1562146147: mosquitto version 1.5.8 starting
1562146147: Config loaded from /etc/mosquitto/mosquitto.conf.
1562146147: Opening ipv4 listen socket on port 8883.
1562146147: Opening ipv6 listen socket on port 8883.

如果配置文件正确,输出应该类似如上,且服务端应该在对应的端口监听请求。

防火墙放行

如果系统防火墙处于开启状态,还需要在防火墙上放行指定的端口,以上面定的8883端口为例:

firewall-cmd --permanent --add-port=8883/tcp
firewall-cmd --reload

客户端连接

配置完服务端之后,可以测试一下客户端的连接情况。我这里使用MQTTBox作为MQTT客户端进行测试。
只需将上述生成的ca.crt复制到客户端,MQTT相关配置截图如下:
mqttbox-ca-certificate-only.png
如果配置没问题,到这一步,客户端应该可以正确连接上Broker了。

PHP连接TLS加密的mosquitto的Broker

业务代码来说,我测试了两个库,都能正确连接上通过TLS加密的mosquitto的Broker,代码地址如下:
https://github.com/karpy47/PhpMqttClient
https://github.com/walkor/mqtt

Laravel下workerman连接MQTT Broker

首先,使用composer安装workerman的对应依赖包,然后创建一个命令行命令,两个步骤对应的命令如下:

composer require workerman/mqtt
php artisan make:command mqtt

接下来编辑app/Console/Commands/mqtt.php,handle方法部分代码如下:

public function handle()
    {
        global $argv;
        $arg = $this->argument('action');
        $argv [1] = $arg;
        $worker = new Worker();
        $worker->count = 1;
        $worker->onWorkerStart = function () {
            $mqtt = new \Workerman\Mqtt\Client("mqtt://" . env('MQTT_HOST') . ":" . env('MQTT_PORT'), array(
                'ssl' => array(
                    'cafile' => '/path/mqtt/ca.crt',
                    'verify_peer' => false,
                    'allow_self_signed' => true,
                ),
                'username' => env('MQTT_USER'),
                'password' => env('MQTT_PASSWORD'),
                'debug' => env('MQTT_DEBUG'),
                'client_id' => $this->client_id,
                'will' => [
                    'topic' => 'status/' . $this->client_id,
                    'content' => 0,
                    'qos' => 2,
                    'retain' => true,
                ]
            ));
            $mqtt->transport = 'ssl';
            $mqtt->onConnect = function ($mqtt) {
                $mqtt->subscribe('/iot/#');
            };
            $mqtt->onMessage = function ($topic, $data, $mqtt) {
                            //some code....
            };
            $mqtt->connect();
        };
        Worker::runAll();
}

如果一切正常,就可以在命令行通过artisan命令来连接并监听对应的topic:

php artisan mqtt start

实际使用来说,因为我们后端框架使用的是Laravel,如果需要在命令行开启两个进程的话,会报错,这问题搜遍了google也无解:

Workerman[artisan] start in DEBUG mode
Workerman[artisan] already running

到目前为止,如果想同时启用两个进程,我的解决办法是再启用一个新的MQTT库,也就是上面列出的两个中的另一个。

配置过程相关报错

报错1:

openssl verify -CAfile ca.crt server.crt
C = AU, ST = Some-State, O = Internet Widgits Pty Ltd, CN = mqtt.cloudbool.com
error 18 at 0 depth lookup: self signed certificate
error server.crt: verification failed

如果出现这个问题,可能是在生成自签名证书时,两个Common Name重复了,解决办法是生成过程输入不同的内容到Common Name
报错2:

1562125450: New connection from 106.6.xxx.xxx on port 8883.
1562125451: Socket error on client <unknown>, disconnecting.
[106.52.xxx.xxx]:188 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJeNn0rtOBYwvV5VuV895RT7GpXsXKEjt5q2hnQS17OMCbee0a0FIdYkQERLMoW3Qo1J25MvX95/z1By/vWgF1w=
1562125452: New connection from 106.6.xxx.xxx on port 8883.
1562125452: Socket error on client <unknown>, disconnecting.

这种错误,请检查签发证书过程中Common Name是否输入的是IP或者主机名。最简单的解决办法是,使用域名代替IP或者主机名进行证书签发。