GCP Secret Manager Integration in Java: Comprehensive Secret Management

Google Cloud Platform's Secret Manager provides secure storage and management of sensitive data like API keys, passwords, and certificates. It offers versioning, access control, and audit logging for secrets.


Architecture Overview

Key Concepts:

  • Secrets: Containers for sensitive data with multiple versions
  • Versions: Immutable instances of secret data
  • Access Control: IAM-based permissions for secrets
  • Replication: Automatic replication across regions
  • Audit Logging: Comprehensive access logging

Java Application → Secret Manager → Encrypted Storage

  • Applications access secrets via GCP client libraries
  • Secret Manager handles encryption, access control, and auditing
  • Secrets are stored encrypted in Google's infrastructure

Dependencies and Setup

Maven Dependencies
<properties>
<google-cloud-secretmanager.version>2.4.0</google-cloud-secretmanager.version>
<google-cloud-bom.version>26.22.0</google-cloud-bom.version>
<spring-boot.version>3.1.0</spring-boot.version>
<spring-cloud-gcp.version>4.5.0</spring-cloud-gcp.version>
</properties>
<dependencies>
<!-- GCP Secret Manager -->
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>google-cloud-secretmanager</artifactId>
<version>${google-cloud-secretmanager.version}</version>
</dependency>
<!-- Spring Cloud GCP -->
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>spring-cloud-gcp-starter-secretmanager</artifactId>
<version>${spring-cloud-gcp.version}</version>
</dependency>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<!-- Caching -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.1.6</version>
</dependency>
<!-- JSON Processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version>
</dependency>
</dependencies>
GCP Configuration
@Configuration
public class GcpConfig {
@Bean
@ConditionalOnMissingBean
public SecretManagerServiceClient secretManagerServiceClient() throws IOException {
return SecretManagerServiceClient.create();
}
@Bean
public SecretManagerServiceSettings secretManagerServiceSettings() throws IOException {
return SecretManagerServiceSettings.newBuilder()
.setCredentialsProvider(GoogleCredentials::getApplicationDefault)
.build();
}
}
Application Properties
# application.yaml
spring:
application:
name: secret-manager-demo
cloud:
gcp:
project-id: ${GCP_PROJECT:my-project-id}
secretmanager:
enabled: true
cache:
enabled: true
ttl: 300000 # 5 minutes
app:
secrets:
cache:
enabled: true
ttl-minutes: 30
max-size: 1000
rotation:
enabled: true
check-interval-minutes: 5
encryption:
enabled: true
management:
endpoints:
web:
exposure:
include: health,info,metrics,secrets
endpoint:
health:
show-details: always
secrets:
enabled: true
logging:
level:
com.google.cloud.secretmanager: INFO
com.example.secretmanager: DEBUG

Core Secret Manager Service

