Secret management (passwords, API keys, user IDs) — Spring Boot best practices

1) Never hard-code or commit secrets

  • Don’t put secrets in code, application.yml, or sample configs.
  • Add to .gitignore: .env, *.secrets.yml, *.key, keystores.
  • For local dev, load from env vars or a local .env (not committed).
# application.yml
spring:
  datasource:
    url: jdbc:postgresql://${DB_HOST:localhost}:5432/app
    username: ${DB_USER}
    password: ${DB_PASSWORD}   # comes from env/secret manager; no inline defaults

2) Prefer a real secret manager (strongly recommended)

Pick one and standardize:

  • HashiCorp Vault (+ Spring Cloud Vault)
  • AWS Secrets Manager / Parameter Store
  • GCP Secret Manager
  • Azure Key Vault
  • Kubernetes Secrets (with KMS/Envelope encryption enabled)

Example: Spring Cloud Vault (most portable)

Dependency

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-vault-config</artifactId>
</dependency>

Bootstrap config (application.yml)

spring:
  cloud:
    vault:
      uri: http://vault:8200
      authentication: approle
      app-role:
        role-id: ${VAULT_ROLE_ID}
        secret-id: ${VAULT_SECRET_ID}
      kv:
        enabled: true
        backend: secret
        default-context: application

Vault path example: secret/application holding spring.datasource.username, spring.datasource.password.
Supports rotation without redeploy (with @RefreshScope or /actuator/refresh if using Spring Cloud).

Example: AWS Secrets Manager (common on AWS)

Resolve secrets into env vars at container start (e.g., init script/sidecar), or use a library that maps secrets to Spring properties. Keep IAM policy scoped to only the secret ARNs you need.

Example: Kubernetes Secrets

Mount as files or env vars; prefer files for long values and to avoid accidental logging.

env:
- name: DB_PASSWORD
  valueFrom:
    secretKeyRef:
      name: db-secret
      key: password

3) Use typed binding & single source of truth

Bind secrets into @ConfigurationProperties (constructor-bound), not scattered @Value strings.

@ConfigurationProperties(prefix = "app.db")
public record DbSecrets(String username, String password) {}
app:
  db:
    username: ${DB_USER}
    password: ${DB_PASSWORD}

4) Limit blast radius (least privilege)

  • Separate DB users per service; no shared superusers.
  • Narrow IAM/roles to only the secrets/paths needed.
  • Use network policies / SGs to limit who can talk to what.

5) Rotate, don’t “set and forget”

  • Choose a rotation strategy (e.g., monthly/quarterly, or automate via the secret manager).
  • For DB creds, use short-lived users or rotate passwords and reload datasource without downtime:
    • With Spring Cloud + Vault: enable rotation & @RefreshScope.
    • Or implement a health-checked HikariCP re-init on password change.

6) Don’t leak secrets in logs, errors, or metrics

  • Never log request bodies or headers that can include tokens.
  • Spring Boot sanitizes common keys in Actuator; extend it:
management:
  endpoint:
    env:
      keys-to-sanitize: password,secret,key,token,.*credentials.*,.*pwd.*,apiKey
  • Avoid printing ConfigurationProperties with .toString().

7) Secure Actuator & configuration endpoints

  • Expose only what you need; secure with auth & network controls.

management:
  endpoints:
    web:
      exposure:
        include: "health,info,metrics,prometheus"
  server:
    port: 8081

8) Protect secrets at rest & in transit

  • Turn on encryption at rest for disks/volumes & DBs.
  • Use TLS everywhere (service ↔ secret manager, app ↔ DB).
  • For custom keys/certs, store in Java Keystore (JKS/PKCS12) managed by a secret manager or mounted as files with restricted permissions.

9) CI/CD handling

  • Keep pipeline secrets in the CI system’s secret store (e.g., GitHub Actions Secrets, GitLab CI Variables).
  • Inject at deploy time → environment of the container/task, not baked into images.
  • Use multi-stage builds; ensure no RUN echo SECRET >> file remains in layers.

10) Testing strategy

  • Use Testcontainers to provision throwaway DBs with ephemeral creds.
  • For integration tests, load fake secrets via env or a test profile; never real keys.

11) Auditing & monitoring

  • Turn on secret-manager audit logs (who read/rotated).
  • Alert on anomalous secret reads, repeated failures, or access from unusual roles/regions.

Quick “good vs bad” cheatsheet

❌ Anti-patterns

  • Secrets in application.yml or code
  • Sharing admin DB user across services
  • Logging tokens/passwords
  • Exposing /actuator/env publicly
  • Long-lived static credentials

✅ Good patterns

  • Externalize via env/secret manager
  • Typed binding with @ConfigurationProperties
  • Least-privilege roles & network controls
  • Automated rotation & reload
  • Extended sanitization & secure Actuator

Minimal starter template

Docker/K8s deployment env

spring:
  datasource:
    url: jdbc:mysql://${DB_HOST}:3306/app
    username: ${DB_USER}
    password: ${DB_PASSWORD}
app:
  jwt:
    issuer: ${JWT_ISSUER}
    secret: ${JWT_SECRET}   # Prefer KMS-managed or Vault-managed

Properties class

@ConfigurationProperties(prefix = "app.jwt")
public record JwtProps(String issuer, String secret) {}
Back to blog

Leave a comment