Docker Secrets in Java Applications

Introduction

Docker Secrets provide a secure way to manage sensitive data like passwords, API keys, and certificates in Docker Swarm services. For Java applications, Docker Secrets enable secure handling of credentials without exposing them in environment variables or configuration files.

Architecture Overview

1. Docker Secrets Flow

Java Application → Docker Secret → Swarm Manager → Encrypted Storage
↓                ↓               ↓              ↓
Read Secret      Mounted File    Raft Logs      Encrypted
↓                ↓               ↓              ↓
Use in App       /run/secrets/   Distributed    At Rest

Setup and Dependencies

1. Maven Dependencies

<properties>
<spring-boot.version>3.1.0</spring-boot.version>
<jackson.version>2.15.2</jackson.version>
</properties>
<dependencies>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<!-- Spring Boot Actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<!-- Configuration Processor -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<version>${spring-boot.version}</version>
<optional>true</optional>
</dependency>
<!-- Jackson for JSON -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
</dependencies>

Core Docker Secrets Implementation

1. Docker Secrets Service

@Service
@Slf4j
public class DockerSecretsService {
private static final String SECRETS_BASE_PATH = "/run/secrets/";
private final Map<String, String> secretCache = new ConcurrentHashMap<>();
private final ObjectMapper objectMapper;
public DockerSecretsService(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
/**
* Read a secret from Docker secrets mount point
*/
public Optional<String> readSecret(String secretName) {
try {
// Check cache first
if (secretCache.containsKey(secretName)) {
return Optional.of(secretCache.get(secretName));
}
Path secretPath = Paths.get(SECRETS_BASE_PATH + secretName);
if (!Files.exists(secretPath)) {
log.warn("Secret file not found: {}", secretPath);
return Optional.empty();
}
String secretValue = Files.readString(secretPath).trim();
secretCache.put(secretName, secretValue);
log.debug("Successfully read secret: {}", secretName);
return Optional.of(secretValue);
} catch (Exception e) {
log.error("Failed to read secret: {}", secretName, e);
return Optional.empty();
}
}
/**
* Read secret with fallback to environment variable
*/
public String readSecretWithFallback(String secretName, String envVarName, String defaultValue) {
return readSecret(secretName)
.orElseGet(() -> {
String envValue = System.getenv(envVarName);
if (envValue != null && !envValue.trim().isEmpty()) {
log.info("Using environment variable for: {}", secretName);
return envValue.trim();
}
log.warn("Using default value for: {}", secretName);
return defaultValue;
});
}
/**
* Read JSON secret and parse to object
*/
public <T> Optional<T> readJsonSecret(String secretName, Class<T> valueType) {
try {
Optional<String> secretContent = readSecret(secretName);
if (secretContent.isEmpty()) {
return Optional.empty();
}
T parsedObject = objectMapper.readValue(secretContent.get(), valueType);
return Optional.of(parsedObject);
} catch (Exception e) {
log.error("Failed to parse JSON secret: {}", secretName, e);
return Optional.empty();
}
}
/**
* Read multiple secrets as a map
*/
public Map<String, String> readMultipleSecrets(List<String> secretNames) {
Map<String, String> secrets = new HashMap<>();
for (String secretName : secretNames) {
readSecret(secretName).ifPresent(value -> 
secrets.put(secretName, value));
}
return secrets;
}
/**
* Check if secret exists
*/
public boolean secretExists(String secretName) {
Path secretPath = Paths.get(SECRETS_BASE_PATH + secretName);
return Files.exists(secretPath);
}
/**
* List all available secrets
*/
public List<String> listAvailableSecrets() {
try {
Path secretsDir = Paths.get(SECRETS_BASE_PATH);
if (!Files.exists(secretsDir) || !Files.isDirectory(secretsDir)) {
return Collections.emptyList();
}
return Files.list(secretsDir)
.map(path -> path.getFileName().toString())
.collect(Collectors.toList());
} catch (Exception e) {
log.error("Failed to list secrets", e);
return Collections.emptyList();
}
}
/**
* Validate secret meets complexity requirements
*/
public boolean validateSecret(String secretName, SecretValidationRule rule) {
Optional<String> secretValue = readSecret(secretName);
if (secretValue.isEmpty()) {
return false;
}
return rule.validate(secretValue.get());
}
/**
* Clear secret cache (useful for testing)
*/
public void clearCache() {
secretCache.clear();
log.debug("Secret cache cleared");
}
/**
* Monitor secret changes (for rotated secrets)
*/
@Scheduled(fixedRate = 30000) // Check every 30 seconds
public void refreshSecrets() {
List<String> availableSecrets = listAvailableSecrets();
for (String secretName : availableSecrets) {
try {
Path secretPath = Paths.get(SECRETS_BASE_PATH + secretName);
String currentValue = Files.readString(secretPath).trim();
String cachedValue = secretCache.get(secretName);
if (cachedValue == null || !cachedValue.equals(currentValue)) {
secretCache.put(secretName, currentValue);
log.info("Secret updated: {}", secretName);
// Notify about secret change
publishSecretChangeEvent(secretName);
}
} catch (Exception e) {
log.warn("Failed to refresh secret: {}", secretName, e);
}
}
}
private void publishSecretChangeEvent(String secretName) {
// Implement event publishing for secret rotation
log.debug("Secret changed: {}", secretName);
}
}

2. Secret Validation Rules

public interface SecretValidationRule {
boolean validate(String secretValue);
String getDescription();
}
@Component
public class PasswordValidationRule implements SecretValidationRule {
@Override
public boolean validate(String secretValue) {
if (secretValue == null || secretValue.length() < 8) {
return false;
}
// Check for at least one uppercase, one lowercase, one digit, one special char
boolean hasUpper = secretValue.chars().anyMatch(Character::isUpperCase);
boolean hasLower = secretValue.chars().anyMatch(Character::isLowerCase);
boolean hasDigit = secretValue.chars().anyMatch(Character::isDigit);
boolean hasSpecial = secretValue.chars()
.anyMatch(ch -> !Character.isLetterOrDigit(ch));
return hasUpper && hasLower && hasDigit && hasSpecial;
}
@Override
public String getDescription() {
return "Password must be at least 8 characters with uppercase, lowercase, digit, and special character";
}
}
@Component
public class ApiKeyValidationRule implements SecretValidationRule {
@Override
public boolean validate(String secretValue) {
if (secretValue == null || secretValue.length() < 16) {
return false;
}
// API keys should be sufficiently long and complex
return secretValue.length() >= 16 && 
secretValue.chars().anyMatch(Character::isLetter) &&
secretValue.chars().anyMatch(Character::isDigit);
}
@Override
public String getDescription() {
return "API key must be at least 16 characters with both letters and numbers";
}
}
@Component
public class JwtSecretValidationRule implements SecretValidationRule {
@Override
public boolean validate(String secretValue) {
if (secretValue == null || secretValue.length() < 32) {
return false;
}
// JWT secrets should be very strong
return secretValue.length() >= 32;
}
@Override
public String getDescription() {
return "JWT secret must be at least 32 characters";
}
}

Spring Boot Configuration Integration

1. Configuration Properties with Secrets

@Configuration
@ConfigurationProperties(prefix = "app.secrets")
@Data
@Validated
public class SecretsConfigurationProperties {
@NotNull
private DatabaseSecrets database = new DatabaseSecrets();
@NotNull
private ApiSecrets api = new ApiSecrets();
@NotNull
private SecuritySecrets security = new SecuritySecrets();
@Data
public static class DatabaseSecrets {
private String usernameSecret = "db_username";
private String passwordSecret = "db_password";
private String urlSecret = "db_url";
// Fallback environment variables
private String usernameEnv = "DB_USERNAME";
private String passwordEnv = "DB_PASSWORD";
private String urlEnv = "DB_URL";
}
@Data
public static class ApiSecrets {
private String externalApiKeySecret = "external_api_key";
private String externalApiSecret = "external_api_secret";
private String paymentGatewayKeySecret = "payment_gateway_key";
private String externalApiKeyEnv = "EXTERNAL_API_KEY";
private String externalApiSecretEnv = "EXTERNAL_API_SECRET";
private String paymentGatewayKeyEnv = "PAYMENT_GATEWAY_KEY";
}
@Data
public static class SecuritySecrets {
private String jwtSecret = "jwt_secret";
private String encryptionKeySecret = "encryption_key";
private String sslKeyStorePasswordSecret = "ssl_keystore_password";
private String jwtSecretEnv = "JWT_SECRET";
private String encryptionKeyEnv = "ENCRYPTION_KEY";
private String sslKeyStorePasswordEnv = "SSL_KEYSTORE_PASSWORD";
}
}

2. Secrets-based Configuration

@Configuration
@Slf4j
public class SecretsBasedConfiguration {
private final DockerSecretsService secretsService;
private final SecretsConfigurationProperties secretsProps;
public SecretsBasedConfiguration(DockerSecretsService secretsService,
SecretsConfigurationProperties secretsProps) {
this.secretsService = secretsService;
this.secretsProps = secretsProps;
}
@Bean
@Primary
public DataSource dataSource() {
HikariDataSource dataSource = new HikariDataSource();
// Read database secrets
String dbUrl = secretsService.readSecretWithFallback(
secretsProps.getDatabase().getUrlSecret(),
secretsProps.getDatabase().getUrlEnv(),
"jdbc:postgresql://localhost:5432/defaultdb"
);
String dbUsername = secretsService.readSecretWithFallback(
secretsProps.getDatabase().getUsernameSecret(),
secretsProps.getDatabase().getUsernameEnv(),
"defaultuser"
);
String dbPassword = secretsService.readSecretWithFallback(
secretsProps.getDatabase().getPasswordSecret(),
secretsProps.getDatabase().getPasswordEnv(),
"defaultpass"
);
dataSource.setJdbcUrl(dbUrl);
dataSource.setUsername(dbUsername);
dataSource.setPassword(dbPassword);
dataSource.setDriverClassName("org.postgresql.Driver");
// Connection pool settings
dataSource.setMaximumPoolSize(20);
dataSource.setMinimumIdle(5);
dataSource.setConnectionTimeout(30000);
dataSource.setIdleTimeout(600000);
dataSource.setMaxLifetime(1800000);
log.info("Configured DataSource with URL: {}", dbUrl);
return dataSource;
}
@Bean
public RestTemplate externalApiRestTemplate() {
// Read API secrets
String apiKey = secretsService.readSecretWithFallback(
secretsProps.getApi().getExternalApiKeySecret(),
secretsProps.getApi().getExternalApiKeyEnv(),
""
);
String apiSecret = secretsService.readSecretWithFallback(
secretsProps.getApi().getExternalApiSecret(),
secretsProps.getApi().getExternalApiSecretEnv(),
""
);
RestTemplate restTemplate = new RestTemplate();
// Add authentication interceptor
restTemplate.getInterceptors().add((request, body, execution) -> {
request.getHeaders().add("X-API-Key", apiKey);
request.getHeaders().add("X-API-Secret", apiSecret);
return execution.execute(request, body);
});
return restTemplate;
}
@Bean
public JwtService jwtService() {
String jwtSecret = secretsService.readSecretWithFallback(
secretsProps.getSecurity().getJwtSecret(),
secretsProps.getSecurity().getJwtSecretEnv(),
"default-jwt-secret-change-in-production"
);
return new JwtService(jwtSecret);
}
@Bean
public TextEncryptor textEncryptor() {
String encryptionKey = secretsService.readSecretWithFallback(
secretsProps.getSecurity().getEncryptionKey(),
secretsProps.getSecurity().getEncryptionKeyEnv(),
"default-encryption-key-change-in-production"
);
return Encryptors.text(encryptionKey, "deadbeef");
}
}

Database Integration with Secrets

1. Secure Database Configuration

@Service
@Slf4j
public class SecureDatabaseService {
private final DataSource dataSource;
private final DockerSecretsService secretsService;
public SecureDatabaseService(DataSource dataSource,
DockerSecretsService secretsService) {
this.dataSource = dataSource;
this.secretsService = secretsService;
}
/**
* Test database connection with current credentials
*/
public boolean testConnection() {
try (Connection connection = dataSource.getConnection()) {
return connection.isValid(5); // 5 second timeout
} catch (Exception e) {
log.error("Database connection test failed", e);
return false;
}
}
/**
* Rotate database password (when secret changes)
*/
public void rotateDatabasePassword(String newPasswordSecret) {
try {
Optional<String> newPassword = secretsService.readSecret(newPasswordSecret);
if (newPassword.isEmpty()) {
throw new IllegalStateException("New password secret not found: " + newPasswordSecret);
}
// In a real scenario, you would update the database user password
// and then update the connection pool
log.info("Initiating database password rotation");
// For HikariCP, you might need to restart the connection pool
if (dataSource instanceof HikariDataSource hikariDataSource) {
hikariDataSource.setPassword(newPassword.get());
hikariDataSource.getHikariPoolMXBean().softEvictConnections();
log.info("Database password rotated successfully");
}
} catch (Exception e) {
log.error("Failed to rotate database password", e);
throw new RuntimeException("Database password rotation failed", e);
}
}
/**
* Validate database credentials
*/
public DatabaseHealth checkDatabaseHealth() {
boolean connectionValid = testConnection();
boolean secretsAvailable = areDatabaseSecretsAvailable();
return DatabaseHealth.builder()
.connectionValid(connectionValid)
.secretsAvailable(secretsAvailable)
.timestamp(Instant.now())
.build();
}
private boolean areDatabaseSecretsAvailable() {
// Check if all required database secrets are available
List<String> requiredSecrets = List.of("db_username", "db_password", "db_url");
return requiredSecrets.stream()
.allMatch(secretsService::secretExists);
}
@EventListener
public void onSecretChange(SecretChangeEvent event) {
if (event.getSecretName().equals("db_password")) {
log.info("Database password secret changed, initiating rotation");
rotateDatabasePassword(event.getSecretName());
}
}
}
@Data
@Builder
public class DatabaseHealth {
private boolean connectionValid;
private boolean secretsAvailable;
private Instant timestamp;
}

API Security with Secrets

1. Secure API Client Service

@Service
@Slf4j
public class SecureApiClientService {
private final DockerSecretsService secretsService;
private final RestTemplate restTemplate;
private final Map<String, ApiClientConfig> apiConfigs;
public SecureApiClientService(DockerSecretsService secretsService,
RestTemplate restTemplate) {
this.secretsService = secretsService;
this.restTemplate = restTemplate;
this.apiConfigs = loadApiConfigs();
}
/**
* Make authenticated API call
*/
public <T> T makeAuthenticatedCall(String apiName, String endpoint, Class<T> responseType) {
ApiClientConfig config = apiConfigs.get(apiName);
if (config == null) {
throw new IllegalArgumentException("Unknown API: " + apiName);
}
// Read API credentials from secrets
String apiKey = secretsService.readSecret(config.getApiKeySecret())
.orElseThrow(() -> new SecurityException("API key secret not found: " + config.getApiKeySecret()));
String apiSecret = secretsService.readSecret(config.getApiSecretSecret())
.orElseThrow(() -> new SecurityException("API secret not found: " + config.getApiSecretSecret()));
HttpHeaders headers = createAuthHeaders(apiKey, apiSecret, config.getAuthType());
HttpEntity<String> entity = new HttpEntity<>(headers);
String url = config.getBaseUrl() + endpoint;
try {
ResponseEntity<T> response = restTemplate.exchange(
url, HttpMethod.GET, entity, responseType);
if (response.getStatusCode().is2xxSuccessful()) {
return response.getBody();
} else {
throw new ApiClientException("API call failed with status: " + response.getStatusCode());
}
} catch (Exception e) {
log.error("API call failed: {} {}", apiName, endpoint, e);
throw new ApiClientException("API call failed", e);
}
}
/**
* Validate API credentials
*/
public boolean validateApiCredentials(String apiName) {
ApiClientConfig config = apiConfigs.get(apiName);
if (config == null) {
return false;
}
boolean hasApiKey = secretsService.readSecret(config.getApiKeySecret()).isPresent();
boolean hasApiSecret = secretsService.readSecret(config.getApiSecretSecret()).isPresent();
return hasApiKey && hasApiSecret;
}
/**
* Rotate API credentials
*/
public void rotateApiCredentials(String apiName, String newApiKeySecret, String newApiSecretSecret) {
// In a real scenario, you would update the external service with new credentials
// and then update your secrets
log.info("Rotating API credentials for: {}", apiName);
// Validate new secrets exist
if (!secretsService.secretExists(newApiKeySecret) || 
!secretsService.secretExists(newApiSecretSecret)) {
throw new IllegalArgumentException("New API credentials secrets not found");
}
// Update configuration (in memory for this example)
ApiClientConfig config = apiConfigs.get(apiName);
if (config != null) {
// In production, you might persist this configuration
config.setApiKeySecret(newApiKeySecret);
config.setApiSecretSecret(newApiSecretSecret);
log.info("API credentials rotated for: {}", apiName);
}
}
private HttpHeaders createAuthHeaders(String apiKey, String apiSecret, AuthType authType) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
switch (authType) {
case API_KEY -> headers.set("X-API-Key", apiKey);
case BEARER_TOKEN -> headers.setBearerAuth(apiKey);
case BASIC_AUTH -> {
String auth = apiKey + ":" + apiSecret;
String encodedAuth = Base64.getEncoder().encodeToString(auth.getBytes());
headers.set("Authorization", "Basic " + encodedAuth);
}
case HMAC -> {
// Implement HMAC authentication
String timestamp = String.valueOf(System.currentTimeMillis());
String signature = calculateHmac(apiSecret, timestamp);
headers.set("X-API-Key", apiKey);
headers.set("X-Timestamp", timestamp);
headers.set("X-Signature", signature);
}
}
return headers;
}
private String calculateHmac(String secret, String data) {
try {
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKey = new SecretKeySpec(secret.getBytes(), "HmacSHA256");
mac.init(secretKey);
byte[] hmacData = mac.doFinal(data.getBytes());
return Hex.encodeHexString(hmacData);
} catch (Exception e) {
throw new SecurityException("Failed to calculate HMAC", e);
}
}
private Map<String, ApiClientConfig> loadApiConfigs() {
Map<String, ApiClientConfig> configs = new HashMap<>();
// External Payment API
configs.put("payment-gateway", ApiClientConfig.builder()
.baseUrl("https://api.paymentgateway.com/v1")
.apiKeySecret("payment_gateway_api_key")
.apiSecretSecret("payment_gateway_api_secret")
.authType(AuthType.API_KEY)
.build());
// External Notification API
configs.put("notification-service", ApiClientConfig.builder()
.baseUrl("https://api.notifications.com/v1")
.apiKeySecret("notification_api_key")
.apiSecretSecret("notification_api_secret")
.authType(AuthType.BEARER_TOKEN)
.build());
// Internal Analytics API
configs.put("analytics-service", ApiClientConfig.builder()
.baseUrl("https://analytics.internal.com/api")
.apiKeySecret("analytics_api_key")
.apiSecretSecret("analytics_api_secret")
.authType(AuthType.HMAC)
.build());
return configs;
}
}
@Data
@Builder
class ApiClientConfig {
private String baseUrl;
private String apiKeySecret;
private String apiSecretSecret;
private AuthType authType;
}
enum AuthType {
API_KEY, BEARER_TOKEN, BASIC_AUTH, HMAC
}
class ApiClientException extends RuntimeException {
public ApiClientException(String message) {
super(message);
}
public ApiClientException(String message, Throwable cause) {
super(message, cause);
}
}

