How to manage Java application secrets using Vault π¨π
Hashicorp Vault with plain Java
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
executableCreate 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
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
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 π§‘