Friday, April 4, 2014

RabbitMQ, .NET and SSL: it can actually be easy!

So in my previous post I explained my troubles with RabbitMQ, a .NET client application and an SSL connection. I got it to work eventually.
As I was playing around with a local install of RabbitMQ I remembered one of the server settings which had triggered a "I should look into this later" thought earlier. It was the SSL option "verify".
Now, according to the RabbitMQ SSL setup guide, this value should be set to "verify_peer". I wanted to know what other options are valid and what they do. So a Google later I found out there is another valid value: "verify_none".
Three guesses what this does? Yes, indeed, it removes the requirement of needing a client SSL certificate. *sigh*. Things we would've liked to have known two days earlier....

So anyway, setting this option means you connect to the RabbitMQ server just like you would to a web site. The connection is protected by SSL and the client doesn't need it's own SSL certificate. So if protecting the communication is the only thing you care about, this settings makes your life a lot easier. Now all you need to do is use an "AMQP://" connection string for an unencrypted connection, or an "AMQPS://" connection string for an SSL protected connection. No other settings are needed on the client side.

Friday, March 28, 2014

RabbitMQ, .NET and SSL: easier said than done

So we are currently implementing a RabbitMQ cluster to be part of our infrastructure and we will be communicating with this cluster through .NET clients using the standard RabbitMQ Client for .NET.
Everything worked just fine until we wanted to use SSL on those connections. Then everything went to hell.

So first the specifics of how we connect. We simply use an AMQP connection string to connect. Setting the Uri property in the ConnectionFactory class:

var cf = new ConnectionFactory();
cf.uri = new Uri("amqp://user:pass@172.20.0.72:5671/vhost");
var connection = cf.CreateConnection();

So first I simply set the Ssl.Enable property in the ConnectionFactory to true:

var cf = new ConnectionFactory();
cf.Ssl.Enabled = true;
cf.uri = new Uri("amqp://user:pass@172.20.0.72:5671/vhost");
var connection = cf.CreateConnection();

But it wasn't that simple. This gave me a BrokerUnreachableException with the message "None of the specified endpoints were reachable". The inner exception was an IOException with the message "Authentication failed because the remote party has closed the transport stream.". Followed by long stack-trace ending at: System.Net.Security.SslState.StartReadFrame(Byte[] buffer, Int32 readBytes, AsyncProtocolRequest asyncRequest).

So then I simply followed the instructions presented at the RabbitMQ pages (http://www.rabbitmq.com/ssl.html). I imported the root certificate, added the client certificate to the program and tried again:

var cf = new ConnectionFactory();
cf.Ssl.Enabled = true;
cf.Ssl.ServerName = "clienthost";
cf.Ssl.CertPath = @"C:\client-certs\keycert.p12";
cf.Ssl.CertPassphrase = "password";
cf.uri = new Uri("amqp://user:pass@172.20.0.72:5671/vhost");
var connection = cf.CreateConnection();

Same error as before, but now the inner exception has the message: "connection.start was never received, likely due to a network timeout."
Then I noticed something somewhere (can't remember where, probably in one of the RabbitMQ documents) about AMQPS. Sounds reasonable. HTTP -> HTTPS, so AMQP -> AMQPS:

var cf = new ConnectionFactory();
cf.Ssl.Enabled = true;
cf.Ssl.ServerName = "clienthost";
cf.Ssl.CertPath = @"C:\client-certs\keycert.p12";
cf.Ssl.CertPassphrase = "password";
cf.uri = new Uri("amqps://user:pass@172.20.0.72:5671/vhost");
var connection = cf.CreateConnection();

Queue Dennis Nedry....

Then I spend literally three hours of googling trying to find answers, but not one helped. I finally downloaded the source code for the RabbitMQ client (thank god for open source) and stepped through the connection process. That's where I noticed something funny. During the connect, the SSL options appeared to have disappeared. That's when I got an awful realization:

var cf = new ConnectionFactory();
cf.uri = new Uri("amqps://user:pass@172.20.0.72:5671/vhost");
cf.Ssl.Enabled = true;
cf.Ssl.ServerName = "clienthost";
cf.Ssl.CertPath = @"C:\client-certs\keycert.p12";
cf.Ssl.CertPassphrase = "password";
cf.uri = new Uri("amqps://user:pass@172.20.0.72:5671/vhost");
var connection = cf.CreateConnection();

...and it works. Are you effing kidding me?!
Setting the Uri property resets the SSL properties. Even though you cannot specify the SSL properties thought the connection string. So there is no reason for it to be reset like that.
If you take change the connection string back to "amqp://" you will get an exception ("The remote certificate is invalid according to the validation procedure."); even if you set the SSL Enabled property after.

So I hope this will save you a few hours of frustration. Right now I'm just happy I got it to work.

UPDATE: It can even be easier!