Spring BootとVaultの連携

Spring BootでHashiCorp Vaultを使ってシークレットを管理する3つのパターンを紹介します。Vaultトークンのハードコードや、適切なアクセス制御・ローテーションを伴わない長期トークンの使用を避けてください。

このガイドを使う場面

  • HashiCorp Vaultが組織の標準である場合
  • KubernetesでVault Agent Injectorを使用できる場合
  • クラスターにVault Secrets Operator(VSO)が導入されている場合
  • Spring Cloud Vaultで直接連携する場合
  • application.ymlにVaultトークンがハードコードされている場合

例

Pattern A: Spring Cloud Vault(直接連携)

アプリケーションがVault APIを呼び出してシークレットを読み込みます。Spring Cloud Vaultは、application.ymlのspring.config.import: vault://を読み取り、Vaultから設定値を供給します。

このパターンを選ぶ場面

  • VMやECSなど、Kubernetes以外でもVaultを使用する場合
  • Spring BootアプリケーションがVaultから直接取得する必要がある場合

変更前

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

変更後

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

Kubernetesでは、KUBERNETES認証でサービスアカウントトークンを使います。VMやECSでは対応するプラットフォーム認証やAPPROLEを検討し、AppRoleのrole-idとsecret-idは保護された経路で渡してください。Projected Service Account Tokenを別のボリュームにマウントする場合は、service-account-token-fileに実際のパス(例:/var/run/secrets/tokens/vault-token)を指定してください。

依存関係の例
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のコード例

Vaultのキーがそのままプロパティ名になるため、db.passwordやjwt.secretなど、アプリケーションの参照名と一致させてください。次の@ConfigurationPropertiesクラスには、検証用の依存関係と、@ConfigurationPropertiesScanまたは@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サイドカー)

Vault Agentをサイドカーとして注入し、シークレットをファイルで供給します。InjectorとVault認証の構成後、アノテーションでファイル注入を要求し、Springのspring.config.importで読み込みます。

このパターンを選ぶ場面

  • KubernetesにVault Agent Injectorが導入されている場合
  • アプリケーションコードを変更せず、Vaultのシークレットをファイルで渡す場合

変更前

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

変更後

Podアノテーションの例

Deploymentの抜粋であり、selectorやlabelなどは省略しています。PodのサービスアカウントとVaultロールを関連付け、必要なパスの読み取り権限を設定してください。

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

Injectorは/vault/secrets/application.propertiesを生成します。テンプレートはKV v2のdb_passwordとjwt_secretをSpringのプロパティに対応付けます。実値に改行や特殊文字がある場合は、properties形式に合わせてエスケープしてください。

Springでの読み込み
yaml
# application.yml: 주입된 파일을 Spring 설정으로 로딩
spring:
  config:
    import: file:/vault/secrets/application.properties

必須のシークレットファイルなので、optional:は付けません。

Pattern C: Vault Secrets Operator

VSOはVaultStaticSecretやVaultDynamicSecretを監視し、Vaultの値をKubernetes Secretに同期します。アプリケーションはVaultに直接連携せず、通常のKubernetes Secretを読み取ります。値はVaultの暗号化境界の外にあるetcdにも保存されるため、保存時の暗号化とRBACを先に構成してください。

このパターンを選ぶ場面

  • KubernetesにVSOが導入されている場合
  • Vault Agentサイドカーを使わず、Kubernetes Secretで管理する場合
  • 同じシークレットを複数の名前空間に供給する場合

変更前

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"

変更後

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
Springでの読み込み

このリソースは、同じ名前空間に構成したmy-vault-authを参照します。Vaultのキーがdb_passwordとjwt_secretの場合は、環境変数とアプリケーションのプロパティに明示的に対応付けてください。

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

実行中のプロセスの環境変数は自動更新されません。同期後の再起動、または対応する再読み込み手順を構成してください。

動的シークレット

Vaultは、DBアカウントやAWSキーなどを有効なリース期間付きで発行できます。更新と失効を監視し、アプリケーションが新しい認証情報を使うように構成してください。

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は起動時にDB認証情報を取得し、DataSourceに供給します。更新可能なリースは管理しますが、max_ttl到達時に新しい認証情報を取得してDataSourceを再構成することはありません。期限前にアプリケーションを再起動するか、認証情報と接続プールを別途更新する手順を用意してください。

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

開発者の作業

  • application.ymlからVaultトークンを削除してください。
  • Pattern A:環境に合った対応認証方式を選び、認証情報の受け渡しを保護してください。
  • Pattern B:Podアノテーションを追加し、注入されたファイルをspring.config.importで参照してください。
  • Pattern C:VaultStaticSecretまたはVaultDynamicSecretを定義してください。
  • @ConfigurationPropertiesの必須値を@NotBlankなどで検証し、欠けている場合は起動を中止してください。
  • ログ、例外、Actuatorエンドポイントにシークレットが出ないことを確認してください。

インフラ・プラットフォーム担当者の作業

  • サービスごとにVaultのポリシーとロールを作成し、最小権限を適用してください。
  • Kubernetes認証を有効にし、各サービスアカウントに適切なロールを関連付けてください。
  • Pattern B:vault-agent-injectorの導入を確認してください。
  • Pattern C:VSOの導入を確認してください。
  • Database Secrets Engineに接続とロールを構成してください。
  • ローテーション時の再起動や再読み込みを運用手順に記載してください。
  • Helm values、ConfigMap、DockerイメージにVaultトークンを含めないでください。

検証方法

  • application.yml、Helm values、ConfigMapにVaultトークンが埋め込まれていないことを確認してください。
  • Pattern A:値をログに出さずに、起動時の取得が成功することを確認してください。
  • Pattern B:kubectl execで/vault/secrets/にファイルがあることを確認してください。
  • Pattern C:kubectl get secretで同期先Secretの作成を確認してください。
  • /actuator/env、デバッグログ、例外応答にシークレットが出ないことを確認してください。
  • 必須のシークレットがなければ起動に失敗することを確認してください。

よくある誤り

  • application.ymlにVaultトークンをハードコードする
  • 長期トークンをアクセス制御やローテーションなしで使う
  • Vaultのパスを設定ではなくアプリケーションコードに埋め込む
  • ローテーション後の再起動手順を用意しない
  • リース更新、最大TTL、アプリケーションが新しい認証情報を使う時点を調整しない

担当者への案内例

  • 「application.ymlからVaultトークンを削除し、KubernetesではKUBERNETES認証を使用してください。」
  • 「Vault認証とInjectorを構成し、アノテーションで注入を要求して、spring.config.importで必須ファイルを読み込んでください。」
  • 「環境に合った認証と最小権限を適用し、ローテーション後に新しい値をアプリケーションへ反映する手順を記載してください。」
  • 「VSOが導入済みなら、VaultStaticSecretやVaultDynamicSecretを定義し、通常のKubernetes Secretとして値を供給してください。」
  • 「動的シークレットではリース更新と最大TTLを確認し、期限前にアプリケーションが新しい認証情報を使うようにしてください。」

関連ドキュメント