Spring Boot with Vault

This guide covers three ways to manage secrets with HashiCorp Vault in Spring Boot. Avoid hard-coded Vault tokens and long-lived tokens without appropriate access controls and rotation procedures.

When to use this guide

  • HashiCorp Vault is your organizational standard.
  • Vault Agent Injector is available in Kubernetes.
  • Vault Secrets Operator (VSO) is deployed in the cluster.
  • The application integrates directly through Spring Cloud Vault.
  • A Vault token is hard-coded in application.yml.

Examples

Pattern A: Spring Cloud Vault (direct integration)

The application calls the Vault API to load secrets. Spring Cloud Vault reads spring.config.import: vault:// in application.yml and supplies values from Vault.

When to choose this pattern

  • Vault is also used outside Kubernetes, such as on VMs or ECS.
  • The Spring Boot application needs to retrieve secrets directly from Vault.

Before

yaml
# application.yml: 토큰 하드코딩
spring:
  config:
    import: vault://
  cloud:
    vault:
      host: vault.internal
      port: 8200
      scheme: https
      authentication: TOKEN
      token: hvs.xxxxxxxxxxxxxxxxxxxxxxxxxxxx   # 절대 커밋하면 안되는 부분
      kv:
        enabled: true
        backend: secret
        default-context: my-service

After

yaml
# application.yml: Kubernetes Auth + role 기반 인증
spring:
  config:
    import: vault://
  cloud:
    vault:
      host: vault.internal
      port: 8200
      scheme: https
      authentication: KUBERNETES
      kubernetes:
        role: my-service-role
        service-account-token-file: /var/run/secrets/kubernetes.io/serviceaccount/token
      kv:
        enabled: true
        backend: secret
        default-context: my-service

In Kubernetes, KUBERNETES authentication uses a service account token. On VMs or ECS, consider supported platform authentication or APPROLE; deliver AppRole's role-id and secret-id securely. For a separately mounted projected service account token, set service-account-token-file to its actual path, such as /var/run/secrets/tokens/vault-token.

Dependency examples
groovy
// build.gradle
dependencies {
    implementation 'org.springframework.cloud:spring-cloud-starter-vault-config'
    // Dynamic Secrets (Database Secret Engine) 사용 시 추가
    implementation 'org.springframework.cloud:spring-cloud-vault-config-databases'
}
xml
<!-- pom.xml -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-vault-config</artifactId>
</dependency>
<!-- Dynamic Secrets (Database Secret Engine) 사용 시 추가 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-vault-config-databases</artifactId>
</dependency>
Spring code examples

Vault keys become property names, so names such as db.password and jwt.secret must match the application's references. The @ConfigurationProperties class below needs a validation dependency and registration through @ConfigurationPropertiesScan or @EnableConfigurationProperties.

java
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class DatabaseConfig {

    // Vault 경로 secret/my-service의 db.password 키에 저장된 값
    @Value("${db.password}")
    private String dbPassword;

    @Value("${jwt.secret}")
    private String jwtSecret;
}
java
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;

import jakarta.validation.constraints.NotBlank;

@Validated
@ConfigurationProperties(prefix = "db")
public class DbProperties {

    @NotBlank
    private String password;

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

Pattern B: Vault Agent Injector (Kubernetes sidecar)

Vault Agent runs as an injected sidecar and supplies secrets as files. With the Injector and Vault authentication configured, annotations request file injection and Spring reads the file through spring.config.import.

When to choose this pattern

  • Vault Agent Injector is deployed in the Kubernetes cluster.
  • You want to inject Vault secrets as files without changing application code.

Before

yaml
# Vault 토큰 하드코딩 및 DB 비밀번호와의 혼동
env:
  - name: VAULT_TOKEN
    value: "hvs.xxxxxxxxxxxxxxxxxxxxxxxxxxxx"
  - name: DB_PASSWORD
    valueFrom:
      secretKeyRef:
        name: my-secret        # DB 비밀번호 대신 Vault 토큰을 참조하는 잘못된 예시
        key: vault-token

After

Pod annotation example

This Deployment excerpt omits selectors, labels, and other settings. Bind the Pod's service account to the Vault role and grant read access to the required path.

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-service
spec:
  template:
    metadata:
      annotations:
        vault.hashicorp.com/agent-inject: "true"
        vault.hashicorp.com/role: "my-service-role"
        vault.hashicorp.com/agent-inject-secret-application.properties: "secret/data/my-service"
        vault.hashicorp.com/agent-inject-template-application.properties: |
          {{- with secret "secret/data/my-service" -}}
          db.password={{ .Data.data.db_password }}
          jwt.secret={{ .Data.data.jwt_secret }}
          {{- end }}
    spec:
      containers:
        - name: my-service
          image: my-service:latest

The Injector creates /vault/secrets/application.properties. The template maps KV v2 keys db_password and jwt_secret to Spring properties. Escape line breaks and special characters in actual values for the properties format.

How Spring reads the file
yaml
# application.yml: 주입된 파일을 Spring 설정으로 로딩
spring:
  config:
    import: file:/vault/secrets/application.properties

The secret file is required, so the import does not use optional:.

Pattern C: Vault Secrets Operator

VSO watches VaultStaticSecret and VaultDynamicSecret resources and synchronizes Vault values into Kubernetes Secrets. The application reads ordinary Kubernetes Secrets without a Vault integration. Since the values are then stored in etcd outside Vault's encryption boundary, configure encryption at rest and RBAC first.

When to choose this pattern

