Securing Sensitive Data: Kubernetes Secrets Best Practices for Java Applications

Article

Kubernetes Secrets are essential for managing sensitive information like passwords, API keys, and tokens in containerized applications. However, many Java developers struggle with implementing Secrets securely and effectively. Proper Secrets management is crucial for security, compliance, and operational excellence in Kubernetes environments.

In this guide, we'll explore comprehensive best practices for using Kubernetes Secrets with Java applications, from basic usage to advanced security patterns.

Why Proper Secrets Management Matters for Java Apps

  • Security: Prevent credential leakage and unauthorized access
  • Compliance: Meet regulatory requirements (SOC2, HIPAA, GDPR)
  • Operational Efficiency: Simplify configuration management
  • DevOps Enablement: Support secure CI/CD pipelines
  • Disaster Recovery: Ensure reliable secret rotation and backup

Part 1: Fundamentals of Kubernetes Secrets

1.1 Types of Kubernetes Secrets

# Generic/Opaque Secrets
apiVersion: v1
kind: Secret
metadata:
name: app-credentials
type: Opaque
data:
username: dXNlcg==
password: cGFzc3dvcmQ=
api-key: YXBpLWtleS1zZWNyZXQ=
# Docker Registry Secrets
apiVersion: v1
kind: Secret
metadata:
name: registry-credentials
type: kubernetes.io/dockerconfigjson
data:
.dockerconfigjson: eyJhdXRocyI6...
# TLS Secrets
apiVersion: v1
kind: Secret
metadata:
name: tls-secret
type: kubernetes.io/tls
data:
tls.crt: LS0tLS1CRUdJTi...
tls.key: LS0tLS1CRUdJTi...
# Service Account Tokens
apiVersion: v1
kind: Secret
metadata:
name: service-account-token
annotations:
kubernetes.io/service-account.name: my-service-account
type: kubernetes.io/service-account-token

1.2 Project Structure for Secrets Management

java-secrets-app/
├── src/
│   ├── main/java/com/example/secrets/
│   │   ├── management/
│   │   ├── rotation/
│   │   └── encryption/
│   └── resources/
├── kubernetes/
│   ├── base/
│   │   └── secrets/
│   ├── overlays/
│   │   ├── development/
│   │   ├── staging/
│   │   └── production/
│   └── policies/
├── scripts/
│   ├── secret-rotation/
│   └── security-scans/
└── helm/
└── templates/
└── secrets/

Part 2: Basic Secrets Integration in Java

2.1 Dependencies for Secrets Management

<!-- pom.xml -->
<properties>
<spring-boot.version>3.1.0</spring-boot.version>
<kubernetes-client.version>6.7.2</kubernetes-client.version>
<vault.version>3.2.0</vault.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-validation</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<!-- Kubernetes Client -->
<dependency>
<groupId>io.kubernetes</groupId>
<artifactId>client-java</artifactId>
<version>${kubernetes-client.version}</version>
</dependency>
<!-- Encryption -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
<version>6.1.0</version>
</dependency>
<!-- HashiCorp Vault (for external secrets) -->
<dependency>
<groupId>org.springframework.vault</groupId>
<artifactId>spring-vault-core</artifactId>
<version>${vault.version}</version>
</dependency>
<!-- AWS Secrets Manager -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>secretsmanager</artifactId>
<version>2.20.0</version>
</dependency>
</dependencies>

2.2 Basic Secrets Access Pattern

// File: src/main/java/com/example/secrets/basic/SecretsConfig.java
package com.example.secrets.basic;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SecretsConfig {
// Basic environment variable injection
@Value("${DATABASE_URL:}")
private String databaseUrl;
@Value("${DATABASE_USERNAME:}")
private String databaseUsername;
@Value("${DATABASE_PASSWORD:}")
private String databasePassword;
@Value("${API_KEY:}")
private String apiKey;
@Value("${JWT_SECRET:}")
private String jwtSecret;
@Bean
public DatabaseConfig databaseConfig() {
return new DatabaseConfig(databaseUrl, databaseUsername, databasePassword);
}
@Bean
public ApiConfig apiConfig() {
return new ApiConfig(apiKey, jwtSecret);
}
public record DatabaseConfig(String url, String username, String password) {
public boolean isValid() {
return url != null && !url.isEmpty() &&
username != null && !username.isEmpty() &&
password != null && !password.isEmpty();
}
}
public record ApiConfig(String apiKey, String jwtSecret) {
public boolean isValid() {
return apiKey != null && !apiKey.isEmpty() &&
jwtSecret != null && !jwtSecret.isEmpty();
}
}
}