1. Secret Manager Service
@Service
@Slf4j
public class GcpSecretManagerService {
private final SecretManagerServiceClient secretManagerClient;
private final String projectId;
private final SecretProperties secretProperties;
public GcpSecretManagerService(SecretManagerServiceClient secretManagerClient,
@Value("${spring.cloud.gcp.project-id}") String projectId,
SecretProperties secretProperties) {
this.secretManagerClient = secretManagerClient;
this.projectId = projectId;
this.secretProperties = secretProperties;
}
/**
* Create a new secret
*/
public Secret createSecret(String secretId, Map<String, String> labels) {
try {
log.info("Creating secret: {}", secretId);
Secret secret = Secret.newBuilder()
.setReplication(
Replication.newBuilder()
.setAutomatic(Replication.Automatic.newBuilder().build())
.build())
.putAllLabels(labels != null ? labels : Map.of())
.build();
CreateSecretRequest request = CreateSecretRequest.newBuilder()
.setParent(ProjectName.of(projectId).toString())
.setSecretId(secretId)
.setSecret(secret)
.build();
Secret createdSecret = secretManagerClient.createSecret(request);
log.info("Successfully created secret: {}", secretId);
return createdSecret;
} catch (Exception e) {
log.error("Failed to create secret: {}", secretId, e);
throw new SecretOperationException("Failed to create secret: " + secretId, e);
}
}
/**
* Add a secret version with payload
*/
public SecretVersion addSecretVersion(String secretId, String payload) {
try {
log.debug("Adding version to secret: {}", secretId);
SecretPayload secretPayload = SecretPayload.newBuilder()
.setData(ByteString.copyFromUtf8(payload))
.build();
AddSecretVersionRequest request = AddSecretVersionRequest.newBuilder()
.setParent(SecretName.of(projectId, secretId).toString())
.setPayload(secretPayload)
.build();
SecretVersion version = secretManagerClient.addSecretVersion(request);
log.info("Successfully added version to secret: {} (version: {})", 
secretId, version.getName());
return version;
} catch (Exception e) {
log.error("Failed to add version to secret: {}", secretId, e);
throw new SecretOperationException("Failed to add version to secret: " + secretId, e);
}
}
/**
* Create secret with initial version
*/
public Secret createSecretWithVersion(String secretId, String payload, Map<String, String> labels) {
Secret secret = createSecret(secretId, labels);
addSecretVersion(secretId, payload);
return secret;
}
/**
* Access the latest version of a secret
*/
public String accessSecret(String secretId) {
return accessSecretVersion(secretId, "latest");
}
/**
* Access specific version of a secret
*/
public String accessSecretVersion(String secretId, String versionId) {
try {
log.debug("Accessing secret: {} version: {}", secretId, versionId);
AccessSecretVersionRequest request = AccessSecretVersionRequest.newBuilder()
.setName(SecretVersionName.of(projectId, secretId, versionId).toString())
.build();
AccessSecretVersionResponse response = secretManagerClient.accessSecretVersion(request);
String payload = response.getPayload().getData().toStringUtf8();
log.debug("Successfully accessed secret: {} version: {}", secretId, versionId);
return payload;
} catch (Exception e) {
log.error("Failed to access secret: {} version: {}", secretId, versionId, e);
throw new SecretAccessException("Failed to access secret: " + secretId, e);
}
}
/**
* List all secrets in the project
*/
public List<Secret> listSecrets() {
try {
log.debug("Listing all secrets in project: {}", projectId);
ListSecretsRequest request = ListSecretsRequest.newBuilder()
.setParent(ProjectName.of(projectId).toString())
.build();
List<Secret> secrets = new ArrayList<>();
for (Secret secret : secretManagerClient.listSecrets(request).iterateAll()) {
secrets.add(secret);
}
log.info("Found {} secrets in project: {}", secrets.size(), projectId);
return secrets;
} catch (Exception e) {
log.error("Failed to list secrets", e);
throw new SecretOperationException("Failed to list secrets", e);
}
}
/**
* List versions of a specific secret
*/
public List<SecretVersion> listSecretVersions(String secretId) {
try {
log.debug("Listing versions for secret: {}", secretId);
ListSecretVersionsRequest request = ListSecretVersionsRequest.newBuilder()
.setParent(SecretName.of(projectId, secretId).toString())
.build();
List<SecretVersion> versions = new ArrayList<>();
for (SecretVersion version : secretManagerClient.listSecretVersions(request).iterateAll()) {
versions.add(version);
}
log.debug("Found {} versions for secret: {}", versions.size(), secretId);
return versions;
} catch (Exception e) {
log.error("Failed to list versions for secret: {}", secretId, e);
throw new SecretOperationException("Failed to list versions for secret: " + secretId, e);
}
}
/**
* Delete a secret
*/
public void deleteSecret(String secretId) {
try {
log.info("Deleting secret: {}", secretId);
DeleteSecretRequest request = DeleteSecretRequest.newBuilder()
.setName(SecretName.of(projectId, secretId).toString())
.build();
secretManagerClient.deleteSecret(request);
log.info("Successfully deleted secret: {}", secretId);
} catch (Exception e) {
log.error("Failed to delete secret: {}", secretId, e);
throw new SecretOperationException("Failed to delete secret: " + secretId, e);
}
}
/**
* Disable a secret version
*/
public SecretVersion disableSecretVersion(String secretId, String versionId) {
try {
log.info("Disabling secret version: {} for secret: {}", versionId, secretId);
DisableSecretVersionRequest request = DisableSecretVersionRequest.newBuilder()
.setName(SecretVersionName.of(projectId, secretId, versionId).toString())
.build();
SecretVersion disabledVersion = secretManagerClient.disableSecretVersion(request);
log.info("Successfully disabled secret version: {} for secret: {}", versionId, secretId);
return disabledVersion;
} catch (Exception e) {
log.error("Failed to disable secret version: {} for secret: {}", versionId, secretId, e);
throw new SecretOperationException("Failed to disable secret version", e);
}
}
/**
* Enable a secret version
*/
public SecretVersion enableSecretVersion(String secretId, String versionId) {
try {
log.info("Enabling secret version: {} for secret: {}", versionId, secretId);
EnableSecretVersionRequest request = EnableSecretVersionRequest.newBuilder()
.setName(SecretVersionName.of(projectId, secretId, versionId).toString())
.build();
SecretVersion enabledVersion = secretManagerClient.enableSecretVersion(request);
log.info("Successfully enabled secret version: {} for secret: {}", versionId, secretId);
return enabledVersion;
} catch (Exception e) {
log.error("Failed to enable secret version: {} for secret: {}", versionId, secretId, e);
throw new SecretOperationException("Failed to enable secret version", e);
}
}
/**
* Destroy a secret version (permanently delete)
*/
public SecretVersion destroySecretVersion(String secretId, String versionId) {
try {
log.warn("Destroying secret version: {} for secret: {}", versionId, secretId);
DestroySecretVersionRequest request = DestroySecretVersionRequest.newBuilder()
.setName(SecretVersionName.of(projectId, secretId, versionId).toString())
.build();
SecretVersion destroyedVersion = secretManagerClient.destroySecretVersion(request);
log.warn("Successfully destroyed secret version: {} for secret: {}", versionId, secretId);
return destroyedVersion;
} catch (Exception e) {
log.error("Failed to destroy secret version: {} for secret: {}", versionId, secretId, e);
throw new SecretOperationException("Failed to destroy secret version", e);
}
}
/**
* Get secret metadata
*/
public Secret getSecret(String secretId) {
try {
log.debug("Getting secret metadata: {}", secretId);
GetSecretRequest request = GetSecretRequest.newBuilder()
.setName(SecretName.of(projectId, secretId).toString())
.build();
Secret secret = secretManagerClient.getSecret(request);
log.debug("Successfully retrieved secret metadata: {}", secretId);
return secret;
} catch (Exception e) {
log.error("Failed to get secret metadata: {}", secretId, e);
throw new SecretOperationException("Failed to get secret: " + secretId, e);
}
}
/**
* Update secret labels
*/
public Secret updateSecretLabels(String secretId, Map<String, String> labels) {
try {
log.info("Updating labels for secret: {}", secretId);
Secret currentSecret = getSecret(secretId);
Secret updatedSecret = currentSecret.toBuilder()
.clearLabels()
.putAllLabels(labels)
.build();
UpdateSecretRequest request = UpdateSecretRequest.newBuilder()
.setSecret(updatedSecret)
.setUpdateMask(FieldMask.newBuilder().addPaths("labels").build())
.build();
Secret result = secretManagerClient.updateSecret(request);
log.info("Successfully updated labels for secret: {}", secretId);
return result;
} catch (Exception e) {
log.error("Failed to update labels for secret: {}", secretId, e);
throw new SecretOperationException("Failed to update secret labels: " + secretId, e);
}
}
}
2. Configuration Properties
@ConfigurationProperties(prefix = "app.secrets")
@Data
public class SecretProperties {
private Cache cache = new Cache();
private Rotation rotation = new Rotation();
private Encryption encryption = new Encryption();
@Data
public static class Cache {
private boolean enabled = true;
private long ttlMinutes = 30;
private int maxSize = 1000;
}
@Data
public static class Rotation {
private boolean enabled = true;
private long checkIntervalMinutes = 5;
private long warningDaysBeforeExpiry = 7;
}
@Data
public static class Encryption {
private boolean enabled = false;
private String algorithm = "AES/GCM/NoPadding";
}
}
3. Cached Secret Service
@Service
@Slf4j
@EnableCaching
public class CachedSecretService {
private final GcpSecretManagerService secretManagerService;
private final SecretProperties secretProperties;
private final ObjectMapper objectMapper;
private final Cache<String, String> secretCache;
private final Cache<String, Secret> metadataCache;
public CachedSecretService(GcpSecretManagerService secretManagerService,
SecretProperties secretProperties,
ObjectMapper objectMapper) {
this.secretManagerService = secretManagerService;
this.secretProperties = secretProperties;
this.objectMapper = objectMapper;
// Initialize caches
this.secretCache = Caffeine.newBuilder()
.expireAfterWrite(secretProperties.getCache().getTtlMinutes(), TimeUnit.MINUTES)
.maximumSize(secretProperties.getCache().getMaxSize())
.build();
this.metadataCache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES) // Shorter TTL for metadata
.maximumSize(500)
.build();
}
/**
* Get secret value with caching
*/
@Cacheable(value = "secrets", key = "#secretId", unless = "#result == null")
public String getSecret(String secretId) {
if (!secretProperties.getCache().isEnabled()) {
return secretManagerService.accessSecret(secretId);
}
return secretCache.get(secretId, id -> {
log.debug("Cache miss for secret: {}, fetching from GCP", id);
return secretManagerService.accessSecret(id);
});
}
/**
* Get secret value without caching
*/
public String getSecretUncached(String secretId) {
return secretManagerService.accessSecret(secretId);
}
/**
* Get secret as specific type (JSON parsing)
*/
public <T> T getSecretAs(String secretId, Class<T> type) {
try {
String secretValue = getSecret(secretId);
return objectMapper.readValue(secretValue, type);
} catch (Exception e) {
log.error("Failed to parse secret {} as type {}", secretId, type.getSimpleName(), e);
throw new SecretParseException("Failed to parse secret: " + secretId, e);
}
}
/**
* Get secret metadata with caching
*/
public Secret getSecretMetadata(String secretId) {
return metadataCache.get(secretId, secretManagerService::getSecret);
}
/**
* Invalidate cache for a specific secret
*/
public void invalidateCache(String secretId) {
log.debug("Invalidating cache for secret: {}", secretId);
secretCache.invalidate(secretId);
metadataCache.invalidate(secretId);
}
/**
* Invalidate entire cache
*/
public void invalidateAllCache() {
log.info("Invalidating entire secret cache");
secretCache.invalidateAll();
metadataCache.invalidateAll();
}
/**
* Get cache statistics
*/
public CacheStats getCacheStats() {
return secretCache.stats();
}
/**
* Check if secret exists and is accessible
*/
public boolean isSecretAccessible(String secretId) {
try {
getSecretMetadata(secretId);
return true;
} catch (Exception e) {
log.debug("Secret is not accessible: {}", secretId);
return false;
}
}
/**
* Get multiple secrets in batch
*/
public Map<String, String> getSecretsBatch(List<String> secretIds) {
Map<String, String> results = new HashMap<>();
for (String secretId : secretIds) {
try {
String value = getSecret(secretId);
results.put(secretId, value);
} catch (Exception e) {
log.warn("Failed to get secret in batch: {}", secretId, e);
results.put(secretId, null);
}
}
return results;
}
}
4. Secret Rotation Service
@Service
@Slf4j
public class SecretRotationService {
private final GcpSecretManagerService secretManagerService;
private final CachedSecretService cachedSecretService;
private final SecretProperties secretProperties;
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
public SecretRotationService(GcpSecretManagerService secretManagerService,
CachedSecretService cachedSecretService,
SecretProperties secretProperties) {
this.secretManagerService = secretManagerService;
this.cachedSecretService = cachedSecretService;
this.secretProperties = secretProperties;
if (secretProperties.getRotation().isEnabled()) {
startRotationMonitoring();
}
}
/**
* Start monitoring for secret rotation
*/
@PostConstruct
public void startRotationMonitoring() {
long interval = secretProperties.getRotation().getCheckIntervalMinutes();
scheduler.scheduleAtFixedRate(this::checkForRotations, 0, interval, TimeUnit.MINUTES);
log.info("Started secret rotation monitoring with {} minute interval", interval);
}
/**
* Check for secret rotations and update caches
*/
public void checkForRotations() {
try {
log.debug("Checking for secret rotations");
List<Secret> secrets = secretManagerService.listSecrets();
int rotatedCount = 0;
for (Secret secret : secrets) {
String secretId = extractSecretId(secret.getName());
try {
SecretVersion latestVersion = getLatestVersion(secretId);
if (isVersionNewer(secretId, latestVersion)) {
log.info("Detected rotation for secret: {}", secretId);
cachedSecretService.invalidateCache(secretId);
rotatedCount++;
}
} catch (Exception e) {
log.warn("Failed to check rotation for secret: {}", secretId, e);
}
}
if (rotatedCount > 0) {
log.info("Detected rotations for {} secrets, caches invalidated", rotatedCount);
}
} catch (Exception e) {
log.error("Error during rotation check", e);
}
}
/**
* Rotate a secret (create new version)
*/
public SecretVersion rotateSecret(String secretId, String newPayload) {
log.info("Rotating secret: {}", secretId);
try {
// Add new version
SecretVersion newVersion = secretManagerService.addSecretVersion(secretId, newPayload);
// Disable old versions (keep last 2 enabled for rollback)
disableOldVersions(secretId);
// Invalidate cache
cachedSecretService.invalidateCache(secretId);
log.info("Successfully rotated secret: {}, new version: {}", 
secretId, extractVersionId(newVersion.getName()));
return newVersion;
} catch (Exception e) {
log.error("Failed to rotate secret: {}", secretId, e);
throw new SecretOperationException("Failed to rotate secret: " + secretId, e);
}
}
/**
* Automated secret rotation with key generation
*/
public SecretVersion rotateSecretAutomated(String secretId) {
log.info("Performing automated rotation for secret: {}", secretId);
try {
// Generate new secure value
String newValue = generateSecureSecret();
// Rotate the secret
return rotateSecret(secretId, newValue);
} catch (Exception e) {
log.error("Failed to perform automated rotation for secret: {}", secretId, e);
throw new SecretOperationException("Automated rotation failed for: " + secretId, e);
}
}
/**
* Check if secret needs rotation based on age
*/
public boolean needsRotation(String secretId) {
try {
SecretVersion latestVersion = getLatestVersion(secretId);
Instant createTime = Instant.ofEpochSecond(latestVersion.getCreateTime().getSeconds());
Instant rotationThreshold = Instant.now().minus(90, ChronoUnit.DAYS); // 90 days
return createTime.isBefore(rotationThreshold);
} catch (Exception e) {
log.warn("Failed to check rotation need for secret: {}", secretId, e);
return false;
}
}
/**
* Get secrets that need rotation
*/
public List<String> getSecretsNeedingRotation() {
List<String> needingRotation = new ArrayList<>();
try {
List<Secret> secrets = secretManagerService.listSecrets();
for (Secret secret : secrets) {
String secretId = extractSecretId(secret.getName());
if (needsRotation(secretId)) {
needingRotation.add(secretId);
}
}
} catch (Exception e) {
log.error("Failed to get secrets needing rotation", e);
}
return needingRotation;
}
private SecretVersion getLatestVersion(String secretId) {
List<SecretVersion> versions = secretManagerService.listSecretVersions(secretId);
return versions.stream()
.filter(v -> v.getState() == SecretVersion.State.ENABLED)
.max(Comparator.comparing(v -> Instant.ofEpochSecond(v.getCreateTime().getSeconds())))
.orElseThrow(() -> new SecretNotFoundException("No enabled versions found for: " + secretId));
}
private boolean isVersionNewer(String secretId, SecretVersion version) {
// Implementation to check if this version is newer than cached version
// This would require storing version metadata in cache
return true; // Simplified implementation
}
private void disableOldVersions(String secretId) {
List<SecretVersion> versions = secretManagerService.listSecretVersions(secretId);
// Keep last 2 enabled versions, disable older ones
versions.stream()
.filter(v -> v.getState() == SecretVersion.State.ENABLED)
.sorted(Comparator.comparing(v -> Instant.ofEpochSecond(v.getCreateTime().getSeconds())))
.limit(Math.max(0, versions.size() - 2))
.forEach(v -> {
String versionId = extractVersionId(v.getName());
secretManagerService.disableSecretVersion(secretId, versionId);
});
}
private String generateSecureSecret() {
SecureRandom random = new SecureRandom();
byte[] bytes = new byte[32];
random.nextBytes(bytes);
return Base64.getEncoder().encodeToString(bytes);
}
private String extractSecretId(String secretName) {
String[] parts = secretName.split("/");
return parts[parts.length - 1];
}
private String extractVersionId(String versionName) {
String[] parts = versionName.split("/");
return parts[parts.length - 1];
}
@PreDestroy
public void shutdown() {
scheduler.shutdown();
try {
if (!scheduler.awaitTermination(10, TimeUnit.SECONDS)) {
scheduler.shutdownNow();
}
} catch (InterruptedException e) {
scheduler.shutdownNow();
Thread.currentThread().interrupt();
}
}
}