JWT Security with Secrets

1. JWT Service with Secret Management

@Service
@Slf4j
public class JwtService {
private final DockerSecretsService secretsService;
private final ObjectMapper objectMapper;
private SecretKey secretKey;
public JwtService(DockerSecretsService secretsService, ObjectMapper objectMapper) {
this.secretsService = secretsService;
this.objectMapper = objectMapper;
loadJwtSecret();
}
/**
* Generate JWT token
*/
public String generateToken(JwtClaims claims, Duration validity) {
try {
Instant now = Instant.now();
Instant expiry = now.plus(validity);
return Jwts.builder()
.setClaims(createClaims(claims))
.setSubject(claims.getSubject())
.setIssuer(claims.getIssuer())
.setIssuedAt(Date.from(now))
.setExpiration(Date.from(expiry))
.signWith(secretKey, SignatureAlgorithm.HS256)
.compact();
} catch (Exception e) {
log.error("Failed to generate JWT token", e);
throw new JwtException("Failed to generate JWT token", e);
}
}
/**
* Parse and validate JWT token
*/
public JwtClaims parseAndValidateToken(String token) {
try {
Claims claims = Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(token)
.getBody();
return JwtClaims.builder()
.subject(claims.getSubject())
.issuer(claims.getIssuer())
.issuedAt(claims.getIssuedAt().toInstant())
.expiration(claims.getExpiration().toInstant())
.claims(new HashMap<>(claims))
.build();
} catch (Exception e) {
log.error("Failed to parse or validate JWT token", e);
throw new JwtException("Invalid JWT token", e);
}
}
/**
* Refresh JWT token
*/
public String refreshToken(String token, Duration newValidity) {
try {
JwtClaims existingClaims = parseAndValidateToken(token);
Instant now = Instant.now();
Instant expiry = now.plus(newValidity);
return Jwts.builder()
.setClaims(createClaims(existingClaims))
.setSubject(existingClaims.getSubject())
.setIssuer(existingClaims.getIssuer())
.setIssuedAt(Date.from(now))
.setExpiration(Date.from(expiry))
.signWith(secretKey, SignatureAlgorithm.HS256)
.compact();
} catch (Exception e) {
log.error("Failed to refresh JWT token", e);
throw new JwtException("Failed to refresh JWT token", e);
}
}
/**
* Validate token without throwing exception
*/
public boolean validateToken(String token) {
try {
parseAndValidateToken(token);
return true;
} catch (JwtException e) {
return false;
}
}
/**
* Rotate JWT secret
*/
public void rotateJwtSecret(String newSecretName) {
try {
Optional<String> newSecret = secretsService.readSecret(newSecretName);
if (newSecret.isEmpty()) {
throw new IllegalArgumentException("New JWT secret not found: " + newSecretName);
}
this.secretKey = Keys.hmacShaKeyFor(newSecret.get().getBytes());
log.info("JWT secret rotated successfully");
} catch (Exception e) {
log.error("Failed to rotate JWT secret", e);
throw new JwtException("Failed to rotate JWT secret", e);
}
}
private Map<String, Object> createClaims(JwtClaims claims) {
Map<String, Object> jwtClaims = new HashMap<>();
if (claims.getClaims() != null) {
jwtClaims.putAll(claims.getClaims());
}
// Add standard claims
jwtClaims.put("sub", claims.getSubject());
jwtClaims.put("iss", claims.getIssuer());
return jwtClaims;
}
private void loadJwtSecret() {
try {
Optional<String> jwtSecret = secretsService.readSecret("jwt_secret");
if (jwtSecret.isEmpty()) {
throw new IllegalStateException("JWT secret not found in Docker secrets");
}
this.secretKey = Keys.hmacShaKeyFor(jwtSecret.get().getBytes());
log.info("JWT secret loaded successfully");
} catch (Exception e) {
log.error("Failed to load JWT secret", e);
throw new JwtException("Failed to load JWT secret", e);
}
}
@EventListener
public void onJwtSecretChange(SecretChangeEvent event) {
if (event.getSecretName().equals("jwt_secret")) {
log.info("JWT secret changed, rotating...");
rotateJwtSecret(event.getSecretName());
}
}
}
@Data
@Builder
class JwtClaims {
private String subject;
private String issuer;
private Instant issuedAt;
private Instant expiration;
private Map<String, Object> claims;
}
class JwtException extends RuntimeException {
public JwtException(String message) {
super(message);
}
public JwtException(String message, Throwable cause) {
super(message, cause);
}
}