Part 3: Advanced Secrets Management Patterns

3.1 Secrets Manager Service

// File: src/main/java/com/example/secrets/management/SecretsManagerService.java
package com.example.secrets.management;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Service
public class SecretsManagerService {
private static final Logger logger = LoggerFactory.getLogger(SecretsManagerService.class);
private final Environment environment;
private final Map<String, String> secretCache = new ConcurrentHashMap<>();
private final Map<String, Long> secretTimestamps = new ConcurrentHashMap<>();
@Value("${app.secrets.cache.ttl:300000}") // 5 minutes default
private long cacheTtl;
@Value("${app.secrets.max-retries:3}")
private int maxRetries;
public SecretsManagerService(Environment environment) {
this.environment = environment;
}
@PostConstruct
public void initialize() {
logger.info("SecretsManagerService initialized with cache TTL: {}ms", cacheTtl);
preloadCriticalSecrets();
}
public String getSecret(String secretName) {
return getSecret(secretName, null);
}
public String getSecret(String secretName, String defaultValue) {
// Check cache first
if (isCachedValid(secretName)) {
return secretCache.get(secretName);
}
// Try multiple sources with retry logic
String secretValue = retrieveSecretWithRetry(secretName);
if (secretValue != null) {
cacheSecret(secretName, secretValue);
return secretValue;
}
logger.warn("Secret '{}' not found, using default value", secretName);
return defaultValue;
}
public byte[] getSecretAsBytes(String secretName) {
String secretValue = getSecret(secretName);
return secretValue != null ? secretValue.getBytes() : null;
}
public String getBase64Secret(String secretName) {
String secretValue = getSecret(secretName);
if (secretValue != null) {
return Base64.getEncoder().encodeToString(secretValue.getBytes());
}
return null;
}
public Map<String, String> getMultipleSecrets(String... secretNames) {
Map<String, String> secrets = new HashMap<>();
for (String secretName : secretNames) {
secrets.put(secretName, getSecret(secretName));
}
return secrets;
}
public void invalidateCache(String secretName) {
secretCache.remove(secretName);
secretTimestamps.remove(secretName);
logger.debug("Invalidated cache for secret: {}", secretName);
}
public void invalidateAllCache() {
secretCache.clear();
secretTimestamps.clear();
logger.info("Cleared all secret cache");
}
public SecretsHealth checkSecretsHealth() {
String[] criticalSecrets = {
"DATABASE_PASSWORD",
"API_KEY", 
"JWT_SECRET"
};
Map<String, Boolean> healthStatus = new HashMap<>();
int healthyCount = 0;
for (String secret : criticalSecrets) {
boolean healthy = getSecret(secret) != null;
healthStatus.put(secret, healthy);
if (healthy) healthyCount++;
}
double healthPercentage = (double) healthyCount / criticalSecrets.length * 100;
return new SecretsHealth(healthPercentage, healthStatus);
}
private String retrieveSecretWithRetry(String secretName) {
for (int attempt = 1; attempt <= maxRetries; attempt++) {
try {
String secretValue = retrieveSecret(secretName);
if (secretValue != null) {
return secretValue;
}
logger.debug("Attempt {}/{} failed for secret: {}", attempt, maxRetries, secretName);
Thread.sleep(1000 * attempt); // Exponential backoff
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
} catch (Exception e) {
logger.warn("Error retrieving secret {} on attempt {}: {}", 
secretName, attempt, e.getMessage());
}
}
logger.error("Failed to retrieve secret '{}' after {} attempts", secretName, maxRetries);
return null;
}
private String retrieveSecret(String secretName) {
// Priority 1: Kubernetes Secrets (mounted as env vars)
String envValue = environment.getProperty(secretName);
if (envValue != null && !envValue.trim().isEmpty()) {
logger.debug("Retrieved secret '{}' from environment", secretName);
return envValue;
}
// Priority 2: Kubernetes Secrets (mounted as files)
String fileValue = readSecretFromFile("/etc/secrets/" + secretName);
if (fileValue != null) {
logger.debug("Retrieved secret '{}' from file", secretName);
return fileValue;
}
// Priority 3: External secrets manager (could be extended)
// String externalValue = getFromVault(secretName);
// if (externalValue != null) return externalValue;
return null;
}
private String readSecretFromFile(String filePath) {
try {
java.nio.file.Path path = java.nio.file.Paths.get(filePath);
if (java.nio.file.Files.exists(path)) {
return java.nio.file.Files.readString(path).trim();
}
} catch (Exception e) {
logger.debug("Could not read secret from file {}: {}", filePath, e.getMessage());
}
return null;
}
private boolean isCachedValid(String secretName) {
Long timestamp = secretTimestamps.get(secretName);
if (timestamp == null) return false;
boolean valid = (System.currentTimeMillis() - timestamp) < cacheTtl;
if (!valid) {
secretCache.remove(secretName);
secretTimestamps.remove(secretName);
}
return valid;
}
private void cacheSecret(String secretName, String secretValue) {
secretCache.put(secretName, secretValue);
secretTimestamps.put(secretName, System.currentTimeMillis());
}
private void preloadCriticalSecrets() {
String[] criticalSecrets = {"DATABASE_PASSWORD", "API_KEY", "JWT_SECRET"};
for (String secret : criticalSecrets) {
if (getSecret(secret) != null) {
logger.info("Preloaded critical secret: {}", secret);
} else {
logger.warn("Critical secret not found: {}", secret);
}
}
}
public record SecretsHealth(double healthPercentage, Map<String, Boolean> secretStatus) {
public boolean isHealthy() {
return healthPercentage >= 90.0;
}
}
}