Integration with Spring Configuration

1. Secret-Backed Configuration
@Configuration
@Slf4j
public class SecretConfiguration {
private final CachedSecretService cachedSecretService;
public SecretConfiguration(CachedSecretService cachedSecretService) {
this.cachedSecretService = cachedSecretService;
}
@Bean
@ConfigurationProperties(prefix = "app.database")
public DatabaseConfig databaseConfig() {
return new DatabaseConfig();
}
@Bean
public DataSource dataSource(DatabaseConfig databaseConfig) {
HikariConfig config = new HikariConfig();
// Get secrets from Secret Manager
String password = cachedSecretService.getSecret(databaseConfig.getPasswordSecretId());
String username = cachedSecretService.getSecret(databaseConfig.getUsernameSecretId());
config.setJdbcUrl(databaseConfig.getUrl());
config.setUsername(username);
config.setPassword(password);
config.setDriverClassName("org.postgresql.Driver");
config.setMaximumPoolSize(10);
config.setMinimumIdle(2);
config.setConnectionTimeout(30000);
config.setIdleTimeout(600000);
config.setMaxLifetime(1800000);
log.info("Configured datasource with secret-backed credentials");
return new HikariDataSource(config);
}
@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
// Get Redis password from Secret Manager
String password = cachedSecretService.getSecret("redis-password");
config.setHostName("localhost");
config.setPort(6379);
config.setPassword(RedisPassword.of(password));
log.info("Configured Redis connection with secret-backed password");
return new LettuceConnectionFactory(config);
}
@Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
// Add interceptor for API keys from Secret Manager
restTemplate.getInterceptors().add((request, body, execution) -> {
String apiKey = cachedSecretService.getSecret("external-api-key");
request.getHeaders().set("X-API-Key", apiKey);
return execution.execute(request, body);
});
return restTemplate;
}
}
@Data
@ConfigurationProperties(prefix = "app.database")
class DatabaseConfig {
private String url;
private String usernameSecretId;
private String passwordSecretId;
private String driverClassName;
}
2. Spring Cloud GCP Bootstrap Configuration
@Configuration
public class GcpBootstrapConfiguration {
@Bean
public SecretManagerPropertySourceLocator secretManagerPropertySourceLocator(
SecretManagerServiceClient secretManagerClient,
@Value("${spring.cloud.gcp.project-id}") String projectId) {
return new SecretManagerPropertySourceLocator(secretManagerClient, projectId);
}
@Bean
@ConditionalOnMissingBean
public SecretManagerTemplate secretManagerTemplate(SecretManagerServiceClient secretManagerClient) {
return new SecretManagerTemplate(secretManagerClient);
}
}
3. Environment Post-Processor
public class SecretManagerEnvironmentPostProcessor implements EnvironmentPostProcessor {
private static final String PREFIX = "sm://";
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, 
SpringApplication application) {
Map<String, Object> secretProperties = new HashMap<>();
// Resolve secrets in property values
for (PropertySource<?> propertySource : environment.getPropertySources()) {
if (propertySource instanceof EnumerablePropertySource) {
EnumerablePropertySource<?> enumerable = (EnumerablePropertySource<?>) propertySource;
for (String key : enumerable.getPropertyNames()) {
Object value = propertySource.getProperty(key);
if (value instanceof String && ((String) value).startsWith(PREFIX)) {
String secretId = ((String) value).substring(PREFIX.length());
String secretValue = resolveSecret(secretId);
secretProperties.put(key, secretValue);
}
}
}
}
if (!secretProperties.isEmpty()) {
environment.getPropertySources()
.addFirst(new MapPropertySource("secret-manager", secretProperties));
}
}
private String resolveSecret(String secretId) {
// Implementation to fetch secret from GCP Secret Manager
// This would use the GCP client libraries
try {
// Simplified implementation
return "resolved-secret-value";
} catch (Exception e) {
throw new RuntimeException("Failed to resolve secret: " + secretId, e);
}
}
}

