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.