Skip to content

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:

Security Audit Report
CRITICAL: Database password found in version control
Location: src/main/resources/application-prod.yml
Risk: Credentials exposed in git history
Recommendation: Rotate all secrets immediately

I 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:

Property Source Priority (Highest to Lowest)
1. Command-line arguments
2. SPRING_APPLICATION_JSON environment variable
3. Servlet parameters
4. OS environment variables
5. application-{profile}.yml OUTSIDE the jar
6. application-{profile}.yml INSIDE the jar
7. application.yml OUTSIDE the jar
8. application.yml INSIDE the jar
9. @PropertySource annotations
10. Default properties

The 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:

src/main/resources/application.yml
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:

application-local.yml (not committed to git)
my:
service:
api-key: dev-test-key-12345
spring:
datasource:
url: jdbc:postgresql://localhost:5432/devdb
username: devuser
password: devpass

Step 2: Use Environment Variables

In production, I don’t use configuration files at all. Instead, I use environment variables:

Setting environment variables
export SPRING_DATASOURCE_URL=jdbc:postgresql://prod-db:5432/mydb
export SPRING_DATASOURCE_USERNAME=myuser
export SPRING_DATASOURCE_PASSWORD=mypassword
export MY_SERVICE_API_KEY=prod-secret-key

Spring Boot automatically converts these to properties:

  • SPRING_DATASOURCE_URL becomes spring.datasource.url
  • MY_SERVICE_API_KEY becomes my.service.api-key

Step 3: Create Profile-Specific Files

I created separate files for each environment:

Project structure
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 # Testing

The production file only contains non-sensitive values:

src/main/resources/application-prod.yml
spring:
jpa:
hibernate:
ddl-auto: validate
my:
service:
timeout: 60000
logging:
level:
root: WARN
com.example: INFO

Step 4: Use Kubernetes ConfigMaps and Secrets

For Kubernetes deployment, I externalize everything:

k8s/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
SPRING_PROFILES_ACTIVE: "prod"
SERVER_PORT: "8080"
MY_SERVICE_TIMEOUT: "60000"
k8s/secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: app-secrets
type: Opaque
stringData:
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:

k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
spec:
template:
spec:
containers:
- name: app
image: myapp:latest
envFrom:
- configMapRef:
name: app-config
- secretRef:
name: app-secrets

Step 5: Add Configuration Validation

To catch missing configuration early, I created a type-safe properties class:

src/main/java/com/example/MyServiceProperties.java
@ConfigurationProperties(prefix = "my.service")
@Validated
public 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:

src/main/java/com/example/MyConfiguration.java
@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:

Configuration separation diagram
┌─────────────────────────────────────────────────────────────┐
│ 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:

  1. Security: Secrets are never in version control
  2. Flexibility: Same artifact works in all environments
  3. Auditability: Can see who accessed secrets (with Vault)
  4. Rotation: Can update secrets without redeployment

When to Use What

Here’s a quick guide for different scenarios:

ScenarioUse
Local developmentapplication-local.yml (gitignored)
CI/CD pipelineEnvironment variables
KubernetesConfigMap + Secrets
Large enterpriseSpring Cloud Config + Vault
AWS deploymentAWS Secrets Manager

Common Mistakes to Avoid

  1. Committing application-prod.yml - Always use placeholders or environment variables for secrets

  2. Using @Value everywhere - Creates scattered configuration. Use @ConfigurationProperties instead

  3. Not validating configuration - Your app should fail fast if required config is missing

  4. Hardcoding URLs - Use ${DATABASE_URL} or similar placeholders

  5. 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:

  1. Remove secrets from source code
  2. Use environment variables for sensitive values
  3. Create profile-specific files for non-secret settings
  4. Use Kubernetes ConfigMaps and Secrets for deployment
  5. 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:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments