How to manage Spring Boot configuration for cloud deployment without exposing secrets
Problem
When I deployed my Spring Boot application to production, I got a security audit failure:
CRITICAL: Database password found in version controlLocation: src/main/resources/application-prod.ymlRisk: Credentials exposed in git historyRecommendation: Rotate all secrets immediatelyI thought I was being careful by using application-prod.yml for production settings. But I forgot - this file was committed to git.
The real problem was bigger than just this one password. I had:
- Hardcoded database URLs in my YAML files
- API keys sitting in my source code
- Different values for dev, staging, and prod scattered everywhere
- No way to update configuration without rebuilding and redeploying
This is a common mistake. Let me show you how I fixed it.
Environment
- Spring Boot 3.2.x
- Java 21
- Kubernetes (production)
- Docker (local development)
Understanding the Configuration Hierarchy
Before fixing the problem, I needed to understand how Spring Boot handles configuration. It loads properties from multiple sources in a specific order:
1. Command-line arguments2. SPRING_APPLICATION_JSON environment variable3. Servlet parameters4. OS environment variables5. application-{profile}.yml OUTSIDE the jar6. application-{profile}.yml INSIDE the jar7. application.yml OUTSIDE the jar8. application.yml INSIDE the jar9. @PropertySource annotations10. Default propertiesThe key insight: later sources can override earlier ones. This means I can put safe defaults in my jar and override them externally.
How I Fixed It
Step 1: Remove Secrets from Source Control
First, I removed all sensitive values from my configuration files:
spring: application: name: my-service profiles: active: ${ACTIVE_PROFILE:dev}
server: port: ${SERVER_PORT:8080}
my: service: api-key: ${API_KEY:} # Empty default - must be provided timeout: ${TIMEOUT:30000}Notice the ${API_KEY:} syntax. The empty value after the colon means: use environment variable if set, otherwise fail.
For development, I created a local file that git ignores:
my: service: api-key: dev-test-key-12345
spring: datasource: url: jdbc:postgresql://localhost:5432/devdb username: devuser password: devpassStep 2: Use Environment Variables
In production, I don’t use configuration files at all. Instead, I use environment variables:
export SPRING_DATASOURCE_URL=jdbc:postgresql://prod-db:5432/mydbexport SPRING_DATASOURCE_USERNAME=myuserexport SPRING_DATASOURCE_PASSWORD=mypasswordexport MY_SERVICE_API_KEY=prod-secret-keySpring Boot automatically converts these to properties:
SPRING_DATASOURCE_URLbecomesspring.datasource.urlMY_SERVICE_API_KEYbecomesmy.service.api-key
Step 3: Create Profile-Specific Files
I created separate files for each environment:
src/main/resources/├── application.yml # Common config├── application-dev.yml # Development settings├── application-staging.yml # Staging settings├── application-prod.yml # Production (no secrets!)└── application-test.yml # TestingThe production file only contains non-sensitive values:
spring: jpa: hibernate: ddl-auto: validate
my: service: timeout: 60000
logging: level: root: WARN com.example: INFOStep 4: Use Kubernetes ConfigMaps and Secrets
For Kubernetes deployment, I externalize everything:
apiVersion: v1kind: ConfigMapmetadata: name: app-configdata: SPRING_PROFILES_ACTIVE: "prod" SERVER_PORT: "8080" MY_SERVICE_TIMEOUT: "60000"apiVersion: v1kind: Secretmetadata: name: app-secretstype: OpaquestringData: SPRING_DATASOURCE_URL: "jdbc:postgresql://postgres:5432/mydb" SPRING_DATASOURCE_USERNAME: "myuser" SPRING_DATASOURCE_PASSWORD: "mypassword" MY_SERVICE_API_KEY: "prod-secret-key"In my deployment, I reference both:
apiVersion: apps/v1kind: Deploymentspec: template: spec: containers: - name: app image: myapp:latest envFrom: - configMapRef: name: app-config - secretRef: name: app-secretsStep 5: Add Configuration Validation
To catch missing configuration early, I created a type-safe properties class:
@ConfigurationProperties(prefix = "my.service")@Validatedpublic class MyServiceProperties {
@NotBlank(message = "API key is required") private String apiKey;
@Min(1000) @Max(60000) private long timeout = 30000;
// getters and setters}Then I enabled it in my configuration:
@Configuration@EnableConfigurationProperties(MyServiceProperties.class)public class MyConfiguration {
@Bean public MyClient myClient(MyServiceProperties props) { return new MyClient(props.getApiKey(), props.getTimeout()); }}Now if I forget to set MY_SERVICE_API_KEY, the application fails fast at startup with a clear error message.
The Reason This Works
I think the key reason is separation of concerns:
┌─────────────────────────────────────────────────────────────┐│ Application Code ││ ┌─────────────────────────────────────────────────────────┐││ │ application.yml (in jar) │││ │ - Safe defaults │││ │ - Property placeholders │││ │ - No secrets! │││ └─────────────────────────────────────────────────────────┘││ ↓ ││ ┌─────────────────────────────────────────────────────────┐││ │ External Configuration (outside jar) │││ │ - Environment variables │││ │ - Kubernetes ConfigMaps │││ │ - Kubernetes Secrets │││ │ - HashiCorp Vault │││ └─────────────────────────────────────────────────────────┘│└─────────────────────────────────────────────────────────────┘By keeping secrets outside the jar, I get:
- Security: Secrets are never in version control
- Flexibility: Same artifact works in all environments
- Auditability: Can see who accessed secrets (with Vault)
- Rotation: Can update secrets without redeployment
When to Use What
Here’s a quick guide for different scenarios:
| Scenario | Use |
|---|---|
| Local development | application-local.yml (gitignored) |
| CI/CD pipeline | Environment variables |
| Kubernetes | ConfigMap + Secrets |
| Large enterprise | Spring Cloud Config + Vault |
| AWS deployment | AWS Secrets Manager |
Common Mistakes to Avoid
-
Committing
application-prod.yml- Always use placeholders or environment variables for secrets -
Using
@Valueeverywhere - Creates scattered configuration. Use@ConfigurationPropertiesinstead -
Not validating configuration - Your app should fail fast if required config is missing
-
Hardcoding URLs - Use
${DATABASE_URL}or similar placeholders -
Same config for all environments - Use profiles to separate concerns
Summary
In this post, I showed how to manage Spring Boot configuration for cloud deployment without exposing secrets. The key point is to externalize all environment-specific configuration and keep secrets out of version control.
The approach I used:
- Remove secrets from source code
- Use environment variables for sensitive values
- Create profile-specific files for non-secret settings
- Use Kubernetes ConfigMaps and Secrets for deployment
- Add configuration validation to catch missing values early
This gives you secure, flexible configuration that works across all environments.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
- 👨💻 Spring Boot Externalized Configuration
- 👨💻 Spring Cloud Config Server
- 👨💻 HashiCorp Vault Spring Integration
- 👨💻 Kubernetes ConfigMaps and Secrets
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments