22 Dec 2011

Securing the ActiveMQ web console using LDAP based authentication

In my previous blog post I described how to configure LDAP based authentication and authorization in ActiveMQ while also allowing anonymous access to certain destinations in the broker. That previous post as well as this post is based on the LDAP tutorial in the ActiveMQ Security Guide provided by Fusesource.com.

In this article I want to expand on the previous post and show how to secure the ActiveMQ web console so that web users will be authenticated against user information stored in LDAP. All the configuration from the previous post can extended in order to secure the web console but I will provide full configuration here as well so that there is no need to copy and paste config snippets from the previous article.
I recommend the LDAP tutorial as a prerequisite to this article as it populates the LDAP server with the right security information (users, passwords, groups and roles), which is needed if you want to follow the steps of this blog post.

The ActiveMQ web console internally uses Jetty as the web container. So securing the web console basically means configuring the Jetty HTTP server to authenticate any web clients. The way how Jetty gets secured is completely independent from the way how ActiveMQ gets secured. However both support JAAS so the actual configuration principles are the same. Jetty also supports LDAP based authentication through its own JAAS LoginModule class org.eclipse.jetty.plus.jaas.spi.LdapLoginModule.



Prerequisites before taking off

The LDAP tutorial in the ActiveMQ Security Guide (version 5.5) makes some configuration that will not work quite well with the JAAS LDAP login module used by the ActiveMQ web console (i.e. Jetty). Two configuration changes are required in your LDAP user and group data to successfully authenticate any web users via Jetty's LdapLoginModule. Here are the required changes to your LDAP data:

1) Store passwords in plain text in LDAP:
The LDAP tutorial instructs you to store any user passwords as SHA hashes. See steps 18 and 19 of the Add User Entries part of the tutorial (and Figure 6.7). The LdapLoginModule from Jetty reads that password from LDAP as it is (i.e. SHA hash) and tries to match it against the password supplied by the web user, which is in plain text. So the SHA hashed password stored in LDAP is string compared against the plain text password, which will obviously fail.
Rather than storing the password as SHA hash in LDAP, store it in plain text.


This needs to be done for all users.
Storing the password in plain text will not change the ability of the ActiveMQ LDAPLoginModule class to authenticate users. This LoginModule takes a different approach. Rather than retrieving the password and comparing it against the password given by the user, it tries to bind the user (an operation on the LDAP server) whereby the password gets verified by the LDAP server and not by the LoginModule class.
Perhaps there is a way to configure the Jetty LdapLoginModule to also work with SHA hashed password, however I did not find it (also tried DIGEST based authentication but that did not help).



2) User to group mapping in LDAP must use users full dn + basedn name:

Steps 30-31 and figure 6.8 of the Add User Entries part of the LDAP tutorial explain how to make users member of a particular group. It uses the member attribute of the groupOfNames LDAP class and the mapping to a user is done via the users uid attribute, like in this example mapping that is taken from the LDAP tutorial:
  member=uid=jdoe

This won't work with the Jetty LdapLoginModule implementation, as it queries for a users group using the user's full name (i.e. uid + base dn), e.g.

  member=uid=jdoe,ou=User,ou=ActiveMQ,ou=system

and as a result won't find any user groups if the group members are only specified using the uid (i.e. jdoe).
To overcome this difference in the LDAP login module implementations, it is necessary to always provide the dn + basedn of the user that you want to add to a group.
E.g. rather than setting the members of a group like this ((see Figure 6.8 of the LDAP tutorial)

use this

This change of users to groups wiring however requires an update on the ActiveMQ login module configuration in login.config from
  roleSearchMatching="(member=uid={1})"

to
  roleSearchMatching="(member=uid={1},ou=User,ou=ActiveMQ,ou=system)"

That way both, the LDAP LoginModule of ActiveMQ and Jetty will be able to retrieve the groups (roles) a user is in.  
With these two changes to the LDAP data we are ready to go and configure the web console for LDAP based authentication.



Steps to securing the web console

All of the Jetty configuration in ActiveMQ is stored in $ACTIVEMQ_HOME/conf/jetty.xml. Its Spring config already instantiates a security handler but authentication is turned off by default via the authenticate=false property.  It needs to be enable in this bean:



<bean id="securityConstraint" class="org.eclipse.jetty.http.security.Constraint">
  <property name="name" value="BASIC" />
  <property name="roles" value="admins" />
  <property name="authenticate" value="true" />
</bean>


Changing the "authenticate" property to "true" is required but isn't enough. Any authenticated user needs to have the role specified by the "roles" attribute in order to get authorized. The administrator role name in LDAP is called "admins" but the Jetty securityConstraint bean uses "admin" out of the box. It is necessary to change the value of the "roles" property to "admins" as in the above example. A fully configured jetty.xml is provided at the end of this article.

Out of the box Jetty is configured to use a HashLoginService class, which simply reads the user credentials from a text file ${activemq.base}/conf/jetty-realm.properties. In order to authenticate against an LDAP server we need to configure a JAAS LoginService class:



<bean id="securityLoginService" class="org.eclipse.jetty.plus.jaas.JAASLoginService">
  <property name="name" value="ActiveMQLDAPRealm" />
  <property name="LoginModuleName" value="jetty-ldap"/>
  <property name="CallbackHandlerClass" value="org.eclipse.jetty.plus.jaas.callback.DefaultCallbackHandler" />
  <property name="roleClassNames" value="org.eclipse.jetty.plus.jaas.JAASRole" />
</bean>


This JAASLoginService is configured for a LoginModuleName called "jetty-ldap". This name refers to the JAAS login module configuration in login.config. In the previous article I already provided a login.config to be used by ActiveMQ. You can simply add the following configuration to that same login.config:


/**
   This LoginModule configuration is only used by Jetty when
   authentication of the web console is enabled.
*/
jetty-ldap {
  org.eclipse.jetty.plus.jaas.spi.LdapLoginModule required
    debug="true"
    contextFactory="com.sun.jndi.ldap.LdapCtxFactory"
    hostname="localhost"
    port="10389"
    bindDn="uid=admin,ou=system"
    bindPassword="secret"
    authenticationMethod="simple"
    forceBindingLogin="false"
    userBaseDn="ou=User,ou=ActiveMQ,ou=system"
    userRdnAttribute="uid"
    userIdAttribute="uid"
    userPasswordAttribute="userPassword"
    userObjectClass="inetOrgPerson"
    roleBaseDn="ou=Group,ou=ActiveMQ,ou=system"
    roleNameAttribute="cn"
    roleMemberAttribute="member"
    roleObjectClass="groupOfNames"
    authenticated="true";
};

Finally Jetty needs to be told where to find this login.config file via a Java property to the JVM.
The easiest way is to add this propery to the bin/activemq script, e.g. 
 
ACTIVEMQ_OPTS="$ACTIVEMQ_OPTS_MEMORY -Dorg.apache.activemq.UseDedicatedTaskRunner=true   -Djava.util.logging.config.file=logging.properties -Djava.security.auth.login.config=login.config"

That's about it. Assuming the data in LDAP is setup according to the prerequisites listed above, the ActiveMQ web console should now be secured. Trying to access http://localhost:8161 should now request a username and password from you which is then authenticated against the LDAP server.

Note we did not need to change the broker configuration (with the exception of pre-requisite #2) as the authentication done in the web console (based on Jetty) is completely independent of the authentication done by ActiveMQ.


Links to all files used in this post:
jetty.xml
login.config
activemq-ldap-with-anon-access.xml

The relevant documentation that helped me on this subject:
Jetty JAAS Tutorial
Jetty Realms Tutorial
Jetty 7 JavaDoc

Let me know if this post was helpful.
This configuration has been tested with ActiveMQ 5.5.1 and 5.6.0.

5 Dec 2011

ActiveMQ: LDAP based authentication and authorization

The FuseSource ActiveMQ Security Guide has two great chapters on how to configure ActiveMQ for authentication and authorization against an LDAP server. Chapter 5 even has a tutorial that contains step-by-step instructions on how to configure your LDAP server and ActiveMQ based on ApacheDS, an open-source LDAP server. If you need to secure your ActiveMQ broker, then I highly recommend this documentation.

The tutorial in the Security Guide shows the relevant configuration needed so that every JMS connection into the broker is authenticated and checked for authorization against the security information stored in an LDAP server.

Sometimes you may want to allow anonymous access to certain destinations on your broker that are not critical while securing access to your critical destinations.  In this post I like to outline a possible solution for such use-case. I will assume the reader is generally familiar with LDAP based authentication and authorization in ActiveMQ. If not, I suggest to first consult the ActiveMQ Security Guide.


When configuring ActiveMQ for JAAS based authentication against an LDAP server, the file login.config could read as follows (pretty much a copy-and-paste from the FuseSource Security Guide):






/** JAAS LoginModule that uses LDAP based authentication.

Every connection into broker must supply username and password

for authentication to succeed. Anonymous access not allowed.

*/

LDAPLogin {

  org.apache.activemq.jaas.LDAPLoginModule required

  debug=true

  initialContextFactory=com.sun.jndi.ldap.LdapCtxFactory

  connectionURL="ldap://localhost:10389"

  connectionUsername="uid=admin,ou=system"

  connectionPassword=secret

  connectionProtocol=""

  authentication=simple

  userBase="ou=User,ou=ActiveMQ,ou=system"

  userSearchMatching="(uid={0})"

  userSearchSubtree=false

  roleBase="ou=Group,ou=ActiveMQ,ou=system"

  roleName=cn

  roleSearchMatching="(member=uid={1})"

  roleSearchSubtree=false;

};

The broker then needs to use the JAAS authentication plug-in referencing the LDAPLogin authentication realm:




<plugins>

  <jaasAuthenticationPlugin configuration="LDAPLogin" />

</plugins>



Using such configuration, every JMS connection will be authenticated using the LDAPLoginModule, no matter if it provides a username and password. Needless to say the authentication will fail if no username/password is supplied. Anonymous access won't be allowed in this configuration.

So in order to also allow anonymous access to the broker we can leverage the ActiveMQ GuestLoginModule.
The GuestLoginModule allows JMS connections that are not configured for username/password to still access a secured broker. It is typically used in conjunction with other JAAS login modules and basically successfully authenticates a JMS connection using a configurable username and group name. Here is a sample configuration of this login module:


org.apache.activemq.jaas.GuestLoginModule sufficient

    debug=true

    org.apache.activemq.jaas.guest.user="guest"

    org.apache.activemq.jaas.guest.group="guests";

Authentication will succeed even if no username/password was supplied by the JMS client. Clients will be authenticated as user "guest" and belong to the group "guests". As you see, these names are configurable.

In JAAS configuration, multiple login modules can be combined in one JAAS authentication realm. They will then be tried in order.

The basic idea is to configure authentication so that the broker first invokes the GuestLoginModule, before trying the LDAPLoginModule. Here is the configuration:



/** JAAS LoginModule configuration that combines LDAPLoginModule with

  GuestLoginModule. 

  LoginModules can be combined in JAAS.

  See http://fusesource.com/docs/broker/5.5/security/Auth-JAAS-GuestLoginModule.html

  for more information.

*/

LDAPLogin-with-Anon-Access {

  org.apache.activemq.jaas.GuestLoginModule sufficient

    debug=true

    credentialsInvalidate=true

    org.apache.activemq.jaas.guest.user="guest"

    org.apache.activemq.jaas.guest.group="guests";



  org.apache.activemq.jaas.LDAPLoginModule requisite

    debug=true

    initialContextFactory=com.sun.jndi.ldap.LdapCtxFactory

    connectionURL="ldap://localhost:10389"

    connectionUsername="uid=admin,ou=system"

    connectionPassword=secret

    connectionProtocol=""

    authentication=simple

    userBase="ou=User,ou=ActiveMQ,ou=system"

    userSearchMatching="(uid={0})"

    userSearchSubtree=false

    roleBase="ou=Group,ou=ActiveMQ,ou=system"

    roleName=cn

    roleSearchMatching="(member=uid={1})"

    roleSearchSubtree=false;

};



<plugins>

  <jaasAuthenticationPlugin configuration="LDAPLogin-with-Anon-Access" />

</plugins>



The GuestLoginModule config above uses an additional property credentialsInvalidate that when set to "true" will only authenticate requests that do not have a username/password supplied. Each  connection without username gets authenticated as user "guest" and belongs
to the group "guest".
Any other JMS connections that contain username and password will be authenticated using the next login module; the LDAP LoginModule. So its important to set credentialsInvalidate=true as otherwise the GuestLoginModule authenticates all requests no matter whether or what username/password is supplied.


Authenticating anonymous connections is only half the story. Without further authorization anonymous users would be allowed all operations on the broker, which is definitely not what we want.
With additional authorization anonymous access to the broker can be restricted to only specific destinations with restricted rights on each destination.
The FuseSource Security Guide also explains how to configure the LDAP Authorization plug-in and how to create groups and destinations in the LDAP server. A step-by-step guide for adding all required entries is given in chapter 5.
Based on the configuration proposed in that documentation it is additionally also necessary to 
  • define all those destinations in LDAP that anonymous access will be allowed on and
  • grant the required permissions to the group "guests" to these destinations.

If for example anonymous access should be allowed to queue "example.A", then the destination "example.A"  needs to be defined in LDAP. Further "read", "write" and potentially "admin" privileges need to be given to the group "guests" on this destination.
This procedure needs to be applied to all destinations that should allow anonymous access.



Finally, every new connection into the broker triggers some advisory messages. Without giving "guests" access to these advisory topics, the connection will fail, typically with an error similar to this:



java.lang.SecurityException: User null is not authorized to create: topic://ActiveMQ.Advisory.Connection


It is necessary to also grant "admin", "write" and potentially "read" rights for the group "guests" to ActiveMQ.Advisory topics.



This will allow for anonymous access on certain destinations while enforcing username/password based authentication on all other destinations in the broker.



Links to all files used in this post:
login.config
activemq-ldap-with-anon-access.xml
activemq-ldap.xml - no anonymous access




14 Nov 2011

Consuming Topic messages in Spring and Camel


When using a Spring to consume topic messages, make sure to cache the JMS consumer somewhere. Otherwise your consumer may not receive all messages.

Consider the following Camel route definition that consumes messages from a JMS topic.

<!-- Can be any JMS broker really, e.g ActiveMQ -->
<bean id="JmsConnectionFactory" class="org.apache.activemq.ActiveMQConnectionFactory" >
  <property name="serverUrl" value="tcp://localhost:61616” />
</bean>
<bean id="SingleConnectionFactory" class="org.springframework.jms.connection.SingleConnectionFactory” >
  <property name="targetConnectionFactory" ref="JmsConnectionFactory" />
  <property name="reconnectOnException" value="true" />
</bean>
<bean id="jms" class="org.apache.camel.component.jms.JmsComponent" >
  <property name="connectionFactory" ref="SingleConnectionFactory" />
  <property name="cacheLevelName" value="CACHE_SESSION" />
  <property name="acknowledgementMode" value="1" />
</bean>

<camelContext xmlns="http://camel.apache.org/schema/spring" trace="true" >
  <route id="Consume-From-JMS">
    <from uri="jms:topic:Test.Topic?deliveryPersistent=true" />
    <to uri="whatever"/>
    ...
  </route>
</camelContext>

The camel-jms component is configured to use a Springs SingleConnectionFactory and a cache level of CACHE_SESSION. This combination will cause problems!

When using Springs SingleConnectionFactory, it will not cache the consumer. This ConnectionFactory only reuses the same connection but does not cache any other JMS resources on top of the connection (i.e. session and consumer).
At the same time Springs DefaultMessageListenerContainer (which is used by Camel to consume messages from a JMS broker) does not cache the consumer either as its cache level is set to CACHE_SESSION. The result of this configuration is that a new JMS consumer instance get created by Springs DMLC prior to requesting the next message from the JMS broker. After the message has been dispatched and processed, the consumer is destroyed (and unregistered from the broker).
In addition a JMS topic works differently from a JMS queue in the way that if there is no subscriber registered, the broker will discard the topic message. As with the above configuration the JMS consumer gets recreated for every message by Spring (and registered in the JMS broker), there are certain time windows when the broker does not have a topic subscriber registered and so it discards the messages it receives. The result will be that your Spring JMS topic consumer will not receive all of the messages from the broker.

When consuming topic messages in Spring, it is necessary to cache the JMS consumer. Either by using a connection factory that supports consumer caching like the Spring CachingConnectionFactory or by configuring the cache level of the DMLC to use CACHE_CONSUMER.
Please note that the ActiveMQ PooledConnectionFactory does not cache consumers!

This only applies to topic consumers in Spring. Topic producers will not use these cache level settings and hence don't have this problem.