REST API Controllers

1. Secret Management Controller
@RestController
@RequestMapping("/api/secrets")
@Slf4j
@Validated
public class SecretController {
private final GcpSecretManagerService secretManagerService;
private final CachedSecretService cachedSecretService;
private final SecretRotationService rotationService;
public SecretController(GcpSecretManagerService secretManagerService,
CachedSecretService cachedSecretService,
SecretRotationService rotationService) {
this.secretManagerService = secretManagerService;
this.cachedSecretService = cachedSecretService;
this.rotationService = rotationService;
}
@PostMapping
public ResponseEntity<ApiResponse<Secret>> createSecret(
@Valid @RequestBody CreateSecretRequest request) {
try {
Secret secret = secretManagerService.createSecretWithVersion(
request.getSecretId(),
request.getPayload(),
request.getLabels());
return ResponseEntity.ok(ApiResponse.success(secret));
} catch (Exception e) {
log.error("Failed to create secret: {}", request.getSecretId(), e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApiResponse.error("Failed to create secret: " + e.getMessage()));
}
}
@GetMapping("/{secretId}")
public ResponseEntity<ApiResponse<String>> getSecret(
@PathVariable String secretId,
@RequestParam(required = false) String version) {
try {
String secretValue = (version != null) 
? secretManagerService.accessSecretVersion(secretId, version)
: cachedSecretService.getSecret(secretId);
// Mask the value for security in logs
String maskedValue = maskSecret(secretValue);
log.debug("Retrieved secret: {} (masked: {})", secretId, maskedValue);
return ResponseEntity.ok(ApiResponse.success(secretValue));
} catch (SecretNotFoundException e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(ApiResponse.error("Secret not found: " + secretId));
} catch (Exception e) {
log.error("Failed to get secret: {}", secretId, e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApiResponse.error("Failed to get secret: " + e.getMessage()));
}
}
@GetMapping("/{secretId}/metadata")
public ResponseEntity<ApiResponse<Secret>> getSecretMetadata(@PathVariable String secretId) {
try {
Secret metadata = cachedSecretService.getSecretMetadata(secretId);
return ResponseEntity.ok(ApiResponse.success(metadata));
} catch (SecretNotFoundException e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(ApiResponse.error("Secret not found: " + secretId));
} catch (Exception e) {
log.error("Failed to get secret metadata: {}", secretId, e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApiResponse.error("Failed to get secret metadata: " + e.getMessage()));
}
}
@GetMapping("/{secretId}/versions")
public ResponseEntity<ApiResponse<List<SecretVersion>>> getSecretVersions(
@PathVariable String secretId) {
try {
List<SecretVersion> versions = secretManagerService.listSecretVersions(secretId);
return ResponseEntity.ok(ApiResponse.success(versions));
} catch (Exception e) {
log.error("Failed to get secret versions: {}", secretId, e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApiResponse.error("Failed to get secret versions: " + e.getMessage()));
}
}
@PostMapping("/{secretId}/rotate")
public ResponseEntity<ApiResponse<SecretVersion>> rotateSecret(
@PathVariable String secretId,
@RequestBody(required = false) RotateSecretRequest request) {
try {
SecretVersion newVersion;
if (request != null && request.getPayload() != null) {
newVersion = secretManagerService.addSecretVersion(secretId, request.getPayload());
} else {
newVersion = rotationService.rotateSecretAutomated(secretId);
}
cachedSecretService.invalidateCache(secretId);
return ResponseEntity.ok(ApiResponse.success(newVersion));
} catch (Exception e) {
log.error("Failed to rotate secret: {}", secretId, e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApiResponse.error("Failed to rotate secret: " + e.getMessage()));
}
}
@PutMapping("/{secretId}/labels")
public ResponseEntity<ApiResponse<Secret>> updateSecretLabels(
@PathVariable String secretId,
@RequestBody Map<String, String> labels) {
try {
Secret updatedSecret = secretManagerService.updateSecretLabels(secretId, labels);
cachedSecretService.invalidateCache(secretId);
return ResponseEntity.ok(ApiResponse.success(updatedSecret));
} catch (Exception e) {
log.error("Failed to update secret labels: {}", secretId, e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApiResponse.error("Failed to update secret labels: " + e.getMessage()));
}
}
@DeleteMapping("/{secretId}")
public ResponseEntity<ApiResponse<String>> deleteSecret(@PathVariable String secretId) {
try {
secretManagerService.deleteSecret(secretId);
cachedSecretService.invalidateCache(secretId);
return ResponseEntity.ok(ApiResponse.success("Secret deleted successfully"));
} catch (Exception e) {
log.error("Failed to delete secret: {}", secretId, e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApiResponse.error("Failed to delete secret: " + e.getMessage()));
}
}
@PostMapping("/cache/invalidate/{secretId}")
public ResponseEntity<ApiResponse<String>> invalidateCache(@PathVariable String secretId) {
try {
cachedSecretService.invalidateCache(secretId);
return ResponseEntity.ok(ApiResponse.success("Cache invalidated for secret: " + secretId));
} catch (Exception e) {
log.error("Failed to invalidate cache for secret: {}", secretId, e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApiResponse.error("Failed to invalidate cache: " + e.getMessage()));
}
}
@GetMapping("/rotation/needed")
public ResponseEntity<ApiResponse<List<String>>> getSecretsNeedingRotation() {
try {
List<String> secrets = rotationService.getSecretsNeedingRotation();
return ResponseEntity.ok(ApiResponse.success(secrets));
} catch (Exception e) {
log.error("Failed to get secrets needing rotation", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApiResponse.error("Failed to get secrets needing rotation: " + e.getMessage()));
}
}
private String maskSecret(String secret) {
if (secret == null || secret.length() <= 4) {
return "****";
}
return secret.substring(0, 2) + "****" + secret.substring(secret.length() - 2);
}
}
@Data
@Validated
class CreateSecretRequest {
@NotBlank
@Pattern(regexp = "^[a-zA-Z0-9_-]+$")
private String secretId;
@NotBlank
private String payload;
private Map<String, String> labels;
}
@Data
class RotateSecretRequest {
private String payload;
}
@Data
@AllArgsConstructor
class ApiResponse<T> {
private boolean success;
private String message;
private T data;
private Instant timestamp;
public static <T> ApiResponse<T> success(T data) {
return new ApiResponse<>(true, "Success", data, Instant.now());
}
public static <T> ApiResponse<T> error(String message) {
return new ApiResponse<>(false, message, null, Instant.now());
}
}
2. Health Check Controller
@RestController
@RequestMapping("/api/health")
@Slf4j
public class HealthController {
private final GcpSecretManagerService secretManagerService;
private final CachedSecretService cachedSecretService;
public HealthController(GcpSecretManagerService secretManagerService,
CachedSecretService cachedSecretService) {
this.secretManagerService = secretManagerService;
this.cachedSecretService = cachedSecretService;
}
@GetMapping("/secrets")
public ResponseEntity<SecretHealth> checkSecretManagerHealth() {
SecretHealth health = new SecretHealth();
try {
// Test connectivity by listing secrets
List<Secret> secrets = secretManagerService.listSecrets();
health.setStatus(HealthStatus.UP);
health.setSecretCount(secrets.size());
health.setMessage("Secret Manager is accessible");
// Check cache health
CacheStats cacheStats = cachedSecretService.getCacheStats();
health.setCacheHitRate(cacheStats.hitRate());
health.setCacheSize(cacheStats.requestCount());
} catch (Exception e) {
health.setStatus(HealthStatus.DOWN);
health.setMessage("Secret Manager is not accessible: " + e.getMessage());
log.error("Secret Manager health check failed", e);
}
HttpStatus status = health.getStatus() == HealthStatus.UP ? 
HttpStatus.OK : HttpStatus.SERVICE_UNAVAILABLE;
return ResponseEntity.status(status).body(health);
}
@GetMapping("/secrets/{secretId}")
public ResponseEntity<SecretAccessHealth> checkSecretAccess(
@PathVariable String secretId) {
SecretAccessHealth health = new SecretAccessHealth();
health.setSecretId(secretId);
try {
boolean accessible = cachedSecretService.isSecretAccessible(secretId);
health.setStatus(accessible ? HealthStatus.UP : HealthStatus.DOWN);
health.setMessage(accessible ? "Secret is accessible" : "Secret is not accessible");
} catch (Exception e) {
health.setStatus(HealthStatus.DOWN);
health.setMessage("Failed to check secret access: " + e.getMessage());
}
HttpStatus status = health.getStatus() == HealthStatus.UP ? 
HttpStatus.OK : HttpStatus.SERVICE_UNAVAILABLE;
return ResponseEntity.status(status).body(health);
}
}
@Data
class SecretHealth {
private HealthStatus status;
private String message;
private int secretCount;
private double cacheHitRate;
private long cacheSize;
private Instant timestamp = Instant.now();
}
@Data
class SecretAccessHealth {
private String secretId;
private HealthStatus status;
private String message;
private Instant timestamp = Instant.now();
}
enum HealthStatus {
UP, DOWN
}

