Dev vs Prod: Why You Can't Store Secrets in Files on Production Servers
"Why do we use application.properties for secrets in development but environment variables in production?" Because file-based secrets are fine for your local machine (only you have access), but dangerous on production servers (many people have access, files can be compromised). Let's understand why this distinction matters and how to properly manage secrets across environments.
The Two Worlds: Development vs Productionβ
Development (Your Local Machine) β β
application.properties (on your computer)
βββ jwt.secret=MySecretKeyForJWTTokensThisNeedsToBeVeryLongForHS256Algorithm
βββ database.password=localpass123
βββ spring.mail.password=gmailAppPassword123
Characteristics:
- β Stored in files on your computer
- β Only you can see it
- β
Added to
.gitignore(doesn't upload to GitHub) - β Easy to manage and modify
- β Safe because your computer is private
Production (Live Server / Cloud) π΄β
β NO application.properties with secrets!
β
Environment Variables / Secrets Manager:
export JWT_SECRET=prod-secret-key-xyz789
export DATABASE_PASSWORD=prod-password-secure
export MAIL_PASSWORD=prod-gmail-password
Characteristics:
- β Set at server startup, not in files
- β Only active in memory when app runs
- β Can be rotated/changed without redeploying code
- β Managed by DevOps/infrastructure team
- β Not visible in your codebase
Why the Difference?β
Problem 1: Server Accessβ
Development (Your Laptop):
Your Computer
β
Only YOU have access
β
application.properties with secrets = SAFE β
Production (Cloud Server):
Production Server
β
Multiple people have access:
- DevOps engineers
- System administrators
- Security team
- Contractors
- Former employees (if not revoked)
β
application.properties with secrets = DANGEROUS β
Problem 2: Server Compromiseβ
If a hacker compromises the server:
With files:
# Hacker gets server access
ssh hacker@your-server
# Reads your secrets file
cat /app/application.properties
# Game over! All secrets exposed:
jwt.secret=MySecretKeyForJWTTokens...
database.password=localpass123
mail.password=gmailAppPassword123
With environment variables:
# Hacker gets server access
ssh hacker@your-server
# Tries to read secrets file
cat /app/application.properties
# β No secrets in file!
# Tries to read environment variables
env | grep SECRET
# β οΈ May see variables IF process is running
# But can rotate keys immediately without redeploying
Problem 3: Secret Rotationβ
With files:
1. Update application.properties with new secret
2. Rebuild application
3. Create new Docker image
4. Deploy new image to production
5. Restart all servers
β±οΈ Time: Hours, Risk: High
With environment variables:
1. Update environment variable on server
2. Restart application
β±οΈ Time: Minutes, Risk: Low
Problem 4: Git Historyβ
With files (even if .gitignored later):
# Developer accidentally commits secrets
git add application.properties
git commit -m "Add config"
git push
# Later adds to .gitignore
echo "application.properties" >> .gitignore
git commit -m "Add gitignore"
# β Secrets STILL in Git history!
git log --all -- application.properties # Shows old commit with secrets
With environment variables:
β
Never in Git
β
Never in commit history
β
Never uploaded anywhere
How Spring Boot Handles Bothβ
Property Resolution Orderβ
Spring Boot looks for configuration values in this order:
1. Environment variables (highest priority)
2. System properties
3. application.properties file
4. Default values (lowest priority)
Example:
@Value("${jwt.secret}")
private String secretKey;
Spring looks for:
1. Environment variable: JWT_SECRET
2. System property: -Djwt.secret=...
3. application.properties: jwt.secret=...
4. Not found β Error
Naming Conventionβ
Property in application.properties:
jwt.secret=value
database.password=value
spring.mail.password=value
Equivalent environment variable:
JWT_SECRET=value # jwt.secret
DATABASE_PASSWORD=value # database.password
SPRING_MAIL_PASSWORD=value # spring.mail.password
Rule: Replace . with _ and make uppercase.
Development Setupβ
Step 1: Create application.propertiesβ
# application.properties (development only)
# Server Configuration
server.port=8082
# Database Configuration
spring.datasource.url=jdbc:h2:file:./data/ecommerce
spring.datasource.username=sa
spring.datasource.password=localpass123
# JWT Configuration
jwt.secret=MySecretKeyForJWTTokensThisNeedsToBeVeryLongForHS256Algorithm
jwt.expiration=86400000
# Email Configuration
spring.mail.host=smtp.gmail.com
spring.mail.port=587
spring.mail.username=your-email@gmail.com
spring.mail.password=gmailAppPassword123
# JPA Configuration
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
Step 2: Add to .gitignoreβ
# .gitignore
# Secrets
application.properties
application-dev.properties
application-local.properties
# But allow example files
!application.properties.example
Step 3: Create application.properties.exampleβ
# application.properties.example
# Copy this to application.properties and fill in real values
# Server Configuration
server.port=8082
# Database Configuration
spring.datasource.url=jdbc:h2:file:./data/ecommerce
spring.datasource.username=sa
spring.datasource.password=YOUR_DATABASE_PASSWORD
# JWT Configuration
jwt.secret=YOUR_JWT_SECRET_KEY_AT_LEAST_256_BITS
jwt.expiration=86400000
# Email Configuration
spring.mail.host=smtp.gmail.com
spring.mail.port=587
spring.mail.username=YOUR_EMAIL@gmail.com
spring.mail.password=YOUR_GMAIL_APP_PASSWORD
# JPA Configuration
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
Step 4: Share with Teamβ
# β
Commit example file (no secrets)
git add application.properties.example
git commit -m "Add configuration example"
git push
# β Never commit actual file (has secrets)
# application.properties is .gitignored
Production Setupβ
Option 1: Environment Variables (Simple)β
On server:
# Set environment variables
export JWT_SECRET="prod-secret-key-xyz789-very-long-and-secure"
export DATABASE_PASSWORD="prod-database-password-secure-123"
export SPRING_MAIL_PASSWORD="prod-gmail-app-password"
# Start application
java -jar app.jar
Or in Docker:
# Dockerfile
FROM openjdk:17-jdk-slim
COPY target/app.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]
# Run with environment variables
docker run -d \
-e JWT_SECRET="prod-secret-xyz789" \
-e DATABASE_PASSWORD="prod-db-pass" \
-e SPRING_MAIL_PASSWORD="prod-mail-pass" \
-p 8080:8080 \
myapp:latest
Option 2: AWS Secrets Manager (Recommended)β
Install dependency:
<dependency>
<groupId>io.awspring.cloud</groupId>
<artifactId>spring-cloud-aws-secrets-manager-config</artifactId>
</dependency>
Configure Spring Boot:
# application.properties (production)
spring.config.import=aws-secretsmanager:/my-app/prod/secrets
Secrets stored in AWS:
{
"jwt.secret": "prod-secret-xyz789",
"database.password": "prod-db-password",
"spring.mail.password": "prod-mail-password"
}
Spring Boot automatically fetches secrets at startup! β
Option 3: Kubernetes Secretsβ
Create secret:
# secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: app-secrets
type: Opaque
data:
jwt-secret: cHJvZC1zZWNyZXQteHl6Nzg5 # base64 encoded
db-password: cHJvZC1kYi1wYXNzd29yZA==
Use in deployment:
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
template:
spec:
containers:
- name: myapp
image: myapp:latest
env:
- name: JWT_SECRET
valueFrom:
secretKeyRef:
name: app-secrets
key: jwt-secret
- name: DATABASE_PASSWORD
valueFrom:
secretKeyRef:
name: app-secrets
key: db-password
Real-World Example: Complete Flowβ
Developmentβ
Your laptop:
# Clone repository
git clone https://github.com/yourcompany/myapp.git
cd myapp
# Copy example config
cp application.properties.example application.properties
# Edit with your dev secrets
nano application.properties
# application.properties (your local file, .gitignored)
jwt.secret=dev-secret-key-12345-for-local-testing
database.password=devpass123
spring.mail.password=dev-gmail-app-password
# Run application
mvn spring-boot:run
# β
Works with secrets from file
Staging (Pre-Production)β
Staging server:
# Set environment variables
export JWT_SECRET="staging-secret-xyz789"
export DATABASE_PASSWORD="staging-db-password"
export SPRING_MAIL_PASSWORD="staging-mail-password"
# Deploy application
java -jar app.jar
# β
Works with secrets from environment variables
Productionβ
Production server (AWS ECS example):
{
"family": "myapp-production",
"containerDefinitions": [
{
"name": "myapp",
"image": "myapp:1.0.0",
"secrets": [
{
"name": "JWT_SECRET",
"valueFrom": "arn:aws:secretsmanager:us-east-1:123456789:secret:prod/jwt-secret"
},
{
"name": "DATABASE_PASSWORD",
"valueFrom": "arn:aws:secretsmanager:us-east-1:123456789:secret:prod/db-password"
}
]
}
]
}
Application code (same everywhere):
@Component
public class JwtService {
@Value("${jwt.secret}") // Works in dev, staging, AND production!
private String secretKey;
// Spring automatically reads from:
// - Development: application.properties file
// - Staging: Environment variables
// - Production: AWS Secrets Manager
}
Security Best Practicesβ
β DO: Developmentβ
-
Use
application.propertiesfor local developmentjwt.secret=dev-secret-key-12345 -
Add to
.gitignoreapplication.properties -
Provide example file
# application.properties.example
jwt.secret=YOUR_SECRET_HERE -
Use weak secrets for development (it's okay, it's local!)
jwt.secret=dev-key-123
database.password=devpass
β DO: Productionβ
-
Use environment variables
export JWT_SECRET="strong-prod-secret-xyz789" -
Or use secrets manager
aws secretsmanager create-secret --name prod/jwt-secret -
Rotate secrets regularly
# Update secret without redeploying code
aws secretsmanager update-secret --secret-id prod/jwt-secret --secret-string "new-secret" -
Use strong secrets for production
# At least 32 characters, random, unique
jwt.secret=vM8f3Kp2Lq9Nn1Rr7Ss5Tt3Uu6Vv4Ww8Xx2Yy0Zz
β DON'T: Everβ
-
Commit secrets to Git
# β NEVER DO THIS
git add application.properties # Contains secrets!
git commit -m "Add config" -
Hardcode secrets in code
// β NEVER DO THIS
private String secretKey = "MySecretKey123"; -
Share secrets in Slack/Email
β "Hey team, the JWT secret is: abc123..." -
Use the same secrets in dev and prod
# β NEVER DO THIS
# Dev and prod using same secret = dangerous!
Comparison Tableβ
| Aspect | Development | Production |
|---|---|---|
| Storage | application.properties file | Environment variables / Secrets Manager |
| Location | Your laptop | Cloud server |
| Access | Only you | Multiple people |
| Git | .gitignored | Never in Git |
| Secret Strength | Weak (dev-key-123) | Strong (random, 32+ chars) |
| Rotation | Rarely needed | Regular rotation required |
| Compromise Risk | Low (only your machine) | High (many attack vectors) |
| Management | You manually | DevOps team / Infrastructure |
| Recovery | Easy (just recreate file) | Requires secret rotation process |
Migration Checklist: Dev to Prodβ
Before Deploymentβ
- Remove
application.propertiesfrom production build - Set all secrets as environment variables on production server
- Test application can read environment variables
- Verify no secrets in Git history
- Generate strong production secrets (32+ characters)
- Document which environment variables are required
During Deploymentβ
- Set environment variables on server
- Deploy application
- Verify application starts successfully
- Test all features requiring secrets (JWT, database, email)
- Monitor logs for configuration errors
After Deploymentβ
- Rotate any secrets that were accidentally exposed
- Set up secret rotation schedule (e.g., every 90 days)
- Document secret management process for team
- Set up alerts for failed authentication (wrong secrets)
- Review access logs for unauthorized access attempts
Troubleshootingβ
Problem 1: Application Can't Find Secretβ
Error:
Error creating bean with name 'jwtService': Unsatisfied dependency expressed through field 'secretKey'
Solution:
# Check environment variable is set
echo $JWT_SECRET
# If not set:
export JWT_SECRET="your-secret-key"
# Restart application
java -jar app.jar
Problem 2: Wrong Secret Usedβ
Error:
JWT verification failed: Invalid signature
Solution:
# Check which secret Spring Boot is using
java -jar app.jar --debug
# Look for: "Property 'jwt.secret' from 'EnvironmentVariable'"
# Verify environment variable value
echo $JWT_SECRET
Problem 3: Secret Visible in Logsβ
Problem:
// β WRONG
logger.info("JWT Secret: {}", secretKey); // Exposes secret in logs!
Solution:
// β
CORRECT
logger.info("JWT Secret configured: {}", secretKey != null ? "Yes" : "No");
Summaryβ
The Key Differenceβ
| Environment | Secret Storage | Why |
|---|---|---|
| Development | application.properties file | Only you have access to your computer |
| Production | Environment variables / Secrets Manager | Many people have access to server |
Why It Mattersβ
Development:
- β Files are convenient
- β Only you can access
- β Easy to modify
Production:
- β Files are dangerous (many people have access)
- β Environment variables are secure
- β Can rotate without redeploying
- β Not in Git history
What You Should Do Nowβ
For Development:
- Create
application.propertieswith dev secrets - Add
application.propertiesto.gitignore - Create
application.properties.examplewithout secrets - Commit example file to Git
For Production (when ready to deploy):
- Set secrets as environment variables on server
- Remove
application.propertiesfrom production build - Test application can read environment variables
- Use strong, random secrets (32+ characters)
The Bottom Lineβ
Development: File-based secrets = β Safe (only you have access)
Production: File-based secrets = β Dangerous (many people + attackers can access)
Solution: Use environment variables or secrets managers in production! π
Now you understand why we store secrets differently in development vs production - it's all about minimizing the attack surface and who has access to your secrets! π―