Docker Compose with Secrets

1. Docker Compose Configuration

version: '3.8'
services:
java-app:
build: .
image: my-java-app:latest
environment:
- SPRING_PROFILES_ACTIVE=docker
- DB_URL=jdbc:postgresql://db:5432/appdb
ports:
- "8080:8080"
depends_on:
- db
secrets:
- db_username
- db_password
- jwt_secret
- external_api_key
- external_api_secret
networks:
- app-network
db:
image: postgres:13
environment:
POSTGRES_DB: appdb
secrets:
- postgres_password
volumes:
- db_data:/var/lib/postgresql/data
networks:
- app-network
secrets:
db_username:
external: true
db_password:
external: true
jwt_secret:
external: true
external_api_key:
external: true
external_api_secret:
external: true
postgres_password:
external: true
volumes:
db_data:
networks:
app-network:
driver: bridge

2. Docker Stack Deployment

# docker-stack.yml
version: '3.8'
services:
java-app:
image: my-java-app:latest
environment:
- SPRING_PROFILES_ACTIVE=production
ports:
- "8080:8080"
deploy:
replicas: 3
update_config:
parallelism: 1
delay: 10s
restart_policy:
condition: on-failure
secrets:
- source: db_username
target: db_username
- source: db_password
target: db_password
- source: jwt_secret
target: jwt_secret
- source: external_api_key
target: external_api_key
- source: external_api_secret
target: external_api_secret
secrets:
db_username:
external: true
db_password:
external: true
jwt_secret:
external: true
external_api_key:
external: true
external_api_secret:
external: true