Security and Best Practices

1. IAM Role Management
@Service
@Slf4j
public class IamSecurityService {
/**
* Recommended IAM roles for different use cases
*/
public enum SecretManagerRole {
ADMIN("roles/secretmanager.admin"),
SECRET_ACCESSOR("roles/secretmanager.secretAccessor"),
SECRET_VERSION_MANAGER("roles/secretmanager.secretVersionManager"),
VIEWER("roles/secretmanager.viewer");
private final String role;
SecretManagerRole(String role) {
this.role = role;
}
public String getRole() {
return role;
}
}
/**
* Validate if current service account has required permissions
*/
public boolean validatePermissions(SecretManagerRole requiredRole) {
// In production, this would use the IAM API to check permissions
// For this example, we'll assume proper IAM configuration
log.debug("Validating IAM permission for role: {}", requiredRole.getRole());
return true;
}
/**
* Get recommended roles for different team members
*/
public Map<String, List<SecretManagerRole>> getRecommendedRoleAssignments() {
return Map.of(
"developers", List.of(SecretManagerRole.SECRET_ACCESSOR),
"devops", List.of(SecretManagerRole.SECRET_VERSION_MANAGER),
"security-team", List.of(SecretManagerRole.ADMIN, SecretManagerRole.VIEWER),
"ci-cd-service", List.of(SecretManagerRole.SECRET_ACCESSOR)
);
}
}
2. Audit Logging Service
@Service
@Slf4j
public class AuditService {
private static final Logger auditLogger = LoggerFactory.getLogger("AUDIT");
public void logSecretAccess(String secretId, String operation, String user, boolean success) {
AuditEntry entry = AuditEntry.builder()
.timestamp(Instant.now())
.secretId(secretId)
.operation(operation)
.user(user)
.success(success)
.build();
auditLogger.info("SECRET_ACCESS: {}", entry);
}
public void logSecretModification(String secretId, String operation, String user, String details) {
AuditEntry entry = AuditEntry.builder()
.timestamp(Instant.now())
.secretId(secretId)
.operation(operation)
.user(user)
.details(details)
.build();
auditLogger.warn("SECRET_MODIFICATION: {}", entry);
}
@Data
@Builder
private static class AuditEntry {
private Instant timestamp;
private String secretId;
private String operation;
private String user;
private boolean success;
private String details;
@Override
public String toString() {
return String.format(
"timestamp=%s, secretId=%s, operation=%s, user=%s, success=%s, details=%s",
timestamp, secretId, operation, user, success, details);
}
}
}

