1. Overview
HashiCorp’s Vault is a tool to store and secure secrets. Vault, in general, solves the software development security problem of how to manage secrets. To learn more about it, check out our article here.
Spring Vault provides Spring abstractions to the HashiCorp’s Vault.
In this tutorial, we’ll go over an example of how to store and retrieve secrets from the Vault.
2. Maven Dependencies
To start with, let’s take a look at the dependencies we need to start working with Spring Vault:
<dependencies>
<dependency>
<groupId>org.springframework.vault</groupId>
<artifactId>spring-vault-core</artifactId>
<version>3.1.1</version>
</dependency>
</dependencies>
The latest version of spring-vault-core can be found on Maven Central.
3. Configuring Vault
Let’s now go through the steps needed to configure Vault.
3.1. Creating a VaultTemplate
To secure our secrets, we’ll need an instance of the VaultTemplate class for which we need VaultEndpoint and TokenAuthentication instances:
VaultTemplate vaultTemplate = new VaultTemplate(new VaultEndpoint(),
new TokenAuthentication("00000000-0000-0000-0000-000000000000"));
3.2. Creating a VaultEndpoint
There’re a few ways to instantiate VaultEndpoint. Let’s take a look at some of them.
The first one is to simply instantiate it using a default constructor, which will create a default endpoint pointing to http://localhost:8200:
VaultEndpoint endpoint = new VaultEndpoint();
Another way is to create a VaultEndpoint by specifying Vault’s host and port:
VaultEndpoint endpoint = VaultEndpoint.create("host", port);
And finally, we can also create it from the Vault URL:
VaultEndpoint endpoint = VaultEndpoint.from(new URI("vault uri"));
There are a few things to notice here – Vault will be configured with a root token of 00000000-0000-0000-0000-000000000000 to run this application.
In our example, we’ve used TokenAuthentication, but there are other authentication methods supported as well.
4. Configuring Vault Beans Using Spring
With Spring, we can configure the Vault in a couple of ways. One is by extending the AbstractVaultConfiguration, and the other one is by using EnvironmentVaultConfiguration, which makes use of Spring’s environment properties.
We’ll now go over both ways.
4.1. Using AbstractVaultConfiguration
Let’s create a class that extends AbstractVaultConfiguration, to configure Spring Vault:
@Configuration
public class VaultConfig extends AbstractVaultConfiguration {
@Override
public ClientAuthentication clientAuthentication() {
return new TokenAuthentication("00000000-0000-0000-0000-000000000000");
}
@Override
public VaultEndpoint vaultEndpoint() {
return VaultEndpoint.create("host", 8020);
}
}
This approach is similar to what we’ve seen in the previous section. What’s different is that we’ve used Spring Vault to configure Vault beans by extending the abstract class AbstractVaultConfiguration.
We just have to provide the implementation to configure VaultEndpoint and ClientAuthentication.
4.2. Using EnvironmentVaultConfiguration
We can also configure Spring Vault using the EnviromentVaultConfiguration:
@Configuration
@PropertySource(value = { "vault-config.properties" })
@Import(value = EnvironmentVaultConfiguration.class)
public class VaultEnvironmentConfig {
}
EnvironmentVaultConfiguration makes use of Spring’s PropertySource to configure Vault beans. We just need to supply the properties file with some acceptable entries.
More information on all of the predefined properties can be found in the official documentation.
To configure the Vault, we need at least a couple of properties:
vault.uri=https://localhost:8200
vault.token=00000000-0000-0000-0000-000000000000
5. Securing Secrets
We’ll create a simple Credentials class that maps to username and password:
public class Credentials {
private String username;
private String password;
// standard constructors, getters, setters
}
Now, let’s see how we can secure our Credentials object using the VaultTemplate:
Credentials credentials = new Credentials("username", "password");
VaultKeyValueOperations vaultKeyValueOperations = vaultTemplate.opsForKeyValue("credentials/myapp",
VaultKeyValueOperationsSupport.KeyValueBackend.KV_2)
vaultKeyValueOperations.put(credentials.getUsername(), credentials);
We must note that we used an instance of VaultKeyValueOperations as it supports both versioned and unversioned key-value backends.
Next, we’ll see how to access them.
6. Accessing Secrets
We can access the secured secrets using the get() method in VaultKeyValueOperations, which returns the VaultResponseSupport as a response:
VaultResponseSupport response = vaultKeyValueOperations.get(username, Credentials.class);
String username = response.getData().getUsername();
String password = response.getData().getPassword();
Our secret values are now ready.
7. Vault Repositories
Vault repository is a handy feature that comes with Spring Vault 2.0. It applies Spring Data’s repository concept on top of Vault.
Let’s dig deep to see how to use this new feature for unversioned key-value backends.
7.1. @Secret and @Id Annotations
Spring provides these two annotations to mark the objects we want to persist inside Vault.
So first, we need to decorate our domain type Credentials:
@Secret(backend = "credentials", value = "myapp")
public class Credentials {
@Id
private String username;
// Same code
]
The value attribute of the @Secret annotation serves to distinguish the domain type. The backend attribute denotes the secret backend mount.
On the other hand, @Id simply demarcates the identifier of our object.
7.2. Vault Repository
Now, let’s define a repository interface that uses our domain object Credentials:
public interface CredentialsRepository extends CrudRepository<Credentials, String> {
}
As we can see, our repository extends CrudRepository, which provides basic CRUD and query methods.
Next, let’s inject CredentialsRepository into CredentialsService and implement some CRUD methods:
public class CredentialsService {
private final VaultTemplate vaultTemplate;
private CredentialsRepository credentialsRepository;
private final VaultKeyValueOperations vaultKeyValueOperations;
@Autowired
public CredentialsService(VaultTemplate vaultTemplate, CredentialsRepository credentialsRepository) {
this.vaultTemplate = vaultTemplate;
this.credentialsRepository = credentialsRepository;
this.vaultKeyValueOperations = vaultTemplate.opsForKeyValue("credentials/myapp",
VaultKeyValueOperationsSupport.KeyValueBackend.KV_2);
}
public Credentials saveCredentials(Credentials credentials) {
return credentialsRepository.save(credentials);
}
public Optional<Credentials> findById(String username) {
return credentialsRepository.findById(username);
}
}
Now that we’ve added all the missing pieces of the puzzle, let’s confirm that everything works as excepted using test cases.
First, let’s start with a test case for the save() method:
@Test
public void givenCredentials_whenSave_thenReturnCredentials() throws InterruptedException {
Assume.assumeTrue("v1".equals(API_VERSION));
credentialsService = new CredentialsService(vaultTemplate, credentialsRepository);
// Given
Credentials credentials = new Credentials("login", "password");
// When
Credentials savedCredentials = credentialsService.saveCredentials(credentials);
// Then
assertNotNull(savedCredentials);
assertEquals(credentials.getUsername(), savedCredentials.getUsername());
assertEquals(credentials.getPassword(), savedCredentials.getPassword());
}
Lastly, let’s confirm the findById() method with a test case:
@Test
public void givenId_whenFindById_thenReturnCredentials() {
// Given
Assume.assumeTrue("v1".equals(API_VERSION));
Credentials expectedCredentials = new Credentials("login", "p@ssw@rd");
credentialsService.saveCredentials(expectedCredentials);
// When
Optional retrievedCredentials = credentialsService.findById(expectedCredentials.getUsername());
// Then
assertNotNull(retrievedCredentials);
assertNotNull(retrievedCredentials.get());
assertEquals(expectedCredentials.get().getUsername(), retrievedCredentials.getUsername());
assertEquals(expectedCredentials.getPassword(), retrievedCredentials.get().getPassword());
}
We must note that we use the CredentialsRepository only for the unversioned (“v1“) key-value backend.
8. Conclusion
In this article, we’ve learned about the basics of Spring Vault with an example showing how the Vault works in typical scenarios**.**
As usual, the source code presented here can be found over on GitHub.