  • VSO is deployed in Kubernetes.
  • You want to use Kubernetes Secrets without a Vault Agent sidecar.
  • The same secret needs to be supplied to multiple namespaces.

Before

yaml
# Kubernetes Secret에 값을 직접 하드코딩하지 않는다
apiVersion: v1
kind: Secret
metadata:
  name: my-service-secret
type: Opaque
stringData:
  db-password: "s3cr3t-password"   # 절대 커밋하면 안되는 부분
  jwt-secret: "my-jwt-secret-key"

After

yaml
# VaultStaticSecret CRD: VSO가 Vault에서 값을 읽어 Kubernetes Secret으로 동기화
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
  name: my-service-secret
  namespace: my-namespace
spec:
  vaultAuthRef: my-vault-auth
  mount: secret
  type: kv-v2
  path: my-service
  refreshAfter: 60s
  destination:
    name: my-service-secret   # 생성될 Kubernetes Secret 이름
    create: true
How Spring reads the values

The resource references my-vault-auth, configured in the same namespace. If the Vault keys are db_password and jwt_secret, explicitly map them to environment variables and application properties:

yaml
# application.yml
db:
  password: ${DB_PASSWORD}
jwt:
  secret: ${JWT_SECRET}
yaml
# 컨테이너 환경 변수 설정의 일부
env:
  - name: DB_PASSWORD
    valueFrom:
      secretKeyRef:
        name: my-service-secret
        key: db_password
  - name: JWT_SECRET
    valueFrom:
      secretKeyRef:
        name: my-service-secret
        key: jwt_secret

Environment variables in a running process do not refresh automatically. Configure a restart or supported reload procedure after synchronization.

Dynamic secrets

Vault can issue DB accounts or AWS keys with a lease. Monitor renewal and revocation, and configure the application to adopt new credentials.

Use with Pattern A (Spring Cloud Vault)

yaml
# application.yml: DB Dynamic Secrets
spring:
  config:
    import: vault://
  cloud:
    vault:
      host: vault.internal
      port: 8200
      scheme: https
      authentication: KUBERNETES
      kubernetes:
        role: my-service-role
        service-account-token-file: /var/run/secrets/kubernetes.io/serviceaccount/token
      database:
        enabled: true
        role: my-service-db-role   # Vault Database Secret Engine에 등록된 role
        backend: database

Spring Cloud Vault obtains database credentials at startup and supplies them to the DataSource. It manages renewable leases, but does not obtain new credentials and reconfigure the DataSource at max_ttl. Arrange an application restart or separate credential and connection-pool refresh before expiry.

Use with Pattern C (VSO)

yaml
# VaultDynamicSecret CRD
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultDynamicSecret
metadata:
  name: my-service-db-secret
  namespace: my-namespace
spec:
  vaultAuthRef: my-vault-auth
  mount: database
  path: creds/my-service-db-role
  destination:
    name: my-service-db-secret
    create: true

Developer tasks

  • Remove Vault tokens from application.yml.
  • Pattern A: Choose a supported authentication method for the environment and protect credential delivery.
  • Pattern B: Add Pod annotations and import the injected file with spring.config.import.
  • Pattern C: Define VaultStaticSecret or VaultDynamicSecret resources.
  • Validate required @ConfigurationProperties values with constraints such as @NotBlank and fail closed when missing.
  • Check logs, exceptions, and Actuator endpoints for secret exposure.

Infrastructure and platform tasks

  • Create service-specific Vault policies and roles with least privilege.
  • Enable the Kubernetes auth method and bind each service account to the appropriate role.
  • Pattern B: Confirm that vault-agent-injector is deployed.
  • Pattern C: Confirm that VSO is deployed.
  • Configure database connections and roles in the Database Secrets Engine.
  • Document restarts or reloads for secret rotation.
  • Keep Vault tokens out of Helm values, ConfigMaps, and Docker images.

Verification

  • Check application.yml, Helm values, and ConfigMaps for embedded Vault tokens.
  • Pattern A: Confirm successful loading at startup without logging secret values.
  • Pattern B: Check that the file exists under /vault/secrets/ with kubectl exec.
  • Pattern C: Confirm creation of the synchronized Secret with kubectl get secret.
  • Check /actuator/env, debug logs, and exception responses for exposure.
  • Confirm that a missing required secret prevents startup.

Common mistakes

  • Hard-coding a Vault token in application.yml
  • Using a long-lived token without access controls or rotation
  • Hard-coding Vault paths in application code instead of configuration
  • Omitting restart procedures after rotation
  • Failing to coordinate lease renewal, maximum TTL, and the application's adoption of new credentials

Example instructions for the owner

  • “Remove the Vault token from application.yml and use KUBERNETES authentication in Kubernetes.”
  • “Configure Vault authentication and the Injector, request file injection through annotations, and import the required file with spring.config.import.”
  • “Use authentication suited to the environment and least privilege, and document how the application adopts rotated secrets.”
  • “With VSO deployed, define VaultStaticSecret or VaultDynamicSecret resources to supply Vault values as ordinary Kubernetes Secrets.”
  • “For dynamic secrets, check renewal and maximum TTL, and ensure the application uses new credentials before expiry.”

Related documentation