Custom Exceptions

public class SecretOperationException extends RuntimeException {
public SecretOperationException(String message) {
super(message);
}
public SecretOperationException(String message, Throwable cause) {
super(message, cause);
}
}
public class SecretAccessException extends RuntimeException {
public SecretAccessException(String message) {
super(message);
}
public SecretAccessException(String message, Throwable cause) {
super(message, cause);
}
}
public class SecretNotFoundException extends RuntimeException {
public SecretNotFoundException(String message) {
super(message);
}
public SecretNotFoundException(String message, Throwable cause) {
super(message, cause);
}
}
public class SecretParseException extends RuntimeException {
public SecretParseException(String message) {
super(message);
}
public SecretParseException(String message, Throwable cause) {
super(message, cause);
}
}
@ControllerAdvice
public class SecretExceptionHandler {
@ExceptionHandler(SecretNotFoundException.class)
public ResponseEntity<ApiResponse<?>> handleSecretNotFound(SecretNotFoundException e) {
log.warn("Secret not found: {}", e.getMessage());
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(ApiResponse.error(e.getMessage()));
}
@ExceptionHandler(SecretAccessException.class)
public ResponseEntity<ApiResponse<?>> handleSecretAccess(SecretAccessException e) {
log.error("Secret access denied: {}", e.getMessage());
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(ApiResponse.error(e.getMessage()));
}
@ExceptionHandler(SecretOperationException.class)
public ResponseEntity<ApiResponse<?>> handleSecretOperation(SecretOperationException e) {
log.error("Secret operation failed: {}", e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApiResponse.error(e.getMessage()));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiResponse<?>> handleGenericException(Exception e) {
log.error("Unexpected error in secret management", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApiResponse.error("An unexpected error occurred"));
}
}

Testing

1. Unit Tests
@ExtendWith(MockitoExtension.class)
class GcpSecretManagerServiceTest {
@Mock
private SecretManagerServiceClient secretManagerClient;
@InjectMocks
private GcpSecretManagerService secretManagerService;
@Test
void shouldCreateSecret() {
// Given
String secretId = "test-secret";
Map<String, String> labels = Map.of("env", "test");
when(secretManagerClient.createSecret(any(CreateSecretRequest.class)))
.thenReturn(Secret.newBuilder().setName("projects/test/secrets/" + secretId).build());
// When
Secret result = secretManagerService.createSecret(secretId, labels);
// Then
assertThat(result).isNotNull();
verify(secretManagerClient).createSecret(any(CreateSecretRequest.class));
}
}
@SpringBootTest
class SecretManagerIntegrationTest {
@Autowired
private GcpSecretManagerService secretManagerService;
@Test
void shouldManageSecretLifecycle() {
// Integration test with real or mocked GCP Secret Manager
// This would test the complete CRUD operations
}
}

Best Practices

