google-site-verification: google61fe8ba583a51912.html
HashiCorp Vault Injector in Java: Comprehensive Secrets Management Integration

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

  1. Least Privilege: Use Vault policies to grant minimal required permissions
  2. Lease Management: Always renew or revoke leases properly
  3. Error Handling: Implement graceful fallbacks for Vault unavailability
  4. Monitoring: Monitor Vault health and secret access patterns
  5. 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.

Leave a Reply

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


Macro Nepal Helper