Using SSL client certificates for authentication with RabbitMQ

RabbitMQ configuration is notoriously tricky business, the SSL setup doubly so. The information in this tutorial was gleaned from the RabbitMQ ssl guide as well as a blog post by John Ruiz. Both sources gloss over what I find to be the most important details of the process. The RabbitMQ tutorial in particular functions more as a tutorial on setting up a CA with openssl than a tutorial on RabbitMQ configuration. In this post, the minting of certificates is left up to an exercise for the reader. I had my server admin use our domain CA to produce them. You can do the same, buy certificates from public CA or even just follow the rabbitmq ssl tutorial to mint them.

You will need:

  • The public key (crt/cer file) of the CA which signed the server & client certs. This will likely be your active directory CA.
  • A keypair (pfx) for the server with a CN matching the servers dns name, a basic keyUsage of keyEncipherment, and an eku for ServerAuthentication (1.3.6.1.5.5.7.3.1)
  • A keypair (pfx) for the client with a basic keyUsage of digitalSignature and an eku for ClientAuthentication (1.3.6.1.5.5.7.3.2)

This gets you all the certs you'll need, unfortunately they're in the wrong format. The RabbitMQ server requires its key and certificate to be in the PEM format used by openssl.

To convert them you can use the openssl command line as such:

openssl x509 -inform der -in MyCA.crt -out MyCA.pem
openssl pkcs12 -in server.pfx -out server.key -nocerts
openssl pkcs12 -in server.pfx -out server.pem -nokeys

Tutorial setup

In RabbitMQ set up a user & vhost named "ssltest" and grant the user access.

Verify that you can connect and send messages through the queue.

Through your CA mint a certificate for the server with a CN matching the dns name of the server with the settings for a server specified above.

Through your CA mint a certificate for the client with a CN of "ssltest" with the settings for a client specified above.


Basic SSL configuration

Once the certificate file are on the server, you'll need to edit your rabbitmq.config file to turn on ssl support. My config file ended up looking like:

%% -*- mode: erlang -*-
[
 {rabbit,  [ 
    {ssl_listeners, [5671]},
    {ssl_options, [{cacertfile,"D:/RabbitMQ/certs/MyCA.pem"},
                  {certfile,"D:/RabbitMQ/certs/MyRabbitServer.pem"},
                  {keyfile,"D:/RabbitMQ/certs/MyRabbitServer.key"},
{verify,verify_none}, {fail_if_no_peer_cert,false}]} ]} ].

Notice that the paths under windows use forward slashes. The RabbitMQ documentation says you can use double-backslashes, in my experience this is incorrect.

At this point, the following program should work:

using System;
using System.Net.Security;
using System.Text;
using RabbitMQ.Client;
using RabbitMQ.Client.Exceptions;
using RabbitMQ.Util;

namespace RabbitMQTest
{
    public class Program
    {
        public static void Main(string[] args)
        {
            
            try
            {
                var hostName = "MyRabbitServer";
var cf = new ConnectionFactory { HostName = hostName, VirtualHost = "ssltest", UserName = "ssltest", Password = "ssltest", Ssl = new SslOption { Enabled = true, ServerName = hostName, AcceptablePolicyErrors = SslPolicyErrors.RemoteCertificateNameMismatch | SslPolicyErrors.RemoteCertificateChainErrors, } }; using (IConnection conn = cf.CreateConnection()) { using (IModel ch = conn.CreateModel()) { ch.QueueDeclare("rabbitmq-dotnet-test", false, false, false, null); ch.BasicPublish("", "rabbitmq-dotnet-test", null, Encoding.UTF8.GetBytes("Hello, World")); BasicGetResult result = ch.BasicGet("rabbitmq-dotnet-test", true); if (result == null) { Console.WriteLine("No message received."); } else { Console.WriteLine("Received:"); DebugUtil.DumpProperties(result, Console.Out, 0); } ch.QueueDelete("rabbitmq-dotnet-test"); } } } catch (BrokerUnreachableException bex) { Exception ex = bex; while (ex != null) { Console.WriteLine(ex.Message); Console.WriteLine("inner:"); ex = ex.InnerException; } } catch (Exception ex) { Console.WriteLine(ex.ToString()); } Console.ReadKey(); } } }