  1. Least Privilege: Assign minimal required IAM roles
  2. Secret Rotation: Implement regular secret rotation
  3. Access Logging: Enable Cloud Audit Logs for Secret Manager
  4. Version Management: Keep only necessary secret versions
  5. Network Security: Use VPC Service Controls where needed
  6. Cost Optimization: Clean up unused secrets and versions
// Example of automated cleanup
@Component
@Slf4j
public class SecretCleanupService {
private final GcpSecretManagerService secretManagerService;
@Scheduled(cron = "0 0 1 * * ?") // Daily at 1 AM
public void cleanupOldSecretVersions() {
log.info("Starting secret version cleanup");
try {
List<Secret> secrets = secretManagerService.listSecrets();
for (Secret secret : secrets) {
String secretId = extractSecretId(secret.getName());
cleanupVersionsForSecret(secretId);
}
} catch (Exception e) {
log.error("Failed to cleanup secret versions", e);
}
}
private void cleanupVersionsForSecret(String secretId) {
try {
List<SecretVersion> versions = secretManagerService.listSecretVersions(secretId);
// Keep only last 5 versions, disable older ones
versions.stream()
.sorted(Comparator.comparing(v -> Instant.ofEpochSecond(v.getCreateTime().getSeconds())))
.limit(Math.max(0, versions.size() - 5))
.forEach(v -> {
String versionId = extractVersionId(v.getName());
secretManagerService.disableSecretVersion(secretId, versionId);
});
} catch (Exception e) {
log.warn("Failed to cleanup versions for secret: {}", secretId, e);
}
}
private String extractSecretId(String secretName) {
String[] parts = secretName.split("/");
return parts[parts.length - 1];
}
private String extractVersionId(String versionName) {
String[] parts = versionName.split("/");
return parts[parts.length - 1];
}
}

Conclusion

GCP Secret Manager integration in Java provides:

  • Secure Storage: Encrypted at rest and in transit
  • Access Control: Fine-grained IAM permissions
  • Version Management: Track and manage secret versions
  • Audit Trail: Comprehensive access logging
  • Integration: Seamless integration with Spring applications
  • Caching: Performance optimization with cache layers
  • Rotation: Automated secret rotation capabilities

By implementing the patterns shown above, you can build robust, secure, and maintainable secret management solutions that leverage GCP's managed services while providing excellent developer experience and operational reliability.

Java Logistics, Shipping Integration & Enterprise Inventory Automation (Tracking, ERP, RFID & Billing Systems)

https://macronepal.com/blog/aftership-tracking-in-java-enterprise-package-visibility/
Explains how to integrate AfterShip tracking services into Java applications to provide real-time shipment visibility, delivery status updates, and centralized tracking across multiple courier services.

https://macronepal.com/blog/shipping-integration-using-fedex-api-with-java-for-logistics-automation/
Explains how to integrate the FedEx API into Java systems to automate shipping tasks such as creating shipments, calculating delivery costs, generating shipping labels, and tracking packages.

https://macronepal.com/blog/shipping-and-logistics-integrating-ups-apis-with-java-applications/
Explains UPS API integration in Java to enable automated shipping operations including rate calculation, shipment scheduling, tracking, and delivery confirmation management.

https://macronepal.com/blog/generating-and-reading-qr-codes-for-products-in-java/
Explains how Java applications generate and read QR codes for product identification, tracking, and authentication, supporting faster inventory handling and product verification processes.

https://macronepal.com/blog/designing-a-robust-pick-and-pack-workflow-in-java/
Explains how to design an efficient pick-and-pack workflow in Java warehouse systems, covering order processing, item selection, packaging steps, and logistics preparation to improve fulfillment efficiency.

https://macronepal.com/blog/rfid-inventory-management-system-in-java-a-complete-guide/
Explains how RFID technology integrates with Java applications to automate inventory tracking, reduce manual errors, and enable real-time stock monitoring in warehouses and retail environments.

https://macronepal.com/blog/erp-integration-with-odoo-in-java/
Explains how Java applications connect with Odoo ERP systems to synchronize inventory, orders, customer records, and financial data across enterprise systems.

https://macronepal.com/blog/automated-invoice-generation-creating-professional-excel-invoices-with-apache-poi-in-java/
Explains how to automatically generate professional Excel invoices in Java using Apache POI, enabling structured billing documents and automated financial record creation.

https://macronepal.com/blog/enterprise-financial-integration-using-quickbooks-api-in-java-applications/
Explains QuickBooks API integration in Java to automate financial workflows such as invoice management, payment tracking, accounting synchronization, and financial reporting.

Leave a Reply

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


Macro Nepal Helper