Over the past decade, my career has evolved from Development to DevOps and most recently to DevSecOps. DevSecOps 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:ZipcodeAdmin@ZipcodeMicroservice": { "id": "ConjurDemo:user:ZipcodeAdmin@ZipcodeMicroservice", "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 ZipcodeAdmin@ZipcodeMicroservice Please enter ZipcodeAdmin@ZipcodeMicroservice'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 Source, I’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 CyberArk Commons Community to network with other engineers and ask the experts for help if you find yourself stuck, or puzzled.
Mike Mackrory is a Global citizen who has settled down in the Pacific Northwest — for now. By day he works as a Lead Engineer on a DevOps team, and by night, he writes and tinkers with other technology projects. When he’s not tapping on the keys, he can be found hiking, fishing and exploring both the urban and rural landscape with his kids. Always happy to help out another developer, he has a definite preference for helping those who bring gifts of gourmet donuts, craft beer and/or single-malt Scotch.