Comprehensive Secret Management Best Practices in Java

A complete guide to securely handling secrets, credentials, and sensitive configuration in Java applications.

Table of Contents

  1. The Problem with Secrets
  2. Secret Management Principles
  3. Implementation Approaches
  4. Cloud Secret Managers
  5. Encryption Strategies
  6. Secure Coding Practices
  7. Monitoring & Audit

The Problem with Secrets

Common Anti-Patterns

  • Hardcoded secrets in source code
  • Plaintext configuration files
  • Environment variables for sensitive data
  • Version control containing secrets
  • Logging sensitive data

Types of Secrets

public enum SecretType {
DATABASE_CREDENTIALS,
API_KEYS,
SSL_CERTIFICATES,
ENCRYPTION_KEYS,
OAUTH_TOKENS,
SSH_KEYS,
JWT_SECRETS,
SERVICE_ACCOUNT_KEYS
}

Secret Management Principles

Core Principles

  1. Never store secrets in code
  2. Use minimal privilege - only access what's needed
  3. Encrypt secrets at rest and in transit
  4. Rotate secrets regularly
  5. Audit secret access
  6. Use secure storage backends

Implementation Approaches

1. Environment-Based Configuration

@Component
public class EnvironmentSecretProvider {
private static final Logger logger = LoggerFactory.getLogger(EnvironmentSecretProvider.class);
public String getSecret(String key) {
return getSecret(key, null);
}
public String getSecret(String key, String defaultValue) {
String value = System.getenv(key);
if (value == null) {
value = System.getProperty(key);
}
if (value == null && defaultValue == null) {
throw new SecretNotFoundException("Secret not found for key: " + key);
}
return value != null ? value : defaultValue;
}
public char[] getSecretAsChars(String key) {
String value = getSecret(key);
return value != null ? value.toCharArray() : null;
}
// Secure cleanup
public void clearSecret(char[] secret) {
if (secret != null) {
Arrays.fill(secret, '\0');
}
}
}

2. Configuration Service with Encryption

@Service
public class SecureConfigService {
private final EncryptionService encryptionService;
private final Map<String, String> secretCache = new ConcurrentHashMap<>();
@Value("${app.secrets.master-key-env-var:APP_MASTER_KEY}")
private String masterKeyEnvVar;
public SecureConfigService(EncryptionService encryptionService) {
this.encryptionService = encryptionService;
}
public String getSecureProperty(String encryptedValue) {
if (encryptedValue == null) return null;
return secretCache.computeIfAbsent(encryptedValue, 
enc -> encryptionService.decrypt(enc, getMasterKey()));
}
public void setSecureProperty(String key, String plaintextValue) {
String encrypted = encryptionService.encrypt(plaintextValue, getMasterKey());
// Store encrypted value in your configuration
System.setProperty(key, encrypted);
}
private char[] getMasterKey() {
String masterKey = System.getenv(masterKeyEnvVar);
if (masterKey == null) {
throw new IllegalStateException("Master key not found in environment variable: " + masterKeyEnvVar);
}
return masterKey.toCharArray();
}
@PreDestroy
public void cleanup() {
secretCache.clear();
}
}

3. Secrets Manager Interface

public interface SecretsManager {
String getSecret(String secretId);
String getSecret(String secretId, String version);
void putSecret(String secretId, String secretValue);
void rotateSecret(String secretId);
List<String> listSecrets();
Map<String, String> getSecretMetadata(String secretId);
}
public class SecretResponse {
private final String value;
private final Instant created;
private final Instant expires;
private final Map<String, String> metadata;
// Constructor, getters, builder...
}

Cloud Secret Managers Integration

1. AWS Secrets Manager

@Service
@Profile("aws")
public class AwsSecretsManager implements SecretsManager {
private final AWSSecretsManager client;
private final ObjectMapper objectMapper;
private final Cache<String, SecretResponse> cache;
public AwsSecretsManager(@Value("${aws.region:us-east-1}") String region) {
this.client = AWSSecretsManagerClient.builder()
.withRegion(region)
.build();
this.objectMapper = new ObjectMapper();
this.cache = Caffeine.newBuilder()
.expireAfterWrite(5, TimeUnit.MINUTES)
.maximumSize(1000)
.build();
}
@Override
public String getSecret(String secretId) {
return cache.get(secretId, this::fetchSecret).getValue();
}
@Override
public String getSecret(String secretId, String version) {
String cacheKey = secretId + "#" + version;
return cache.get(cacheKey, 
k -> fetchSecretVersion(secretId, version)).getValue();
}
private SecretResponse fetchSecret(String secretId) {
try {
GetSecretValueRequest request = GetSecretValueRequest.builder()
.secretId(secretId)
.build();
GetSecretValueResponse response = client.getSecretValue(request);
return SecretResponse.builder()
.value(parseSecretString(response.secretString()))
.created(Instant.now())
.metadata(Map.of(
"arn", response.arn(),
"createdDate", response.createdDate().toString()
))
.build();
} catch (ResourceNotFoundException e) {
throw new SecretNotFoundException("Secret not found: " + secretId, e);
} catch (Exception e) {
throw new SecretAccessException("Failed to access secret: " + secretId, e);
}
}
private String parseSecretString(String secretString) {
try {
// Handle JSON secrets
JsonNode jsonNode = objectMapper.readTree(secretString);
if (jsonNode.has("password")) {
return jsonNode.get("password").asText();
} else if (jsonNode.has("value")) {
return jsonNode.get("value").asText();
}
// Return first field if no standard keys found
Iterator<String> fieldNames = jsonNode.fieldNames();
if (fieldNames.hasNext()) {
return jsonNode.get(fieldNames.next()).asText();
}
} catch (Exception e) {
// Not JSON, return as-is
}
return secretString;
}
@Override
public void putSecret(String secretId, String secretValue) {
try {
String secretString = objectMapper.writeValueAsString(
Map.of("value", secretValue, "updated", Instant.now().toString()));
PutSecretValueRequest request = PutSecretValueRequest.builder()
.secretId(secretId)
.secretString(secretString)
.build();
client.putSecretValue(request);
// Invalidate cache
cache.invalidate(secretId);
} catch (Exception e) {
throw new SecretUpdateException("Failed to update secret: " + secretId, e);
}
}
@PreDestroy
public void shutdown() {
if (client != null) {
client.close();
}
}
}

2. Azure Key Vault

@Service
@Profile("azure")
public class AzureKeyVaultService implements SecretsManager {
private final SecretClient secretClient;
private final Cache<String, SecretResponse> cache;
public AzureKeyVaultService(
@Value("${azure.keyvault.uri}") String keyVaultUri,
TokenCredential tokenCredential) {
this.secretClient = new SecretClientBuilder()
.vaultUrl(keyVaultUri)
.credential(tokenCredential)
.buildClient();
this.cache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(1000)
.build();
}
@Override
public String getSecret(String secretName) {
return cache.get(secretName, this::fetchSecret).getValue();
}
private SecretResponse fetchSecret(String secretName) {
try {
KeyVaultSecret secret = secretClient.getSecret(secretName);
return SecretResponse.builder()
.value(secret.getValue())
.created(secret.getProperties().getCreatedOn().toInstant())
.expires(secret.getProperties().getExpiresOn() != null ? 
secret.getProperties().getExpiresOn().toInstant() : null)
.metadata(Map.of(
"version", secret.getProperties().getVersion(),
"enabled", String.valueOf(secret.getProperties().isEnabled())
))
.build();
} catch (ResourceNotFoundException e) {
throw new SecretNotFoundException("Secret not found: " + secretName, e);
}
}
}

3. Google Cloud Secret Manager

@Service
@Profile("gcp")
public class GcpSecretManager implements SecretsManager {
private final SecretManagerServiceClient client;
private final String projectId;
private final Cache<String, SecretResponse> cache;
public GcpSecretManager(
SecretManagerServiceClient client,
@Value("${spring.cloud.gcp.project-id}") String projectId) {
this.client = client;
this.projectId = projectId;
this.cache = Caffeine.newBuilder()
.expireAfterWrite(15, TimeUnit.MINUTES)
.maximumSize(1000)
.build();
}
@Override
public String getSecret(String secretId) {
return getSecret(secretId, "latest");
}
@Override
public String getSecret(String secretId, String version) {
String cacheKey = secretId + "#" + version;
return cache.get(cacheKey, k -> fetchSecret(secretId, version)).getValue();
}
private SecretResponse fetchSecret(String secretId, String version) {
try {
SecretVersionName secretVersionName = SecretVersionName.of(projectId, secretId, version);
AccessSecretVersionResponse response = client.accessSecretVersion(secretVersionName);
String secretValue = response.getPayload().getData().toStringUtf8();
return SecretResponse.builder()
.value(secretValue)
.created(Instant.now()) // Would parse from secret metadata
.build();
} catch (StatusRuntimeException e) {
if (e.getStatus().getCode() == Status.Code.NOT_FOUND) {
throw new SecretNotFoundException("Secret not found: " + secretId, e);
}
throw new SecretAccessException("Failed to access secret: " + secretId, e);
}
}
}