3.2 Secure Secrets Configuration

// File: src/main/java/com/example/secrets/management/SecureSecretsConfig.java
package com.example.secrets.management;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.validation.annotation.Validated;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
@Configuration
@ConfigurationProperties(prefix = "app.secrets")
@Validated
public class SecureSecretsConfig {
private EncryptionConfig encryption;
private RotationConfig rotation;
private StorageConfig storage;
@Bean
public SecretsManagerService secretsManagerService() {
return new SecretsManagerService(null); // Injected via constructor
}
@Bean
public SecretKey encryptionKey() {
String keyBase64 = System.getenv("ENCRYPTION_KEY");
if (keyBase64 == null || keyBase64.isEmpty()) {
throw new IllegalStateException("ENCRYPTION_KEY environment variable is required");
}
byte[] keyBytes = Base64.getDecoder().decode(keyBase64);
if (keyBytes.length != 32) { // 256-bit key
throw new IllegalStateException("Encryption key must be 32 bytes (256-bit)");
}
return new SecretKeySpec(keyBytes, "AES");
}
// Getters and Setters
public EncryptionConfig getEncryption() { return encryption; }
public void setEncryption(EncryptionConfig encryption) { this.encryption = encryption; }
public RotationConfig getRotation() { return rotation; }
public void setRotation(RotationConfig rotation) { this.rotation = rotation; }
public StorageConfig getStorage() { return storage; }
public void setStorage(StorageConfig storage) { this.storage = storage; }
public static class EncryptionConfig {
private boolean enabled = true;
private String algorithm = "AES/GCM/NoPadding";
private int keySize = 256;
public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }
public String getAlgorithm() { return algorithm; }
public void setAlgorithm(String algorithm) { this.algorithm = algorithm; }
public int getKeySize() { return keySize; }
public void setKeySize(int keySize) { this.keySize = keySize; }
}
public static class RotationConfig {
private boolean autoRotate = false;
private int rotationIntervalDays = 90;
private int warningDaysBefore = 7;
public boolean isAutoRotate() { return autoRotate; }
public void setAutoRotate(boolean autoRotate) { this.autoRotate = autoRotate; }
public int getRotationIntervalDays() { return rotationIntervalDays; }
public void setRotationIntervalDays(int rotationIntervalDays) { this.rotationIntervalDays = rotationIntervalDays; }
public int getWarningDaysBefore() { return warningDaysBefore; }
public void setWarningDaysBefore(int warningDaysBefore) { this.warningDaysBefore = warningDaysBefore; }
}
public static class StorageConfig {
private String backend = "kubernetes"; // kubernetes, vault, aws-secrets-manager
private boolean encryptAtRest = true;
private String namespace = "default";
public String getBackend() { return backend; }
public void setBackend(String backend) { this.backend = backend; }
public boolean isEncryptAtRest() { return encryptAtRest; }
public void setEncryptAtRest(boolean encryptAtRest) { this.encryptAtRest = encryptAtRest; }
public String getNamespace() { return namespace; }
public void setNamespace(String namespace) { this.namespace = namespace; }
}
}

