How to manage Java application secrets using Vault πŸš¨πŸ”

How to manage Java application secrets using Vault πŸš¨πŸ”
Photo by rc.xyz NFT gallery / Unsplash

Howdy friend πŸ‘‹

Here's the plan.

We will explain what is Vault and why we need it to manage secrets in applications; Then we'll run Vault and implement a Java project to explain how to use it for managing secrets.

Let's Go.

What is Vault?

Vault is a centralized key management system that helps you safely manage secrets. By secrets, we mean sensitive information like digital certificates, database credentials, passwords, and API encryption keys.

Vault has many use cases:

  • Secrets storage: Securely store and manage access to secrets
  • Dynamic secrets
  • Automate credential rotation
  • Encryption as a service
  • And more...

To learn more about Vault here is a nice article about it devoteam.com or you can check the official vault website.

How does Vault work?

In the following video, Armon Dadgar Co-Founder and CTO at Hashicorp, is explaining in very simple words how Hashicorp Vault works.

Honestly, I couldn't explain it better than him.

Example Use Case: Static secrets

For brevity, we will use the same application from the previous article (Calling an external API), but instead of fetching the API password from Java KeyStore, we will be fetching it from Vault as a static secret.

You can clone the application from the previous article here: https://github.com/camelcodes/java-keystore-example

Install and configure Vault Server

Download Vault from the official website: https://developer.hashicorp.com/vault/downloads

We are using the version 1.13.0 in this tutorial.

Now create a folder and name it vault_1.13.0

Inside the folder:

  • Put the downloaded vault executable
  • Create a new folder and name it vault_data (it will be used by vault)
  • Create a config.hcl file and put this config in it
# Enable Vault UI
# You can access the UI on port 8200
ui = true

disable_mlock = true

storage "raft" {
  path    = "vault_data"
  node_id = "node0"
}

listener "tcp" {
  address     = "0.0.0.0:8200"
  # Disable TLS on localhost
  tls_disable = "true"
}

api_addr = "http://127.0.0.1:8200"
cluster_addr = "https://127.0.0.1:8201"
Disclaimer⚠️: This is for development purposes only do not use these vault configurations in production. Please refer to this manual to securely deploy Vault in a production environment.

Now Our vault folder structure will look exactly like this:

Now open a command prompt from this folder and run the following command to start the vault app.

vault server -config=config.hcl

Vault service started on port 8200.

Once the server is started, we can access using the UI.

In your browser access http://127.0.0.1:8200

Select "Create a new Raft cluster"

In the next step, we'll need to configure Vault key shares (unseal keys).

We'll need these key shares to unseal the Vault each time we restart the server.

The philosophy behind key shares is that the key would be given to separate users and each time you need to unseal the vault, you will need a defined number of keys to be able to unseal the vault.

For this tutorial, we will create 5 key shares and we will use 2 of them as Key thresholds.

