A complete guide to securely handling secrets, credentials, and sensitive configuration in Java applications.
Table of Contents
- The Problem with Secrets
- Secret Management Principles
- Implementation Approaches
- Cloud Secret Managers
- Encryption Strategies
- Secure Coding Practices
- 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
- Never store secrets in code
- Use minimal privilege - only access what's needed
- Encrypt secrets at rest and in transit
- Rotate secrets regularly
- Audit secret access
- 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
- Never log secrets - Use masking in logs
- Use secure memory handling - Clear char arrays after use
- Implement proper key rotation - Automated and regular
- Audit all secret access - Who accessed what and when
- Use minimal privilege - Principle of least privilege
- Secure transmission - TLS for all API calls
- Regular security scanning - For dependencies and code
- 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.