How To Secure Secrets Within Your Java Application With an Open Source Secrets Management SDK

Over the past decade, my career has evolved from Development to DevOps and most recently to DevSecOpsDevSecOps is the result of organizations shifting left with security earlier in the development stage, and empowering the engineers to manage the security of their applications. 

As I’ve researched application security, and attended corporate training events, one of the primary themes emerging is leveraging tools and utilities designed to provide security, rather than relying on homegrown solutions. 

In this article, we’re going to talk about securing Java applications with the Conjur Java API. As an industry leader in Privileged access security, CyberArk has invested in providing secure and robust solutions for their clients.  

Setting Up

For this example, I’m going to be retrofitting a small project I put together a couple years ago. The project creates a microservice that returns the distance between two ZIP codes and uses a third-party service to perform the calculation. The call to the third-party service requires an API key, which I added to my project with the following line. 

private String apiKey = "P6wa1NepBwp5wssOz9sXj7rfL3sPOvGDBdOC022CyrH5U9UtjmrDuS";

Obviously, including an API key directly in code is a bad practice. An ideal solution would be to retrieve the key from a secret store at runtime, allowing the key to remain secure and making it easy to update the key as needed. Let’s see how we can do this with Conjur. 

 You can find the Conjur API for Java on GitHub, and the README.doc includes instructions to include the library with Maven. I’m more of a Gradle guy myself, so let’s look at the steps to include it with Gradle, and then we’ll implement the necessary code to retrieve the API Key from a Conjur instance. 

I’ll be using a Conjur server that I set up in AWS on an EC2 instance. If you want to try this yourself, you can set an instance using the Quickstart documentation. I made a couple of changes in my installation.  

  • The name of the account I set up is ConjurDemo instead of myConjurAccount  
  • I updated the docker-compose.yml file to expose port 443 instead of 8443 
  • I updated the CN (Common Name) on the Certificate configuration from proxy to the URL of my AWS instance. 

Installing the Conjur Java API

This example requires us to have the Conjur Java API library available in our local Maven repository. You’ll need to have Maven installed on your workstation and then clone the Git Repository. Once you have the repository local, open a command line client, and navigate to the root folder of the project. Execute the following command to build the project and install it in your local M2 repository. 

$ mvn install -DskipTests

The integration tests in the project require an active Conjur instance to run, so we use the DskipTests flag to build the project without running them. Assuming it executes successfully, you should see output to the screen which ends similarly to that shown below. 

[INFO] Installing C:\Users\mike\Development\github\cyberark\conjur-api-java\pom.xml to C:\Users\mike\.m2\repository\net\conjur\api\conjur-api\2.1.0\conjur-api-2.1.0.pom 

[INFO] ------------------------------------------------------------------- 

[INFO] BUILD SUCCESS 

[INFO] ------------------------------------------------------------------- 

[INFO] Total time:  34.900 s 

[INFO] Finished at: 2019-07-06T20:51:58-07:00 

[INFO] -------------------------------------------------------------------

The Conjur Java API has been installed in our M2 repository and is ready to use in our project. 

Adding The API Key with Policy

As our first step, let’s add the API Key to our Conjur store. Having SSH’d into the instance hosting the Conjur Server, log in as the admin user. If you set up your server using the Quickstart documentation, you can find the API key for this user in the admin_data file created during that tutorial. 

$ docker-compose exec client conjur authn login -u admin 

Please enter admin's password (it will not be echoed): 

Logged in

Now we’ll create a policy which creates an Admin user for our application, and an account for the services to use. Save this as a YAML file. I chose ZipcodeMicroservice.yml 

- !policy 

  id: ZipcodeMicroservice 

  body: 

    # Define an administrator, service account and variable for the API Key 

  - !user ZipcodeAdmin 

  - !host ZipcodeApp 

  - !variable apiKey 

  - !permit 

    # Give read and write permissions to the administrator. 

    role: !user ZipcodeAdmin 

    privileges: [read, update, execute] 

    resource: !variable apiKey 

  - !permit 

    # Give read permissions to the service account to fetch the API Key. 

    role: !host ZipcodeApp 

    privileges: [read, execute] 

    resource: !variable apiKey

