Secrets & Vault
Database passwords, API keys, and signing keys are secrets — leaking them is a breach. Yet the path of least resistance is to drop them straight into application.yml, where they end up in Git history, build artifacts, and log dumps. This page covers why that is dangerous and the spectrum of safer options, from environment variables to a dedicated secret manager like HashiCorp Vault, integrated through Spring Cloud Vault.
The problem with plaintext secrets
A secret committed to application.properties is exposed in every clone of the repository, survives in Git history even after deletion, gets baked into Docker image layers, and frequently shows up in stack traces or config-dump endpoints. Rotating it means a code change and redeploy. Anyone with read access to the repo or the image has the keys.
# DON'T do this
spring:
datasource:
password: SuperSecret123 # now in Git forever
Step one: environment variables
The minimum bar is to externalize secrets out of source and inject them at runtime. Spring Boot’s relaxed binding maps SPRING_DATASOURCE_PASSWORD to spring.datasource.password, so a placeholder reads from the environment:
spring:
datasource:
password: ${SPRING_DATASOURCE_PASSWORD}
This is a real improvement — the secret is no longer in the repo — but env vars are still static, visible to anyone who can inspect the process or container, hard to rotate without a restart, and not audited. See Externalized Configuration for the full property-resolution order.
Step two: a secret manager
A secret manager stores secrets encrypted, controls who can read each one, audits every access, and supports rotation and dynamic (short-lived) credentials. The application authenticates to the manager at startup and fetches what it needs.
| Approach | Encrypted at rest | Access control | Rotation | Audit log | Dynamic secrets |
|---|---|---|---|---|---|
| Plaintext in config | No | No | Manual + redeploy | No | No |
| Environment variables | No (process-visible) | OS/orchestrator | Restart | No | No |
| Kubernetes Secrets | base64 (optional encryption) | RBAC | Manual | Limited | No |
| HashiCorp Vault | Yes | Policies/tokens | Yes | Yes | Yes |
| AWS Secrets Manager | Yes | IAM | Yes (managed) | CloudTrail | Partial |
Spring Cloud Vault
HashiCorp Vault is a popular secret manager. Spring Cloud Vault fetches secrets at startup and exposes them as ordinary Spring properties — your code uses @Value or @ConfigurationProperties with no idea the value came from Vault.
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-vault-config</artifactId>
</dependency>
Modern Spring Boot pulls Vault in through config import rather than legacy bootstrap. Add it in application.yml:
spring:
config:
import: "vault://"
cloud:
vault:
uri: https://vault.example.com:8200
authentication: TOKEN
token: ${VAULT_TOKEN}
kv:
enabled: true
backend: secret
default-context: orders
Note: The
spring.config.import=vault://line is required in Spring Boot 2.4+. Without it, the Vault config is not loaded. You can make it optional withoptional:vault://so the app still starts in environments without Vault (e.g. local dev).
Reading KV secrets
With the KV (key/value) engine enabled, write a secret in Vault:
vault kv put secret/orders \
spring.datasource.password=s3cr3t \
app.api-key=ak_live_123
Spring Cloud Vault maps each key directly onto a Spring property. So this just works with no extra wiring:
spring:
datasource:
password: ${spring.datasource.password} # resolved from Vault KV
Or bind a group of secrets with type-safe @ConfigurationProperties:
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "app")
public record AppSecrets(String apiKey) {}
Output (startup log confirms the source):
Located property source: [VaultConfigDataResource path='secret/orders']
Fetched 2 secrets from Vault backend 'secret'
Started OrdersApplication in 1.9 seconds
Alternatives
- AWS Secrets Manager / Parameter Store — managed, IAM-controlled. Spring Cloud AWS exposes them via the same
spring.config.importmechanism (aws-secretsmanager://). - Kubernetes Secrets — mounted as env vars or files; simplest in a k8s deployment but only base64-encoded unless you enable encryption at rest. See Kubernetes Deployment.
- Jasypt — encrypts values inside the config file (
ENC(...)) using a master password supplied at runtime. Useful when you can’t run a secret manager, but it just shifts the problem to protecting the master key.
# Jasypt: encrypted value in the file, decrypted with a runtime master password
spring:
datasource:
password: ENC(7sf9aB3kQ2lP...)
Warning: Jasypt only moves the secret problem — you still must distribute and protect the
jasypt.encryptor.passwordmaster key securely. A dedicated secret manager is preferable whenever one is available.
Best Practices
- Never commit secrets to source control or bake them into images.
- Prefer a secret manager (Vault, AWS Secrets Manager) over static env vars for audit, rotation, and access control.
- Use
spring.config.import=vault://(mark itoptional:for local dev) — bootstrap is legacy. - Favor dynamic, short-lived credentials over long-lived static ones when the backend supports them.
- Rotate secrets regularly and ensure the app can pick up rotated values without a code change.