Click Initialize and then Download Keys (it's a JSON file, containing the keys required to unseal vault"

The keys file content is something like this:

Click on "Continue to Unseal" to proceed.

Congratulations πŸŽ‰ Your vault is initialized.

Unseal the Vault

Once the vault is initialized we need to unseal it.

We'll use the two first keys (on lines 3 and 4 of the downloaded JSON file).

  • 7ccd70d90b76fd5acae654efd3b43b0438dec74dde5b47e0c431ca810a8684797c
  • b681745f0b2b18fd1cfb8e927224d02cfa0d5229554ce31c71ee0dc0d8a97c67f3

Paste the first key, click on "Unseal" and repeat for the second one.

Once the vault is unsealed we now can log in to configure our Vault.

We'll use the Token authentication method to connect to the vault.

We'll use the root_token field from the JSON file to authenticate as a root user.

Once authenticated we are in the vault.

Adding the login token as an environment variable

Create an environment variable VAULT_TOKEN, we will need it later.

VAULT_TOKEN=hvs.ileUzK8ZCDQhyJFIqgUu88Nu

Please see how to secure vault authentication tokens for production use.

Adding a static secret to the Vault

We'll add our API_KEY to the Vault.

Once added, Vault will encrypt it and we can use the Vault Java API to fetch it securely in our Java code.

For this, we will need to create a Secret Engine. We'll name it camelcodes_app

Enable a secret engine in Vault

When created, we'll add a new secret (our external API static API_KEY).

For the secret path, we will put external_api and for the secret data, we'll use API_KEY with the value 735b1fa1-6513-4549-bf96-cb492bb45387

Create a static secret value in secret engine

You can use the API Explorer to check that the secret path is set correctly. you can access it using this URL: http://127.0.0.1:8200/ui/vault/api-explorer?filter=camelcodes_app.

Now we are all set.

We configured Vault with a secret engine that encrypts and stores static secrets, and we used it to store our external API key.

Now let's update our application to switch from using Java Keystore API to Hashicorp Vault API.

The Code:

Initializing a new Gradle project

Our base project was zero dependencies. But in this tutorial we need some dependencies; mainly we need the Vault java driver.

For this, we will need to add a dependency manager like Gradle.

Install Gradle if you don't have it already.

I have installed Gradle 7.6.

In a new directory, we need to initialize our project.

gradle init

Follow the steps, use this package name for the sake of this tutorial: net.camelcodes.vault

Once the project is initialized open it with your favorite IDE and load the gradle project.

./gradlew build

Adding dependencies

Add the following dependency in your build.gradle

dependencies {
    implementation 'com.bettercloud:vault-java-driver:5.1.0'
}

now refresh dependencies

./gradlew build

Implementing the application

For this example, we will use the VAULT_TOKEN environment variable that we set earlier to authenticate our application with the vault.

⚠️This is dangerous because we are using a Root token that has complete access to the vault, consider using a limited user that has only read access to the specific vault path that you are using in your application if you only need to read secrets not writing them.

Now, We will add four files to our project.

  • App.java
  • VaultStaticSecretLoader.java
  • AppPropertiesReader.java
  • resources/application.properties

Files content:

application.properties

externalapi.api-key-vault-path=camelcodes_app/external_api
externalapi.api-key-vault-property=API_KEY

vault.url=http://127.0.0.1:8200

AppPropertiesReader.java

package net.camelcodes.vault;

import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;

public class AppPropertiesReader {

  static final Properties appProperties = readPropertiesFile();

  private AppPropertiesReader() {
  }

  private static Properties readPropertiesFile() {
    Properties prop = new Properties();
    try (InputStream stream = ClassLoader.getSystemClassLoader()
        .getResourceAsStream("application.properties")) {
      prop.load(stream);
      return prop;
    } catch (IOException e) {
      throw new IllegalStateException("Unable to load properties file", e);
    }
  }
}

VaultStaticSecretLoader.java

package net.camelcodes.vault;

import com.bettercloud.vault.SslConfig;
import com.bettercloud.vault.Vault;
import com.bettercloud.vault.VaultConfig;
import com.bettercloud.vault.VaultException;
import java.util.Optional;
import java.util.logging.Level;
import java.util.logging.Logger;

public class VaultStaticSecretLoader {

  Logger log = Logger.getLogger(VaultStaticSecretLoader.class.getName());

  private static final String API_KEY_VAULT_PATH_PROPERTY = 
        "externalapi.api-key-vault-path";
  private static final String API_KEY_VAULT_PROPERTY = 
        "externalapi.api-key-vault-property";

  private final Vault vault;

  VaultStaticSecretLoader() {
    try {
      // Vault auth token, will be fetched from 
      // environment variable VAULT_TOKEN
      // make sure you initialize it before starting the application
      final VaultConfig config = new VaultConfig().address(
              appProperties.getProperty(VAULT_URL_PROPERTY))
          .token(System.getenv(VAULT_TOKEN_PROPERTY))
          // Disabling ssl for local usage
          .sslConfig(new SslConfig().verify(false).build());
      // initializing vault with version 1 
      // (for K/V secret engine compatibility)
      // check: https://github.com/BetterCloud/vault-java-driver
      vault = new Vault(config, 1);
      log.info("Vault initialized");

    } catch (VaultException e) {
      throw new IllegalStateException("Unable to initialize Vault", e);
    }
  }

  public Optional<String> getStringKeyEntry(String path, String key) {
    Optional<String> secret = Optional.empty();
    try {
      secret = Optional.ofNullable(vault.logical()
                   .read(path).getData().get(key));
    } catch (VaultException e) {
      log.log(Level.SEVERE, "Unable to read secret from Vault", e);
    }
    return secret;
  }
}

App.java

package net.camelcodes.vault;

import static net.camelcodes.vault.AppPropertiesReader.appProperties;

import java.util.Optional;
import java.util.logging.Logger;

public class App {

  Logger log = Logger.getLogger(App.class.getName());

  private static final String API_KEY_VAULT_PATH_PROPERTY = 
        "externalapi.api-key-vault-path";
  private static final String API_KEY_VAULT_PROPERTY = 
        "externalapi.api-key-vault-property";


  public App() {
    callExternalApi();
  }

  /**
   * This method is simulating a web service call, 
   * in our case will just log a message.
   * <p>
   * The idea is to fetch the API_KEY from the Vault and 
   * log it in the console
   */
  private void callExternalApi() {

    VaultStaticSecretLoader vault = new VaultStaticSecretLoader();

    Optional<String> apiKey = vault.getStringKeyEntry(
        appProperties.getProperty(API_KEY_VAULT_PATH_PROPERTY),
        appProperties.getProperty(API_KEY_VAULT_PROPERTY));

    if (apiKey.isEmpty()) {
      throw new IllegalStateException("Couldn't retrieve key from Vault");
    }

    log.info(
    String.format("[Mock Call] External api called with API_KEY: %s",
        apiKey.get()));
  }

  public static void main(String[] args) {
    new App();
  }
}

When you run the application you should have an output like this:

mars 10, 2023 10:31:27 PM net.camelcodes.vault.VaultStaticSecretLoader <init>
INFOS: Vault initialized
mars 10, 2023 10:31:28 PM net.camelcodes.vault.App callExternalApi
INFOS: [Mock Call] External api called with API_KEY: 735b1fa1-6513-4549-bf96-cb492bb45387

That's it for now.

If you have questions or suggestions, drop them in the comments.

Keep coding 🧑

Source code

https://github.com/camelcodes/java-vault-example

Happy Coding !

Sponsorship