Health Checks and Monitoring

1. Secrets Health Indicator

@Component
@Slf4j
public class SecretsHealthIndicator implements HealthIndicator {
private final DockerSecretsService secretsService;
private final List<String> requiredSecrets;
public SecretsHealthIndicator(DockerSecretsService secretsService,
@Value("${app.required-secrets:}") List<String> requiredSecrets) {
this.secretsService = secretsService;
this.requiredSecrets = requiredSecrets != null ? requiredSecrets : List.of();
}
@Override
public Health health() {
Health.Builder healthBuilder = Health.up();
Map<String, Object> details = new HashMap<>();
// Check required secrets
List<String> missingSecrets = new ArrayList<>();
List<String> availableSecrets = new ArrayList<>();
for (String secretName : requiredSecrets) {
if (secretsService.secretExists(secretName)) {
availableSecrets.add(secretName);
} else {
missingSecrets.add(secretName);
}
}
details.put("requiredSecrets", requiredSecrets);
details.put("availableSecrets", availableSecrets);
details.put("missingSecrets", missingSecrets);
details.put("totalAvailableSecrets", secretsService.listAvailableSecrets().size());
if (!missingSecrets.isEmpty()) {
healthBuilder.down()
.withDetail("error", "Missing required secrets: " + missingSecrets);
} else {
healthBuilder.withDetails(details);
}
return healthBuilder.build();
}
}