Encryption Strategies

1. Master Key Encryption Service

@Service
public class AesGcmEncryptionService {
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 static final int SALT_LENGTH_BYTE = 16;
private static final int KEY_LENGTH_BIT = 256;
private final SecureRandom secureRandom = new SecureRandom();
public String encrypt(String plaintext, char[] password) {
try {
// Generate salt and IV
byte[] salt = new byte[SALT_LENGTH_BYTE];
byte[] iv = new byte[IV_LENGTH_BYTE];
secureRandom.nextBytes(salt);
secureRandom.nextBytes(iv);
// Derive key from password
SecretKey key = deriveKey(password, salt);
// Encrypt
Cipher cipher = Cipher.getInstance(ALGORITHM);
GCMParameterSpec parameterSpec = new GCMParameterSpec(TAG_LENGTH_BIT, iv);
cipher.init(Cipher.ENCRYPT_MODE, key, parameterSpec);
byte[] cipherText = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
// Combine salt + iv + ciphertext
byte[] encrypted = new byte[salt.length + iv.length + cipherText.length];
System.arraycopy(salt, 0, encrypted, 0, salt.length);
System.arraycopy(iv, 0, encrypted, salt.length, iv.length);
System.arraycopy(cipherText, 0, encrypted, salt.length + iv.length, cipherText.length);
return Base64.getEncoder().encodeToString(encrypted);
} catch (Exception e) {
throw new EncryptionException("Encryption failed", e);
}
}
public String decrypt(String encryptedData, char[] password) {
try {
byte[] decoded = Base64.getDecoder().decode(encryptedData);
// Extract components
byte[] salt = Arrays.copyOfRange(decoded, 0, SALT_LENGTH_BYTE);
byte[] iv = Arrays.copyOfRange(decoded, SALT_LENGTH_BYTE, SALT_LENGTH_BYTE + IV_LENGTH_BYTE);
byte[] cipherText = Arrays.copyOfRange(decoded, SALT_LENGTH_BYTE + IV_LENGTH_BYTE, decoded.length);
// Derive key
SecretKey key = deriveKey(password, salt);
// Decrypt
Cipher cipher = Cipher.getInstance(ALGORITHM);
GCMParameterSpec parameterSpec = new GCMParameterSpec(TAG_LENGTH_BIT, iv);
cipher.init(Cipher.DECRYPT_MODE, key, parameterSpec);
byte[] plaintext = cipher.doFinal(cipherText);
return new String(plaintext, StandardCharsets.UTF_8);
} catch (Exception e) {
throw new EncryptionException("Decryption failed", e);
}
}
private SecretKey deriveKey(char[] password, byte[] salt) {
try {
PBEKeySpec spec = new PBEKeySpec(password, salt, 65536, KEY_LENGTH_BIT);
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
byte[] keyBytes = factory.generateSecret(spec).getEncoded();
return new SecretKeySpec(keyBytes, "AES");
} catch (Exception e) {
throw new EncryptionException("Key derivation failed", e);
}
}
}

2. Key Rotation Service

@Service
public class KeyRotationService {
private final SecretsManager secretsManager;
private final AesGcmEncryptionService encryptionService;
private final Map<String, String> keyVersions = new ConcurrentHashMap<>();
@Value("${app.encryption.current-key-version:v1}")
private String currentKeyVersion;
public KeyRotationService(SecretsManager secretsManager, 
AesGcmEncryptionService encryptionService) {
this.secretsManager = secretsManager;
this.encryptionService = encryptionService;
loadKeyVersions();
}
public void rotateMasterKey() {
try {
// Generate new key
String newKeyVersion = "v" + (keyVersions.size() + 1);
String newMasterKey = generateRandomKey();
// Store new key in secrets manager
secretsManager.putSecret("app/master-key/" + newKeyVersion, newMasterKey);
// Update current version
this.currentKeyVersion = newKeyVersion;
keyVersions.put(newKeyVersion, newMasterKey);
// Re-encrypt sensitive data (asynchronous)
reencryptDataWithNewKey(newKeyVersion, newMasterKey);
} catch (Exception e) {
throw new KeyRotationException("Key rotation failed", e);
}
}
public String getCurrentMasterKey() {
return keyVersions.get(currentKeyVersion);
}
public char[] getCurrentMasterKeyAsChars() {
String key = getCurrentMasterKey();
return key != null ? key.toCharArray() : null;
}
private void loadKeyVersions() {
// Load all key versions from secrets manager
List<String> secretKeys = secretsManager.listSecrets().stream()
.filter(key -> key.startsWith("app/master-key/"))
.collect(Collectors.toList());
for (String secretKey : secretKeys) {
String version = secretKey.substring("app/master-key/".length());
String keyValue = secretsManager.getSecret(secretKey);
keyVersions.put(version, keyValue);
}
}
private String generateRandomKey() {
byte[] keyBytes = new byte[32]; // 256 bits
new SecureRandom().nextBytes(keyBytes);
return Base64.getEncoder().encodeToString(keyBytes);
}
@Async
public void reencryptDataWithNewKey(String newKeyVersion, String newMasterKey) {
// Implementation for re-encrypting all data with new key
// This should process data in batches
logger.info("Starting data re-encryption with key version: {}", newKeyVersion);
// Example: Re-encrypt database credentials, API keys, etc.
// This is application-specific
}
}

