
O Mosquitto é um broker MQTT de código aberto e desenvolvido com apoio da Eclipse Foundation. O MQTT é um protocolo de comunicação no estilo pub/sub, muito utilizado em aplicações de Internet das Coisas. Neste post, vou mostrar como configurar e publicar o Mosquitto com suporte a criptografia, autenticação e autorização em um cluster do Kubernetes.
Esse artigo assume que você já tem alguma experiência com Docker e possui acesso a um cluster Kubernetes para fazer o deploy do Mosquitto.
Antes de mais nada, precisamos colocar o Mosquitto em um container. Já existem algumas imagens prontas no Docker Hub, mas, além dele, também vamos precisar de um plugin para fornecer a autenticação. Vou utilizar o mosquitto-auth-plug, que é um dos plugins mais completos. Para isso, eu criei esse Dockerfile que instala o Mosquitto e também o plugin:
FROM alpine:3.6
EXPOSE 1883
RUN apk add --update --no-cache mosquitto libcurl libssl1.0 && \
rm -rf /var/cache/apk
ADD auth-plug-build-config.mk /
RUN apk add --update --no-cache --virtual build-deps \
mosquitto-dev git build-base curl-dev openssl-dev && \
git clone git://github.com/jpmens/mosquitto-auth-plug.git \
/mosquitto-auth-plug && \
cd /mosquitto-auth-plug && \
mv /auth-plug-build-config.mk config.mk && \
make && \
cp auth-plug.so /usr/local/lib/auth-plug.so && \
cd / && \
rm -rf /mosquitto-auth-plug && \
apk del --purge build-deps && \
rm -rf /var/cache/apk
ADD mosquitto.conf /etc/mosquitto/mosquitto.conf
ADD auth-plug.conf /etc/mosquitto.d/auth-plug.conf
CMD ["mosquitto", "-c", "/etc/mosquitto/mosquitto.conf"]
Esse Dockerfile começa com a imagem do Alpine 3.6. A porta 1883 é exposta, que é a porta padrão do MQTT sem SSL. O MQTT com criptografia utiliza a porta 8883 por padrão, mas vamos configurar o SSL mais adiante, então, por enquanto, vamos deixar assim.
Depois disso, são instalados os pacotes do Mosquitto e outras dependências de runtime para o auth-plug. O plugin suporta vários backends de autenticação, mas iremos utilizar apenas o HTTP, então essas são as dependências necessárias. Se você quiser utilizar outros backends, as dependências podem ser diferentes.
O comando seguinte adiciona para dentro do container o arquivo de configuração da compilação do plugin. Esse é o arquivo que configura quais backends serão suportados. Um modelo desse arquivo é incluído no projeto (config.mk.in). As únicas alterações que eu fiz foi habilitar apenas o HTTP e configurar o caminho para as bibliotecas do Mosquitto em /usr. Resumindo, o arquivo ficou assim:
# Select your backends from this list
BACKEND_CDB ?= no
BACKEND_MYSQL ?= no
BACKEND_SQLITE ?= no
BACKEND_REDIS ?= no
BACKEND_POSTGRES ?= no
BACKEND_LDAP ?= no
BACKEND_HTTP ?= yes
BACKEND_JWT ?= no
BACKEND_MONGO ?= no
BACKEND_FILES ?= no
# Specify the path to the Mosquitto sources here
# MOSQUITTO_SRC = /usr/local/Cellar/mosquitto/1.4.12
MOSQUITTO_SRC = /usr
# Specify the path the OpenSSL here
OPENSSLDIR = /usr
# Specify optional/additional linker/compiler flags here
# On macOS, add
# CFG_LDFLAGS = -undefined dynamic_lookup
# as described in https://github.com/eclipse/mosquitto/issues/244
#
# CFG_LDFLAGS = -undefined dynamic_lookup -L/usr/local/Cellar/openssl/1.0.2l/lib
# CFG_CFLAGS = -I/usr/local/Cellar/openssl/1.0.2l/include -I/usr/local/Cellar/mosquitto/1.4.12/include
CFG_LDFLAGS =
CFG_CFLAGS =
Em seguida, aparece uma única instrução RUN com vários comandos. Isso é assim
para deixar a imagem o menor possível, já que o Docker separa a imagem final em
várias camadas, sendo que cada instrução do Dockerfile cria uma camada separada.
Então, nessa única instrução RUN, são instalados os pacotes necessários para a
compilação, o repositório é clonado, o projeto é compilado, a biblioteca
auth-plug.so
é copiada para o local adequado e, por fim, tudo o que é
desnecessário é removido. O resultado é que essa camada contém um único arquivo,
o auto-plug.so
.
No final do Dockerfile, são adicionados os arquivos de configuração do Mosquitto
e do auth-plug. O Mosquitto possui diversas opções de configuração,
que não serão abordadas aqui. O principal a ser garantido é que a opção
include_dir
aponte para /etc/mosquitto.d
, para onde é copiado o arquivo de
configuração do auth-plug, que contém o seguinte:
auth_plugin /usr/local/lib/auth-plug.so
auth_opt_backends http
auth_opt_http_hostname auth-server
auth_opt_http_port 8000
# POST /auth com username e password
auth_opt_http_getuser_uri /auth
# POST /superuser com username
auth_opt_http_superuser_uri /superuser
# POST /acl com username, clientid, topic e acc(1 = read, 2 = read-write)
auth_opt_http_aclcheck_uri /acl
Com o Mosquitto pronto, agora resta criar a API de autenticação. Para esse exemplo, eu vou utilizar uma simples aplicação em NodeJS com Express. O código básico da aplicação é mostrado abaixo (os módulos auth e acl não são incluídos aqui porque a lógica deles depende de cada aplicação):
const express = require('express');
const bodyParser = require('body-parser');
const { checkPassword } = require('./auth');
const { validateTopic } = require('./acl');
const app = express();
app.use(bodyParser.urlencoded({ extended: true }));
function allow(res) {
res.sendStatus(200);
}
function deny(res) {
res.sendStatus(403);
}
function error(e, res) {
console.error(e);
res.sendStatus(500);
}
// POST /auth com username e password
app.post('/auth', function (req, res) {
try {
let { username, password } = req.body;
console.log('auth: ', { username, password });
if (checkPassword(username, password)) {
allow(res);
}
else {
deny(res);
}
}
catch (e) {
error(e, res);
}
});
// POST /superuser com username
app.post('/superuser', function (req, res) {
// Nesse exemplo eu não usei superusuários, então sempre é negado
deny(res);
});
// POST /acl com username, clientid, topic e acc(1 = read, 2 = read-write)
app.post('/acl', function (req, res) {
try {
let { username, clientid, topic, acc } = req.body;
console.log('acl: ', { username, clientid, topic, acc });
if (validateTopic(username, topic, acc)) {
allow(res);
}
else {
deny(res);
}
}
catch (e) {
error(e, res);
}
});
app.listen(8000, () => {
console.log('mosquitto-auth listening on port 8000');
});
E o Dockerfile para construir a imagem dessa API é bem simples:
FROM node:6-alpine
EXPOSE 8000
WORKDIR /app
ADD . /app
RUN npm install
CMD npm start
Agora, antes de publicar tudo isso no Kubernetes, vamos rodar localmente para garantir que tudo está funcionando. Usando um arquivo do docker-compose isso é uma tarefa bem simples:
version: "3"
services:
auth:
build:
context: ./auth
broker:
build:
context: ./broker
links:
- auth:auth-server
ports:
- 1883:1883
Agora, utilizando um cliente MQTT qualquer já é possível verificar se a
autenticação está funcionando, conectando no broker em localhost:1883
.
$ mosquitto_pub -h localhost -p 1883 -t 'topico' \
-m 'mensagem' -u usuario -P senha_incorreta
Connection Refused: not authorised.
Error: The connection was refused.
$ mosquitto_pub -h localhost -p 1883 -t 'topico' \
-m 'mensagem' -u usuario -P senha_correta
Nenhum erro reportado com a senha correta = sucesso!
Agora está quase tudo pronto para publicar no Kubernetes. Só é necessário enviar as imagens para um Registry Docker que o cluster consiga acessar. Eu utilizei o Registry do GitLab.com e criei um ImagePullSecret no Kubernetes para acessá-lo, mas você pode utilizar o que achar melhor.
Os recursos necessários no Kubernetes para publicar o Mosquitto com autenticação são, basicamente, dois: um Deployment e um Service. O Mosquitto que iremos publicar irá rodar apenas como um servidor, sem nenhum tipo de redundância. Teremos apenas um container rodando o Mosquitto e um container rodando a API. Para simplificar, vamos deixar tudo isso em um único Pod (não faça isso em produção).
O Deployment ficou assim:
apiVersion: apps/v1beta1
kind: Deployment
metadata:
name: mqtt-broker
spec:
template:
metadata:
labels:
app: mqtt
spec:
hostAliases:
# O plugin de autenticação do Mosquitto conecta na
# API em `auth-server`. Como os containers estão no
# mesmo Pod, isso deve ser mapeado para localhost.
- ip: "127.0.0.1"
hostnames:
- "auth-server"
containers:
- name: mosquitto-broker
featuredImage: registry.gitlab.com/namespace/mosquitto:latest
resources:
requests:
cpu: 100m
memory: 100Mi
ports:
- containerPort: 1883
- name: mosquitto-auth
featuredImage: registry.gitlab.com/namespace/mosquitto-auth:latest
resources:
requests:
cpu: 100m
memory: 50Mi
ports:
- containerPort: 8000
imagePullSecrets:
- name: gitlab-registry
E agora, no Service, é onde iremos configurar o SSL. O cluster Kubernetes que eu estou utilizando está rodando na Amazon Web Services, e a AWS fornece um serviço de certificados com renovação automática e que pode ser vinculado a um LoadBalancer de forma bem simples.
apiVersion: v1
kind: Service
metadata:
name: mosquitto-broker
annotations:
service.beta.kubernetes.io/aws-load-balancer-backend-protocol: tcp
service.beta.kubernetes.io/aws-load-balancer-ssl-cert: "arn do certificado na AWS"
service.beta.kubernetes.io/aws-load-balancer-ssl-ports: "mqtts"
spec:
selector:
app: mqtt
type: LoadBalancer
ports:
- port: 8883
name: mqtts
targetPort: 1883
protocol: TCP
Com essa configuração de serviço, a terminação SSL é feita pelo LoadBalancer da AWS e a conexão TCP sem criptografia é redirecionada para o Pod do Mosquitto no Kubernetes. Toda essa “mágica” é configurada pelas anotações adicionadas nos metadados do serviço.
Agora esses arquivos podem ser aplicados com kubectl apply -f
e em breve os
containers estarão em execução com o LoadBalancer configurado. Para obter o
endereço do LoadBalancer que deve ser utilizado para conectar no broker podemos
utilizar o comando kubectl describe service mosquitto-broker
. Na saída desse
comando, uma linha identificada com LoadBalancer Ingress deve apresentar um
endereço parecido com xxxxxxxxxxxxxxxxxxxxx-xxxxxxxx.sa-east-1.elb.amazonaws.com
.
Depois de configurar o DNS para direcionar o domínio configurado no certificado
SSL para o LoadBalancer, podemos fazer novamente o teste com o mosquitto_pub:
$ mosquitto_pub -h mqtt.dominio.com -p 8883 -t 'topico' \
-m 'mensagem' -u usuario -P senha_incorreta \
--cafile /etc/pki/tls/certs/ca-bundle.crt
Connection Refused: not authorised.
Error: The connection was refused.
$ mosquitto_pub -h mqtt.dominio.com -p 8883 -t 'topico' \
-m 'mensagem' -u usuario -P senha_correta \
--cafile /etc/pki/tls/certs/ca-bundle.crt
Nenhum erro reportado com a senha correta = sucesso!
Agora você tem um broker Mosquitto rodando em um cluster Kubernetes com criptografia SSL, autenticação de usuários e autorização de acesso aos tópicos.