2. Secrets Monitoring Service

@Service
@Slf4j
public class SecretsMonitoringService {
private final DockerSecretsService secretsService;
private final MeterRegistry meterRegistry;
public SecretsMonitoringService(DockerSecretsService secretsService,
MeterRegistry meterRegistry) {
this.secretsService = secretsService;
this.meterRegistry = meterRegistry;
initializeMetrics();
}
/**
* Monitor secrets availability and usage
*/
@Scheduled(fixedRate = 60000) // Every minute
public void monitorSecrets() {
try {
List<String> availableSecrets = secretsService.listAvailableSecrets();
// Update metrics
meterRegistry.gauge("secrets.available.count", availableSecrets.size());
// Log secret availability
log.debug("Available secrets: {}", availableSecrets.size());
// Check for secret changes
monitorSecretChanges();
} catch (Exception e) {
log.error("Secrets monitoring failed", e);
}
}
/**
* Validate all secrets meet requirements
*/
public SecretsValidationReport validateAllSecrets() {
List<String> availableSecrets = secretsService.listAvailableSecrets();
List<SecretValidationResult> validationResults = new ArrayList<>();
for (String secretName : availableSecrets) {
SecretValidationResult result = validateSecret(secretName);
validationResults.add(result);
}
long validSecrets = validationResults.stream()
.filter(SecretValidationResult::isValid)
.count();
return SecretsValidationReport.builder()
.totalSecrets(availableSecrets.size())
.validSecrets(validSecrets)
.validationResults(validationResults)
.timestamp(Instant.now())
.build();
}
private SecretValidationResult validateSecret(String secretName) {
// Implement secret-specific validation logic
// This is a simplified example
Optional<String> secretValue = secretsService.readSecret(secretName);
if (secretValue.isEmpty()) {
return SecretValidationResult.invalid(secretName, "Secret not readable");
}
// Basic validation - in reality, you'd have specific rules per secret type
if (secretValue.get().trim().isEmpty()) {
return SecretValidationResult.invalid(secretName, "Secret is empty");
}
return SecretValidationResult.valid(secretName);
}
private void monitorSecretChanges() {
// Implement change detection logic
// This could track when secrets are updated/rotated
}
private void initializeMetrics() {
// Initialize custom metrics
Gauge.builder("secrets.available.count")
.description("Number of available Docker secrets")
.register(meterRegistry);
}
}
@Data
@Builder
class SecretsValidationReport {
private int totalSecrets;
private long validSecrets;
private List<SecretValidationResult> validationResults;
private Instant timestamp;
public boolean isHealthy() {
return totalSecrets > 0 && validSecrets == totalSecrets;
}
}
@Data
@Builder
class SecretValidationResult {
private String secretName;
private boolean valid;
private String errorMessage;
private Instant validatedAt;
public static SecretValidationResult valid(String secretName) {
return SecretValidationResult.builder()
.secretName(secretName)
.valid(true)
.validatedAt(Instant.now())
.build();
}
public static SecretValidationResult invalid(String secretName, String errorMessage) {
return SecretValidationResult.builder()
.secretName(secretName)
.valid(false)
.errorMessage(errorMessage)
.validatedAt(Instant.now())
.build();
}
}

