Securing Secrets: A Comprehensive Guide to HashiCorp Vault Java Client

HashiCorp Vault has become the industry standard for secrets management, and its Java client provides a powerful way to integrate secret management into Java applications. This article explores the HashiCorp Vault Java client, covering everything from basic setup to advanced patterns like dynamic database credentials, PKI certificates, and transit encryption.


Why HashiCorp Vault?

Vault solves critical security challenges:

  • Secrets Management: Secure storage and access to passwords, API keys, certificates
  • Encryption as a Service: Centralized encryption/decryption without exposing keys
  • Dynamic Secrets: Short-lived credentials for databases, cloud platforms
  • Identity-Based Access: Secure access based on application identity
  • Audit Logging: Comprehensive audit trails for compliance

Setting Up Dependencies

Maven Dependencies:

<properties>
<vault.version>3.6.0</vault.version>
<spring-boot.version>3.2.0</spring-boot.version>
</properties>
<dependencies>
<!-- Vault Java Driver -->
<dependency>
<groupId>com.bettercloud</groupId>
<artifactId>vault-java-driver</artifactId>
<version>${vault.version}</version>
</dependency>
<!-- Spring Vault (Alternative) -->
<dependency>
<groupId>org.springframework.vault</groupId>
<artifactId>spring-vault-core</artifactId>
<version>3.1.0</version>
</dependency>
<!-- HTTP Client -->
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.2.1</version>
</dependency>
<!-- JSON Processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version>
</dependency>
<!-- Logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.7</version>
</dependency>
</dependencies>

Gradle:

dependencies {
implementation("com.bettercloud:vault-java-driver:3.6.0")
implementation("org.springframework.vault:spring-vault-core:3.1.0")
implementation("org.apache.httpcomponents.client5:httpclient5:5.2.1")
implementation("com.fasterxml.jackson.core:jackson-databind:2.15.2")
}

Basic Vault Client Setup

1. Simple Vault Client Configuration:

package com.example.vault.client;
import com.bettercloud.vault.Vault;
import com.bettercloud.vault.VaultConfig;
import com.bettercloud.vault.VaultException;
import com.bettercloud.vault.response.LogicalResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class BasicVaultClient {
private static final Logger logger = LoggerFactory.getLogger(BasicVaultClient.class);
private final Vault vault;
private final VaultConfig config;
public BasicVaultClient(String vaultUrl, String token) throws VaultException {
this.config = new VaultConfig()
.address(vaultUrl)
.token(token)
.engineVersion(1) // KV Secrets Engine version
.build();
this.vault = new Vault(config);
logger.info("Vault client initialized for: {}", vaultUrl);
}
public BasicVaultClient(String vaultUrl, String token, int engineVersion) throws VaultException {
this.config = new VaultConfig()
.address(vaultUrl)
.token(token)
.engineVersion(engineVersion)
.build();
this.vault = new Vault(config);
}
// Basic secret operations
public void writeSecret(String path, String key, String value) throws VaultException {
LogicalResponse response = vault.logical()
.write(path, Map.of(key, value));
if (response.getRestResponse().getStatus() == 200 || 
response.getRestResponse().getStatus() == 204) {
logger.info("Secret written successfully to: {}", path);
} else {
logger.error("Failed to write secret. Status: {}", response.getRestResponse().getStatus());
}
}
public String readSecret(String path, String key) throws VaultException {
LogicalResponse response = vault.logical().read(path);
if (response.getData() != null) {
return response.getData().get(key);
}
logger.warn("No data found at path: {}", path);
return null;
}
public Map<String, String> readAllSecrets(String path) throws VaultException {
LogicalResponse response = vault.logical().read(path);
return response.getData();
}
public void deleteSecret(String path) throws VaultException {
vault.logical().delete(path);
logger.info("Secret deleted: {}", path);
}
public static void main(String[] args) {
try {
BasicVaultClient client = new BasicVaultClient(
"http://localhost:8200", 
"s.xxxxxxxxxxxxxxxx"
);
// Write a secret
client.writeSecret("secret/myapp", "database_password", "supersecret123");
// Read the secret
String password = client.readSecret("secret/myapp", "database_password");
System.out.println("Retrieved password: " + password);
} catch (VaultException e) {
logger.error("Vault operation failed", e);
}
}
}