Secure Coding Practices

1. Secure Configuration Class

@Configuration
@ConfigurationProperties(prefix = "app.security")
@Validated
public class SecurityConfig {
@NotNull
private SecretsManagerType secretsManager;
@NotBlank
private String masterKeyEnvVar;
private boolean enableKeyRotation = true;
private int keyRotationDays = 90;
private boolean auditSecretAccess = true;
// Getters and setters
public SecretsManagerType getSecretsManager() { return secretsManager; }
public void setSecretsManager(SecretsManagerType secretsManager) { 
this.secretsManager = secretsManager; 
}
public String getMasterKeyEnvVar() { return masterKeyEnvVar; }
public void setMasterKeyEnvVar(String masterKeyEnvVar) { 
this.masterKeyEnvVar = masterKeyEnvVar; 
}
public boolean isEnableKeyRotation() { return enableKeyRotation; }
public void setEnableKeyRotation(boolean enableKeyRotation) { 
this.enableKeyRotation = enableKeyRotation; 
}
public int getKeyRotationDays() { return keyRotationDays; }
public void setKeyRotationDays(int keyRotationDays) { 
this.keyRotationDays = keyRotationDays; 
}
public boolean isAuditSecretAccess() { return auditSecretAccess; }
public void setAuditSecretAccess(boolean auditSecretAccess) { 
this.auditSecretAccess = auditSecretAccess; 
}
public enum SecretsManagerType {
AWS, AZURE, GCP, HASHICORP_VAULT, CUSTOM
}
}

2. Secret Protection Aspect

@Aspect
@Component
public class SecretProtectionAspect {
private static final Logger logger = LoggerFactory.getLogger(SecretProtectionAspect.class);
private static final Set<String> SENSITIVE_PARAMS = Set.of(
"password", "secret", "key", "token", "credential"
);
@Around("execution(* *(.., @SecretParameter (*), ..))")
public Object protectSecretParameters(ProceedingJoinPoint joinPoint) throws Throwable {
Object[] args = joinPoint.getArgs();
Object[] protectedArgs = new Object[args.length];
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof String || args[i] instanceof char[]) {
protectedArgs[i] = args[i];
} else {
protectedArgs[i] = args[i];
}
}
try {
return joinPoint.proceed(protectedArgs);
} finally {
// Cleanup sensitive data
for (int i = 0; i < protectedArgs.length; i++) {
if (protectedArgs[i] instanceof char[]) {
Arrays.fill((char[]) protectedArgs[i], '\0');
}
}
}
}
@AfterThrowing(pointcut = "execution(* com.yourcompany..*.*(..))", throwing = "ex")
public void logSecretAccessError(JoinPoint joinPoint, Exception ex) {
if (ex.getMessage() != null && 
(ex.getMessage().contains("secret") || ex.getMessage().contains("password"))) {
logger.warn("Potential secret access error in {}: {}", 
joinPoint.getSignature(), ex.getMessage());
}
}
}

3. Secure Password Handling

