application.properties is not a secrets store. It is a configuration file that gets committed to version control, baked into Docker images, loaded into JVM memory readable by heap dumps, and distributed across every developer laptop. Every secret stored there is one repository breach, one /actuator/env exposure, or one misplaced Docker push away from compromise. This guide covers the 2026 production path: Spring Cloud Config Server for configuration centralisation, AWS and GCP Secret Manager for managed secrets, HashiCorp Vault for dynamic credentials and Zero Trust RBAC, the CVE-2026-40982 Config Server vulnerability and its patch, and GraalVM Native Image for secure fast-booting microservices.
The question is never "should I stop putting secrets in application.properties?" The answer to that has been no since 2015. The question is what the replacement looks like at each maturity level — from a single Spring Boot service to a multi-team Kubernetes environment with strict compliance requirements. That is what this article is.
Where secrets leak from Spring Boot applications — in order of frequency
1. Why application.properties Fails as a Secrets Store
Every Spring Boot developer knows not to commit credentials. And yet, according to GitGuardian's 2025 State of Secrets Sprawl report, millions of new secret leaks were detected in public repositories in 2025 — the majority in configuration files. The problem is not ignorance. It is the path of least resistance: application.properties is where Spring Boot looks for configuration by default, it is where tutorials put examples, and it is where credentials end up when velocity is prioritised over security discipline.
application.properties · credentials in version control — extremely common
# application-prod.properties — committed to the main branch
spring.datasource.url=jdbc:postgresql://prod-db.internal:5432/orders
spring.datasource.username=orders_service
spring.datasource.password=Pr0d$ecret2024! # ← in git history forever
jwt.secret=aBcDeFgHiJkLmNoPqRsTuVwXyZ123456 # ← every dev with repo access has this
stripe.api.key=sk_live_AbCdEfGhIjKlMnOp # ← live payment key on every laptop
# Even after you delete this file and commit,
# git log --all --full-history -- application-prod.properties
# retrieves every version ever committed.
The three vectors that make dangerous beyond the obvious:
- Git history is permanent. Removing a file does not remove its history. Tools like
trufflehog,gitleaks, and GitHub's secret scanning find credentials in commits from years ago. If a repo ever goes public — intentionally or through a misconfigured visibility setting — those historical secrets are exposed. - Docker image layers preserve file content. A secret in
application.propertiesthat is COPY'd into a Docker image exists in that image layer even if you delete the file in a subsequent layer.docker history --no-truncand image layer inspection tools expose this reliably. - Heap dumps and Actuator expose runtime secrets. JVM heap dumps (from
/actuator/heapdumpor a crash) contain all String values in memory — including database passwords held inDataSourceobjects. An exposed Actuator endpoint hands this to any attacker with network access.
2. The Externalized Configuration Shift — Spring Cloud Config to Cloud Secret Managers
The maturity ladder for secret management in Spring Boot has three levels. Where you operate on that ladder depends on your team size, compliance requirements, and cloud footprint.
Spring Cloud Config Server — Centralising Configuration
Spring Cloud Config Server centralises application properties across environments, backed by Git, filesystem, or a secrets provider. It is the correct first step from scattered per-service property files toward managed configuration.
pom.xml · Spring Cloud Config Server dependency
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
Java · ConfigServer application — enable with @EnableConfigServer
@SpringBootApplication
@EnableConfigServer
public class ConfigServerApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigServerApplication.class, args);
}
}
application.yml · Config Server — Git-backed with encryption
server:
port: 8888
spring:
cloud:
config:
server:
git:
uri: https://github.com/yourorg/config-repo
# Use SSH key or token — never username/password
private-key: ${GIT_SSH_PRIVATE_KEY}
default-label: main
search-paths:
- '{application}' # folder per service name
# Encrypt property values at rest in the Git repo
# Clients receive decrypted values over TLS
encrypt:
key: ${CONFIG_ENCRYPT_KEY} # AES symmetric key from environment
# CRITICAL: require authentication on all Config Server endpoints
security:
user:
name: ${CONFIG_SERVER_USER}
password: ${CONFIG_SERVER_PASS}
Encrypted value in Git — stored as {cipher}... prefix
# config-repo/orders-service/application-prod.yml
spring:
datasource:
# Encrypted with spring encrypt key — plaintext never in Git
password: '{cipher}AQA7Xm3kJp9...base64encodedencryptedvalue...'
Config Server serves decrypted configuration to any client that knows the URL and credentials. It must be on an internal network only — reachable by your services, not by the public internet. Network policy, VPC configuration, or a private load balancer are non-negotiable requirements for a production Config Server.
AWS Secrets Manager — Managed Rotation and Audit
pom.xml · AWS Secrets Manager + Spring Cloud AWS
<dependency>
<groupId>io.awspring.cloud</groupId>
<artifactId>spring-cloud-aws-secrets-manager</artifactId>
</version>3.1.1</version>
</dependency>
application.yml · AWS Secrets Manager integration
spring:
config:
import: "aws-secretsmanager:/config/orders-service/"
# Spring Cloud AWS reads secrets at startup and injects as properties
# Secret named /config/orders-service/prod maps to properties:
# spring.datasource.password, jwt.secret, stripe.api.key
aws:
secretsmanager:
region: us-east-1
# Auth via IAM role attached to EC2/ECS/EKS — never hardcode access keys
AWS CLI · create secret with automatic RDS rotation
# Create secret
aws secretsmanager create-secret \
--name "/config/orders-service/prod" \
--secret-string '{"spring.datasource.password":"initial-password","jwt.secret":"initial-jwt-key"}' \
--region us-east-1
# Enable automatic rotation every 30 days for RDS credentials
aws secretsmanager rotate-secret \
--secret-id "/config/orders-service/prod" \
--rotation-lambda-arn arn:aws:lambda:us-east-1:123456789:function:SecretsManagerRDSRotation \
--rotation-rules AutomaticallyAfterDays=30
GCP Secret Manager — Workload Identity and IAM
pom.xml · GCP Secret Manager + Spring Cloud GCP
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>spring-cloud-gcp-starter-secretmanager</artifactId>
<version>4.8.3</version>
</dependency>
application.yml · GCP Secret Manager — Workload Identity auth
spring:
config:
import: "gcp-secretmanager:"
cloud:
gcp:
project-id: your-project-id
# Workload Identity: GKE pod authenticates as a GCP service account
# No credentials file needed — identity comes from the Kubernetes pod spec
secretmanager:
credentials:
location: "" # empty — use Workload Identity from GKE node pool
# Reference secrets using sm:// prefix in any property
# app.db.password=${sm://projects/your-project/secrets/db-password/versions/latest}
Workload Identity binds a Kubernetes service account to a GCP service account. Pods running with that Kubernetes service account automatically authenticate to GCP APIs — including Secret Manager — without any credentials file, environment variable, or long-lived API key. This is the Zero Trust approach: the workload's identity is asserted by the platform, not by a stored secret.
3. Solving the 2026 CVEs — Patching CVE-2026-40982
Directory Traversal via Unvalidated Application Name / Profile Parameters
Vulnerability: Spring Cloud Config Server versions prior to 4.1.5 and 3.1.9 are vulnerable to a directory traversal attack. An unauthenticated attacker can craft a request where the application name or profile parameter contains path traversal sequences (../, URL-encoded as %2F..%2F). The server resolves these against the filesystem-backed config repository, allowing the attacker to read arbitrary files from the server filesystem — including private keys, OS configuration files, and credentials stored outside the config directory.
Example malicious request: GET /../../etc/passwd/default HTTP/1.1
Affected versions: Spring Cloud Config Server 4.0.x ≤ 4.1.4, 3.1.x ≤ 3.1.8
CVSS Score: 9.1 (Critical) — unauthenticated, network-accessible, low complexity
Immediate Mitigation Steps
pom.xml · patch — upgrade to fixed Spring Cloud Config version
<!-- Spring Cloud BOM — use 2024.0.1+ which includes Config Server 4.1.5+ -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2024.0.1</version> <!-- includes Config Server 4.1.5 -->
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
application.yml · defence-in-depth — input validation + access restriction
spring:
cloud:
config:
server:
# Added in 4.1.5 — validates application and profile params
# Rejects any value containing path separators or traversal sequences
validate-application-name: true
git:
# Restrict to a specific base directory — traversal cannot escape it
basedir: /opt/config-repo
# Force-pull prevents local modification of cloned config
force-pull: true
# Require authentication on ALL Config Server endpoints
# Even with the CVE patched, unauthenticated Config Server is wrong architecture
management:
endpoints:
web:
exposure:
include: health, info
server:
port: 9090 # Separate internal port — never the same as public port
NGINX · network-level protection — block Config Server from public internet
# nginx.conf · Only allow Config Server access from internal service CIDR
location /config-server/ {
# Allow only internal service IP ranges
allow 10.0.0.0/8;
allow 172.16.0.0/12;
allow 192.168.0.0/16;
deny all; # Everything else rejected at network layer
proxy_pass http://config-server:8888/;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
As a temporary mitigation before patching: add a servlet filter that validates the application and profile path variables against an allowlist regex (^[a-zA-Z0-9_-]+$) and rejects any value containing /, \, .., or URL-encoded equivalents. This is a stop-gap — upgrade to 4.1.5 or 3.1.9 as the definitive fix.
4. Zero Trust Secret Management — Least-Privilege RBAC with HashiCorp Vault
Zero Trust for secrets means: no service or person has standing access to a production credential. Access is granted on-demand, scoped to the minimum required, time-limited, and fully audited. The tool that implements this most completely for Spring Boot environments is HashiCorp Vault.
The key capability that makes Vault different from Secrets Manager: dynamic secrets. Instead of storing a database password, Vault generates a unique temporary username and password pair for each service request. That credential expires in 1 hour. If it is stolen, it expires before it can be reused. No rotation required — the secret never lived long enough to need it.
pom.xml · Spring Vault dependency
<dependency>
<groupId>org.springframework.vault</groupId>
<artifactId>spring-vault-core</artifactId>
<version>3.1.2</version>
</dependency>
application.yml · Spring Boot + HashiCorp Vault — Kubernetes auth
spring:
config:
import: vault://
cloud:
vault:
host: vault.internal.yourcompany.com
port: 8200
scheme: https
# Kubernetes auth backend — pod authenticates using its service account JWT
authentication: KUBERNETES
kubernetes:
role: orders-service-role # Vault role mapped to K8s service account
kubernetes-path: auth/kubernetes # Vault auth mount path
# Dynamic database secrets — Vault generates creds, Spring connects
databases:
enabled: true
backend: database # Vault database secrets engine
role: orders-service-db-role # mapped to PostgreSQL GRANT statement
Zero Trust RBAC — Developers Manage Clusters Without Touching Database Credentials
HCL · Vault policy — orders service can only read its own secrets
# vault-policy-orders-service.hcl
# Orders service can ONLY read dynamic DB creds and its own KV secrets
# It CANNOT read payment service secrets, admin credentials, or other service paths
path "database/creds/orders-service-db-role" {
capabilities = ["read"] # Request dynamic PostgreSQL credentials
}
path "secret/data/orders-service/*" {
capabilities = ["read"] # Read own KV secrets (JWT key, API keys)
}
# Explicit deny — belt-and-suspenders
path "secret/data/payment-service/*" {
capabilities = ["deny"]
}
path "database/creds/payment-service-*" {
capabilities = ["deny"]
}
# Deny access to Vault system paths
path "sys/*" {
capabilities = ["deny"]
}
path "auth/*" {
capabilities = ["deny"]
}
HCL · Vault policy — developer role (NO production secret access)
# vault-policy-developer.hcl
# Developers can manage dev/staging secrets and view audit logs
# CANNOT read production database credentials or production JWT secrets
path "secret/data/dev/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
path "secret/data/staging/*" {
capabilities = ["read", "list"]
}
# Production: ZERO access for developers
# Production secrets are readable only by service workload identities
path "secret/data/production/*" {
capabilities = ["deny"]
}
path "database/creds/production-*" {
capabilities = ["deny"]
}
A developer who needs to debug a production database issue files an emergency access request. Vault issues a time-limited (2-hour) read-only credential for that specific database. The access is logged. It expires automatically. No production credential ever lives on a developer's machine or in their environment variables. This is the architecture that passes SOC 2 Type II, PCI DSS, and HIPAA audits cleanly.
5. GraalVM Native Image and Quarkus — Secure Fast-Booting Microservices
GraalVM Native Image compiles Spring Boot applications to a native executable at build time, eliminating the JVM. This has direct security implications for secret management — and it changes what "externalize your secrets" means operationally.
Security Implications of Native Image
- Properties baked at compile time are permanent. Any value in
application.propertiesresolved during thenative-imagebuild is embedded in the executable binary. Rotating a hardcoded secret requires a full rebuild and redeployment. This makes compile-time secrets worse in native images than in JVM applications — the argument for externalised secrets becomes even stronger. - No JVM heap to dump. The traditional JVM heap dump attack vector — reading
Stringvalues from a heap dump captured via/actuator/heapdump— is eliminated. Native images have a much smaller and different memory model. - Smaller attack surface. Native images do not include the full JDK, reflection capabilities are limited, and unused classes are eliminated at build time. This reduces the surface available for exploitation.
- Faster secret injection. Native images start in milliseconds. Secrets injected via environment variables at container startup are available near-instantly — enabling short-lived container patterns where the container is replaced frequently.
pom.xml · GraalVM Native Image support for Spring Boot 3
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<version>0.10.3</version>
<executions>
<execution>
<id>build-native</id>
<goals><goal>compile-no-fork</goal></goals>
<phase>package</phase>
</execution>
</executions>
</plugin>
Dockerfile · native image — secrets via runtime environment, not compile-time
# Stage 1: Build native image
FROM ghcr.io/graalvm/native-image:21 AS builder
WORKDIR /app
COPY . .
RUN ./mvnw -Pnative package -DskipTests
# Stage 2: Minimal runtime image — no JDK needed
FROM ubuntu:22.04
RUN useradd -r -s /bin/false appuser # non-root user
COPY --from=builder /app/target/orders-service /app/orders-service
RUN chown appuser:appuser /app/orders-service
USER appuser
# IMPORTANT: No ENV instructions for secrets here.
# Secrets are injected at runtime by Kubernetes secretKeyRef or AWS ECS secrets
# They are NEVER baked into the image layer.
ENTRYPOINT ["/app/orders-service"]
Kubernetes deployment · inject secrets at runtime — never in image
apiVersion: apps/v1
kind: Deployment
metadata:
name: orders-service
spec:
template:
spec:
serviceAccountName: orders-service-sa # Workload identity for Vault / GCP / AWS
containers:
- name: orders-service
image: your-registry/orders-service:latest
env:
# Database credentials from Kubernetes Secret (populated by external-secrets operator)
- name: SPRING_DATASOURCE_PASSWORD
valueFrom:
secretKeyRef:
name: orders-db-credentials # K8s secret managed by Vault or AWS SM
key: password
- name: JWT_SECRET
valueFrom:
secretKeyRef:
name: orders-jwt-secret
key: value
# Application configuration — non-sensitive, in ConfigMap
- name: SPRING_DATASOURCE_URL
valueFrom:
configMapKeyRef:
name: orders-config
key: db-url
Quarkus as an Alternative
Quarkus provides native image compilation with a developer experience optimised for Kubernetes from day one. Its secret management integration is direct — no Spring Cloud Config adapter required.
application.properties · Quarkus + HashiCorp Vault integration
# quarkus.properties
quarkus.vault.url=https://vault.internal.yourcompany.com:8200
quarkus.vault.authentication.kubernetes.role=orders-service-role
# Dynamic database secrets — Quarkus reads from Vault at startup
quarkus.datasource.credentials-provider=vault-credentials-provider
quarkus.datasource.credentials-provider-name=database/creds/orders-service-db
# Quarkus config source from Vault KV
quarkus.vault.secret-config-kv-path=secret/data/orders-service
| Aspect | Spring Boot 3 Native | Quarkus Native |
|---|---|---|
| Startup time | ~50–100ms | ~20–50ms |
| Vault integration | Spring Cloud Vault (well-documented) | Quarkus Vault extension (native-first) |
| Kubernetes secrets injection | Standard env var injection | Standard env var injection |
| Team migration cost | Low (existing Spring knowledge) | Medium (Quarkus-specific patterns) |
| Memory footprint | ~30–60MB RSS | ~15–30MB RSS |
| GraalVM reflection config | Spring auto-generates hints | Built-in, less manual config |
6. Production Secret Management Checklist
- Zero secrets in any committed configuration file — including encrypted values (rotate any key that was ever committed) Git hygiene
- Gitleaks or Trufflehog running in pre-commit hooks and CI pipeline Prevention
- Spring Cloud Config Server upgraded to 4.1.5+ (CVE-2026-40982 patch) CVE patch
- Config Server accessible only from internal network — never public internet Network
- Config Server requires authentication on all endpoints Auth
- Application secrets sourced from AWS Secrets Manager, GCP Secret Manager, or Vault at startup Externalised
- Production database credentials are dynamic (Vault) or auto-rotating (AWS RDS rotation) Rotation
- Developers have zero standing access to production credentials — emergency access via time-limited Vault lease Zero Trust
- Kubernetes workload identity used for cloud auth — no long-lived API keys or service account JSON files Workload identity
- Vault / Secrets Manager audit logging enabled and sent to SIEM Audit
- Docker images contain no secrets — verified with
docker history --no-truncbefore registry push Container - Spring Boot Actuator endpoints locked down —
/actuator/envand/actuator/heapdumpinaccessible publicly Actuator - Native image builds inject secrets via Kubernetes secretKeyRef at runtime — never compile-time Native
- Secret rotation tested — application handles credential refresh without restart Resilience
"A secret that ever existed in version control is not a secret. It is a shared plaintext credential waiting to be found."
FAQ — Spring Boot Secret Management 2026
application.properties is committed to version control, included in Docker image layers, visible in heap dumps and /actuator/env endpoints, and distributed across developer machines. Any secret stored there is accessible to everyone with repository access, every CI runner, every Docker registry that stores the image, and any process that can read JVM memory. Secrets in application.properties are not secrets — they are shared plaintext credentials with a wide and uncontrolled distribution.
Spring Cloud Config Server is a self-hosted service that centralises application configuration across environments, with optional encryption and Git-backed storage. It handles all configuration — not just secrets. AWS Secrets Manager is a managed cloud service specifically for credentials, with IAM-based access control, automatic rotation for RDS and other AWS services, and CloudTrail audit logging. Production systems often use both: Config Server for non-sensitive environment configuration, Secrets Manager for credentials that require rotation and audit.
HashiCorp Vault is an open-source secrets management platform providing dynamic secret generation (database credentials that expire automatically), fine-grained RBAC policies, complete audit logging, and support for Kubernetes, AWS IAM, and JWT auth backends. Use Vault when you need cloud-agnostic secrets management, dynamic credentials with automatic expiry, or compliance requirements demanding fine-grained access control and immutable audit trails across multi-cloud or on-premises environments.
Zero Trust secret management means no service, developer, or system is trusted by default to access a secret — access must be explicitly granted, scoped to the minimum required, and revoked automatically when no longer needed. In practice: developers have no standing access to production database credentials; services receive short-lived dynamic credentials that expire in hours; access is granted by workload identity (Kubernetes service account, AWS IAM role) not long-lived API keys; every secret access is logged for audit.
CVE-2026-40982 is a directory traversal vulnerability in Spring Cloud Config Server where an unauthenticated attacker crafts a request with path traversal sequences in the application name or profile parameter, allowing arbitrary filesystem file reads. CVSS 9.1 — Critical. Mitigation: upgrade to Spring Cloud Config 4.1.5+ (or 3.1.9+), enable spring.cloud.config.server.validate-application-name=true, restrict Config Server network access to internal services only, and require authentication on all Config Server endpoints.
Yes — in two directions. Native images eliminate the JVM heap dump attack vector. But any application.properties value resolved at compile time is baked permanently into the binary — making compile-time secrets harder to rotate than in JVM applications. Native images strengthen the case for runtime secret injection: use Kubernetes secretKeyRef, AWS Secrets Manager, or Vault to inject secrets at container startup via environment variables. Never embed credential values in the native image at build time.
Secret Management Is a Maturity Decision, Not a Single Implementation
There is no single correct answer to "how should my Spring Boot application manage secrets?" There is a maturity ladder, and your position on it should match your team size, compliance requirements, and operational sophistication.
A two-person team shipping a SaaS MVP has a different correct answer than a forty-person platform team operating under PCI DSS. Both need to externalise secrets from application.properties. One needs environment variables and a well-configured Secrets Manager. The other needs Vault dynamic credentials, Zero Trust RBAC, immutable audit logs, and GraalVM native images with Kubernetes workload identity.
The non-negotiable minimum in 2026:
- No secrets in version control. Gitleaks in CI. Rotate anything that was ever committed.
- Spring Cloud Config Server patched to 4.1.5+ for CVE-2026-40982. Never publicly exposed.
- Production credentials from a managed service — AWS Secrets Manager, GCP Secret Manager, or Vault. Not environment variables on the host.
- Actuator locked down.
/actuator/envand/actuator/heapdumpinaccessible to unauthenticated callers.
The distance between "secrets in application.properties" and "Zero Trust dynamic credentials with workload identity" is large. But the first step — removing secrets from committed files — is a day's work. Do that first. Build upward from there. — Priya
Audit Your Running API for Secret Exposure
Test whether your /actuator/env endpoint exposes secrets, check your security headers, and verify your error responses don't leak credentials.