RabbitMQ with SSL, STOMP, and Websockets

I had fun this weekend setting up RabbitMQ with STOMP, over Websocket, over SSL. This is a debugging story: so by fun, I guess I mean it took 2 days and a great deal of head-banging on the wall. Many thanks to the original authors of the libraries I used for their work: any mistakes were on my part and I hope by presenting them here they might be a learning experience for you, too.

I wanted to configure SSL on the two services I mainly use RabbitMQ for: STOMP, and STOMP over Websocket. (I set it up with AMQP too, but that’s by-the-by).

SSL Setup with RabbitMQ

By following the guides here and here I managed to get the server-side working. I don’t need my clients to have certificates for client validation, so I turned the verify and fail peer cert options off in the config:

{rabbitmq_stomp, [
    {tcp_listeners, [61613]},
    {ssl_listeners, [61614]},
    {ssl_options, [
        {cacertfile, "/etc/ssl/private/host.ca"},
        {certfile,   "/etc/ssl/private/host.cert"},
        {keyfile,    "/etc/ssl/private/host.key"},
        {verify,     verify_none},
        {fail_if_no_peer_cert, false}
    ]}
]},

The rabbitmq_web_stomp config was pretty much the same, except for some reason it uses `ssl_config` instead of `ssl_options`. There were a couple of ‘gotchas’ too. The first was that my keys were not readable for rabbitmq’s user, so I ran `chown root:ssl-cert *` on them and `chmod 640 *` to get the right permissions:

-rw-r----- 1 root ssl-cert 4066 Oct 19 12:22 host.ca
-rw-r----- 1 root ssl-cert 1757 Oct 19 10:34 host.cert
-rw-r----- 1 root ssl-cert 1054 Oct 18 22:02 host.csr
-rw-r----- 1 root ssl-cert 1704 Oct 18 22:02 host.key
-rw-r----- 1 root ssl-cert 5823 Oct 19 10:43 host.pem

I’m not sure if Rabbit was already in the ssl-cert group, but you should check `groups rabbitmq` and add it if it’s not by usermod -a -G ssl-cert rabbitmq`.
The second was that I missed out the cacertfile: the intermediate certificate of the certification authority in PEM format which I downloaded from my provider (I’d previously concatenated this onto my .cert file- as this works for my web browser). I decided to get a CA signed certificate (rather than self-signed) as this way other people using Websockets in the browser don’t need to manually add my certificate & self-signed CA. Luckily, my DNS registrar, Gandi, offered the first year of SSL free for the domains I have with them.

Client Connections

Using the openssl client to connect now succeeds:

$ openssl s_client -connect localhost:61614
[...]
Verify return code: 0 (ok)

OK, cool. Using stomp.js I was able to connect to STOMP from the browser via SSL-secured Websocket. I just had to make sure that it tried the SSL port when the page was loaded over https:

    https = (window.location.protocol.lastIndexOf('s') != -1)
    if ("WebSocket" in window) {    // Pure websocket
        if(https)
            client = Stomp.client('wss://' + config.host +':'+ config.sslport + '/stomp/websocket');
        else
            client = Stomp.client('ws://'  + config.host +':'+ config.port    + '/stomp/websocket');

        console.warn("Using native WebSockets");
    }
    else {
        /* ... */

Great. Websocket STOMP is now nicely SSL-ified.

stomp.py

Next I moved onto my Python clients, which use stomp.py to negotiate a pure STOMP connection (no websocket) to RabbitMQ on port 61614. You should use this library over the other STOMP python library still kicking about in pypi.

I cracked in a call to Connection.set_ssl() to set up the SSL config for the library, and then spent the next 6 hours trying to track down the following error:

=ERROR REPORT==== 19-Oct-2015::15:03:10 ===
STOMP detected TLS upgrade error on  (127.0.0.1:52065 > 127.0.0.1:61614): alert record overflow

In the end, Google pulled through with a mailing list from 2012 which gave me a hint:
http://erlang.org/pipermail/erlang-questions/2012-December/071099.html

Stomp.py wasn’t setting up an SSL connection. Instead, it was spewing the STOMP “CONNECT” packet straight into the server socket expecting an SSL negotiation header. My code was at fault, though the library didn’t help me track down the error much. While tailing the RabbitMQ log (tail -f /var/log/rabbitmq/rabbit@finnigan.log.1), I opened a Python3 terminal and tried negotiating an SSL connection:

import socket, ssl                                              
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)        
sock.settimeout(10)                                             
wrapped= ssl.wrap_socket(sock, ssl_version=ssl.PROTOCOL_TLSv1_2)
wrapped.connect(('localhost', 61614))

Success. That worked: RabbitMQ’s log shows a successful connection:

=INFO REPORT==== 19-Oct-2015::15:31:32 ===
accepting STOMP connection  (127.0.0.1:57757 -> 127.0.0.1:61614)

The plot thickens. Why wasn’t STOMP.py negotiating an SSL connection? I was using the set_ssl method, but obviously it wasn’t working. There doesn’t seem to be any good code examples in the docs for this library for doing exactly this, so I dug down into stomp/transport.py inserting log statements to see what was going on- and traced it back to my parameter to set_ssl which was a python dict instead of a tuple inside a dict. Damn it. I therefore include below my final working test code as an example for those who might follow…

#!/usr/bin/python3
import stomp, ssl
import logging, sys

# Logging: this turns on the log output from stomp.py
root = logging.getLogger()
root.setLevel(logging.DEBUG)
ch = logging.StreamHandler(sys.stdout)
ch.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
ch.setFormatter(formatter)
root.addHandler(ch)

# Listener class for stomp.py to echo messages
class MyListener(stomp.ConnectionListener):
    def on_error(self, headers, msg):
        print('RX error "%s"' % msg)
    def on_message(self, headers, msg):
        print('RX message "%s"' % msg)

print("Yo. Connecting:")

conn = stomp.Connection(
        host_and_ports=[('localhost',61614)])
conn.set_ssl(
        for_hosts=[('localhost',61614)],
        ssl_version=ssl.PROTOCOL_TLSv1_2)

conn.set_listener('', MyListener())

conn.start()
conn.connect('guest', 'guest', wait=True)
conn.subscribe(destination='/topic/myqueue', id=1, ack='auto')

print("We're good.")
# Maybe sleep here, if you want
# I only cared that it connected properly.

conn.disconnect()

Hopefully this might be useful to someone trying to use SSL, STOMP, stomp.py and/or RabbitMQ together!