Now we can load the policy into Conjur with a command similar to that shown below. 

$ docker-compose exec client conjur policy load root policy/ZipcodeMicroservice.yml

You should see results similar to those shown below. We’ll be using the keys next, so be sure to store them in a safe place. 

$ Loaded policy 'root' 

{ 

  "created_roles": { 

    "ConjurDemo:user:[email protected]": { 

      "id": "ConjurDemo:user:[email protected]", 

      "api_key": "2zg9cy53h2q3smh4bded1z08ea1191912011mza4etpzna13kyq80p" 

    }, 

    "ConjurDemo:host:ZipcodeMicroservice/ZipcodeApp": { 

      "id": "ConjurDemo:host:ZipcodeMicroservice/ZipcodeApp", 

      "api_key": "166szy87a1xwp3xsnfzw1esfazs10z6w0x2ypcmkz3ztqpe62sv4akn" 

    } 

  }, 

  "version": 1 

}

Log out of the admin account, and log in as the ZipcodeAdmin user, using the API Key that was returned when we created the account. 

$ docker-compose exec client conjur authn logout 

Logged out 

$ docker-compose exec client conjur authn login -u [email protected] 

Please enter [email protected]'s password (it will not be echoed): 

Logged in

Now we can store the API Key for the third-party ZIP Code service in the secure store. 

$ docker-compose exec client conjur variable values add ZipcodeMicroservice/apiKey P6wa1NepBwp5wssOz9sXj7rfL3sPOvGDBdOC022CyrH5U9UtjmrDuS

Conjur should respond with Value added. Now we’re ready to implement this solution in our project. 

Securing the Connection

Conjur uses a self-signed certificate to secure its connections, and we need to import this certificate into our local Java CA Keystore to allow it to communicate. If you run into a problem which references an SSLHandshakeException or PKIX path building failed, then you’re likely experiencing a problem validating the certificate. 

We’ll use the conjur-cli to pull the certificate down from our Conjur Server. You can easily install this using the following command. 

$ gem install conjur-cli

Once it has installed, execute the ‘conjur init’ command and follow the prompts to download the certificate. I’ve used green to indicate user input. My Conjur server has a public URL of https://ec2-34-215-72-157.us-west-2.compute.amazonaws.com:8443 and an account named ConjurDemo. Your entries may be different. 

$ conjur init 

Enter the URL of your Conjur service: https://ec2-34-215-72-157.us-west-2.compute.amazonaws.com:8443 

Trust this certificate (yes/no): yes 

Enter your organization account name: ConjurDemo 

File C:/Users/mike/.conjurrc exists. Overwrite (yes/no): yes 

 

SHA1 Fingerprint=65:15:1A:39:2F:3D:E4:B6:1A:12:86:3F:E6:08:FB:1F:3B:90:CD:5C 

 

Please verify this certificate on the appliance using command: 

              openssl x509 -fingerprint -noout -in ~conjur/etc/ssl/conjur.pem 

 

Wrote certificate to C:/Users/mike/conjur-ConjurDemo.pem 

Wrote configuration to C:/Users/mike/.conjurrc

Notice in the output above that a PEM formatted certificate was written to my local home directory. We need to convert this from a Base64 certificate to a Binary formatted certificate. We can do this using the following command. Ensure that you update the location of the source certificate to match the one written by the previous command. 

$ openssl x509 -outform der -in C:/Users/mike/conjur-ConjurDemo.pem -out conjur-default.der

So now we can import this certificate into the Java keystore. This process can be a little tricky depending on your system, but remember that Google is your friend, and many have walked this path before. The following command should import the new certificate into your keystore. 