@Component
public class SecurePasswordManager {
private final SCryptPasswordEncoder sCryptEncoder;
private final BCryptPasswordEncoder bCryptEncoder;
public SecurePasswordManager() {
this.sCryptEncoder = new SCryptPasswordEncoder(16384, 8, 1, 32, 64);
this.bCryptEncoder = new BCryptPasswordEncoder(12);
}
public String hashPassword(char[] password) {
try {
return sCryptEncoder.encode(CharBuffer.wrap(password));
} finally {
Arrays.fill(password, '\0');
}
}
public boolean verifyPassword(char[] rawPassword, String encodedPassword) {
try {
return sCryptEncoder.matches(CharBuffer.wrap(rawPassword), encodedPassword);
} finally {
Arrays.fill(rawPassword, '\0');
}
}
public String generateSecureRandomPassword(int length) {
String upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
String lower = "abcdefghijklmnopqrstuvwxyz";
String digits = "0123456789";
String special = "!@#$%^&*()-_=+[]{}|;:,.<>?";
String all = upper + lower + digits + special;
SecureRandom random = new SecureRandom();
StringBuilder password = new StringBuilder(length);
// Ensure at least one of each type
password.append(upper.charAt(random.nextInt(upper.length())));
password.append(lower.charAt(random.nextInt(lower.length())));
password.append(digits.charAt(random.nextInt(digits.length())));
password.append(special.charAt(random.nextInt(special.length())));
// Fill remaining
for (int i = 4; i < length; i++) {
password.append(all.charAt(random.nextInt(all.length())));
}
// Shuffle
return shuffleString(password.toString());
}
private String shuffleString(String input) {
List<Character> characters = input.chars()
.mapToObj(c -> (char) c)
.collect(Collectors.toList());
Collections.shuffle(characters);
return characters.stream()
.collect(StringBuilder::new, StringBuilder::append, StringBuilder::append)
.toString();
}
}

Monitoring & Audit

1. Secret Access Auditor

@Component
public class SecretAccessAuditor {
private static final Logger auditLogger = LoggerFactory.getLogger("SECURITY_AUDIT");
public void auditSecretAccess(String secretId, String operation, String user) {
auditLogger.info("Secret access - ID: {}, Operation: {}, User: {}, Timestamp: {}, IP: {}", 
secretId, operation, user, Instant.now(), getClientIp());
}
public void auditSecretCreation(String secretId, String user) {
auditLogger.info("Secret created - ID: {}, User: {}, Timestamp: {}", 
secretId, user, Instant.now());
}
public void auditSecretDeletion(String secretId, String user) {
auditLogger.warn("Secret deleted - ID: {}, User: {}, Timestamp: {}", 
secretId, user, Instant.now());
}
private String getClientIp() {
// Implementation to get client IP
return "unknown";
}
}

2. Health Check for Secret Services

@Component
public class SecretsManagerHealthIndicator implements HealthIndicator {
private final SecretsManager secretsManager;
public SecretsManagerHealthIndicator(SecretsManager secretsManager) {
this.secretsManager = secretsManager;
}
@Override
public Health health() {
try {
// Test access with a dummy operation
secretsManager.listSecrets();
return Health.up()
.withDetail("service", "secrets-manager")
.withDetail("timestamp", Instant.now())
.build();
} catch (Exception e) {
return Health.down(e)
.withDetail("service", "secrets-manager")
.withDetail("error", e.getMessage())
.build();
}
}
}

Complete Spring Boot Configuration

application.yml

app:
security:
secrets-manager: AWS
master-key-env-var: APP_MASTER_KEY
enable-key-rotation: true
key-rotation-days: 90
audit-secret-access: true
encryption:
algorithm: AES/GCM/NoPadding
key-length: 256
iv-length: 12
spring:
profiles:
active: ${SPRING_PROFILES_ACTIVE:default}
cloud:
vault:
enabled: false
management:
endpoints:
web:
exposure:
include: health,info,metrics
endpoint:
health:
show-details: when_authorized
logging:
level:
com.yourcompany.security: DEBUG
SECURITY_AUDIT: INFO

Main Application Class

@SpringBootApplication
@EnableConfigurationProperties(SecurityConfig.class)
@EnableAsync
@EnableScheduling
@EnableAspectJAutoProxy
public class SecureApplication {
public static void main(String[] args) {
SpringApplication.run(SecureApplication.class, args);
}
@Bean
@Profile("!test")
public SecretsManager secretsManager(SecurityConfig securityConfig) {
return switch (securityConfig.getSecretsManager()) {
case AWS -> new AwsSecretsManager();
case AZURE -> new AzureKeyVaultService();
case GCP -> new GcpSecretManager();
case HASHICORP_VAULT -> new HashiCorpVaultService();
default -> new EnvironmentSecretProvider();
};
}
@Bean
public EncryptionService encryptionService() {
return new AesGcmEncryptionService();
}
}

Best Practices Summary

  1. Never log secrets - Use masking in logs
  2. Use secure memory handling - Clear char arrays after use
  3. Implement proper key rotation - Automated and regular
  4. Audit all secret access - Who accessed what and when
  5. Use minimal privilege - Principle of least privilege
  6. Secure transmission - TLS for all API calls
  7. Regular security scanning - For dependencies and code
  8. Secure disposal - Properly wipe sensitive data from memory

This comprehensive approach ensures your Java applications handle secrets securely while maintaining operational efficiency and compliance with security standards.

Leave a Reply

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


Macro Nepal Helper