Part 4: Kubernetes Secrets Deployment Patterns

4.1 Environment-Specific Secrets

# kubernetes/base/secrets/database-secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: database-credentials
labels:
app: java-app
secrets-version: "1.0"
type: Opaque
data:
username: {{ .Values.database.username | b64enc }}
password: {{ .Values.database.password | b64enc }}
url: {{ .Values.database.url | b64enc }}
---
# kubernetes/overlays/development/secrets-patch.yaml
apiVersion: v1
kind: Secret
metadata:
name: database-credentials
data:
username: ZGV2X3VzZXI=  # dev_user
password: ZGV2X3Bhc3N3b3Jk  # dev_password
url: amRiYzptYXF1ZXN0Oi8vbG9jYWxob3N0OjMzMDYvZGV2X2Ri # jdbc:mysql://localhost:3306/dev_db
---
# kubernetes/overlays/production/secrets-patch.yaml
apiVersion: v1
kind: Secret
metadata:
name: database-credentials
data:
username: cHJvZF91c2Vy  # prod_user
password: cHJvZF9wYXNzd29yZA==  # prod_password
url: amRiYzptYXF1ZXN0Oi8vcHJvZC1kYi5leGFtcGxlLmNvbTozMzA2L3Byb2RfZGI= # jdbc:mysql://prod-db.example.com:3306/prod_db

4.2 Deployment with Secrets