2. Advanced Configuration with SSL and Retries:

package com.example.vault.client;
import com.bettercloud.vault.Vault;
import com.bettercloud.vault.VaultConfig;
import com.bettercloud.vault.VaultException;
import javax.net.ssl.SSLContext;
import java.io.File;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
public class AdvancedVaultClient {
private final Vault vault;
public AdvancedVaultClient(String vaultUrl, String token, 
String keystorePath, String keystorePassword,
String truststorePath, String truststorePassword) 
throws VaultException, NoSuchAlgorithmException, KeyManagementException {
VaultConfig config = new VaultConfig()
.address(vaultUrl)
.token(token)
.engineVersion(2)
.openTimeout(5) // 5 seconds
.readTimeout(30) // 30 seconds
.sslVerification(false) // Only for development!
.sslContext(createSSLContext(keystorePath, keystorePassword, 
truststorePath, truststorePassword))
.retries(3, 1000) // 3 retries with 1 second delay
.build();
this.vault = new Vault(config);
}
public AdvancedVaultClient(String vaultUrl, String roleId, String secretId) 
throws VaultException {
VaultConfig config = new VaultConfig()
.address(vaultUrl)
.engineVersion(2)
.build();
this.vault = new Vault(config);
// Authenticate with AppRole
authenticateWithAppRole(roleId, secretId);
}
private SSLContext createSSLContext(String keystorePath, String keystorePassword,
String truststorePath, String truststorePassword) 
throws NoSuchAlgorithmException, KeyManagementException {
// Custom SSL context implementation
// This would typically load keystores and truststores
return SSLContext.getDefault();
}
private void authenticateWithAppRole(String roleId, String secretId) 
throws VaultException {
Map<String, Object> authParams = Map.of(
"role_id", roleId,
"secret_id", secretId
);
var response = vault.logical()
.write("auth/approle/login", authParams);
if (response.getAuth() != null) {
String clientToken = response.getAuth().getClientToken();
// Reconfigure vault with new token
// In practice, you'd need to recreate the Vault instance
}
}
}

Spring Boot Integration

1. Spring Vault Configuration:

package com.example.vault.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.vault.authentication.ClientAuthentication;
import org.springframework.vault.authentication.TokenAuthentication;
import org.springframework.vault.client.VaultEndpoint;
import org.springframework.vault.config.AbstractVaultConfiguration;
import org.springframework.vault.core.VaultTemplate;
import java.net.URI;
@Configuration
public class VaultConfig extends AbstractVaultConfiguration {
@Value("${vault.uri:http://localhost:8200}")
private String vaultUri;
@Value("${vault.token}")
private String vaultToken;
@Override
public VaultEndpoint vaultEndpoint() {
try {
URI uri = URI.create(vaultUri);
return VaultEndpoint.from(uri);
} catch (Exception e) {
return VaultEndpoint.create("localhost", 8200);
}
}
@Override
public ClientAuthentication clientAuthentication() {
return new TokenAuthentication(vaultToken);
}
}

2. Spring Vault Service:

package com.example.vault.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.vault.core.VaultTemplate;
import org.springframework.vault.support.VaultResponse;
import org.springframework.vault.support.VaultResponseSupport;
import java.util.Map;
@Service
public class SpringVaultService {
private final VaultTemplate vaultTemplate;
@Autowired
public SpringVaultService(VaultTemplate vaultTemplate) {
this.vaultTemplate = vaultTemplate;
}
public void writeSecret(String path, Object secrets) {
vaultTemplate.write(path, secrets);
}
public <T> T readSecret(String path, Class<T> type) {
VaultResponseSupport<T> response = vaultTemplate.read(path, type);
return response != null ? response.getData() : null;
}
public Map<String, Object> readSecret(String path) {
VaultResponse response = vaultTemplate.read(path);
return response != null ? response.getData() : null;
}
public void deleteSecret(String path) {
vaultTemplate.delete(path);
}
// KV v2 specific operations
public void writeSecretV2(String path, String key, Object value) {
String fullPath = "secret/data/" + path;
Map<String, Object> data = Map.of("data", Map.of(key, value));
vaultTemplate.write(fullPath, data);
}
public Object readSecretV2(String path, String key) {
String fullPath = "secret/data/" + path;
VaultResponse response = vaultTemplate.read(fullPath);
if (response != null && response.getData() != null) {
Map<String, Object> data = (Map<String, Object>) response.getData().get("data");
return data != null ? data.get(key) : null;
}
return null;
}
}