If you've done everything correctly, it should work. if it doesnt there are a few things to check:

  1. Verify that the certificate files & key files on the server are in PEM format. Open them in notepad and you should see lines that look like "-----BEGIN CERTIFICATE-----" or "-----BEGIN RSA PRIVATE KEY-----"
  2. Check that you can establish and openssl connection to the server with the command:

    openssl s_client -connect my-server:5671
  3. Check that the certificates are properly minted with correct key usage and CN values.
  4. Check that the CA is trusted by the client machine.

Configuring Client authentication via certificates

Once basic ssl is configured you can begin configuring client certificate support. Open a rabbitmq command console and enable the ssl authentication plugin with the command:

rabbitmq-plugins enable rabbitmq_auth_mechanism_ssl

Every certificate used by a client needs a corresponding user in RabbitMQ. There are two options on how rabbitmq can transform certificates into usernames. By default the plugin uses the full DN on the certificate as the username or the CN can be used by setting the ssl_cert_login_from option to common_name. I use the common name because its more readable in the web ui. In the RabbitMQ administration tool, create a user "CN=ssltest" without a password and grant that user access to the ssltest virtual host. Finally, modify the ssl per the highlighted sections below

%% -*- mode: erlang -*-
[
 {rabbit,  [ 
    {ssl_listeners, [5671]},
    {auth_mechanisms, ['EXTERNAL', 'PLAIN']},
    {ssl_options, [{cacertfile,"D:/RabbitMQ/certs/MyCA.pem"},
                  {certfile,"D:/RabbitMQ/certs/MyRabbitServer.pem"},
{keyfile,"D:/RabbitMQ/certs/MyRabbitServer.key"},
{verify,verify_peer}, {ssl_cert_login_from, common_name}, {fail_if_no_peer_cert,true}]} ]} ].

Once this is done correctly the following program should work. Notice that the username/password settings have been removed, the AuthMechanisms is now set and the CertPath/CertPassphrase is populated.

using System;
using System.Net.Security;
using System.Text;
using RabbitMQ.Client;
using RabbitMQ.Client.Exceptions;
using RabbitMQ.Util;

namespace RabbitMQTest
{
    public class Program
    {
        public static void Main(string[] args)
        {
            
            try
            {
                var hostName = "MyRabbitServer";
var cf = new ConnectionFactory { HostName = hostName, VirtualHost = "ssltest", AuthMechanisms = new AuthMechanismFactory[]{ new ExternalMechanismFactory()}, Ssl = new SslOption { Enabled = true, ServerName = hostName, AcceptablePolicyErrors = SslPolicyErrors.RemoteCertificateNameMismatch | SslPolicyErrors.RemoteCertificateChainErrors, CertPath = @"C:\Code\RabbitMQTest\certs\ssltest.pfx", CertPassphrase = "Pas$w0Rd" } }; using (IConnection conn = cf.CreateConnection()) { using (IModel ch = conn.CreateModel()) { ch.QueueDeclare("rabbitmq-dotnet-test", false, false, false, null); ch.BasicPublish("", "rabbitmq-dotnet-test", null, Encoding.UTF8.GetBytes("Hello, World")); BasicGetResult result = ch.BasicGet("rabbitmq-dotnet-test", true); if (result == null) { Console.WriteLine("No message received."); } else { Console.WriteLine("Received:"); DebugUtil.DumpProperties(result, Console.Out, 0); } ch.QueueDelete("rabbitmq-dotnet-test"); } } } catch (BrokerUnreachableException bex) { Exception ex = bex; while (ex != null) { Console.WriteLine(ex.Message); Console.WriteLine("inner:"); ex = ex.InnerException; } } catch (Exception ex) { Console.WriteLine(ex.ToString()); } Console.ReadKey(); } } }

No Comments