이런 경우에 참고
- 조직 표준이 HashiCorp Vault인 경우
- Kubernetes 환경에서 Vault Agent Injector 사용이 가능한 경우
- Vault Secrets Operator(VSO)가 클러스터에 배포된 경우
- Spring Cloud Vault 라이브러리로 직접 연동하는 경우
application.yml에 Vault 토큰이 하드코딩된 경우
예시
Pattern A: Spring Cloud Vault (직접 연동)
앱이 Vault API를 직접 호출해 Secret을 로딩하는 방식입니다. Spring Cloud Vault 라이브러리가 application.yml의 spring.config.import: vault:// 설정을 읽어 Vault에서 값을 주입합니다.
언제 이 패턴을 선택하는가
- Kubernetes 외 환경(VM, ECS 등)에서도 Vault를 사용하는 경우
- Spring Boot 앱이 직접 Vault API를 호출해 Secret을 로딩해야 하는 경우
변경 전
# 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
변경 후
# 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 인증 방식을 사용해 서비스 어카운트 토큰으로 Vault에 인증합니다. VM/ECS에서는 지원되는 플랫폼 인증이나 APPROLE을 검토하고, AppRole을 쓸 경우 role-id와 secret-id를 보호된 경로로 전달합니다. Projected Service Account Token을 별도 볼륨으로 마운트한 경우에는 실제 마운트 경로(예: /var/run/secrets/tokens/vault-token)를 service-account-token-file에 직접 지정해야 합니다.
필요 의존성 예시
// 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'
}
<!-- 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 등록이 필요합니다.
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;
}
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가 사이드카로 Pod에 주입되어 Secret을 파일로 제공하는 방식입니다. Injector와 Vault 인증이 구성된 환경에서 annotation으로 파일 주입을 요청하고, Spring의 spring.config.import로 파일을 읽습니다.
언제 이 패턴을 선택하는가
- Kubernetes 환경이며 Vault Agent Injector가 클러스터에 배포되어 있는 경우
- 앱 코드 변경 없이 Vault Secret을 파일로 주입하고 싶은 경우
변경 전
# Vault 토큰 하드코딩 및 DB 비밀번호와의 혼동
env:
- name: VAULT_TOKEN
value: "hvs.xxxxxxxxxxxxxxxxxxxxxxxxxxxx"
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: my-secret # DB 비밀번호 대신 Vault 토큰을 참조하는 잘못된 예시
key: vault-token
변경 후
Pod annotation 예시
Deployment의 일부이며 selector, label 등은 생략했습니다. Pod의 서비스 어카운트와 Vault role을 연결하고 필요한 경로의 읽기 권한을 구성하세요.
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
Vault Agent Injector는 위 annotation을 읽어 /vault/secrets/application.properties 파일을 생성합니다. KV v2의 db_password, jwt_secret을 Spring 속성 이름으로 매핑합니다. 실제 값에 줄바꿈이나 특수 문자가 있다면 properties 형식에 맞게 이스케이프하세요.
Spring이 읽는 방식
# application.yml: 주입된 파일을 Spring 설정으로 로딩
spring:
config:
import: file:/vault/secrets/application.properties
필수 Secret 파일이므로 optional:을 사용하지 않습니다.
Pattern C: Vault Secrets Operator
VSO가 VaultStaticSecret / VaultDynamicSecret CRD를 감시해 Vault 시크릿을 Kubernetes Secret으로 동기화합니다. 앱은 Vault를 전혀 모르고 일반 Kubernetes Secret만 읽습니다. 단, Vault의 암호화 계층을 벗어나 etcd에 저장되므로 etcd 암호화-at-rest 및 RBAC 설정이 선행되어야 합니다.
언제 이 패턴을 선택하는가
- Kubernetes 환경이며 Vault Secrets Operator가 클러스터에 배포되어 있는 경우
- Vault Agent Injector(sidecar) 없이 Kubernetes Secret 방식으로 시크릿을 관리하고 싶은 경우
- 여러 네임스페이스에 동일 시크릿을 복제해야 하는 경우
변경 전
# Kubernetes Secret에 값을 직접 하드코딩하지 않는다
apiVersion: v1
kind: Secret
metadata:
name: my-service-secret
type: Opaque
stringData:
db-password: "s3cr3t-password" # 절대 커밋하면 안되는 부분
jwt-secret: "my-jwt-secret-key"
변경 후
# 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이 읽는 방식
위 VaultStaticSecret은 같은 네임스페이스에 구성된 my-vault-auth를 참조합니다. Vault의 키가 db_password, jwt_secret인 경우 아래처럼 환경 변수와 앱 속성에 명시적으로 연결합니다.
# application.yml
db:
password: ${DB_PASSWORD}
jwt:
secret: ${JWT_SECRET}
# 컨테이너 환경 변수 설정의 일부
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
환경 변수는 실행 중인 프로세스에서 자동 갱신되지 않습니다. Secret 동기화 후 앱 재시작 또는 지원되는 재로딩 절차를 구성하세요.
Dynamic Secrets
Vault는 DB 계정이나 AWS 키 등을 임대 기간이 있는 자격 증명으로 발급할 수 있습니다. 갱신과 폐기 동작을 모니터링하고, 앱이 새 자격 증명을 적용하도록 구성해야 합니다.
Pattern A에서 사용 (Spring Cloud Vault)
# 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가 앱 기동 시 Vault Database Secret Engine에서 단기 DB 계정을 발급받아 DataSource에 주입합니다. 갱신 가능한 lease를 관리하지만, max_ttl에 도달했을 때 새 자격 증명을 받아 DataSource를 다시 구성하지는 않습니다. 만료 전에 앱 재시작이나 별도의 자격 증명 갱신·연결 풀 재구성 절차를 마련하세요.
Pattern C에서 사용 (VSO)
# 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 annotation을 추가하고
spring.config.import로 주입된 파일을 참조합니다. - Pattern C: VSO
VaultStaticSecret/VaultDynamicSecretCRD를 정의합니다. @ConfigurationProperties에서 필수 값 누락 시 fail closed 하도록@NotBlank등을 적용합니다.- 로그, exception, actuator endpoint에 Secret 값이 노출되지 않도록 점검합니다.
인프라/플랫폼 해야 할 일
- Vault에 서비스별 policy와 role을 생성하고 최소 권한 원칙을 적용합니다.
- Kubernetes Auth Method를 활성화하고 각 서비스 어카운트에 적절한 role을 바인딩합니다.
- Pattern B: Vault Agent Injector(
vault-agent-injector)가 클러스터에 배포되어 있는지 확인합니다. - Pattern C: VSO가 클러스터에 배포되어 있는지 확인합니다.
- Database Secret Engine에 role과 connection을 구성합니다.
- Secret rotation 시 재시작 또는 reload 절차를 운영 문서에 포함합니다.
- Helm values, ConfigMap, Docker image에 Vault 토큰이 포함되지 않도록 배포 표준을 강제합니다.
검증 방법
application.yml, Helm values, ConfigMap에 Vault 토큰이 없는지 확인합니다.- Pattern A: 앱 기동 시 Vault에서 값을 정상적으로 로딩하는지 로그로 확인합니다.
- Pattern B:
/vault/secrets/경로에 파일이 생성되었는지 확인합니다 (kubectl exec). - Pattern C: 동기화된 Kubernetes Secret이 생성되었는지 확인합니다 (
kubectl get secret). /actuator/env, 디버그 로그, 예외 응답에 Secret 값이 노출되지 않는지 확인합니다.- 필수 Secret 누락 시 애플리케이션이 정상적으로 실패하는지 확인합니다.
자주 하는 실수
application.yml에 Vault 토큰 하드코딩- 장기 Vault 토큰을 접근 통제나 교체 절차 없이 사용
- Vault 경로를 코드에 하드코딩 (설정 파일로 분리해야 함)
- Secret rotation 후 재시작 절차 미비
- Dynamic Secrets의 lease 갱신, 최대 TTL, 앱의 새 자격 증명 적용 시점을 함께 관리하지 않음
담당자에게 안내할 문구 예시
- "
application.yml에 저장된 Vault 토큰은 제거하고, Kubernetes 환경이라면KUBERNETES인증 방식으로 전환하세요." - "Vault 인증과 Injector를 구성한 뒤 annotation으로 파일 주입을 요청하고, Spring의
spring.config.import에서 필수 파일을 참조하세요." - "환경에 맞는 Vault 인증 방식과 최소 권한을 적용하고, Secret 교체 후 앱에 새 값을 반영하는 절차를 문서화하세요."
- "Kubernetes 환경에서 VSO가 배포되어 있다면
VaultStaticSecret/VaultDynamicSecretCRD를 정의해 Vault 시크릿을 Kubernetes Secret으로 동기화하세요. 앱 코드 변경 없이 일반 Kubernetes Secret처럼 사용할 수 있습니다." - "Dynamic Secrets를 사용할 때는 lease 갱신과 최대 TTL을 확인하고, 만료 전에 앱이 새 자격 증명을 사용하도록 구성하세요."