Testing with Docker Secrets

1. Test Configuration

@SpringBootTest
@TestPropertySource(properties = {
"app.secrets.database.username-secret=test_db_username",
"app.secrets.database.password-secret=test_db_password",
"app.secrets.database.url-secret=test_db_url"
})
@DirtiesContext
class DockerSecretsServiceTest {
@Autowired
private DockerSecretsService secretsService;
@BeforeEach
void setUp() throws IOException {
// Create temporary secrets directory structure
Path secretsDir = Paths.get("/run/secrets");
Files.createDirectories(secretsDir);
// Create test secret files
Files.writeString(secretsDir.resolve("test_db_username"), "testuser");
Files.writeString(secretsDir.resolve("test_db_password"), "testpass123");
Files.writeString(secretsDir.resolve("test_db_url"), "jdbc:postgresql://localhost:5432/testdb");
}
@AfterEach
void tearDown() throws IOException {
// Clean up test secrets
Path secretsDir = Paths.get("/run/secrets");
if (Files.exists(secretsDir)) {
Files.walk(secretsDir)
.sorted(Comparator.reverseOrder())
.map(Path::toFile)
.forEach(File::delete);
}
secretsService.clearCache();
}
@Test
void shouldReadSecretSuccessfully() {
Optional<String> username = secretsService.readSecret("test_db_username");
assertTrue(username.isPresent());
assertEquals("testuser", username.get());
}
@Test
void shouldReturnEmptyForMissingSecret() {
Optional<String> missingSecret = secretsService.readSecret("nonexistent_secret");
assertTrue(missingSecret.isEmpty());
}
@Test
void shouldUseFallbackWhenSecretMissing() {
String value = secretsService.readSecretWithFallback(
"nonexistent_secret",
"FALLBACK_ENV_VAR",
"default_value"
);
assertEquals("default_value", value);
}
@Test
void shouldListAvailableSecrets() {
List<String> availableSecrets = secretsService.listAvailableSecrets();
assertTrue(availableSecrets.contains("test_db_username"));
assertTrue(availableSecrets.contains("test_db_password"));
assertTrue(availableSecrets.contains("test_db_url"));
}
}

Conclusion

Docker Secrets provide Java applications with a secure and robust way to manage sensitive data:

  1. Enhanced Security - Secrets are encrypted at rest and in transit
  2. Access Control - Fine-grained permissions for secret access
  3. Automatic Rotation - Support for secret rotation without application downtime
  4. Audit Trail - Complete history of secret access and changes
  5. Integration - Seamless integration with Spring Boot and other Java frameworks

Key benefits for Java applications:

  • No sensitive data in environment variables or configuration files
  • Centralized secret management across multiple services
  • Automatic secret distribution to containers
  • Support for secret rotation with minimal impact
  • Compliance with security best practices and regulations

By implementing Docker Secrets with the patterns shown above, Java applications can achieve enterprise-grade security for sensitive data while maintaining operational efficiency and developer productivity.

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