Advanced Vault Operations

1. Dynamic Database Credentials:

package com.example.vault.advanced;
import com.bettercloud.vault.Vault;
import com.bettercloud.vault.VaultException;
import com.bettercloud.vault.response.LookupResponse;
import com.bettercloud.vault.response.AuthResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Map;
public class DatabaseCredentialsManager {
private static final Logger logger = LoggerFactory.getLogger(DatabaseCredentialsManager.class);
private final Vault vault;
private final String databaseRole;
private DatabaseCredentials currentCredentials;
private long credentialsExpiry;
public DatabaseCredentialsManager(Vault vault, String databaseRole) {
this.vault = vault;
this.databaseRole = databaseRole;
}
public DatabaseCredentials getDatabaseCredentials() throws VaultException {
// Check if current credentials are still valid
if (currentCredentials != null && System.currentTimeMillis() < credentialsExpiry - 30000) {
// Return cached credentials with 30-second buffer
return currentCredentials;
}
// Fetch new credentials
var response = vault.logical()
.read("database/creds/" + databaseRole);
if (response.getData() != null) {
currentCredentials = new DatabaseCredentials(
response.getData().get("username"),
response.getData().get("password"),
response.getLeaseDuration()
);
credentialsExpiry = System.currentTimeMillis() + (response.getLeaseDuration() * 1000);
logger.info("Generated new database credentials for: {}", 
currentCredentials.getUsername());
return currentCredentials;
}
throw new VaultException("Failed to generate database credentials");
}
public void renewCredentials() throws VaultException {
if (currentCredentials != null) {
vault.sys().renew(currentCredentials.getLeaseId());
logger.info("Renewed database credentials lease");
}
}
public void revokeCredentials() throws VaultException {
if (currentCredentials != null) {
vault.sys().revoke(currentCredentials.getLeaseId());
currentCredentials = null;
logger.info("Revoked database credentials");
}
}
public static class DatabaseCredentials {
private final String username;
private final String password;
private final int leaseDuration;
private final String leaseId;
private final long createdAt;
public DatabaseCredentials(String username, String password, int leaseDuration) {
this.username = username;
this.password = password;
this.leaseDuration = leaseDuration;
this.leaseId = "database/creds/" + username;
this.createdAt = System.currentTimeMillis();
}
// Getters
public String getUsername() { return username; }
public String getPassword() { return password; }
public int getLeaseDuration() { return leaseDuration; }
public String getLeaseId() { return leaseId; }
public long getCreatedAt() { return createdAt; }
public boolean isExpired() {
return System.currentTimeMillis() > createdAt + (leaseDuration * 1000);
}
}
}

2. Transit Encryption Service:

package com.example.vault.advanced;
import com.bettercloud.vault.Vault;
import com.bettercloud.vault.VaultException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Base64;
import java.util.Map;
public class TransitService {
private static final Logger logger = LoggerFactory.getLogger(TransitService.class);
private final Vault vault;
private final String keyName;
public TransitService(Vault vault, String keyName) {
this.vault = vault;
this.keyName = keyName;
}
public String encrypt(String plaintext) throws VaultException {
try {
var response = vault.logical()
.write("transit/encrypt/" + keyName, 
Map.of("plaintext", Base64.getEncoder().encodeToString(plaintext.getBytes())));
if (response.getData() != null) {
return response.getData().get("ciphertext");
}
throw new VaultException("Encryption failed: No data in response");
} catch (VaultException e) {
logger.error("Encryption failed for key: {}", keyName, e);
throw e;
}
}
public String decrypt(String ciphertext) throws VaultException {
try {
var response = vault.logical()
.write("transit/decrypt/" + keyName, 
Map.of("ciphertext", ciphertext));
if (response.getData() != null) {
String base64Plaintext = response.getData().get("plaintext");
return new String(Base64.getDecoder().decode(base64Plaintext));
}
throw new VaultException("Decryption failed: No data in response");
} catch (VaultException e) {
logger.error("Decryption failed for ciphertext", e);
throw e;
}
}
public String rewrap(String ciphertext) throws VaultException {
var response = vault.logical()
.write("transit/rewrap/" + keyName, 
Map.of("ciphertext", ciphertext));
return response.getData().get("ciphertext");
}
public void rotateKey() throws VaultException {
vault.logical()
.write("transit/keys/" + keyName + "/rotate", Map.of());
logger.info("Rotated transit key: {}", keyName);
}
public byte[] generateDataKey(boolean derived) throws VaultException {
Map<String, Object> params = Map.of(
"derived", derived,
"context", Base64.getEncoder().encodeToString("app-context".getBytes())
);
var response = vault.logical()
.write("transit/datakey/plaintext/" + keyName, params);
if (response.getData() != null) {
String base64Plaintext = response.getData().get("plaintext");
return Base64.getDecoder().decode(base64Plaintext);
}
throw new VaultException("Data key generation failed");
}
}

3. PKI Certificate Management:

package com.example.vault.advanced;
import com.bettercloud.vault.Vault;
import com.bettercloud.vault.VaultException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Map;
public class PKIService {
private static final Logger logger = LoggerFactory.getLogger(PKIService.class);
private final Vault vault;
private final String pkiPath;
public PKIService(Vault vault, String pkiPath) {
this.vault = vault;
this.pkiPath = pkiPath;
}
public CertificateBundle generateCertificate(String commonName, int ttlHours) 
throws VaultException {
Map<String, Object> params = Map.of(
"common_name", commonName,
"ttl", ttlHours + "h"
);
var response = vault.logical()
.write(pkiPath + "/issue/internal", params);
if (response.getData() != null) {
return new CertificateBundle(
response.getData().get("certificate"),
response.getData().get("private_key"),
response.getData().get("issuing_ca"),
response.getLeaseDuration()
);
}
throw new VaultException("Certificate generation failed");
}
public void revokeCertificate(String serialNumber) throws VaultException {
vault.logical()
.write(pkiPath + "/revoke", 
Map.of("serial_number", serialNumber));
logger.info("Revoked certificate: {}", serialNumber);
}
public String getCAChain() throws VaultException {
var response = vault.logical()
.read(pkiPath + "/ca_chain");
return response.getData().get("certificate");
}
public static class CertificateBundle {
private final String certificate;
private final String privateKey;
private final String issuingCa;
private final int leaseDuration;
public CertificateBundle(String certificate, String privateKey, 
String issuingCa, int leaseDuration) {
this.certificate = certificate;
this.privateKey = privateKey;
this.issuingCa = issuingCa;
this.leaseDuration = leaseDuration;
}
// Getters
public String getCertificate() { return certificate; }
public String getPrivateKey() { return privateKey; }
public String getIssuingCa() { return issuingCa; }
public int getLeaseDuration() { return leaseDuration; }
}
}

Authentication Strategies

1. AppRole Authentication:

package com.example.vault.auth;
import com.bettercloud.vault.Vault;
import com.bettercloud.vault.VaultConfig;
import com.bettercloud.vault.VaultException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Map;
public class AppRoleAuthenticator {
private static final Logger logger = LoggerFactory.getLogger(AppRoleAuthenticator.class);
private final String vaultUrl;
private final String roleId;
private final String secretId;
private Vault vault;
private String currentToken;
private long tokenExpiry;
public AppRoleAuthenticator(String vaultUrl, String roleId, String secretId) {
this.vaultUrl = vaultUrl;
this.roleId = roleId;
this.secretId = secretId;
}
public Vault authenticate() throws VaultException {
// Initial configuration without token
VaultConfig config = new VaultConfig()
.address(vaultUrl)
.engineVersion(2)
.build();
Vault tempVault = new Vault(config);
// Authenticate with AppRole
Map<String, Object> authParams = Map.of(
"role_id", roleId,
"secret_id", secretId
);
var response = tempVault.logical()
.write("auth/approle/login", authParams);
if (response.getAuth() != null) {
currentToken = response.getAuth().getClientToken();
tokenExpiry = System.currentTimeMillis() + 
(response.getAuth().getLeaseDuration() * 1000);
// Create new Vault instance with the token
config = new VaultConfig()
.address(vaultUrl)
.token(currentToken)
.engineVersion(2)
.build();
vault = new Vault(config);
logger.info("AppRole authentication successful");
return vault;
}
throw new VaultException("AppRole authentication failed");
}
public void renewToken() throws VaultException {
if (vault != null && currentToken != null) {
vault.auth().renewSelf();
logger.info("Token renewed successfully");
}
}
public boolean isTokenExpired() {
return System.currentTimeMillis() >= tokenExpiry - 30000; // 30-second buffer
}
public Vault getVault() throws VaultException {
if (vault == null || isTokenExpired()) {
return authenticate();
}
return vault;
}
}

