HashiCorp Vault Injector automatically injects secrets into Kubernetes pods using mutating webhooks. This guide covers Java integration for secure secrets retrieval, dynamic credentials, and sidecar patterns.
Core Concepts
What is Vault Injector?
- Kubernetes mutating webhook that injects Vault sidecars and init containers
- Automatic secrets injection into pods via volumes or environment variables
- Dynamic database credentials, PKI certificates, and cloud credentials
- Zero-trust secrets management
Key Benefits:
- Automatic Secrets Injection: No code changes required for basic secrets
- Dynamic Credentials: Short-lived credentials with automatic rotation
- Centralized Management: Single source of truth for all secrets
- Audit Logging: Comprehensive audit trails for secrets access
Architecture Overview
Java Application Pod ├── Init Container (vault-agent) ├── Sidecar Container (vault-agent) ├── Application Container │ ├── Secrets Volume (/vault/secrets) │ └── Environment Variables └── Vault Injector Webhook
Dependencies and Setup
Maven Dependencies
<properties>
<spring-boot.version>3.1.0</spring-boot.version>
<vault-java-driver.version>3.1.1</vault-java-driver.version>
<kubernetes-client.version>6.7.2</kubernetes-client.version>
</properties>
<dependencies>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<!-- Vault Java Driver -->
<dependency>
<groupId>org.springframework.vault</groupId>
<artifactId>spring-vault-core</artifactId>
<version>${vault-java-driver.version}</version>
</dependency>
<!-- Kubernetes Client -->
<dependency>
<groupId>io.fabric8</groupId>
<artifactId>kubernetes-client</artifactId>
<version>${kubernetes-client.version}</version>
</dependency>
<!-- Database -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${spring-boot.version}</version>
<scope>test</scope>
</dependency>
<!-- Vault Test Containers -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>vault</artifactId>
<version>1.18.3</version>
<scope>test</scope>
</dependency>
</dependencies>
Kubernetes Manifest with Vault Injection
# k8s/deployment-with-vault.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: java-app
labels:
app: java-app
annotations:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: "java-app"
vault.hashicorp.com/agent-inject-secret-database: "database/creds/java-app-role"
vault.hashicorp.com/agent-inject-template-database: |
{{- with secret "database/creds/java-app-role" -}}
postgresql://{{ .Data.username }}:{{ .Data.password }}@postgresql:5432/myapp
{{- end }}
vault.hashicorp.com/agent-inject-secret-api-key: "kv/data/app/secrets"
vault.hashicorp.com/agent-inject-template-api-key: |
{{- with secret "kv/data/app/secrets" -}}
{{ .Data.data.api_key }}
{{- end }}
vault.hashicorp.com/agent-inject-status: "update"
spec:
replicas: 3
selector:
matchLabels:
app: java-app
template:
metadata:
labels:
app: java-app
annotations:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: "java-app"
vault.hashicorp.com/agent-inject-secret-database: "database/creds/java-app-role"
vault.hashicorp.com/agent-inject-secret-api-key: "kv/data/app/secrets"
vault.hashicorp.com/agent-inject-secret-tls-cert: "pki/issue/java-app"
vault.hashicorp.com/tls-secret: "tls-cert"
spec:
serviceAccountName: java-app
containers:
- name: java-app
image: java-app:latest
ports:
- containerPort: 8080
env:
- name: SPRING_PROFILES_ACTIVE
value: "kubernetes,vault"
- name: VAULT_ADDR
value: "http://vault:8200"
- name: DATABASE_URL_FILE
value: "/vault/secrets/database"
- name: API_KEY_FILE
value: "/vault/secrets/api-key"
volumeMounts:
- name: vault-secrets
mountPath: /vault/secrets
readOnly: true
- name: tls-certs
mountPath: /etc/tls
readOnly: true
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "1Gi"
cpu: "500m"
livenessProbe:
httpGet:
path: /actuator/health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
volumes:
- name: vault-secrets
emptyDir:
medium: Memory
- name: tls-certs
secret:
secretName: tls-cert
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: java-app
annotations:
vault.hashicorp.com/service-account: "java-app"
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: java-app-vault-binding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: system:auth-delegator
subjects:
- kind: ServiceAccount
name: java-app
namespace: default
Spring Boot Configuration
1. Application Properties
# application-vault.yml
spring:
application:
name: java-app
vault:
uri: http://vault:8200
authentication: KUBERNETES
kubernetes:
role: java-app
service-account-token-file: /var/run/secrets/kubernetes.io/serviceaccount/token
kv:
enabled: true
backend: kv
default-context: app/secrets
ssl:
trust-store: /etc/tls/tls.crt
key-store: /etc/tls/tls.p12
app:
vault:
enabled: true
secrets-path: /vault/secrets
auto-reload: true
reload-interval: 300000 # 5 minutes
database:
url-file: ${DATABASE_URL_FILE:/vault/secrets/database}
security:
api-key-file: ${API_KEY_FILE:/vault/secrets/api-key}
management:
endpoints:
web:
exposure:
include: health,info,metrics,vault
endpoint:
vault:
enabled: true
health:
vault:
enabled: true
logging:
level:
org.springframework.vault: DEBUG
com.example.vault: INFO
2. Vault Configuration Class
@Configuration
@ConfigurationProperties(prefix = "app.vault")
@Data
@Validated
public class VaultConfig {
private boolean enabled = true;
private String secretsPath = "/vault/secrets";
private boolean autoReload = true;
private long reloadInterval = 300000; // 5 minutes
private Map<String, String> secretFiles = new HashMap<>();
// Default secret file mappings
public VaultConfig() {
secretFiles.put("database", "database");
secretFiles.put("api-key", "api-key");
secretFiles.put("tls-cert", "tls-cert");
}
}
@Configuration
@EnableConfigurationProperties(VaultConfig.class)
public class VaultAutoConfiguration {
@Bean
@ConditionalOnProperty(name = "app.vault.enabled", havingValue = "true")
public VaultTemplate vaultTemplate(VaultEndpoint vaultEndpoint,
ClientAuthentication clientAuthentication) {
return new VaultTemplate(vaultEndpoint, clientAuthentication);
}
@Bean
@ConditionalOnProperty(name = "app.vault.enabled", havingValue = "true")
public KubernetesAuthentication kubernetesAuthentication(
@Value("${spring.vault.kubernetes.role}") String role,
@Value("${spring.vault.kubernetes.service-account-token-file}") String serviceAccountTokenFile) {
return new KubernetesAuthentication(role, new File(serviceAccountTokenFile));
}
}
Core Vault Integration
1. Secrets Manager
@Service
@Slf4j
public class VaultSecretsManager {
private final VaultOperations vaultOperations;
private final VaultConfig vaultConfig;
private final Map<String, String> secretsCache = new ConcurrentHashMap<>();
private final ScheduledExecutorService reloadExecutor;
public VaultSecretsManager(VaultOperations vaultOperations,
VaultConfig vaultConfig) {
this.vaultOperations = vaultOperations;
this.vaultConfig = vaultConfig;
this.reloadExecutor = Executors.newSingleThreadScheduledExecutor(
r -> new Thread(r, "vault-secrets-reloader"));
if (vaultConfig.isAutoReload()) {
startSecretsReloader();
}
}
@PostConstruct
public void initializeSecrets() {
log.info("Initializing Vault secrets from path: {}", vaultConfig.getSecretsPath());
// Load secrets from files (injected by Vault)
loadSecretsFromFiles();
// Load additional secrets directly from Vault
loadSecretsFromVault();
}
public String getSecret(String secretName) {
return secretsCache.get(secretName);
}
public String getSecret(String secretPath, String secretKey) {
String cacheKey = secretPath + ":" + secretKey;
return secretsCache.computeIfAbsent(cacheKey, key -> {
try {
VaultResponseSupport<Map<String, Object>> response =
vaultOperations.read(secretPath, Map.class);
if (response != null && response.getData() != null) {
Object value = response.getData().get(secretKey);
return value != null ? value.toString() : null;
}
} catch (Exception e) {
log.warn("Failed to read secret from Vault: {}/{}", secretPath, secretKey, e);
}
return null;
});
}
public <T> T getSecret(String secretPath, Class<T> type) {
try {
VaultResponseSupport<T> response = vaultOperations.read(secretPath, type);
return response != null ? response.getData() : null;
} catch (Exception e) {
log.warn("Failed to read secret from Vault: {}", secretPath, e);
return null;
}
}
public DatabaseCredentials getDatabaseCredentials(String role) {
String path = "database/creds/" + role;
return getSecret(path, DatabaseCredentials.class);
}
public void renewLease(String leaseId) {
try {
vaultOperations.doWithSession(restOperations -> {
restOperations.postForObject("sys/leases/renew",
Map.of("lease_id", leaseId), Map.class);
return null;
});
log.debug("Renewed lease: {}", leaseId);
} catch (Exception e) {
log.warn("Failed to renew lease: {}", leaseId, e);
}
}
public void revokeLease(String leaseId) {
try {
vaultOperations.doWithSession(restOperations -> {
restOperations.postForObject("sys/leases/revoke",
Map.of("lease_id", leaseId), Map.class);
return null;
});
log.info("Revoked lease: {}", leaseId);
} catch (Exception e) {
log.warn("Failed to revoke lease: {}", leaseId, e);
}
}
public VaultHealth getVaultHealth() {
try {
return vaultOperations.doWithSession(restOperations -> {
ResponseEntity<Map> response = restOperations.getForEntity("sys/health", Map.class);
if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
Map<String, Object> healthData = response.getBody();
return new VaultHealth(
(Boolean) healthData.get("initialized"),
(Boolean) healthData.get("sealed"),
(Boolean) healthData.get("standby"),
(Integer) healthData.get("server_time_utc")
);
}
return new VaultHealth(false, true, false, 0);
});
} catch (Exception e) {
log.warn("Failed to get Vault health", e);
return new VaultHealth(false, true, false, 0);
}
}
private void loadSecretsFromFiles() {
File secretsDir = new File(vaultConfig.getSecretsPath());
if (!secretsDir.exists() || !secretsDir.isDirectory()) {
log.warn("Vault secrets directory does not exist: {}", vaultConfig.getSecretsPath());
return;
}
File[] secretFiles = secretsDir.listFiles();
if (secretFiles != null) {
for (File secretFile : secretFiles) {
if (secretFile.isFile() && secretFile.canRead()) {
try {
String secretName = secretFile.getName();
String secretValue = Files.readString(secretFile.toPath()).trim();
secretsCache.put(secretName, secretValue);
log.info("Loaded secret from file: {}", secretName);
} catch (IOException e) {
log.warn("Failed to read secret file: {}", secretFile.getName(), e);
}
}
}
}
}
private void loadSecretsFromVault() {
// Load additional secrets that aren't injected as files
vaultConfig.getSecretFiles().forEach((key, path) -> {
if (!secretsCache.containsKey(key)) {
String value = getSecret(path, "value");
if (value != null) {
secretsCache.put(key, value);
log.info("Loaded secret from Vault: {} -> {}", key, path);
}
}
});
}
private void startSecretsReloader() {
reloadExecutor.scheduleAtFixedRate(() -> {
try {
log.debug("Reloading secrets from Vault");
loadSecretsFromFiles();
loadSecretsFromVault();
} catch (Exception e) {
log.error("Failed to reload secrets", e);
}
}, vaultConfig.getReloadInterval(), vaultConfig.getReloadInterval(), TimeUnit.MILLISECONDS);
}
@PreDestroy
public void shutdown() {
log.info("Shutting down Vault secrets manager");
reloadExecutor.shutdown();
try {
if (!reloadExecutor.awaitTermination(10, TimeUnit.SECONDS)) {
reloadExecutor.shutdownNow();
}
} catch (InterruptedException e) {
reloadExecutor.shutdownNow();
Thread.currentThread().interrupt();
}
}
}
@Data
class DatabaseCredentials {
private String username;
private String password;
private String lease_id;
private String lease_duration;
private boolean renewable;
}
@Data
class VaultHealth {
private final boolean initialized;
private final boolean sealed;
private final boolean standby;
private final long serverTimeUtc;
public boolean isHealthy() {
return initialized && !sealed;
}
}
2. Dynamic Database Configuration
@Service
@Slf4j
public class VaultDatabaseConfig {
private final VaultSecretsManager secretsManager;
private final VaultConfig vaultConfig;
private volatile String databaseUrl;
private volatile String databaseUsername;
private volatile String databasePassword;
private volatile String leaseId;
private final ScheduledExecutorService credentialRenewalExecutor;
public VaultDatabaseConfig(VaultSecretsManager secretsManager,
VaultConfig vaultConfig) {
this.secretsManager = secretsManager;
this.vaultConfig = vaultConfig;
this.credentialRenewalExecutor = Executors.newSingleThreadScheduledExecutor(
r -> new Thread(r, "db-credential-renewal"));
initializeDatabaseCredentials();
}
@PostConstruct
public void initializeDatabaseCredentials() {
try {
// First, try to read from file (injected by Vault)
String databaseSecretFile = vaultConfig.getSecretFiles().get("database");
if (databaseSecretFile != null) {
loadCredentialsFromFile();
} else {
// Fall back to dynamic credentials
loadDynamicCredentials();
}
// Start credential renewal if using dynamic credentials
if (leaseId != null) {
startCredentialRenewal();
}
} catch (Exception e) {
log.error("Failed to initialize database credentials from Vault", e);
throw new IllegalStateException("Database credentials initialization failed", e);
}
}
public String getDatabaseUrl() {
return databaseUrl;
}
public String getDatabaseUsername() {
return databaseUsername;
}
public String getDatabasePassword() {
return databasePassword;
}
private void loadCredentialsFromFile() {
try {
String databaseConfig = secretsManager.getSecret("database");
if (databaseConfig != null) {
// Parse the database URL format: postgresql://username:password@host:port/database
Pattern pattern = Pattern.compile("postgresql://([^:]+):([^@]+)@([^:]+):(\\d+)/(.+)");
Matcher matcher = pattern.matcher(databaseConfig);
if (matcher.matches()) {
databaseUsername = matcher.group(1);
databasePassword = matcher.group(2);
String host = matcher.group(3);
String port = matcher.group(4);
String database = matcher.group(5);
databaseUrl = String.format("jdbc:postgresql://%s:%s/%s", host, port, database);
log.info("Loaded database credentials from Vault file for user: {}", databaseUsername);
} else {
log.warn("Failed to parse database URL from Vault file");
}
}
} catch (Exception e) {
log.error("Failed to load database credentials from file", e);
}
}
private void loadDynamicCredentials() {
try {
DatabaseCredentials credentials = secretsManager.getDatabaseCredentials("java-app-role");
if (credentials != null) {
databaseUsername = credentials.getUsername();
databasePassword = credentials.getPassword();
leaseId = credentials.getLease_id();
// Construct database URL (you might want to get this from config)
databaseUrl = "jdbc:postgresql://postgresql:5432/myapp";
log.info("Loaded dynamic database credentials for user: {} (lease: {})",
databaseUsername, leaseId);
} else {
throw new IllegalStateException("Failed to get database credentials from Vault");
}
} catch (Exception e) {
log.error("Failed to load dynamic database credentials", e);
throw new IllegalStateException("Dynamic credentials unavailable", e);
}
}
private void startCredentialRenewal() {
// Renew credentials at half the lease duration
long leaseDuration = 3600; // Default 1 hour, in practice get from credentials
long renewalInterval = leaseDuration / 2 * 1000; // Convert to milliseconds
credentialRenewalExecutor.scheduleAtFixedRate(() -> {
try {
log.debug("Renewing database credentials lease: {}", leaseId);
secretsManager.renewLease(leaseId);
} catch (Exception e) {
log.error("Failed to renew database credentials lease: {}", leaseId, e);
// Try to get new credentials
loadDynamicCredentials();
}
}, renewalInterval, renewalInterval, TimeUnit.MILLISECONDS);
}
@PreDestroy
public void cleanup() {
log.info("Cleaning up database credentials");
credentialRenewalExecutor.shutdown();
// Revoke the lease if we're using dynamic credentials
if (leaseId != null) {
try {
secretsManager.revokeLease(leaseId);
log.info("Revoked database credentials lease: {}", leaseId);
} catch (Exception e) {
log.warn("Failed to revoke database credentials lease: {}", leaseId, e);
}
}
}
}
3. Spring DataSource Configuration
@Configuration
public class DatabaseConfig {
@Bean
@Primary
public DataSource dataSource(VaultDatabaseConfig vaultDatabaseConfig) {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl(vaultDatabaseConfig.getDatabaseUrl());
dataSource.setUsername(vaultDatabaseConfig.getDatabaseUsername());
dataSource.setPassword(vaultDatabaseConfig.getDatabasePassword());
dataSource.setDriverClassName("org.postgresql.Driver");
dataSource.setMaximumPoolSize(10);
dataSource.setMinimumIdle(2);
dataSource.setConnectionTimeout(30000);
dataSource.setIdleTimeout(300000);
dataSource.setMaxLifetime(1800000);
// Add health check properties
dataSource.setHealthCheckRegistry(new HealthCheckRegistry());
return dataSource;
}
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource) {
LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
em.setDataSource(dataSource);
em.setPackagesToScan("com.example.entity");
JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
em.setJpaVendorAdapter(vendorAdapter);
Properties properties = new Properties();
properties.setProperty("hibernate.hbm2ddl.auto", "validate");
properties.setProperty("hibernate.dialect", "org.hibernate.dialect.PostgreSQLDialect");
properties.setProperty("hibernate.show_sql", "false");
em.setJpaProperties(properties);
return em;
}
@Bean
public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
JpaTransactionManager transactionManager = new JpaTransactionManager();
transactionManager.setEntityManagerFactory(entityManagerFactory);
return transactionManager;
}
}
4. API Key Security Service
@Service
@Slf4j
public class ApiKeyService {
private final VaultSecretsManager secretsManager;
private final VaultConfig vaultConfig;
private volatile String apiKey;
private final AtomicReference<Instant> lastReload = new AtomicReference<>(Instant.now());
public ApiKeyService(VaultSecretsManager secretsManager, VaultConfig vaultConfig) {
this.secretsManager = secretsManager;
this.vaultConfig = vaultConfig;
loadApiKey();
}
public String getApiKey() {
// Reload API key if auto-reload is enabled and it's time to reload
if (vaultConfig.isAutoReload() && shouldReload()) {
loadApiKey();
}
return apiKey;
}
public boolean validateApiKey(String providedKey) {
if (providedKey == null || providedKey.trim().isEmpty()) {
return false;
}
String currentKey = getApiKey();
return currentKey != null && currentKey.equals(providedKey.trim());
}
private void loadApiKey() {
try {
// Try to load from file first (injected by Vault)
String fileKey = secretsManager.getSecret("api-key");
if (fileKey != null) {
apiKey = fileKey.trim();
lastReload.set(Instant.now());
log.info("API key reloaded from Vault file");
return;
}
// Fall back to reading directly from Vault
String vaultKey = secretsManager.getSecret("kv/data/app/secrets", "api_key");
if (vaultKey != null) {
apiKey = vaultKey.trim();
lastReload.set(Instant.now());
log.info("API key reloaded from Vault");
} else {
log.warn("Failed to load API key from Vault");
}
} catch (Exception e) {
log.error("Failed to load API key", e);
}
}
private boolean shouldReload() {
Instant last = lastReload.get();
return last.plusMillis(vaultConfig.getReloadInterval()).isBefore(Instant.now());
}
}
5. TLS Certificate Service
@Service
@Slf4j
public class TlsCertificateService {
private final VaultSecretsManager secretsManager;
private final VaultOperations vaultOperations;
private X509Certificate certificate;
private PrivateKey privateKey;
private String leaseId;
public TlsCertificateService(VaultSecretsManager secretsManager,
VaultOperations vaultOperations) {
this.secretsManager = secretsManager;
this.vaultOperations = vaultOperations;
loadCertificate();
}
public void loadCertificate() {
try {
// Try to load from file (injected by Vault)
loadCertificateFromFile();
if (certificate == null) {
// Fall back to dynamic certificate generation
generateDynamicCertificate();
}
} catch (Exception e) {
log.error("Failed to load TLS certificate", e);
}
}
public SSLContext getSslContext() {
try {
KeyStore keyStore = KeyStore.getInstance("PKCS12");
keyStore.load(null, null);
if (certificate != null && privateKey != null) {
X509Certificate[] certChain = new X509Certificate[]{certificate};
keyStore.setKeyEntry("tls", privateKey, "".toCharArray(), certChain);
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(keyStore, "".toCharArray());
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(kmf.getKeyManagers(), null, null);
return sslContext;
}
} catch (Exception e) {
log.error("Failed to create SSL context", e);
}
return null;
}
public X509Certificate getCertificate() {
return certificate;
}
private void loadCertificateFromFile() {
try {
File certFile = new File("/etc/tls/tls.crt");
File keyFile = new File("/etc/tls/tls.key");
if (certFile.exists() && keyFile.exists()) {
certificate = loadCertificateFromPem(certFile);
privateKey = loadPrivateKeyFromPem(keyFile);
log.info("Loaded TLS certificate from files");
}
} catch (Exception e) {
log.warn("Failed to load TLS certificate from files", e);
}
}
private void generateDynamicCertificate() {
try {
Map<String, String> certRequest = Map.of(
"common_name", "java-app.example.com",
"ttl", "24h"
);
VaultResponseSupport<Map> response = vaultOperations.write(
"pki/issue/java-app", certRequest, Map.class);
if (response != null && response.getData() != null) {
Map<String, Object> data = response.getData();
String certPem = (String) data.get("certificate");
String keyPem = (String) data.get("private_key");
leaseId = (String) data.get("lease_id");
certificate = loadCertificateFromPem(certPem);
privateKey = loadPrivateKeyFromPem(keyPem);
log.info("Generated dynamic TLS certificate (lease: {})", leaseId);
}
} catch (Exception e) {
log.error("Failed to generate dynamic TLS certificate", e);
}
}
private X509Certificate loadCertificateFromPem(File file) throws Exception {
String pemContent = Files.readString(file.toPath());
return loadCertificateFromPem(pemContent);
}
private X509Certificate loadCertificateFromPem(String pemContent) throws Exception {
String base64 = pemContent
.replace("-----BEGIN CERTIFICATE-----", "")
.replace("-----END CERTIFICATE-----", "")
.replaceAll("\\s", "");
byte[] certBytes = Base64.getDecoder().decode(base64);
CertificateFactory cf = CertificateFactory.getInstance("X.509");
return (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(certBytes));
}
private PrivateKey loadPrivateKeyFromPem(File file) throws Exception {
String pemContent = Files.readString(file.toPath());
return loadPrivateKeyFromPem(pemContent);
}
private PrivateKey loadPrivateKeyFromPem(String pemContent) throws Exception {
String base64 = pemContent
.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("\\s", "");
byte[] keyBytes = Base64.getDecoder().decode(base64);
KeyFactory kf = KeyFactory.getInstance("RSA");
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
return kf.generatePrivate(keySpec);
}
}
Security Configuration
1. API Key Authentication
@Component
public class ApiKeyAuthFilter extends OncePerRequestFilter {
private final ApiKeyService apiKeyService;
private static final String API_KEY_HEADER = "X-API-Key";
public ApiKeyAuthFilter(ApiKeyService apiKeyService) {
this.apiKeyService = apiKeyService;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String requestPath = request.getRequestURI();
// Skip auth for health checks and public endpoints
if (requestPath.startsWith("/actuator/health") ||
requestPath.startsWith("/public/")) {
filterChain.doFilter(request, response);
return;
}
String apiKey = request.getHeader(API_KEY_HEADER);
if (apiKey == null) {
sendError(response, "API key required", HttpStatus.UNAUTHORIZED);
return;
}
if (!apiKeyService.validateApiKey(apiKey)) {
sendError(response, "Invalid API key", HttpStatus.UNAUTHORIZED);
return;
}
filterChain.doFilter(request, response);
}
private void sendError(HttpServletResponse response, String message, HttpStatus status) throws IOException {
response.setStatus(status.value());
response.setContentType("application/json");
Map<String, Object> error = Map.of(
"error", status.getReasonPhrase(),
"message", message,
"status", status.value(),
"timestamp", Instant.now()
);
response.getWriter().write(new ObjectMapper().writeValueAsString(error));
}
}
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http, ApiKeyAuthFilter apiKeyAuthFilter) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(authz -> authz
.requestMatchers("/actuator/health/**").permitAll()
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(apiKeyAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
REST Controllers
1. Application Controller
@RestController
@RequestMapping("/api")
@Slf4j
public class ApplicationController {
private final VaultSecretsManager secretsManager;
private final VaultDatabaseConfig databaseConfig;
private final TlsCertificateService tlsCertificateService;
public ApplicationController(VaultSecretsManager secretsManager,
VaultDatabaseConfig databaseConfig,
TlsCertificateService tlsCertificateService) {
this.secretsManager = secretsManager;
this.databaseConfig = databaseConfig;
this.tlsCertificateService = tlsCertificateService;
}
@GetMapping("/secrets/database")
public ResponseEntity<Map<String, String>> getDatabaseInfo() {
Map<String, String> info = new HashMap<>();
info.put("username", databaseConfig.getDatabaseUsername());
info.put("url", databaseConfig.getDatabaseUrl());
// Don't expose password in response
return ResponseEntity.ok(info);
}
@GetMapping("/secrets/status")
public ResponseEntity<Map<String, Object>> getSecretsStatus() {
VaultHealth vaultHealth = secretsManager.getVaultHealth();
Map<String, Object> status = new HashMap<>();
status.put("vaultHealth", vaultHealth.isHealthy());
status.put("vaultInitialized", vaultHealth.isInitialized());
status.put("vaultSealed", vaultHealth.isSealed());
status.put("databaseConfigured", databaseConfig.getDatabaseUsername() != null);
status.put("tlsConfigured", tlsCertificateService.getCertificate() != null);
status.put("timestamp", Instant.now());
return ResponseEntity.ok(status);
}
@PostMapping("/secrets/reload")
public ResponseEntity<Map<String, String>> reloadSecrets() {
try {
// This would trigger a reload of secrets
// In a real implementation, you might have a method to force reload
Map<String, String> response = new HashMap<>();
response.put("message", "Secrets reload triggered");
response.put("timestamp", Instant.now().toString());
return ResponseEntity.ok(response);
} catch (Exception e) {
log.error("Failed to reload secrets", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Map.of("error", "Failed to reload secrets"));
}
}
}
2. Vault Actuator Endpoint
@Component
@Endpoint(id = "vault")
@Slf4j
public class VaultActuatorEndpoint {
private final VaultSecretsManager secretsManager;
private final VaultConfig vaultConfig;
public VaultActuatorEndpoint(VaultSecretsManager secretsManager,
VaultConfig vaultConfig) {
this.secretsManager = secretsManager;
this.vaultConfig = vaultConfig;
}
@ReadOperation
public VaultStatus getVaultStatus() {
VaultHealth health = secretsManager.getVaultHealth();
Map<String, Object> details = new HashMap<>();
details.put("enabled", vaultConfig.isEnabled());
details.put("autoReload", vaultConfig.isAutoReload());
details.put("reloadInterval", vaultConfig.getReloadInterval());
details.put("secretsPath", vaultConfig.getSecretsPath());
return new VaultStatus(
health.isHealthy(),
health.isInitialized(),
health.isSealed(),
health.isStandby(),
details
);
}
@WriteOperation
public OperationResult reloadSecrets() {
try {
// Trigger secrets reload
// This would typically call a method on secretsManager to force reload
return new OperationResult(true, "Secrets reload initiated");
} catch (Exception e) {
return new OperationResult(false, "Failed to reload secrets: " + e.getMessage());
}
}
@Data
public static class VaultStatus {
private final boolean healthy;
private final boolean initialized;
private final boolean sealed;
private final boolean standby;
private final Map<String, Object> details;
private final Instant timestamp = Instant.now();
}
@Data
public static class OperationResult {
private final boolean success;
private final String message;
private final Instant timestamp = Instant.now();
}
}
Health Indicators
@Component
public class VaultHealthIndicator implements HealthIndicator {
private final VaultSecretsManager secretsManager;
private final VaultDatabaseConfig databaseConfig;
public VaultHealthIndicator(VaultSecretsManager secretsManager,
VaultDatabaseConfig databaseConfig) {
this.secretsManager = secretsManager;
this.databaseConfig = databaseConfig;
}
@Override
public Health health() {
try {
VaultHealth vaultHealth = secretsManager.getVaultHealth();
Health.Builder healthBuilder = vaultHealth.isHealthy() ?
Health.up() : Health.down();
healthBuilder
.withDetail("vaultInitialized", vaultHealth.isInitialized())
.withDetail("vaultSealed", vaultHealth.isSealed())
.withDetail("vaultStandby", vaultHealth.isStandby())
.withDetail("databaseConfigured", databaseConfig.getDatabaseUsername() != null)
.withDetail("timestamp", Instant.now());
return healthBuilder.build();
} catch (Exception e) {
return Health.down()
.withDetail("error", e.getMessage())
.withDetail("timestamp", Instant.now())
.build();
}
}
}
@Component
public class DatabaseHealthIndicator implements HealthIndicator {
private final DataSource dataSource;
public DatabaseHealthIndicator(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public Health health() {
try (Connection conn = dataSource.getConnection()) {
boolean valid = conn.isValid(5); // 5 second timeout
if (valid) {
return Health.up()
.withDetail("database", "connected")
.withDetail("timestamp", Instant.now())
.build();
} else {
return Health.down()
.withDetail("database", "connection failed")
.withDetail("timestamp", Instant.now())
.build();
}
} catch (SQLException e) {
return Health.down()
.withDetail("database", "connection error")
.withDetail("error", e.getMessage())
.withDetail("timestamp", Instant.now())
.build();
}
}
}
Testing
1. Test Configuration
@TestConfiguration
public class TestVaultConfig {
@Bean
@Primary
public VaultSecretsManager testVaultSecretsManager() {
VaultConfig vaultConfig = new VaultConfig();
vaultConfig.setEnabled(true);
vaultConfig.setSecretsPath("/tmp/vault-test-secrets");
// Create mock VaultOperations
VaultOperations vaultOperations = mock(VaultOperations.class);
return new VaultSecretsManager(vaultOperations, vaultConfig);
}
@Bean
@Primary
public ApiKeyService testApiKeyService() {
ApiKeyService apiKeyService = mock(ApiKeyService.class);
when(apiKeyService.validateApiKey(anyString())).thenReturn(true);
return apiKeyService;
}
}
2. Integration Tests with Testcontainers
@SpringBootTest
@Testcontainers
@ActiveProfiles("test")
class VaultIntegrationTest {
@Container
static VaultContainer vaultContainer = new VaultContainer("hashicorp/vault:1.15")
.withVaultToken("test-token")
.withSecretInVault("kv/data/app/secrets", "api_key=test-api-key");
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.vault.uri", vaultContainer::getHttpHostAddress);
registry.add("spring.vault.authentication", () -> "TOKEN");
registry.add("spring.vault.token", () -> "test-token");
}
@Autowired
private VaultSecretsManager secretsManager;
@Test
void testVaultConnection() {
VaultHealth health = secretsManager.getVaultHealth();
assertTrue(health.isHealthy());
assertTrue(health.isInitialized());
}
@Test
void testSecretsRetrieval() {
String apiKey = secretsManager.getSecret("kv/data/app/secrets", "api_key");
assertEquals("test-api-key", apiKey);
}
}
Best Practices
- Least Privilege: Use Vault policies to grant minimal required permissions
- Lease Management: Always renew or revoke leases properly
- Error Handling: Implement graceful fallbacks for Vault unavailability
- Monitoring: Monitor Vault health and secret access patterns
- Rotation: Implement automatic secret rotation where possible
// Example of secure secret rotation
@Component
@Slf4j
public class SecretRotationService {
private final VaultSecretsManager secretsManager;
private final Map<String, Instant> lastRotation = new ConcurrentHashMap<>();
@Scheduled(fixedRate = 3600000) // Check every hour
public void rotateExpiringSecrets() {
// Check and rotate secrets approaching expiration
// This is particularly important for dynamic credentials
}
}
Conclusion
HashiCorp Vault Injector integration provides:
- Automatic secrets injection into Kubernetes pods
- Dynamic credentials with automatic rotation
- Centralized secrets management with audit logging
- Zero-trust security model implementation
This Java implementation enables seamless integration with Vault, providing secure secret management while maintaining application performance and reliability. The solution supports both file-based injection (via Vault Agent) and direct Vault API access for dynamic scenarios.
Java Logistics, Shipping Integration & Enterprise Inventory Automation (Tracking, ERP, RFID & Billing Systems)
https://macronepal.com/blog/aftership-tracking-in-java-enterprise-package-visibility/
Explains how to integrate AfterShip tracking services into Java applications to provide real-time shipment visibility, delivery status updates, and centralized tracking across multiple courier services.
https://macronepal.com/blog/shipping-integration-using-fedex-api-with-java-for-logistics-automation/
Explains how to integrate the FedEx API into Java systems to automate shipping tasks such as creating shipments, calculating delivery costs, generating shipping labels, and tracking packages.
https://macronepal.com/blog/shipping-and-logistics-integrating-ups-apis-with-java-applications/
Explains UPS API integration in Java to enable automated shipping operations including rate calculation, shipment scheduling, tracking, and delivery confirmation management.
https://macronepal.com/blog/generating-and-reading-qr-codes-for-products-in-java/
Explains how Java applications generate and read QR codes for product identification, tracking, and authentication, supporting faster inventory handling and product verification processes.
https://macronepal.com/blog/designing-a-robust-pick-and-pack-workflow-in-java/
Explains how to design an efficient pick-and-pack workflow in Java warehouse systems, covering order processing, item selection, packaging steps, and logistics preparation to improve fulfillment efficiency.
https://macronepal.com/blog/rfid-inventory-management-system-in-java-a-complete-guide/
Explains how RFID technology integrates with Java applications to automate inventory tracking, reduce manual errors, and enable real-time stock monitoring in warehouses and retail environments.
https://macronepal.com/blog/erp-integration-with-odoo-in-java/
Explains how Java applications connect with Odoo ERP systems to synchronize inventory, orders, customer records, and financial data across enterprise systems.
https://macronepal.com/blog/automated-invoice-generation-creating-professional-excel-invoices-with-apache-poi-in-java/
Explains how to automatically generate professional Excel invoices in Java using Apache POI, enabling structured billing documents and automated financial record creation.
https://macronepal.com/blog/enterprise-financial-integration-using-quickbooks-api-in-java-applications/
Explains QuickBooks API integration in Java to automate financial workflows such as invoice management, payment tracking, accounting synchronization, and financial reporting.