前言

MQTT协议有许多实现,这里以开源的Mosquitto软件为例子,构建一个docker镜像,使用自签名证书,启用TLS加密传输。测试使用的Mosquitto版本为1.4.14,客户端使paho.mqtt.golang库以及mosquitto_sub分别加载证书后连接。

Mosquitto的作者提供了一个文档指导如何生成自签名证书,并启用TLS,教程在这里:mosquitto-tls

不过按照它或者谷歌出来的其他教程,发现都有一个问题,会出现错误

1
cannot validate certificate for 127.0.0.1 because it doesn't contain any IP SANs

一看到这种问题,我就想到肯定是服务端证书不对了。证书签名一直是个十分麻烦的问题,不过之前使用过IPSec PKI工具自签名过证书,最后通过添加多个san,生成了一个可以兼容测试和生产环境的证书,下面是正文。

生成证书

参考之前配置EAP-TLS证书:IPSecAndIKEv2VPNWithStrongswan

这次只生成根证书与服务端证书,ipsec程序可以在Ubuntu或macOS下,分别使用apt或者brew按照strongSwan,当然也可以从源码编译,下面是生成证书的命令

1
2
3
4
ipsec pki --gen --outform pem > caKey.pem
ipsec pki --self --in caKey.pem --dn "C=CH, O=Wbuntu, CN=Wbuntu CA" --ca --outform pem > caCert.pem
ipsec pki --gen --outform pem --lifetime 3650 > serverKey.pem
ipsec pki --pub --in serverKey.pem | ipsec pki --issue --cacert caCert.pem --cakey caKey.pem --dn "C=CH, O=Wbuntu, CN=MQTT" --san="127.0.0.1" --flag serverAuth --outform pem --lifetime 3650 > serverCert.pem

共四条命令,前两条分别生成了根证书密钥与根证书,后两条分别生成服务端密钥与服务端证书(为了保险起见,这里把根证书和服务端证书的有效期都设置为10年了)。

这里解决前言中所提到的问题,在生成服务端证书时追加多个san。假如你的Mosquitto镜像分别运行在本地(san为127.0.0.1),公网服务器(假定域名为mqtt.wbuntu.com)或者k8s集群中(假定内部域名mqtt.fxck),那么只需要追加三个san,然后生成服务端证书,格式如下:

1
ipsec pki --pub --in serverKey.pem | ipsec pki --issue --cacert caCert.pem --cakey caKey.pem --dn "C=CH, O=Wbuntu, CN=MQTT" --san="127.0.0.1" --san="mqtt.wbuntu.com" --san="mqtt.fxck" --flag serverAuth --outform pem --lifetime 3650 > serverCert.pem

根据自己的情况生成证书后,我们需要的只有三个,分别是caCert.pem,serverKey.pem,serverCert.pem

注意!正式环境需要重新生成全部的证书!否则其他人也可以使用我提供证书签名和伪造您正在使用的服务端证书!

编译Moquitto并构建镜像

编译与构建参考了这位大佬的Dockerfile:ansi/mosquitto

不过大佬把编译过程也涵盖进去,导致编译出来的镜像大小超过600MB,这里我们采用一个折衷的方式,首先在Ubuntu下配置编译环境,编译后将相关可执行文件、证书、配置文件复制进镜像,当然镜像中也预先配置好相同的编译环境,这样得到的镜像体积大小就不会太大了,而且更新时也不用反复构建了。

编译环境为Ubuntu 16.04 64位,使用的镜像也是Ubuntu 16.04 64位。

首先安装编译环境

1
apt-get install -y build-essential libwrap0-dev libssl-dev python-distutils-extra libc-ares-dev uuid-dev libwebsockets-dev

下载源码,解压后修改config.mk文件,将下面两项去掉注释,设为yes,支持websockets和tcpd

1
2
WITH_WRAP:=yes
WITH_WEBSOCKETS:=yes

然后执行make编译,在src目录下,得到mosquittomosquitto_passwd两个可执行文件

使用mosquitto_passwd工具,先生成一个passwdfile,然后执行下面的命令,最后两项分别是用户名和密码

1
mosquitto_passwd -b passwdfile wbuntu ESZXKUB8Y962meL67flnFGYsUXC0E95l1JnBAtBTWTuUw2sg

查看文件passwdfile内容

1
wbuntu:$6$tUIhR1teVifXJvX+$BYaDbHZsEGzcyNfAdR29AAA3zrbdXAv80odxXTZqGEK10AX4snMKqxHgJ3UQSs6jNRacjrdGqo/R/4xCS2CXfw==

修改mosquitto.conf,最后简化的内容如下

1
2
3
4
5
6
7
8
port 1883
protocol mqtt
password_file /etc/mosquitto/passwdfile
allow_anonymous false
cafile /etc/mosquitto/caCert.pem
certfile /etc/mosquitto/serverCert.pem
keyfile /etc/mosquitto/serverKey.pem
tls_version tlsv1.2

编写Dockerfile,内容如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
FROM ubuntu:16.04
ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update && apt-get install --no-install-recommends libwrap0-dev libssl-dev libc-ares-dev uuid-dev libwebsockets-dev -y
RUN mkdir -p /etc/mosquitto
RUN adduser --system --disabled-password --disabled-login mosquitto
USER mosquitto
expose 1883
COPY mosquitto /usr/bin/mosquitto
COPY mosquitto.conf /etc/mosquitto/mosquitto.conf
COPY passwdfile /etc/mosquitto/passwdfile
COPY caCert.pem /etc/mosquitto/caCert.pem
COPY serverCert.pem /etc/mosquitto/serverCert.pem
COPY serverKey.pem /etc/mosquitto/serverKey.pem
CMD ["/usr/bin/mosquitto","-c","/etc/mosquitto/mosquitto.conf"]