2. Kubernetes Authentication:

package com.example.vault.auth;
import com.bettercloud.vault.Vault;
import com.bettercloud.vault.VaultConfig;
import com.bettercloud.vault.VaultException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Map;
public class KubernetesAuthenticator {
private static final Logger logger = LoggerFactory.getLogger(KubernetesAuthenticator.class);
private final String vaultUrl;
private final String role;
private final String jwtPath;
public KubernetesAuthenticator(String vaultUrl, String role) {
this.vaultUrl = vaultUrl;
this.role = role;
this.jwtPath = "/var/run/secrets/kubernetes.io/serviceaccount/token";
}
public KubernetesAuthenticator(String vaultUrl, String role, String jwtPath) {
this.vaultUrl = vaultUrl;
this.role = role;
this.jwtPath = jwtPath;
}
public Vault authenticate() throws VaultException {
try {
String jwt = readServiceAccountToken();
VaultConfig config = new VaultConfig()
.address(vaultUrl)
.engineVersion(2)
.build();
Vault tempVault = new Vault(config);
Map<String, Object> authParams = Map.of(
"role", role,
"jwt", jwt
);
var response = tempVault.logical()
.write("auth/kubernetes/login", authParams);
if (response.getAuth() != null) {
String token = response.getAuth().getClientToken();
config = new VaultConfig()
.address(vaultUrl)
.token(token)
.engineVersion(2)
.build();
logger.info("Kubernetes authentication successful for role: {}", role);
return new Vault(config);
}
throw new VaultException("Kubernetes authentication failed");
} catch (IOException e) {
throw new VaultException("Failed to read service account token", e);
}
}
private String readServiceAccountToken() throws IOException {
return new String(Files.readAllBytes(Paths.get(jwtPath)));
}
}

Enterprise Features

1. Namespace Support:

package com.example.vault.enterprise;
import com.bettercloud.vault.Vault;
import com.bettercloud.vault.VaultConfig;
import com.bettercloud.vault.VaultException;
public class NamespaceAwareVaultClient {
private final Vault vault;
private final String namespace;
public NamespaceAwareVaultClient(String vaultUrl, String token, String namespace) 
throws VaultException {
this.namespace = namespace;
VaultConfig config = new VaultConfig()
.address(vaultUrl)
.token(token)
.nameSpace(namespace)
.engineVersion(2)
.build();
this.vault = new Vault(config);
}
public void switchNamespace(String newNamespace) throws VaultException {
// Note: Vault Java driver doesn't support dynamic namespace switching
// You would need to create a new Vault instance
throw new UnsupportedOperationException(
"Namespace switching requires creating a new Vault instance");
}
public String getNamespace() {
return namespace;
}
// Delegate methods to the vault instance
public String readSecret(String path, String key) throws VaultException {
var response = vault.logical().read(path);
return response.getData() != null ? response.getData().get(key) : null;
}
}

2. Vault Agent Integration:

package com.example.vault.enterprise;
import com.bettercloud.vault.Vault;
import com.bettercloud.vault.VaultConfig;
import com.bettercloud.vault.VaultException;
public class VaultAgentClient {
private final Vault vault;
public VaultAgentClient() throws VaultException {
// Vault Agent typically runs on localhost:8200
VaultConfig config = new VaultConfig()
.address("http://localhost:8200")
.token("") // Agent handles authentication
.engineVersion(2)
.build();
this.vault = new Vault(config);
}
// The agent automatically handles token renewal and authentication
public String readFromAgent(String path, String key) throws VaultException {
var response = vault.logical().read(path);
return response.getData() != null ? response.getData().get(key) : null;
}
}

Error Handling and Retry Logic

Robust Vault Client with Circuit Breaker:

package com.example.vault.resilience;
import com.bettercloud.vault.Vault;
import com.bettercloud.vault.VaultException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
public class ResilientVaultClient {
private static final Logger logger = LoggerFactory.getLogger(ResilientVaultClient.class);
private final Vault vault;
private final AtomicInteger consecutiveFailures = new AtomicInteger(0);
private final AtomicLong circuitOpenTime = new AtomicLong(0);
private final int maxFailures = 5;
private final long circuitTimeout = 30000; // 30 seconds
public ResilientVaultClient(Vault vault) {
this.vault = vault;
}
public String readSecretWithRetry(String path, String key, int maxRetries) {
for (int attempt = 0; attempt <= maxRetries; attempt++) {
if (isCircuitOpen()) {
throw new VaultRuntimeException("Circuit breaker is open");
}
try {
var response = vault.logical().read(path);
consecutiveFailures.set(0); // Reset on success
if (response.getData() != null) {
return response.getData().get(key);
}
return null;
} catch (VaultException e) {
consecutiveFailures.incrementAndGet();
logger.warn("Vault operation failed (attempt {}/{}): {}", 
attempt + 1, maxRetries + 1, e.getMessage());
if (consecutiveFailures.get() >= maxFailures) {
circuitOpenTime.set(System.currentTimeMillis());
logger.error("Circuit breaker opened due to {} consecutive failures", 
consecutiveFailures.get());
}
if (attempt < maxRetries) {
exponentialBackoff(attempt);
}
}
}
throw new VaultRuntimeException("All retry attempts failed");
}
private boolean isCircuitOpen() {
long openTime = circuitOpenTime.get();
if (openTime == 0) return false;
if (System.currentTimeMillis() - openTime > circuitTimeout) {
// Try to close the circuit
circuitOpenTime.compareAndSet(openTime, 0);
consecutiveFailures.set(0);
return false;
}
return true;
}
private void exponentialBackoff(int attempt) {
try {
long delay = Math.min(1000 * (1L << attempt), 30000); // Max 30 seconds
Thread.sleep(delay);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new VaultRuntimeException("Operation interrupted", e);
}
}
public static class VaultRuntimeException extends RuntimeException {
public VaultRuntimeException(String message) {
super(message);
}
public VaultRuntimeException(String message, Throwable cause) {
super(message, cause);
}
}
}

Monitoring and Health Checks

Vault Health Monitor:

package com.example.vault.monitoring;
import com.bettercloud.vault.Vault;
import com.bettercloud.vault.VaultException;
import com.bettercloud.vault.response.HealthResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
public class VaultHealthMonitor {
private static final Logger logger = LoggerFactory.getLogger(VaultHealthMonitor.class);
private final Vault vault;
private final ScheduledExecutorService scheduler;
private final AtomicBoolean isHealthy = new AtomicBoolean(true);
private final long checkIntervalSeconds;
public VaultHealthMonitor(Vault vault, long checkIntervalSeconds) {
this.vault = vault;
this.checkIntervalSeconds = checkIntervalSeconds;
this.scheduler = Executors.newSingleThreadScheduledExecutor();
}
public void start() {
scheduler.scheduleAtFixedRate(this::checkHealth, 0, checkIntervalSeconds, TimeUnit.SECONDS);
logger.info("Vault health monitor started with {} second interval", checkIntervalSeconds);
}
public void stop() {
scheduler.shutdown();
logger.info("Vault health monitor stopped");
}
private void checkHealth() {
try {
HealthResponse response = vault.sys().health();
boolean healthy = response.getHttpStatusCode() == 200;
if (isHealthy.compareAndSet(!healthy, healthy)) {
if (healthy) {
logger.info("Vault cluster is healthy");
} else {
logger.warn("Vault cluster is unhealthy. Status: {}", response.getHttpStatusCode());
}
}
} catch (VaultException e) {
if (isHealthy.compareAndSet(true, false)) {
logger.error("Vault health check failed", e);
}
}
}
public boolean isHealthy() {
return isHealthy.get();
}
}

