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!
Post a Comment