将所需文件移动到mqtt目录下,如图所示

构建镜像并运行

1
2
docker build -t wbuntu/mosquitto-tls:1.0.0 .
docker run --name mqtt -d -p 1883:1883 wbuntu/mosquitto-tls:1.0.0

由于是在mosquitto.conf中配置的协议,证书路径,因此可以直接挂载一个文件夹到/etc/mosquitto,替换全部的配置文件和证书,或者使用我提供的证书生成serverCert.pem,只替换服务端证书,默认镜像中的san为127.0.0.1,适合在本地运行。

连接服务端

使用mosquitto_sub订阅

1
mosquitto_sub -h 127.0.0.1 -p 1883 -u wbuntu -P ESZXKUB8Y962meL67flnFGYsUXC0E95l1JnBAtBTWTuUw2sg --cafile ~/mqtt/caCert.pem -v -t '#'

使用paho.mqtt.golang库

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
package main

import (
    "crypto/tls"
    "crypto/x509"
    "io/ioutil"
    "os"
    "os/signal"
    "syscall"

    log "github.com/Sirupsen/logrus"
    "github.com/codegangsta/cli"
    MQTT "github.com/eclipse/paho.mqtt.golang"
)

func main() {
    app := cli.NewApp()
    app.Name = "MQTT Go"
    app.Usage = "For MQTT TLS TEST"
    app.Version = "1.0"
    app.Action = run
    app.Flags = []cli.Flag{
        cli.StringFlag{
            Name:   "mqtt-server",
            Usage:  "MQTT Server Address",
            EnvVar: "MQTT_SERVER",
        },
        cli.StringFlag{
            Name:   "mqtt-username",
            Usage:  "mqtt server username (optional)",
            EnvVar: "MQTT_USERNAME",
        },
        cli.StringFlag{
            Name:   "mqtt-password",
            Usage:  "mqtt server password (optional)",
            EnvVar: "MQTT_PASSWORD",
        },
        cli.StringFlag{
            Name:   "mqtt-ca-cert",
            Usage:  "mqtt CA certificate file (optional)",
            EnvVar: "MQTT_CA_CERT",
        },
    }
    log.Info("begin running")
    app.Run(os.Args)
}

func run(c *cli.Context) error {
    topic := "#"
    broker := c.String("mqtt-server")
    username := c.String("mqtt-username")
    password := c.String("mqtt-password")
    cafile := c.String("mqtt-ca-cert")
    opts := MQTT.NewClientOptions()
    opts.AddBroker(broker)
    opts.SetUsername(username)
    opts.SetPassword(password)
    tlsconfig, err := NewTLSConfig(cafile)
    if err == nil {
        opts.SetTLSConfig(tlsconfig)
    }
    client := MQTT.NewClient(opts)
    if token := client.Connect(); token.Wait() && token.Error() != nil {
        log.WithField("MQTT", token.Error()).Info("failed to connect MQTT broker")
    }

    defer client.Disconnect(250)

    if token := client.Subscribe(topic, 0, msgHandler); token.Wait() && token.Error() != nil {
        log.WithField("MQTT", token.Error()).Info("failed to subscribe topic")
    }

    sigChan := make(chan os.Signal)
    exitChan := make(chan struct{})
    signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
    log.WithField("signal", <-sigChan).Info("signal received")
    go func() {
        log.Warning("stopping Client")
        exitChan <- struct{}{}
    }()
    select {
    case <-exitChan:
    case s := <-sigChan:
        log.WithField("signal", s).Info("signal received, stopping immediately")
    }
    return nil
}

// NewTLSConfig returns the TLS configuration.
func NewTLSConfig(cafile string) (*tls.Config, error) {
    // Import trusted certificates from CAfile.pem.
    cert, err := ioutil.ReadFile(cafile)
    if err != nil {
        log.Errorf("couldn't load cafile: %s", err)
        return nil, err
    }
    certpool := x509.NewCertPool()
    certpool.AppendCertsFromPEM(cert)
    // Create tls.Config with desired tls properties
    return &tls.Config{
        // RootCAs = certs used to verify server cert.
        RootCAs: certpool,
    }, nil
}

//MsgHandler for custom payload
func msgHandler(client MQTT.Client, msg MQTT.Message) {
    payload := msg.Payload()
    log.Info(string(payload))
}

首先将上面的代码保存为main.go文件,然后导出环境变量

1
export MQTT_USERNAME=wbuntu MQTT_PASSWORD=ESZXKUB8Y962meL67flnFGYsUXC0E95l1JnBAtBTWTuUw2sg MQTT_SERVER=wss://127.0.0.1:1883 MQTT_CA_CERT=~/mqtt/caCert.pem

接着运行

go run main.go

最后使用mosquitto_pub来发布一条消息

1
mosquitto_pub -t 'mqtt' -m 'hello from mosquitto_pub' -h 127.0.0.1 -p 1883 -u wbuntu -P ESZXKUB8Y962meL67flnFGYsUXC0E95l1JnBAtBTWTuUw2sg --cafile ~/mqtt/caCert.pem

订阅者这时可以收到消息”hello from mosquitto_pub”

如果需要使用websockets,将mosquitto.conf文件中的protocol值设为websockets即可,客户端上使用wss://开头的链接访问服务端,mosquitto_sub与mosquitto_sub似乎还未支持websockets,无法使用wss://格式的链接

最后附上

Docker镜像的链接:wbuntu/mosquitto-tls

Dockerfile链接:mikumaycry/mosquitto-tls