This guide will show you the required steps and some common pitfalls when setting up a HiveMQ cluster and enforcing TLS for each step of communication.
Certificate and keystore generation as well as their use will be demonstrated.
Requirements
HiveMQ
JDK 11 or higher
openssl
MQTT CLI
Instructions
We will create a keystore used by our HiveMQ nodes
keytool -genkey -keyalg RSA -alias hivemq -keystore hivemq.jks -storepass changeme -validity 360 -keysize 2048
keytool will ask for the necessary information to create our root certificate and private key.
These will be stored in hivemq.jks
which we need to make available to all our cluster nodes.
The common name should match the hostname of the HiveMQ node. For testing purposes ‘localhost’ will suffice.
From this keystore we need to export the server’s certificate (in this case called server.pem) and make it accessible to all clients that wish to connect
keytool -exportcert -alias hivemq -keystore hivemq.jks -rfc -file server.pem
2. Next we need to create a client certificate (mqtt-client-cert.pem), generate an x509, DER encoded certificate from it (mqtt-client.crt) and make them available to HiveMQ in form of a keystore (hivemq-trust-store.jks)
openssl req -x509 -newkey rsa:2048 -keyout mqtt-client-key.pem -out mqtt-client-cert.pem -days 360 openssl x509 -outform der -in mqtt-client-cert.pem -out mqtt-client-cert.crt keytool -import -file mqtt-client-cert.crt -alias client -keystore hivemq-trust-store.jks -storepass changeme
3. Now it is time to configure HiveMQ to require TLS
In our config.xml we adjust:
<path> in <keystore> must point to our earlier generated hivemq.jks
<path> in <truststore> will be our hivemq-trust-store.jks
<private-key-password> must be identical to the one we set during creation and
<client-authentication-mode> must be set to REQUIRED
When your are running multiple HiveMQ nodes on a single server remember that each instance requires a unique listening port
<listeners> <tls-tcp-listener> <port>1883</port> <bind-address>0.0.0.0</bind-address> <proxy-protocol>true</proxy-protocol> <tls> <keystore> <path>/opt/hivemq/conf/hivemq.jks</path> <password>changeme</password> <private-key-password>changeme</private-key-password> </keystore> <client-authentication-mode>REQUIRED</client-authentication-mode> <truststore> <path>/opt/hivemq/conf/hivemq-trust-store.jks</path> <password>changeme</password> </truststore> </tls> </tls-tcp-listener> </listeners>
In order to ensure the use of TLS between cluster nodes we need to modify the <cluster> section of our configuration.
We must again adjust the paths of our server’s keystore and truststore (path to our generated server.jks
)
and since we are running our cluster nodes on a single machine, each must bind to a different <bind-port>
<cluster> <enabled>true</enabled> <transport> <tcp> <bind-address>YOUR_IP_HERE</bind-address> <bind-port>7800</bind-port> <tls> <enabled>true</enabled> <server-keystore> <path>/opt/hivemq/conf/hivemq.jks</path> <password>changeme</password> <private-key-password>changeme</private-key-password> </server-keystore> <server-certificate-truststore> <path>/opt/hivemq/conf/hivemq.jks</path> <password>changeme</password> </server-certificate-truststore> </tls> </tcp> </transport> <discovery> <extension/> </discovery> </cluster>
In a production environment we would create a separate trust store for each node and modify the location accordingly.
4. We are now ready to launch our brokers (run.sh in the bin/ subdirectory)
To avoid further binding conflicts, modify jmxremote.port to a unique value in each node’s JAVA_OPTS variable within the corresponding run.sh
Validating our setup with MQTT CLI
Subscription attempt without providing any certificates
mqtt sub -t topic -q 1 -h localhost -i testclient -d CLIENT testclient: sending CONNECT SUBSCRIBE ERROR:: Server closed connection without DISCONNECT.
As expected HiveMQ disconnects the client since no certificate was presented to the server.
2. .. while including our server.pem
mqtt sub -t topic -q 1 -h localhost -i testclient --cafile /some/dir/server.pem -d CLIENT testclient: sending CONNECT PUBLISH: io.netty.handler.codec.DecoderException: javax.net.ssl.SSLHandshakeException: Received fatal alert: bad_certificate PUBLISH: javax.net.ssl.SSLHandshakeException: Received fatal alert: bad_certificate
We can see the TLS handshake was initiated but is aborted on the server side due to a missing client certificate
3. .. while supplying all required certificates
mqtt sub -t topic -q 1 -h localhost -i testclient --cafile /some/dir/server.pem --key /some/dir/mqtt-client-key.pem --cert /some/dir/mqtt-client-cert.pem -d CLIENT testclient: sending CONNECT CLIENT testclient: received CONNACK SUCCESS CLIENT testclient: sending SUBSCRIBE: (Topic: topic, QoS: AT_LEAST_ONCE) CLIENT testclient: received SUBACK: [GRANTED_QOS_1]
as we can see, the server accepts our subscription and we have demonstrated a successful connection attempt!