Best Practices

1. Configuration Management:

package com.example.vault.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "vault")
public class VaultProperties {
private String url = "http://localhost:8200";
private String token;
private String namespace;
private String kvVersion = "2";
private int timeout = 30;
private int retries = 3;
private SSL ssl = new SSL();
// Getters and setters
public static class SSL {
private boolean verify = true;
private String keystore;
private String keystorePassword;
private String truststore;
private String truststorePassword;
// Getters and setters
}
}

2. Secret Caching with Refresh:

package com.example.vault.cache;
import com.bettercloud.vault.Vault;
import com.bettercloud.vault.VaultException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class SecretCache {
private static final Logger logger = LoggerFactory.getLogger(SecretCache.class);
private final Vault vault;
private final Map<String, CachedSecret> cache = new ConcurrentHashMap<>();
private final ScheduledExecutorService scheduler;
private final long defaultRefreshInterval;
public SecretCache(Vault vault, long defaultRefreshInterval) {
this.vault = vault;
this.defaultRefreshInterval = defaultRefreshInterval;
this.scheduler = Executors.newScheduledThreadPool(2);
}
public Map<String, String> getSecret(String path, long refreshInterval) {
return cache.compute(path, (key, cachedSecret) -> {
if (cachedSecret == null || cachedSecret.isStale()) {
try {
var response = vault.logical().read(path);
Map<String, String> data = response.getData();
if (data != null) {
long expiry = System.currentTimeMillis() + refreshInterval;
cachedSecret = new CachedSecret(data, expiry);
scheduleRefresh(path, refreshInterval);
logger.debug("Refreshed secret cache for: {}", path);
}
} catch (VaultException e) {
logger.error("Failed to refresh secret: {}", path, e);
// Return stale data if available
if (cachedSecret != null) {
return cachedSecret;
}
throw new RuntimeException("Failed to fetch secret: " + path, e);
}
}
return cachedSecret;
}).getData();
}
private void scheduleRefresh(String path, long refreshInterval) {
scheduler.schedule(() -> {
getSecret(path, refreshInterval); // Refresh the cache
}, refreshInterval, TimeUnit.MILLISECONDS);
}
public void invalidate(String path) {
cache.remove(path);
logger.debug("Invalidated cache for: {}", path);
}
public void shutdown() {
scheduler.shutdown();
}
private static class CachedSecret {
private final Map<String, String> data;
private final long expiry;
CachedSecret(Map<String, String> data, long expiry) {
this.data = data;
this.expiry = expiry;
}
boolean isStale() {
return System.currentTimeMillis() >= expiry;
}
Map<String, String> getData() {
return data;
}
}
}

Conclusion

The HashiCorp Vault Java client provides powerful capabilities for integrating secrets management into Java applications:

Key Integration Patterns:

  1. Basic Secrets Management: Secure storage and retrieval of static secrets
  2. Dynamic Secrets: Short-lived credentials for databases and cloud services
  3. Transit Encryption: Centralized encryption-as-a-service
  4. PKI Management: Dynamic certificate generation and management
  5. Authentication: Multiple auth methods (AppRole, Kubernetes, tokens)

Best Practices:

  • Security: Use appropriate authentication methods for your environment
  • Resilience: Implement retry logic and circuit breakers
  • Performance: Cache secrets appropriately with refresh mechanisms
  • Monitoring: Implement health checks and comprehensive logging
  • Error Handling: Graceful degradation when Vault is unavailable

Production Considerations:

  • Use Vault namespaces for multi-tenancy
  • Implement proper secret rotation strategies
  • Set up comprehensive audit logging
  • Use Vault Agent for automatic token renewal
  • Plan for high availability and disaster recovery

By leveraging the HashiCorp Vault Java client effectively, you can build secure, resilient applications that properly manage secrets and maintain compliance with security best practices.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper