Vault k8s injector 소개 : 쿠버네티스에서 Vault를 다루는 방법 본문

Vault k8s injector 소개 : 쿠버네티스에서 Vault를 다루는 방법

JinHwan Kim 2024. 5. 31. 18:43

애플리케이션은 Vault의 존재를 몰라야 하지 않을까?

개발자 친구를 만나고 팀에서 Vault를 사용한다는 이야기를 들으면 항상 하는 질문이, 'Vault에 대한 인증은 어떻게 해?' 였다. 그리고 다들 본인의 역할이 아니다보니 명확한 관리 방법이나 안전한 방식의 대답을 듣지 못했다. 비밀 키를 관리하는 금고를 여는 비밀 키 관리는 정말 중요해보인다. 아무리 안전한 금고할지라도 열쇠를 그 금고에 보관할 순 없는 노릇이고, 그렇다고 열쇠가 제대로 관리되지 못하면 말짱 도루묵일테니 말이다.

 

내가 그간 경험했던 환경에선 애플리케이션에서 Vault 인증을 위한 키를 갖고 있었고, 그 키로 Vault를 직접 호출해 필요한 비밀 값을 조회했다. 이런 방식은 애플리케이션에서 Vault에서 사용할 Secret 정보나 Vault 접속 방법을 알고 있어야 했다. 내 생각에는 애플리케이션 단과 Vault는 완전히 의존이 분리되어, Vault의 변경에 영향을 받아서도, 비밀 값을 가져오는 방법을 알아서도 안된다고 생각했다. 그래서 Vault와 애플리케이션을 분리하는 방법을 고민하게 되었다.

 

기존의 의존적인 방식

애플리케이션에서 Vault 를 직접 호출하게 되면 Vault 접근 정보와 참조할 Secret 정보를 알아야 한다. Vault 정보와 Secret 키가 포함된 코드가 작성되고, 이는 개발자에게 노출된다.

 

잘못되었다고 생각하는 기존의 방식을 예를 들면 다음과 같다. Spring cloud vault는 Spring 애플리케이션에서 간단한 설정만으로 Vault의 값을 참조하고 가져올 수 있는 방법을 제공한다.

 

https://spring.io/guides/gs/vault-config

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

 

만약 Vault의 접근 방법이 바뀐다고 하면, 이를 사용하고 있는 애플리케이션에서 이를 추적하여 코드를 수정해야 한다. 단순히 Vault를 접근할 수 있는 방법이 아니라, Secret path 등 사용하는 값의 설정이 바뀌어도 마찬가지다. 그런 변경 사항에 애플리케이션 개발자들이 직접적인 영향을 받고, Vault 값 조차도 여러 사람에게 노출되기 쉽상이다.

# application.properties
spring.application.name=gs-vault-config
spring.cloud.vault.token=00000000-0000-0000-0000-000000000000
spring.cloud.vault.scheme=http
spring.cloud.vault.kv.enabled=true
spring.config.import:  vault://

 

배포 시점에 Secret 값 주입하기

그래서 위 Spring cloud Vault에서 제공하는, 애플리케이션에서 직접 Vault에 접근하는 방법이 아니라, 배포 시점에서 Vault에 접근하고, 이를 환경 변수로 넣어주는 방식은 어떨까 싶다. 특히 Vault k8s Injector를 사용하면, 쉽게 Vault secret 을 조회하고, 조회한 값은 Container 실행 시에 환경 변수로 등록되어 애플리케이션에서는 다른 의존성 추가나 설정 파일없이 키 값을 사용할 수 있게 된다.

 

앞선 예시처럼 Vault 접근 방법이나 secret path가 바뀐다면, 이번에는 애플리케이션에서는 전혀 관여할게 없어진다. Vault k8s injector를 사용한다면, 그저 Deployment에서 Vault injector가 바라보는 Secret 값의 path만 바꿔주면 그만이다. 개발자들은 변경되었는지도 모를 것이다.

# application properties
object.storage.credential.accessKey=${AWS_ACCESS_KEY}
object.storage.credential.secretKey=${AWS_SECRET_KEY}
aws.cloudfront.domain=${CLOUDFRONT_DOMAIN}
aws.cloudfront.publicKeyId=${CLOUDFRONT_PUBLIC_KEY_ID}
aws.cloudfront.privateKeyPath=${CLOUDFRONT_PRIVATE_PEM_KEY_PATH}

 

동작 원리와 사이드카 패턴

Vault k8s injector는 다음 순서로 동작한다.

1. Pod가 생성될 때 Init container에서 Service account(token)으로 Vault k8s auth에 로그인을 요청한다.

2. Vault agent container는 발급 받은 로그인 토큰과 함께 필요한 Secret 값을 요청한다.

3. Vault에서는 로그인 토큰의 유효함, 사용자의 권한을 확인한다.

4. 요청한 Secret 값을 전달하고, Vault agent는 이를 임시 저장 공간(Container의 메모리 볼륨)에 저장한다.

5. Application container에서 저장된 Secret 값을 참조하여 환경 변수를 구성한다.

 

 

Helm을 사용한 설치 방법

1. Helm values.yaml 준비

global:
  externalVaultAddr: "${KUBERNETES_IP:PORT}
injector:
  enabled: "true"
  port: ${PORT}

# KUBERNETES_IP : 쿠버네티스 host ip
# PORT : 쿠버네티스 port

 

2. Helm Vault-agent-injector 설치

helm repo add hashicorp https://helm.releases.hashicorp.com
helm install vault -f values.yaml hashicorp/vault

 

3. Vault kubernetes 인증에 사용할 Secret 생성

apiVersion: v1
kind: Secret
metadata:
  name: $SECRET_NAME
  annotations:
    kubernetes.io/service-account.name: vault
type: kubernetes.io/service-account-token

# SECRET_NAME : 생성할 secret 이름

 

4. Vault kubernetes 인증 생성

- Vault auth 로 등록할 Kubernetes 설정을 생성한다. Vault는 Kubernetes에서 접속이 가능해야 한다.

 

# kubectl 측, kubernetes 설정 확인, 인증에 사용할 secret 의 jwt 키 확인

TOKEN_REVIEWER_JWT=$(kubectl get secret $SECRET_NAME --output='go-template={{ .data.token }}' | base64 --decode)
KUBE_CA_CERT=$(kubectl config view --raw --minify --flatten --output 'jsonpath={.clusters[].cluster.certificate-authority-data}' | base64 --decode)
KUBE_HOST=$(kubectl config view --raw --minify --flatten -o jsonpath='{.clusters[].cluster.server}')

 

# vault 측, 설정 등록

vault auth enable kubernetes

vault write auth/kubernetes/config  \
   token_reviewer_jwt="$TOKEN_REVIEW_JWT} \
   kubernetes_ca_cert="$KUBE_CA_CERT" \
   kubernetes_host="$KUBE_HOST"

 

5. Vault auth role 정의

vault write auth/kubernetes/role/$ROLE_NAME \
    bound_service_account_names=$SERVICE_ACCOUNT_NAME_OF_POD \
    bound_service_account_namespaces=$POD_NAME_SPACE \
    policies=$POLICY_NAME \
    ttl=$TTL
    
# ROLE_NAME : 생성할 Role 이름
# SERVICE_ACCOUNT_NAME_OF_POD : Vault agent 가 생성될 Pod 의 Service account 이름
# POD_NAME_SPACE : Vault agent 가 생성될 Pod 의 namespace
# POLICY_NAME : 미리 정의한 Vault policy 이름, 사용할 Secret 의 Read 권한이 포함된 Policy 이어야 한다.
# TTL : 로그인 유효 기간 (ex, 24h)

 

6. Pod에서 사용할 Vault의 Secret 정보 기입

- 어노테이션을 추가하여 Init container에서 읽을 Vault의 Secret 정보 기입한다.

- 읽은 secret 값은 파일로 저장되고 컨테이너 실행 시에 파일의 값을 환경 변수로 등록한다.

- vault.hashicorp.com/role
: 사용할 Vault auth role 이름을 정의한다.

- vault.hashicorp.com/agent-inject-secret-$PREFIX 
: 키는 저장될 파일 경로 prefix를 표시한다. 
: 예를 들어 'agent-inject-secret-ecsimsw' 을 key로 하면 읽어온 데이터는 '/vault/secret/ecsimsw'에 저장된다.
: 값은 읽을 vault secret path를 의미한다.

- vault.hashicorp.com/agent-inject-template-$PREFIX 
: 파일에 저장되는 형태를 결정한다. 
: Data 의 형태를 확인하고 저장할 포맷을 정의할 수 있다. 
: 예시의 .Data는 map[created:20240516 version:1] 꼴이었고, k=v 로 저장하는 Template 코드를 정의했다.

- vault.hashicorp.com/agent-pre-populate-only
: Vault injector 의 Side car 등록 여부를 결정한다. 
: True로 설정하면 init container에서 최초 한 회 Secret 를 읽고 Side car 를 생성하지 않는다.

 

7. Deployment 예시

- 아래는 테스트에 사용한 Deployment 예시이다. 

- vault 에 /ecsimsw/common 을 경로로 kv 타입의 secret 을 생성해두었다. 

- 읽은 secret 값은 {Key=Value} 형식으로 컨테이너 메모리 볼륨에 "/vault/secrets/ecsimsw" 경로에 파일로 저장된다.

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: vault-injector-test
  name: vault-injector-test
  namespace: ecsimsw
spec:
  replicas: 1
  selector:
    matchLabels:
      app: vault-injector-test
  template:
    metadata:
      annotations:
        vault.hashicorp.com/role: internal-app
        vault.hashicorp.com/agent-inject: "true"
        vault.hashicorp.com/agent-inject-secret-ecsimsw: ecsimsw/common
        vault.hashicorp.com/agent-inject-template-ecsimsw: |
            {{- with secret "ecsimsw/common" -}}
            {{- range $k, $v := .Data }}
            {{- $k }}={{ $v }}
            {{end}}{{end}}
      labels:
        app: vault-injector-test
    spec:
      containers:
        - image: alpine
          args:
            - "sh"
            - "-c"
            - "source /vault/secrets/ecsimsw && sleep 10000"
          imagePullPolicy: IfNotPresent
          name: vault-injector-test
          resources: {}
      terminationGracePeriodSeconds: 30

 

- 정의한 Deployment 로 Pod가 실행될 때 Init container 에서 vault-agent-init 수행, Side car로 vault-agent 가 실행되는 것을 확인할 수 있다.

 

 

 

 

Comments