16 KiB
16 KiB
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
<!-- AWS Secrets Manager -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>secretsmanager</artifactId>
</dependency>
<!-- AWS Secrets Manager Caching -->
<dependency>
<groupId>com.amazonaws.secretsmanager</groupId>
<artifactId>aws-secretsmanager-caching-java</artifactId>
<version>2.0.0</version> // Use the latest version compatible with sdk v2
</dependency>
<!-- Spring Boot Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Jackson for JSON processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- Connection Pooling -->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</dependency>
Configuration Properties
application.yml
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
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
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> T getSecretAsObject(String secretName, Class<T> 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<String, String> getSecretAsMap(String secretName) {
try {
String secretJson = secretCache.getSecretString(secretName);
return objectMapper.readValue(secretJson,
new TypeReference<Map<String, String>>() {});
} 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
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<String, String> 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
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
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<SecretsService> {
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
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<String, String> 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<String> entity = new HttpEntity<>(headers);
ResponseEntity<String> response = restTemplate.exchange(
endpoint,
HttpMethod.GET,
entity,
String.class);
return response.getBody();
}
}
Configuration for REST Template
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
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
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
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<String, String> expectedMap = Map.of("key", "value");
when(secretCache.getSecretString(secretName))
.thenReturn(secretJson);
Map<String, String> result = secretsService.getSecretAsMap(secretName);
assertEquals(expectedMap, result);
}
}
Best Practices
-
Environment-Specific Configuration:
- Use different secret names for development, staging, and production
- Implement proper environment variable management
- Use Spring profiles for environment-specific configurations
-
Security Considerations:
- Never log secret values
- Use appropriate IAM roles and policies
- Enable encryption in transit and at rest
- Implement proper access controls
-
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
-
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
-
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