Seguridad en MQTT
MQTT es un protocolo de comunicación que usa TCP/IP para que dispositivos se subscriban/publiquen a tópicos. El reparto de la información de estos tópicos es gestionado por un dispositivo llamado broker, que gestiona el envío y recepción de información entre dispositivos.
El problema surge cuando un proyecto hobby quiere pasar a un ámbito más industrial, en estos casos una configuración por defecto de un servidor MQTT no cumple los estándares de seguridad ya que:
- La comunicación no está encriptada, es decir, cualquier persona que lea el tráfico de información, verá todos los datos de la comunicación, desde el tópico al que se envía la información hasta la información en sí misma, lo cual supone un problema grave de confidencialidad.
- Cualquier usuario puede leer y publicar, sabiendo la IP del broker, cualquier usuario puede leer cualquier tópico y escribir a cualquier tópico, lo cual supone un problema de confidencialidad y de integridad de los datos publicados.
- El broker puede ser suplantado, no teniendo forma de verificar que el broker sea el original, cualquier dispositivo puede suplantar su IP y recibir la información de los.dispositivos y mandar información falsa por los tópicos que se deseen.
Solución de estos problemas
Identificación del broker
Usando una Autoridad Certificadora (CA) propia, que firma el certificado del broker, los clientes pueden verificar que están conectándose al broker legítimo y así evitar la suplantación del mismo, ya que todos los certificados deberán ser dados por él y cualquiera otro broker que aparezca en la red no tendrá las credenciales correctas. Este certificado se puede conseguir de la siguiente forma:
1# Construct subject strings
2INFO_CA="/C=$COUNTRY/ST=$STATE/L=$CITY/O=$ORG/OU=CA/CN=${PROJECT}-Root-CA"
3INFO_SERVER="/C=$COUNTRY/ST=$STATE/L=$CITY/O=$ORG/OU=Server/CN=mqtt.${PROJECT}.com"
4
5
6# Step 1: Generate CA
7openssl req -x509 -nodes -sha256 -newkey rsa:2048 \
8-subj "$INFO_CA" -days 99999 \
9-keyout ca.key -out ca.crt
10
11# Step 2: Generate server SAN config (include both DNS and IP)
12cat > server_ext.cnf <<EOF
13subjectAltName=DNS:mqtt.${PROJECT}.com,IP:${SERVER_IP}
14EOF
15
16# Step 3: Generate Server Key + CSR
17openssl req -nodes -sha256 -newkey rsa:2048 \
18-subj "$INFO_SERVER" -keyout server/server.key -out server/server.csr
19
20# Step 4: Sign Server Cert with CA
21openssl x509 -req -sha256 -in server/server.csr \
22-CA ca.crt -CAkey ca.key -CAcreateserial \
23-out server/server.crt -days 99999 \
24-extfile server_ext.cnfIdentificación de los clientes
Todos los clientes deberán tener una llave firmada por el broker, lo cual creará una comunicación segura entre el broker y el cliente. Se puede tener una única llave para todos los clientes o una llave por cliente, en función de la seguridad que se desee del sistema.
1# --- Generate client certificate signed by the project CA ---
2INFO_CLIENT="/C=$COUNTRY/ST=$STATE/L=$CITY/O=$ORG/OU=Client/CN=${CLIENT}"
3
4# Generate client key + CSR
5openssl req -new -nodes -sha256 \
6-subj "$INFO_CLIENT" -out "${CLIENT_DIR}/client.csr" -keyout "${CLIENT_DIR}/client.key"
7
8# Sign client cert with project CA
9openssl x509 -req -sha256 -in "${CLIENT_DIR}/client.csr" \
10-CA "${PROJECT_DIR}/ca.crt" -CAkey "${PROJECT_DIR}/ca.key" -CAcreateserial \
11-out "${CLIENT_DIR}/client.crt" -days 99999Limitación de los clientes
Aunque los clientes cuenten con una llave única para cada dispositivo, no se debería dejar que cualquier dispositivo pueda decir o leer lo que le plazca, es por ello que se deberán limitar los clientes a ciertos tópicos.
Usuarios
Los usuarios permiten un acceso de los recursos, que puede hacerse más granular. Para crear un usuario se deberá establecer un nombre y una contraseña.
1USER="mqttuser"
2PASS="mypassword"
3HASH=$(python3 -c "import crypt; print(crypt.crypt('$PASS', crypt.mksalt(crypt.METHOD_SHA512)))")
4echo "$USER:$HASH" | sudo tee /etc/mosquitto/passwd > /dev/nullRestricción de usuarios a tópicos
Para conseguir esto se editara el archivo aclfile con los permisos de cada usuario y los tópicos que podrá leer o sobre los que podrá publicar.
Por ejemplo, en este caso se tienen 2 usuarios uno que tiene acceso a la lectura y escritura de cualquier tópico y otro usuario que solo tiene acceso a la lectura de tópicos que comiencen por test/ (# es usado como wildcard).
user username1
topic write #
topic read #
user username2
topic read test/#Configuración de MQTT
Ya sabiendo todo esto, se podrá configurar un servidor MQTT mediante lo siguiente:
.
├── certs
│ ├── aclfile
│ ├── ca.crt
│ ├── passwd
│ ├── server.crt
│ └── server.key
└── mosquitto.confmosquitto.conf
allow_anonymous false
listener 8883
password_file /mosquitto/config/certs/passwd
acl_file /mosquitto/config/certs/aclfile
cafile /mosquitto/config/certs/ca.crt
certfile /mosquitto/config/certs/server.crt
keyfile /mosquitto/config/certs/server.key
require_certificate true
use_identity_as_username truedocker-compose.yml
1services:
2 mqtt5:
3 image: eclipse-mosquitto:latest
4 container_name: mqtt5
5 volumes:
6 - ./Mqtt:/mosquitto/config/
7 - ./Mqtt/data:/mosquitto/data:rw
8 - ./Mqtt/log:/mosquitto/log:rw
9 ports:
10 - '8883:8883'
11 networks:
12 - default
13 restart: unless-stoppedpasswd
Test:$6$PHvuLoqs2dHc4Xfb$4SlRmtoPzWrmZkOn7x/0J1MiQVZsPvoXdftuXnmnOR25xFzPAVFyVtg4cOJActJ4LJf1erz2G/S5uVsjRiRm7Q==aclfile
user username1
topic write #
topic read #
user username2
topic read test/#Script
A continuación se muestra un script para generar estas credenciales y un tester para probar esta conexión.
1#!/bin/bash
2
3clear
4tree .
5
6# -----------------------------
7# Location info and base dirs
8# -----------------------------
9COUNTRY="ES"
10STATE="Badajoz"
11CITY="Badajoz"
12ORG="SScertificate"
13BASE_DIR="./"
14
15# =========================================================
16# Create Project (CA + Server certificate)
17# =========================================================
18create_project() {
19
20 echo "Creating new project..."
21
22 read -p "Enter project name: " PROJECT
23 read -p "Enter server IP address: " SERVER_IP
24
25 PROJECT_DIR="${BASE_DIR}${PROJECT}"
26 SERVER_DIR="${PROJECT_DIR}/server"
27
28 INFO_CA="/C=$COUNTRY/ST=$STATE/L=$CITY/O=$ORG/OU=CA/CN=${PROJECT}-Root-CA"
29 INFO_SERVER="/C=$COUNTRY/ST=$STATE/L=$CITY/O=$ORG/OU=Server/CN=mqtt.${PROJECT}.com"
30
31 mkdir -p "${SERVER_DIR}"
32 cd "${PROJECT_DIR}" || exit 1
33
34 # --- Generate CA ---
35 openssl req -x509 -nodes -sha256 -newkey rsa:2048 \
36 -subj "$INFO_CA" -days 99999 \
37 -keyout ca.key -out ca.crt
38
39 # --- Server SAN (DNS + IP) ---
40 cat > server_ext.cnf <<EOF
41subjectAltName=DNS:mqtt.${PROJECT}.com,IP:${SERVER_IP}
42EOF
43
44 # --- Server key + CSR ---
45 openssl req -nodes -sha256 -newkey rsa:2048 \
46 -subj "$INFO_SERVER" \
47 -keyout server/server.key \
48 -out server/server.csr
49
50 # --- Sign server cert ---
51 openssl x509 -req -sha256 \
52 -in server/server.csr \
53 -CA ca.crt -CAkey ca.key -CAcreateserial \
54 -out server/server.crt \
55 -days 99999 \
56 -extfile server_ext.cnf
57
58 cp ca.crt server/ca.crt
59
60 rm -f server/server.csr server_ext.cnf *.srl
61
62 echo "Project '${PROJECT}' initialized (Server IP: ${SERVER_IP})"
63}
64
65# =========================================================
66# Add Client
67# =========================================================
68add_client() {
69
70 echo "Adding new client..."
71
72 read -p "Enter project name: " PROJECT
73 read -p "Enter client name: " CLIENT
74 read -s -p "Enter password for ${CLIENT}: " CLIENT_PASS
75 echo
76 read -s -p "Confirm password: " CLIENT_PASS2
77 echo
78
79 [[ "$CLIENT_PASS" != "$CLIENT_PASS2" ]] && echo "Passwords do not match." && exit 1
80
81 PROJECT_DIR="${BASE_DIR}${PROJECT}"
82 CLIENT_DIR="${PROJECT_DIR}/${CLIENT}"
83 PASSWORD_FILE="${PROJECT_DIR}/passwd"
84
85 [[ ! -d "$PROJECT_DIR" ]] && echo "Project not found." && exit 1
86
87 mkdir -p "$CLIENT_DIR"
88
89 if [[ ! -f "$PASSWORD_FILE" ]]; then
90 mosquitto_passwd -c -b -H sha512 "$PASSWORD_FILE" "$CLIENT" "$CLIENT_PASS"
91 else
92 mosquitto_passwd -b -H sha512 "$PASSWORD_FILE" "$CLIENT" "$CLIENT_PASS"
93 fi
94
95 chmod 0700 "$PASSWORD_FILE"
96
97 INFO_CLIENT="/C=$COUNTRY/ST=$STATE/L=$CITY/O=$ORG/OU=Client/CN=${CLIENT}"
98
99 openssl req -new -nodes -sha256 \
100 -subj "$INFO_CLIENT" \
101 -out "${CLIENT_DIR}/client.csr" \
102 -keyout "${CLIENT_DIR}/client.key"
103
104 openssl x509 -req -sha256 \
105 -in "${CLIENT_DIR}/client.csr" \
106 -CA "${PROJECT_DIR}/ca.crt" \
107 -CAkey "${PROJECT_DIR}/ca.key" \
108 -CAcreateserial \
109 -out "${CLIENT_DIR}/client.crt" \
110 -days 99999
111
112 cp "${PROJECT_DIR}/ca.crt" "${CLIENT_DIR}/ca.crt"
113
114 rm -f "${CLIENT_DIR}/client.csr" "${PROJECT_DIR}"/*.srl
115
116 echo "Client '${CLIENT}' added to project '${PROJECT}'"
117}
118
119# =========================================================
120# Test MQTT (IP SELECTABLE)
121# =========================================================
122test_mqtt() {
123
124 echo "Testing MQTT..."
125
126 PROJECTS=($(find "$BASE_DIR" -maxdepth 1 -mindepth 1 -type d))
127 [[ ${#PROJECTS[@]} -eq 0 ]] && echo "No projects found." && exit 1
128
129 echo "Available projects:"
130 select PROJECT in "${PROJECTS[@]##*/}"; do
131 [[ -n "$PROJECT" ]] && break
132 done
133
134 PROJECT_DIR="${BASE_DIR}${PROJECT}"
135
136 CLIENTS=($(find "$PROJECT_DIR" -maxdepth 1 -mindepth 1 -type d ! -name "server"))
137 [[ ${#CLIENTS[@]} -eq 0 ]] && echo "No clients found." && exit 1
138
139 echo "Available clients:"
140 select CLIENT_DIR in "${CLIENTS[@]}"; do
141 [[ -n "$CLIENT_DIR" ]] && CLIENT=$(basename "$CLIENT_DIR") && break
142 done
143
144 read -s -p "Enter password for ${CLIENT}: " PASSWORD
145 echo
146
147 read -p "Enter MQTT server IP or hostname: " SERVER_IP
148
149 echo "Choose action:"
150 select ACTION in "Publish" "Subscribe"; do
151 case $ACTION in
152 Publish ) MODE="pub"; break ;;
153 Subscribe ) MODE="sub"; break ;;
154 esac
155 done
156
157 read -p "Enter topic: " TOPIC
158
159 BASE_CMD="mosquitto_${MODE} \
160 --cafile ${CLIENT_DIR}/ca.crt \
161 --cert ${CLIENT_DIR}/client.crt \
162 --key ${CLIENT_DIR}/client.key \
163 -h ${SERVER_IP} \
164 -p 8883 \
165 -u ${CLIENT} \
166 -P ${PASSWORD} \
167 -t ${TOPIC}"
168
169 if [[ "$MODE" == "pub" ]]; then
170 echo "Publishing mode (Ctrl+C to exit)"
171 while true; do
172 read -p "Message: " MESSAGE
173 [[ -n "$MESSAGE" ]] && $BASE_CMD -m "$MESSAGE"
174 done
175 else
176 echo "Subscribing..."
177 $BASE_CMD
178 fi
179}
180
181# =========================================================
182# Main Menu
183# =========================================================
184echo "Select an action:"
185select ACTION in "Create Project" "Add Client" "Test MQTT" "Exit"; do
186 case $ACTION in
187 "Create Project" ) create_project ;;
188 "Add Client" ) add_client ;;
189 "Test MQTT" ) test_mqtt ;;
190 "Exit" ) exit 0 ;;
191 * ) echo "Invalid choice" ;;
192 esac
193done