# AWS Secrets Manager Spring Boot Integration ## Overview Integrate AWS Secrets Manager with Spring Boot applications using the caching library for optimal performance and security. ## Dependencies ### Required Dependencies ```xml software.amazon.awssdk secretsmanager com.amazonaws.secretsmanager aws-secretsmanager-caching-java 2.0.0 // Use the latest version compatible with sdk v2 org.springframework.boot spring-boot-starter-web com.fasterxml.jackson.core jackson-databind com.zaxxer HikariCP ``` ## Configuration Properties ### application.yml ```yaml spring: application: name: aws-secrets-manager-app datasource: url: jdbc:postgresql://localhost:5432/mydb username: ${db.username} password: ${db.password} hikari: maximum-pool-size: 10 minimum-idle: 5 aws: secrets: region: us-east-1 # Secret names for different environments database-credentials: prod/database/credentials api-keys: prod/external-api/keys redis-config: prod/redis/config app: external-api: secret-name: prod/external/credentials base-url: https://api.example.com ``` ## Core Components ### SecretsManager Configuration ```java import com.amazonaws.secretsmanager.caching.SecretCache; import com.amazonaws.secretsmanager.caching.SecretCacheConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient; @Configuration public class SecretsManagerConfiguration { @Value("${aws.secrets.region}") private String region; @Bean public SecretsManagerClient secretsManagerClient() { return SecretsManagerClient.builder() .region(Region.of(region)) .build(); } @Bean public SecretCache secretCache(SecretsManagerClient secretsClient) { SecretCacheConfiguration config = SecretCacheConfiguration.builder() .maxCacheSize(100) .cacheItemTTL(3600000) // 1 hour .build(); return new SecretCache(secretsClient, config); } } ``` ### Secrets Service ```java import com.amazonaws.secretsmanager.caching.SecretCache; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.stereotype.Service; import java.util.Map; @Service public class SecretsService { private final SecretCache secretCache; private final ObjectMapper objectMapper; public SecretsService(SecretCache secretCache, ObjectMapper objectMapper) { this.secretCache = secretCache; this.objectMapper = objectMapper; } /** * Get secret as string */ public String getSecret(String secretName) { try { return secretCache.getSecretString(secretName); } catch (Exception e) { throw new RuntimeException("Failed to retrieve secret: " + secretName, e); } } /** * Get secret as object of specified type */ public T getSecretAsObject(String secretName, Class type) { try { String secretJson = secretCache.getSecretString(secretName); return objectMapper.readValue(secretJson, type); } catch (Exception e) { throw new RuntimeException("Failed to parse secret: " + secretName, e); } } /** * Get secret as Map */ public Map getSecretAsMap(String secretName) { try { String secretJson = secretCache.getSecretString(secretName); return objectMapper.readValue(secretJson, new TypeReference>() {}); } catch (Exception e) { throw new RuntimeException("Failed to parse secret map: " + secretName, e); } } /** * Get secret with fallback */ public String getSecretWithFallback(String secretName, String defaultValue) { try { String secret = secretCache.getSecretString(secretName); return secret != null ? secret : defaultValue; } catch (Exception e) { return defaultValue; } } } ``` ## Database Configuration Integration ### Dynamic DataSource Configuration ```java import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.sql.DataSource; @Configuration public class DatabaseConfiguration { private final SecretsService secretsService; @Value("${aws.secrets.database-credentials}") private String dbSecretName; public DatabaseConfiguration(SecretsService secretsService) { this.secretsService = secretsService; } @Bean public DataSource dataSource() { Map credentials = secretsService.getSecretAsMap(dbSecretName); HikariConfig config = new HikariConfig(); config.setJdbcUrl(credentials.get("url")); config.setUsername(credentials.get("username")); config.setPassword(credentials.get("password")); config.setMaximumPoolSize(10); config.setMinimumIdle(5); config.setConnectionTimeout(30000); config.setIdleTimeout(600000); config.setMaxLifetime(1800000); config.setLeakDetectionThreshold(15000); return new HikariDataSource(config); } } ``` ### Configuration Properties with Secrets ```java import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @Component @ConfigurationProperties(prefix = "app") public class AppProperties { private final SecretsService secretsService; @Value("${app.external-api.secret-name}") private String apiSecretName; public AppProperties(SecretsService secretsService) { this.secretsService = secretsService; } private String apiKey; public String getApiKey() { if (apiKey == null) { apiKey = secretsService.getSecret(apiSecretName); } return apiKey; } // Additional application properties private String externalApiBaseUrl; public String getExternalApiBaseUrl() { return externalApiBaseUrl; } public void setExternalApiBaseUrl(String externalApiBaseUrl) { this.externalApiBaseUrl = externalApiBaseUrl; } } ``` ## Property Source Integration ### Custom Property Source ```java import org.springframework.core.env.Environment; import org.springframework.core.env.PropertySource; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; import java.util.HashMap; import java.util.Map; @Component public class SecretsManagerPropertySource extends PropertySource { public static final String SECRETS_MANAGER_PROPERTY_SOURCE_NAME = "secretsManagerPropertySource"; private final SecretsService secretsService; private final Environment environment; public SecretsManagerPropertySource(SecretsService secretsService, Environment environment) { super(SECRETS_MANAGER_PROPERTY_SOURCE_NAME, secretsService); this.secretsService = secretsService; this.environment = environment; } @PostConstruct public void loadSecrets() { // Load secrets specified in application.yml String secretPrefix = "aws.secrets."; environment.getPropertyNames().forEach(propertyName -> { if (propertyName.startsWith(secretPrefix)) { String secretName = environment.getProperty(propertyName); String secretValue = secretsService.getSecret(secretName); if (secretValue != null) { // Add to property source (note: this is simplified) // In practice, you'd need to work with PropertySources } } }); } @Override public Object getProperty(String name) { if (name.startsWith("aws.secret.")) { String secretName = name.substring("aws.secret.".length()); return secretsService.getSecret(secretName); } return null; } } ``` ## API Integration ### REST Client with Secrets ```java import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; @Service public class ExternalApiClient { private final SecretsService secretsService; private final RestTemplate restTemplate; private final AppProperties appProperties; public ExternalApiClient(SecretsService secretsService, RestTemplate restTemplate, AppProperties appProperties) { this.secretsService = secretsService; this.restTemplate = restTemplate; this.appProperties = appProperties; } public String callExternalApi(String endpoint) { Map apiCredentials = secretsService.getSecretAsMap( appProperties.getExternalApiSecretName()); HttpHeaders headers = new HttpHeaders(); headers.set("Authorization", "Bearer " + apiCredentials.get("api_token")); headers.set("X-API-Key", apiCredentials.get("api_key")); headers.set("Content-Type", "application/json"); HttpEntity entity = new HttpEntity<>(headers); ResponseEntity response = restTemplate.exchange( endpoint, HttpMethod.GET, entity, String.class); return response.getBody(); } } ``` ### Configuration for REST Template ```java import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.client.RestTemplate; @Configuration public class RestTemplateConfiguration { @Bean public RestTemplate restTemplate() { return new RestTemplate(); } } ``` ## Security Configuration ### Security Setup ```java import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.web.SecurityFilterChain; @Configuration @EnableWebSecurity public class SecurityConfiguration { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth .requestMatchers("/api/secrets/**").hasRole("ADMIN") .anyRequest().permitAll() ) .httpBasic() .and() .csrf().disable(); return http.build(); } } ``` ## Testing Configuration ### Test Configuration ```java import com.amazonaws.secretsmanager.caching.SecretCache; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Primary; import org.springframework.mock.env.MockEnvironment; import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient; import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueResponse; import static org.mockito.Mockito.*; @TestConfiguration public class TestSecretsConfiguration { @Bean @Primary public SecretsManagerClient secretsManagerClient() { SecretsManagerClient mockClient = mock(SecretsManagerClient.class); // Mock successful secret retrieval when(mockClient.getSecretValue(any())) .thenReturn(GetSecretValueResponse.builder() .secretString("{\"username\":\"test\",\"password\":\"testpass\"}") .build()); return mockClient; } @Bean @Primary public SecretCache secretCache(SecretsManagerClient mockClient) { SecretCache mockCache = mock(SecretCache.class); when(mockCache.getSecretString(anyString())) .thenReturn("{\"username\":\"test\",\"password\":\"testpass\"}"); return mockCache; } @Bean public MockEnvironment mockEnvironment() { MockEnvironment env = new MockEnvironment(); env.setProperty("aws.secrets.region", "us-east-1"); env.setProperty("aws.secrets.database-credentials", "test-db-credentials"); return env; } } ``` ### Unit Tests ```java import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import static org.mockito.Mockito.*; import static org.junit.jupiter.api.Assertions.*; @ExtendWith(MockitoExtension.class) class SecretsServiceTest { @Mock private SecretCache secretCache; @InjectMocks private SecretsService secretsService; @Test void shouldGetSecret() { String secretName = "test-secret"; String expectedValue = "secret-value"; when(secretCache.getSecretString(secretName)) .thenReturn(expectedValue); String result = secretsService.getSecret(secretName); assertEquals(expectedValue, result); verify(secretCache).getSecretString(secretName); } @Test void shouldGetSecretAsMap() throws Exception { String secretName = "test-secret"; String secretJson = "{\"key\":\"value\"}"; Map expectedMap = Map.of("key", "value"); when(secretCache.getSecretString(secretName)) .thenReturn(secretJson); Map result = secretsService.getSecretAsMap(secretName); assertEquals(expectedMap, result); } } ``` ## Best Practices 1. **Environment-Specific Configuration**: - Use different secret names for development, staging, and production - Implement proper environment variable management - Use Spring profiles for environment-specific configurations 2. **Security Considerations**: - Never log secret values - Use appropriate IAM roles and policies - Enable encryption in transit and at rest - Implement proper access controls 3. **Performance Optimization**: - Use caching for frequently accessed secrets - Configure appropriate TTL values - Monitor cache hit rates and adjust accordingly - Use connection pooling for database connections 4. **Error Handling**: - Implement fallback mechanisms for critical secrets - Handle partial secret retrieval gracefully - Provide meaningful error messages without exposing sensitive information - Implement circuit breakers for external API calls 5. **Monitoring and Logging**: - Monitor secret retrieval performance - Track cache hit/miss ratios - Log secret access patterns (without values) - Set up alerts for abnormal secret access patterns