$ keytool -import -alias conjur-default -keystore "$JRE_HOME/lib/security/cacerts"  -file ./conjur-default.der

Now that we have our API Key stored securely and all the plumbing in place for our service to connect to the Conjur server, we can start setting up our local environment and then move onto implementing Conjur in our project. 

Configuring Your Environment

Conjur provides several different ways for configuring the connection between your application and the Conjur instance. In this example, I use Environment Variables to set the required fields. Other methods for authenticating your connection are explained in the README file in the Conjur Java API project. 

Ideally, you want to inject these variables as part of your deployment process. Using a trusted deployment server like Jenkins allows you to store these values in Conjur as well, and inject them into the instance as it is deployed. 

We need to provide the following information: 

  • CONJUR_APPLIANCE_URL: Address of the Conjur Instance 
  • CONJUR_ACCOUNT: Conjur Account 
  • CONJUR_AUTHN_LOGIN: Service Login 
  • CONJUR_AUTHN_API_KEY: Service API Key

Your configuration will be slightly different from mine. Ensure that you enter your Conjur account name and your specific URL for your Conjur Server. 

$ export CONJUR_ACCOUNT=ConjurDemo 

$ export CONJUR_APPLIANCE_URL=https://ec2-34-215-72-157.us-west-2.compute.amazonaws.com 

$ export CONJUR_AUTHN_LOGIN=ZipcodeApp 

$ export CONJUR_AUTHN_API_KEY=P6wa1NepBwp5wssOz9sXj7rfL3sPOvGDBdOC022CyrH5U9UtjmrDuS

Configuring Your Project

I’m going to start with the original project here, but you can use these steps to add Conjur support to any Gradle project. First, we’ll open up the build.gradle file in the root folder. We need to add a reference to include the local Maven library, and then add the Conjur API as a dependency. 

repositories {
mavenLocal()

mavenCentral()

}

dependencies {

compile('org.springframework.boot:spring-boot-starter-web')

compile('net.conjur.api:conjur-api:2.1.0')

testCompile('org.springframework.boot:spring-boot-starter-test')

}

If your IDE automatically refreshes the dependencies for the project, you should now have the Conjur API available to use, otherwise, rebuilding your project forces the Conjur API library to be included. 

 Implementing the API in Code

The Conjur library reads the required variables from the System properties. Since we’ll be reading them in from the local environment, we’ll need to move them into the System properties at runtime. If you set the System properties as part of your startup process, you can skip this step. 

System.setProperty("CONJUR_ACCOUNT", System.getenv("CONJUR_ACCOUNT")); 

System.setProperty("CONJUR_AUTHN_API_KEY", System.getenv("CONJUR_AUTHN_API_KEY")); 

System.setProperty("CONJUR_AUTHN_LOGIN", System.getenv("CONJUR_AUTHN_LOGIN")); 

System.setProperty("CONJUR_APPLIANCE_URL", System.getenv("CONJUR_APPLIANCE_URL"));

I changed the hard-coded apiKey property to remove the assignment of the API Key. 

private String apiKey;

Within the getDistance function, I added a check to see if the apiKey needs to be set. If I need to set the apiKey, I create a new Conjur object, and then use the retrieveSecret function to retrieve the API Key from the Conjur Server. 

Conjur conjur = new Conjur(); 

apiKey = conjur.variables().retrieveSecret("ZipcodeMicroservice/apiKey");

Very simple and elegant. The beauty of this solution is that once you’ve done the plumbing for your application, adding additional secrets is just a matter of adding a new policy, setting the variable through the command line, and including another retrieveSecret function in your code. 

Learning More

If you would like to find out more about Conjur Open SourceI’ve found the resources in the Getting Started and Documentation sections of the website to be invaluable. They also provide a collection of Tutorials on Conjur policy language, and on integrations and libraries that you can use to integrate your systems with Conjur. 

Finally, consider joining the Conjur Slack Community to network with other engineers and ask the experts for help if you find yourself stuck, or puzzled.