# kubernetes/base/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: java-app
labels:
app: java-app
spec:
replicas: 3
selector:
matchLabels:
app: java-app
template:
metadata:
labels:
app: java-app
annotations:
secrets-version: "1.0"
spec:
serviceAccountName: java-app-sa
containers:
- name: java-app
image: my-registry/java-app:latest
ports:
- containerPort: 8080
env:
# Environment Variables from Secrets
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: database-credentials
key: url
- name: DATABASE_USERNAME
valueFrom:
secretKeyRef:
name: database-credentials
key: username
- name: DATABASE_PASSWORD
valueFrom:
secretKeyRef:
name: database-credentials
key: password
- name: API_KEY
valueFrom:
secretKeyRef:
name: api-credentials
key: key
- name: JWT_SECRET
valueFrom:
secretKeyRef:
name: jwt-secret
key: secret
# Additional configuration
- name: JAVA_OPTS
value: "-Xmx512m -Djava.security.egd=file:/dev/./urandom"
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
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
securityContext:
runAsNonRoot: true
runAsUser: 1000
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
volumeMounts:
- name: secrets-volume
mountPath: /etc/secrets
readOnly: true
volumes:
- name: secrets-volume
secret:
secretName: file-based-secrets
defaultMode: 0400
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 1000
---
# Service Account with minimal permissions
apiVersion: v1
kind: ServiceAccount
metadata:
name: java-app-sa
namespace: default
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: default
name: secret-reader
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get"]
resourceNames: ["database-credentials", "api-credentials", "jwt-secret"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: read-secrets
namespace: default
subjects:
- kind: ServiceAccount
name: java-app-sa
namespace: default
roleRef:
kind: Role
name: secret-reader
apiGroup: rbac.authorization.k8s.io

Part 5: Secrets Rotation and Lifecycle Management

5.1 Secrets Rotation Service

// File: src/main/java/com/example/secrets/rotation/SecretsRotationService.java
package com.example.secrets.rotation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Service
public class SecretsRotationService {
private static final Logger logger = LoggerFactory.getLogger(SecretsRotationService.class);
private final SecretsManagerService secretsManager;
private final Map<String, LocalDateTime> rotationHistory = new ConcurrentHashMap<>();
public SecretsRotationService(SecretsManagerService secretsManager) {
this.secretsManager = secretsManager;
}
@Scheduled(cron = "${app.secrets.rotation.check-cron:0 0 6 * * *}") // Daily at 6 AM
public void checkAndRotateSecrets() {
logger.info("Starting scheduled secrets rotation check");
try {
// Check if secrets need rotation
checkSecretAge("DATABASE_PASSWORD", 90); // Rotate every 90 days
checkSecretAge("API_KEY", 30); // Rotate every 30 days
checkSecretAge("JWT_SECRET", 180); // Rotate every 180 days
} catch (Exception e) {
logger.error("Error during secrets rotation check", e);
}
}
public RotationStatus rotateSecret(String secretName) {
logger.info("Initiating rotation for secret: {}", secretName);
try {
// Step 1: Invalidate cache
secretsManager.invalidateCache(secretName);
// Step 2: Notify about rotation start
notifyRotationStart(secretName);
// Step 3: Get new secret (this would typically call an external API)
String newSecret = generateNewSecret(secretName);
// Step 4: Update cache with new secret
// In a real scenario, you'd update the actual secret store
// Step 5: Record rotation
rotationHistory.put(secretName, LocalDateTime.now());
logger.info("Successfully rotated secret: {}", secretName);
return new RotationStatus(true, "Rotation completed successfully", secretName, LocalDateTime.now());
} catch (Exception e) {
logger.error("Failed to rotate secret: {}", secretName, e);
return new RotationStatus(false, "Rotation failed: " + e.getMessage(), secretName, LocalDateTime.now());
}
}
public SecretAgeInfo getSecretAge(String secretName) {
LocalDateTime lastRotation = rotationHistory.get(secretName);
if (lastRotation == null) {
return new SecretAgeInfo(secretName, null, "Never rotated");
}
long daysSinceRotation = java.time.Duration.between(lastRotation, LocalDateTime.now()).toDays();
String status = daysSinceRotation > 90 ? "OVERDUE" : 
daysSinceRotation > 80 ? "WARNING" : "HEALTHY";
return new SecretAgeInfo(secretName, lastRotation, status);
}
private void checkSecretAge(String secretName, int maxAgeDays) {
SecretAgeInfo ageInfo = getSecretAge(secretName);
if ("OVERDUE".equals(ageInfo.status())) {
logger.warn("Secret '{}' is overdue for rotation ({} days)", secretName, maxAgeDays);
// In production, this would trigger an alert
} else if ("WARNING".equals(ageInfo.status())) {
logger.info("Secret '{}' will soon need rotation", secretName);
}
}
private void notifyRotationStart(String secretName) {
// In production, this would send notifications to Slack, email, etc.
logger.info("Rotation started for secret: {}", secretName);
}
private String generateNewSecret(String secretName) {
// In production, this would call your secrets management API
// For demonstration, generate a random string
String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*";
StringBuilder sb = new StringBuilder(32);
for (int i = 0; i < 32; i++) {
int index = (int) (Math.random() * chars.length());
sb.append(chars.charAt(index));
}
return sb.toString();
}
public record RotationStatus(boolean success, String message, String secretName, LocalDateTime timestamp) {}
public record SecretAgeInfo(String secretName, LocalDateTime lastRotation, String status) {}
}

5.2 Secrets Rotation Controller

// File: src/main/java/com/example/secrets/rotation/SecretsRotationController.java
package com.example.secrets.rotation;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/api/v1/secrets/rotation")
public class SecretsRotationController {
private final SecretsRotationService rotationService;
public SecretsRotationController(SecretsRotationService rotationService) {
this.rotationService = rotationService;
}
@PostMapping("/{secretName}")
public ResponseEntity<SecretsRotationService.RotationStatus> rotateSecret(
@PathVariable String secretName) {
SecretsRotationService.RotationStatus status = rotationService.rotateSecret(secretName);
if (status.success()) {
return ResponseEntity.ok(status);
} else {
return ResponseEntity.status(500).body(status);
}
}
@GetMapping("/status/{secretName}")
public ResponseEntity<SecretsRotationService.SecretAgeInfo> getSecretAge(
@PathVariable String secretName) {
return ResponseEntity.ok(rotationService.getSecretAge(secretName));
}
@GetMapping("/health")
public ResponseEntity<Map<String, Object>> getRotationHealth() {
String[] criticalSecrets = {"DATABASE_PASSWORD", "API_KEY", "JWT_SECRET"};
Map<String, Object> health = Map.of(
"timestamp", java.time.LocalDateTime.now(),
"secrets", Map.of(
"DATABASE_PASSWORD", rotationService.getSecretAge("DATABASE_PASSWORD"),
"API_KEY", rotationService.getSecretAge("API_KEY"),
"JWT_SECRET", rotationService.getSecretAge("JWT_SECRET")
)
);
return ResponseEntity.ok(health);
}
}

Part 6: Security and Encryption

6.1 Client-Side Encryption

// File: src/main/java/com/example/secrets/encryption/SecretEncryptionService.java
package com.example.secrets.encryption;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import javax.crypto.*;
import javax.crypto.spec.GCMParameterSpec;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Base64;
@Service
public class SecretEncryptionService {
private static final Logger logger = LoggerFactory.getLogger(SecretEncryptionService.class);
private static final String ALGORITHM = "AES/GCM/NoPadding";
private static final int TAG_LENGTH_BIT = 128;
private static final int IV_LENGTH_BYTE = 12;
private final SecretKey secretKey;
private final SecureRandom secureRandom;
public SecretEncryptionService(SecretKey secretKey) {
this.secretKey = secretKey;
this.secureRandom = new SecureRandom();
}
public String encrypt(String plaintext) {
try {
byte[] iv = new byte[IV_LENGTH_BYTE];
secureRandom.nextBytes(iv);
Cipher cipher = Cipher.getInstance(ALGORITHM);
GCMParameterSpec parameterSpec = new GCMParameterSpec(TAG_LENGTH_BIT, iv);
cipher.init(Cipher.ENCRYPT_MODE, secretKey, parameterSpec);
byte[] ciphertext = cipher.doFinal(plaintext.getBytes());
byte[] encrypted = new byte[iv.length + ciphertext.length];
System.arraycopy(iv, 0, encrypted, 0, iv.length);
System.arraycopy(ciphertext, 0, encrypted, iv.length, ciphertext.length);
return Base64.getEncoder().encodeToString(encrypted);
} catch (Exception e) {
logger.error("Error encrypting secret", e);
throw new RuntimeException("Encryption failed", e);
}
}
public String decrypt(String encryptedBase64) {
try {
byte[] encrypted = Base64.getDecoder().decode(encryptedBase64);
if (encrypted.length < IV_LENGTH_BYTE) {
throw new IllegalArgumentException("Encrypted data too short");
}
byte[] iv = new byte[IV_LENGTH_BYTE];
byte[] ciphertext = new byte[encrypted.length - IV_LENGTH_BYTE];
System.arraycopy(encrypted, 0, iv, 0, IV_LENGTH_BYTE);
System.arraycopy(encrypted, IV_LENGTH_BYTE, ciphertext, 0, ciphertext.length);
Cipher cipher = Cipher.getInstance(ALGORITHM);
GCMParameterSpec parameterSpec = new GCMParameterSpec(TAG_LENGTH_BIT, iv);
cipher.init(Cipher.DECRYPT_MODE, secretKey, parameterSpec);
byte[] plaintext = cipher.doFinal(ciphertext);
return new String(plaintext);
} catch (Exception e) {
logger.error("Error decrypting secret", e);
throw new RuntimeException("Decryption failed", e);
}
}
public boolean validateSecret(String secretName, String secretValue) {
if (secretValue == null || secretValue.trim().isEmpty()) {
logger.warn("Secret '{}' is null or empty", secretName);
return false;
}
// Add validation based on secret type
switch (secretName) {
case "JWT_SECRET":
return secretValue.length() >= 32; // JWT secrets should be at least 32 chars
case "API_KEY":
return secretValue.length() >= 16; // API keys should be at least 16 chars
case "DATABASE_PASSWORD":
return secretValue.length() >= 8; // Passwords should be at least 8 chars
default:
return true;
}
}
public String generateSecureSecret(String secretName, int length) {
String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*";
SecureRandom random = new SecureRandom();
StringBuilder sb = new StringBuilder(length);
for (int i = 0; i < length; i++) {
int index = random.nextInt(chars.length());
sb.append(chars.charAt(index));
}
String generatedSecret = sb.toString();
if (!validateSecret(secretName, generatedSecret)) {
throw new IllegalArgumentException("Generated secret does not meet validation criteria");
}
return generatedSecret;
}
}

Part 7: Monitoring and Auditing

7.1 Secrets Audit Service

// File: src/main/java/com/example/secrets/audit/SecretsAuditService.java
package com.example.secrets.audit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
@Service
public class SecretsAuditService {
private static final Logger logger = LoggerFactory.getLogger(SecretsAuditService.class);
private final List<AuditEvent> auditLog = new CopyOnWriteArrayList<>();
private final Map<String, Integer> accessCounts = new ConcurrentHashMap<>();
private final Set<String> sensitiveSecrets = Set.of(
"DATABASE_PASSWORD", "API_KEY", "JWT_SECRET", "PRIVATE_KEY"
);
public void logAccess(String secretName, String user, boolean success) {
AuditEvent event = new AuditEvent(
LocalDateTime.now(),
"ACCESS",
secretName,
user,
success,
sensitiveSecrets.contains(secretName) ? "MASKED" : "ACCESSED"
);
auditLog.add(event);
accessCounts.merge(secretName, 1, Integer::sum);
if (auditLog.size() > 10000) { // Keep last 10,000 events
auditLog.subList(0, 1000).clear();
}
logger.debug("Secret access logged: {} by {}", secretName, user);
}
public void logRotation(String secretName, String user, boolean success) {
AuditEvent event = new AuditEvent(
LocalDateTime.now(),
"ROTATION",
secretName,
user,
success,
"Secret rotation attempted"
);
auditLog.add(event);
logger.info("Secret rotation logged: {} by {}", secretName, user);
}
public List<AuditEvent> getRecentEvents(int count) {
int startIndex = Math.max(0, auditLog.size() - count);
return new ArrayList<>(auditLog.subList(startIndex, auditLog.size()));
}
public Map<String, Object> getAccessStatistics() {
Map<String, Object> stats = new HashMap<>();
stats.put("totalAccessEvents", auditLog.size());
stats.put("uniqueSecretsAccessed", accessCounts.size());
stats.put("accessCounts", new HashMap<>(accessCounts));
// Calculate success rate
long successCount = auditLog.stream().filter(AuditEvent::success).count();
double successRate = auditLog.isEmpty() ? 0.0 : (double) successCount / auditLog.size() * 100;
stats.put("successRate", String.format("%.2f%%", successRate));
return stats;
}
public List<AuditEvent> findSuspiciousActivity() {
List<AuditEvent> suspicious = new ArrayList<>();
// Detect multiple failed attempts
Map<String, Integer> failedAttempts = new HashMap<>();
for (AuditEvent event : auditLog) {
if (!event.success() && event.timestamp().isAfter(LocalDateTime.now().minusHours(1))) {
failedAttempts.merge(event.secretName(), 1, Integer::sum);
}
}
failedAttempts.entrySet().stream()
.filter(entry -> entry.getValue() > 5)
.forEach(entry -> {
suspicious.add(new AuditEvent(
LocalDateTime.now(),
"SUSPICIOUS",
entry.getKey(),
"SYSTEM",
false,
"Multiple failed access attempts: " + entry.getValue()
));
});
return suspicious;
}
public record AuditEvent(
LocalDateTime timestamp,
String action,
String secretName,
String user,
boolean success,
String details
) {}
}

Part 8: Application Configuration

8.1 Application Properties

# src/main/resources/application.yaml
app:
secrets:
management:
enabled: true
cache-ttl: 300000  # 5 minutes
max-retries: 3
validation:
enabled: true
min-password-length: 8
require-special-chars: true
encryption:
enabled: true
algorithm: AES/GCM/NoPadding
key-size: 256
rotation:
auto-rotate: false
check-cron: "0 0 6 * * *"  # Daily at 6 AM
rotation-interval-days: 90
warning-days-before: 7
audit:
enabled: true
max-events: 10000
log-suspicious-activity: true
spring:
application:
name: java-secrets-app
security:
crypto:
enabled: true
management:
endpoints:
web:
exposure:
include: health,info,metrics,secrets
endpoint:
secrets:
enabled: true
health:
show-details: always
metrics:
tags:
application: ${spring.application.name}
logging:
level:
com.example.secrets: INFO

8.2 Health Check Integration

// File: src/main/java/com/example/secrets/health/SecretsHealthIndicator.java
package com.example.secrets.health;
import com.example.secrets.management.SecretsManagerService;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;
@Component
public class SecretsHealthIndicator implements HealthIndicator {
private final SecretsManagerService secretsManager;
public SecretsHealthIndicator(SecretsManagerService secretsManager) {
this.secretsManager = secretsManager;
}
@Override
public Health health() {
try {
SecretsManagerService.SecretsHealth secretsHealth = secretsManager.checkSecretsHealth();
if (secretsHealth.isHealthy()) {
return Health.up()
.withDetail("secretsHealth", secretsHealth.healthPercentage() + "%")
.withDetail("secretsStatus", secretsHealth.secretStatus())
.build();
} else {
return Health.down()
.withDetail("secretsHealth", secretsHealth.healthPercentage() + "%")
.withDetail("secretsStatus", secretsHealth.secretStatus())
.withDetail("message", "Some critical secrets are missing")
.build();
}
} catch (Exception e) {
return Health.down(e)
.withDetail("message", "Error checking secrets health")
.build();
}
}
}

Best Practices Summary

  1. Never Hardcode Secrets: Always use Kubernetes Secrets or external secret managers
  2. Use Least Privilege: Restrict service account permissions to only necessary secrets
  3. Enable Encryption: Use Kubernetes envelope encryption or external key management
  4. Implement Rotation: Regularly rotate secrets and implement automated rotation where possible
  5. Monitor and Audit: Track secret access and detect suspicious activity
  6. Validate Secrets: Check secret strength and format during application startup
  7. Use Namespaces: Isolate secrets by environment using Kubernetes namespaces
  8. Secure Delivery: Use tools like Sealed Secrets or External Secrets Operator for GitOps

Conclusion

Implementing robust Kubernetes Secrets management in Java applications requires a comprehensive approach that combines proper Kubernetes configuration, secure application code, and operational best practices. By following the patterns and examples in this guide, you can:

  • Securely manage sensitive data without hardcoding credentials
  • Implement reliable secret rotation to maintain security compliance
  • Monitor secret usage for security auditing and troubleshooting
  • Handle secret failures gracefully with proper error handling and fallbacks
  • Integrate with existing ecosystems like HashiCorp Vault or AWS Secrets Manager

The key to successful secrets management is treating secrets as first-class citizens in your application architecture, with the same level of care and attention as your application code and infrastructure.

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