In this article, we will learn how to configure JAAS authentication in a JVM application without resorting to configuration files. This is motivated by the fact that, in a cloud setup, persisting a configuration file to an ephemeral instance/container is not a good practice.
The problem
You are writing an application that depends on a ZooKeeper cluster. The moment comes when you need to put your code into production. During the early development of your application you were running a ZooKeeper instance on your laptop. Now that you are deploying to production, you need a secure connection between your app and the production ZooKeeper cluster you just set up.
Reading the ZooKeeper documentation you learn that you can achieve that through multiple ways: X509, SASL Kerberos, SASL Digest. Since you just want to demonstrate your POC, you have no wish to take on the burden of using X509 client certificates or to deploy a Kerberos architecture. So you fallback to SASL Digest. It's simple and easy, it's a user/password pair like you are used to.
Reading some more documentation you learn that you have to put the credentials in a jaas.conf file. You then look into the internet to find a way to bypass this requirement.
Well, your search is over, your quest ends with this very article!
You just want to copy some code and don't care about the whole "let's talk about X" part? Jump to the "How to get rid of jaas.config file" section!
Some explanations first
Since Java 1.4, a security framework has been added to the JRE: the Java Authentication and Authorization Service (JAAS).
JAAS is a quite comprehensive and pluggable framework. The only downside is that it's thought as a service meant to authenticate "the user running the code". So it has a tendency to use configuration files or even system JVM configuration.
Configuration based on files containing secrets is a growing no-go in the cloud era. Since most clouds rely on disposable systems, using a JAAS file means one of the following:
- commiting the file, with the secrets in it;
- commiting a file and use pre-run hooks to set the right value in it;
- running an external service that would provide this file.
Solution 1 is a strict no-go. Solution 2 is doable, but as a developer, I don't like having to resort to such practices. However, it's still better than solution 1, I'm just stating my very personal point on view! Solution 3 is a bit like solution 2. It needs some kind of pre-run hook. In addition, it needs to trust the external source.
In this article, we will see how to abstract ourselves from the file-based JAAS login configuration. I will use a ZooKeeper Java/Scala client as an example.
What's in the box JAAS file?
First, what is a JAAS file made of?
As the Oracle documentation states, it is made of entries. Each entry is composed of multiple modules. Each module provides a login/authorization mechanism with all its options. An authentication/authorization process has to satisfy all modules of the entry to succeed.
Example
We assume that the server part is already configured. Here we focus on the client part only.
Let's say we want to authenticate a Java application against a ZooKeeper cluster using the DIGEST-MD5 SASL mechanism. We write a jaas.conf file somewhere on the filesystem. Here is what the file content will look like:
Client {
org.apache.zookeeper.server.auth.DigestLoginModule required
username="buffysummers"
password="vampiresbeware";
};
The entry name is Client
(the default client session name for ZooKeeper). There is one module identified by the FQDN of the LoginModule
class that will process the authentication/authorization. This module is required
and has two parameters: username
and password
. No spaces around the =
, the semicolon separates the modules. So only one is needed at the end of the module. One is also needed after the entry.
"Executing" this entry is quite straightforward: Java instanciates the org.apache.zookeeper.server.auth.DigestLoginModule
class and calls initialize("subject", callback, sharedState, Map(username=buffysummers, password=vampirebeware))
on the instance. After that, the actual client will use the instance's methods to perform authentication/authorization.
It's the developer's responsibility to ensure the module class is present in the classpath. In the case of ZooKeeper, the library provides the org.apache.zookeeper.server.auth.DigestLoginModule
class.
Oh, one last thing about the JAAS file syntax: for each module you must say if the module is required, sufficient, optional, etc. That's what the required
keyword is about! That way you can require multiple authentication methods to pass or you can just ask for one of them to pass.
How does it work in the JVM?
When running your application, the client code will use the javax.security.auth.login.Configuration
static class to access your configured JAAS. Actually, it will use the only JRE provided implementation com.sun.security.auth.login.ConfigFile
, which parses your provided jaas.conf file.
The Configuration singleton provides one method, getAppConfigurationEntry(String entryName)
. In our example, by default, the ZooKeeper client calls the method with the string "Client".
In most JREs, there is no default JAAS file. That means Configuration
has no entries.
How do I tell the JVM where to find jaas.conf?
According to the javadoc, there are multiple ways to do that:
- edit the java.security system file (in
$JAVA_HOME
); - run your app with the java option
-Djava.security.auth.login.config=path/to/jaas.conf
; - put the file in the default location:
$HOME/.java.login.config
.
The next section will show us how to setup your JVM using Java code, without relying on the filesystem. It will keep on using the ZooKeeper client example.
How to get rid of jaas.config file
The answer to that is quite simple: override Java's Configuration default implementation!
Disclaimer
Before we dive in the details, please note that There is only one Configuration object installed in the runtime at any given time. When we override it, we remove all previous login configurations. Most JREs do not have any default configuration, so you should be safe. However, if you deploy your application in your company's VMs or container, they may already have specific configuration. So ask around!
Implementing a new Configuration
First, we define a class that implements javax.security.auth.login.Configuration:
import javax.security.auth.login.AppConfigurationEntry;
import javax.security.auth.login.Configuration;
import java.util.HashMap;
import java.util.Map;
public class MyConfiguration extends Configuration {
AppConfigurationEntry[] getAppConfigurationEntry(s: String) {
Map<String,String> options = new HashMap<>();
options.put("username", "buffysummers");
options.put("password", "vampiresbeware");
return {
new AppConfigurationEntry(
"org.apache.zookeeper.server.auth.DigestLoginModule",
AppConfigurationEntry.LoginModuleControlFlag.REQUIRED,
options
)
};
}
}
Of course, you are highly encouraged to get the values for username
and password
from the environment, from a config database, or whatever you want! You might even add a constructor and feed it the credentials. You do you!
The next step is to instanciate it. You need to do it before you create a new ZooKeeper client instance:
// 1. Do that somewhere *before* creating the ZooKeeper instance.
// I would say do it close enough so other people can understand why you do it, or
// document this call because it is kind of cryptic.
Configuration.setConfiguration(new MyConfiguration());
// 2. Just create the zookeeper instance as usual!
ZooKeeper zookeeper = new ZooKeeper("localhost:2181", 3600000, watchCallback);
// 3. ?
// 4. PROFIT!
And that's it.
Alternative method
When writing this article, I read the documentation a bit more in depth and found out that I could use Java properties in the JAAS configuration file!
So instead of the code-only solution, we could actually just commit a jaas.conf file containing:
Client {
org.apache.zookeeper.server.auth.DigestLoginModule required
username="${app.zookeeper.username}"
password="${app.zookeeper.password}";
};
Then run your Java application with the Java options: -Dapp.zookeeper.username=buffysummers
and -Dapp.zookeeper.password=vampirebeware
.
I tried it, it works!
Depending on your application, using this solution might be easier than the full-code one.
Conclusion
Now that we understand a bit more about JAAS and how to avoid using the file format, there is actually only one advice to keep in mind: DO NOT STORE CREDENTIALS IN YOUR GIT REPOSITORY! By using one of the two proposed solutions, you should be able to have the secrets provided by the environment.
Besides, both of them should work on Clever Cloud. Your environment variables are injected in your application both as environement and as Java properties! You can also define a variable named app.zookeeper.username
. It will only appear as a Java property, due to limitations in environment variables names.