..

vault eks

개요

EKS 클러스터에 Vault 클러스터를 설치하고 AWS KMS를 사용한 Auto Unseal을 구성하는 방법을 설명합니다.

vault logo

 

주요 내용:

  • Vault HA 구성 (3개의 vault 파드로 구성된 Raft 클러스터)
  • AWS KMS를 사용한 자동 봉인해제(Auto Unseal) 설정
  • EKS IRSA를 통한 AWS 권한 관리

이 가이드는 쿠버네티스 클러스터에 Vault를 설치하는 방법을 설명합니다. 쿠버네티스 클러스터 설치 및 관리 방법에 대한 설명은 포함되지 않습니다.

 

환경

  • EKS v1.30 (amd64)
  • Vault v1.18.1
  • Vault Chart 0.29.0
  • 클러스터에 서비스메시로 Linkerd가 설치되어 있습니다.

 

가이드

사전 네트워크 구성

설치 전에 EKS 컨트롤플레인과 vault sidecar injector 간의 통신이 가능한 환경이 되어야 합니다.

컨트롤플레인의 kube-apiservervault-agent-injector 파드 간의 네트워크 연결이 구성되지 않으면, Mutating Webhook 호출에 대한 대기 시간이 길어져 클러스터 전체적으로 롤아웃 재시작, 포드 종료, 컨테이너 생성 등의 문제가 발생할 수 있습니다.

Network connectivity between kube-apiserver and vault-agent-injector

가장 큰 영향은 클러스터의 모든 파드의 생성(containerCreating)과 종료(Terminating) 상태가 지연되는 것입니다. vault가 쿠버네티스의 Mutating Webhook을 통해 파드에 vault-agent를 삽입하는지 검증하기 위해서는 네트워크 연결이 필요합니다. kube-apiserver의 Mutating Webhook 호출이 지연되는 경우, 파드 생성과 종료가 지연되는 것을 확인할 수 있습니다.

 

네트워크 구성이 안된 경우, kube-apiserver의 CloudWatch Logs 로그에서 context deadline exceeded 메시지가 반복되는 것을 확인할 수 있습니다.

E0610 20:50:30.214031      10 dispatcher.go:214] failed calling webhook "vault.hashicorp.com": failed to call webhook: Post "[https://vault-agent-injector-svc.vault.svc:443/mutate?timeout=30s](https://vault-agent-injector-svc.vault.svc/mutate?timeout=30s)": context deadline exceeded

주의사항: 항상 Mutating Webhook을 사용하는 서비스에 대해서는 컨트롤플레인과 파드 간의 네트워크 연결이 구성되어 있는지 확인해야 합니다.

 

Mutating Webhook 문제에 대한 자세한 원인 및 해결방법은 다음 이슈를 참고하세요.

 

EKS 테라폼 모듈을 사용해서 클러스터를 생성하는 경우, node_security_group_additional_rules 옵션을 통해 워커노드의 보안그룹에 추가 인바운드 룰을 설정할 수 있습니다.

module "eks" {
  source  = "terraform-aws-modules/eks/aws"

  node_security_group_additional_rules = {
    ingress_vault_agent_injector_mutating_webhook = {
      description                   = "Allow ingress mutating webhook traffic from kube-apiserver to vault-agent-injector pod"
      protocol                      = "tcp"
      from_port                     = 8080
      to_port                       = 8080
      type                          = "ingress"
      source_cluster_security_group = true
    }
  }
  # ... omitted for brevity ...
}

 

기본적으로 Mutating Webhook의 API 서버 역할을 하는 vault-agent-injector 파드는 8080 포트를 통해 컨트롤플레인과 통신합니다. 아래는 vault 차트의 injector 설정입니다.

# charts/vault/values.yaml
# chart version 0.29.0
injector:
  # True if you want to enable vault agent injection.
  # @default: global.enabled
  enabled: "-"

  replicas: 1

  # Configures the port the injector should listen on
  port: 8080

 

vault 설치

vault 네임스페이스를 먼저 생성합니다. vault 네임스페이스에 vault 차트를 설치할 예정입니다.

kubectl create namespace vault

 

vault 차트에서 사용할 values.yaml 파일을 작성합니다. 세부 설정으로는 High Availability(HA) 모드를 활성화하고 3개의 파드를 배포합니다.

# charts/vault/values.yaml
server:
  ha:
    enabled: true
    replicas: 3

 

또한 ingress-nginx-controller 등의 인그레스 컨트롤러가 있다는 가정하에 Ingress 설정인 server.ingress를 추가합니다. server.ingress.hostshost 값에는 Vault 클러스터에 접근할 도메인을 입력합니다.

완성된 values.yaml 파일은 다음과 같습니다.

# charts/vault/values.yaml
server:
  ha:
    enabled: true
    replicas: 3

  ingress:
    enabled: true
    labels: {}
      # traffic: external
    annotations: {}
      # |
      # kubernetes.io/ingress.class: nginx
      # kubernetes.io/tls-acme: "true"
      #   or
      # kubernetes.io/ingress.class: nginx
      # kubernetes.io/tls-acme: "true"

    # Optionally use ingressClassName instead of deprecated annotation.
    # See: https://kubernetes.io/docs/concepts/services-networking/ingress/#deprecated-annotation

    ## `kubernetes.io/ingress.class: nginx` 어노테이션은 쿠버네티스 1.19 버전부터 사용 중단되었기 때문에,
    ## `ingressClassName` 옵션을 사용합니다.
    ingressClassName: "nginx"

    # As of Kubernetes 1.19, all Ingress Paths must have a pathType configured. The default value below should be sufficient in most cases.
    # See: https://kubernetes.io/docs/concepts/services-networking/ingress/#path-types for other possible values.
    pathType: Prefix

    # When HA mode is enabled and K8s service registration is being used,
    # configure the ingress to point to the Vault active service.
    activeService: true
    hosts:
      - host: vault.example.com
        paths: []

ui:
  # True if you want to create a Service entry for the Vault UI.
  #
  # serviceType can be used to control the type of service created. For
  # example, setting this to "LoadBalancer" will create an external load
  # balancer (for supported K8S installations) to access the UI.
  enabled: true
  publishNotReadyAddresses: true
  # The service should only contain selectors for active Vault pod
  activeVaultPodOnly: false
  serviceType: "ClusterIP"
  serviceNodePort: null
  externalPort: 8200
  targetPort: 8200

 

Vault 권장사항인 dataStorage와 auditStorage를 통해 PVC를 구성합니다.

Vault의 시크릿 데이터와 감사 로그는 파드가 재시작되어도 유지되어야 하므로 쿠버네티스의 PersistentVolume을 사용합니다. 또한 Raft 클러스터의 안정적인 운영과 감사 로그의 안전한 저장을 위해 PVC 구성을 권장합니다.

# charts/vault/values.yaml
server:
  dataStorage:
    enabled: true
    size: 10Gi
  auditStorage:
    enabled: true
    size: 10Gi

 

dataStorageauditStorage 설정을 통해 추후 클러스터 확장 시 데이터 저장소와 감사 로그를 위한 추가 스토리지를 사용할 수 있습니다.

# 추후에 다음과 같이 각 vault 파드에 대한 PVC가 생성되는 것을 확인할 수 있습니다.
$ kubectl get pvc -n vault
NAME            STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
audit-vault-0   Bound    pvc-43ab46eb-e60e-422a-90d0-384ca8c0e6df   10Gi       RWO            gp3            <unset>                 39m
audit-vault-1   Bound    pvc-8b5bc4a9-b66b-4ee8-a8fa-d8a432447ddd   10Gi       RWO            gp3            <unset>                 39m
audit-vault-2   Bound    pvc-60a1e129-8c7d-4deb-baba-abd420ce6140   10Gi       RWO            gp3            <unset>                 39m
data-vault-0    Bound    pvc-30e6cd3c-3a73-4178-b643-a5b8cd886ea7   10Gi       RWO            gp3            <unset>                 39m
data-vault-1    Bound    pvc-0d8c03ad-bc6f-4275-a3eb-3c1a4aeee555   10Gi       RWO            gp3            <unset>                 39m
data-vault-2    Bound    pvc-d06b18ea-2a72-4f32-bfd7-1af5cd74b1e7   10Gi       RWO            gp3            <unset>                 39m

 

vault 네임스페이스에 vault 차트를 설치합니다.

kubectl config current-context
helm upgrade \
    --install \
    --values values.yaml \
    --namespace vault \
    --create-namespace \
    vault . \
    --wait

 

vault 차트를 설치한 후 다음과 같이 설치된 차트 버전을 확인할 수 있습니다.

$ helm list -n vault
NAME 	NAMESPACE	REVISION	UPDATED                             	STATUS  	CHART       	APP VERSION
vault	vault    	2       	2024-11-20 19:04:06.041512 +0900 KST	deployed	vault-0.29.0	1.18.1

 

vault 파드들은 statefulset으로 배포됩니다.

kubectl get sts -n vault -o wide

NAME    READY   AGE   CONTAINERS   IMAGES
vault   0/3     14m   vault        hashicorp/vault:1.18.1

 

Vault는 시크릿을 보호하기 위해 시작할 때마다 항상 Sealed 상태로 시작됩니다. 기본적으로 Vault 운영자가 직접 마스터 키를 입력하여 수동으로 Unseal을 수행해야 하며, 이는 클러스터 재시작이나 장애 복구 시에도 매번 필요한 작업입니다. 특히나 쿠버네티스에서는 빈번히 파드가 언제든 삭제되고 다시 생성될 수 있는 환경이므로 Auto Unseal 구성을 권장합니다.

$ kubectl get pod -n vault
NAME                                    READY   STATUS    RESTARTS   AGE
vault-0                                 1/2     Running   0          2m46s
vault-1                                 1/2     Running   0          2m46s
vault-2                                 1/2     Running   0          2m46s
vault-agent-injector-7c59f6dc9f-xxfwk   2/2     Running   0          2m46s

 

vault 파드 로그를 모니터링합니다.

kubectl logs -l app.kubernetes.io/name=vault -n vault -c vault
2024-11-20T07:23:18.974Z [INFO]  core: security barrier not initialized
2024-11-20T07:23:18.974Z [INFO]  core: seal configuration missing, not initialized
2024-11-20T07:23:23.990Z [INFO]  core: security barrier not initialized
2024-11-20T07:23:23.990Z [INFO]  core: seal configuration missing, not initialized
... omitted for brevity ...

Vault 파드가 초기화되지 않았고(not initialized), 잠긴(Sealed) 상태임을 확인할 수 있습니다.

 

vault-0 파드에 접속해서 vault의 상태를 확인합니다.

kubectl exec vault-0 -n vault -c vault -- vault status
Key                Value
---                -----
Seal Type          shamir
Initialized        false
Sealed             true
Total Shares       0
Threshold          0
Unseal Progress    0/0
Unseal Nonce       n/a
Version            1.18.1
Build Date         2024-10-29T14:21:31Z
Storage Type       raft
HA Enabled         true
command terminated with exit code 2

Initialized 상태가 false이고 Sealed 상태가 true임을 확인할 수 있습니다. 이는 아직 Vault가 초기화되지 않았고 잠긴 상태임을 의미합니다.

Key                Value
---                -----
Seal Type          shamir
Initialized        false
Sealed             true
...                ...
command terminated with exit code 2

exit code 2 응답은 Vault가 잠긴 상태로 초기화되었음을 의미합니다.

 

Vault를 처음 설치한 후에는 초기화(initialization) 작업이 필요합니다. 초기화는 Vault가 처음 시작될 때 한 번만 수행하는 작업으로, 이 과정에서 Unseal Key와 Root Token이 생성됩니다. 이 키들은 Vault의 보안 및 데이터 보호에 중요한 역할을 합니다.

마스터 키는 Vault의 암호화 키를 암호화하는 데 사용되며, 이를 통해 Vault의 데이터를 보호합니다. 보안 강화를 위해 마스터 키는 여러 조각으로 나눌 수 있습니다.

kubectl exec vault-0 -n vault -c vault \
  -- vault operator init \
  -key-shares=1 \
  -key-threshold=1 \
  -format=json > cluster-keys.json

-key-shares=1-key-threshold=1 옵션은 마스터 키를 암호화하는 데 사용되는 조각의 수와 해제에 필요한 조각의 수를 지정합니다. 이 예제에서는 단일 키 공유와 단일 키 임계값을 사용합니다. 안전한 운영을 위해 프로덕션에서 권장하는 옵션 값은 -key-shares=5-key-threshold=3입니다.

 

vault operator init 명령어를 통해 생성된 cluster-keys.json 파일에서 Unseal Key를 확인합니다.

$ cat cluster-keys.json | jq -r ".unseal_keys_b64[]"
cwNeAen3nwDJDtkHtP2AdCivNVI4sWwqHQ6MOVM+trA=

Key 주의사항: 실제로 운영하는 vault 클러스터에서는 단일 키 공유(-key-shares=1)와 단일 키 임계값(-key-threshold=1) 설정은 권장하지 않습니다. 이 예제는 초기 봉인 해제 프로세스를 단순화하기 위해 여기에서만 사용됩니다.

단일 키 공유와 단일 키 임계값을 사용하면 보안성이 크게 저하됩니다. 만약 마스터 키가 유출되거나 분실될 경우, Vault의 모든 데이터에 대한 접근이 가능해지며, 이는 심각한 보안 위협을 초래할 수 있습니다. 따라서, 여러 개의 키 조각을 사용하여 보안을 강화하고, 복구 시 여러 조각 중 일부만 필요하도록 설정하는 것이 좋습니다. 권장하는 옵션 값은 -key-shares=5-key-threshold=3입니다.

 

vault operator init 과정에서 생성된 cluster-keys.json 파일에서 Vault 클러스터의 Unseal Key를 추출합니다.

VAULT_UNSEAL_KEY=$(cat cluster-keys.json | jq -r ".unseal_keys_b64[]")

 

Unseal Key를 사용해서 vault-0 파드의 Vault를 초기화합니다.

kubectl exec vault-0 -n vault -c vault \
  -- vault operator unseal $VAULT_UNSEAL_KEY
Key                     Value
---                     -----
Seal Type               shamir
Initialized             true
Sealed                  false
Total Shares            1
Threshold               1
Version                 1.18.1
Build Date              2024-10-29T14:21:31Z
Storage Type            raft
HA Enabled              true

 

Vault 관리자가 Unseal Key를 사용해서 봉인 해제(Unseal) 한 후 정상적으로 파드가 실행되는 것을 확인할 수 있습니다.

$ kubectl get pod -n vault
NAME                                    READY   STATUS    RESTARTS   AGE
vault-0                                 2/2     Running   0          98m
vault-1                                 1/2     Running   0          98m
vault-2                                 1/2     Running   0          98m
vault-agent-injector-7c59f6dc9f-xxfwk   2/2     Running   0          98m

 

cluster-keys.json 파일에서 Root Token을 추출합니다.

$ cat cluster-keys.json | jq -r ".root_token"
hvs.q7y...REDACTED...BZb

$ CLUSTER_ROOT_TOKEN=$(cat cluster-keys.json | jq -r ".root_token")

 

초기화 과정에서 생성된 Root Token을 사용해서 Vault에 로그인합니다.

kubectl exec vault-0 -n vault -c vault \
  -- vault login $CLUSTER_ROOT_TOKEN
Success! You are now authenticated. The token information displayed below
is already stored in the token helper. You do NOT need to run "vault login"
again. Future Vault requests will automatically use this token.

Key                  Value
---                  -----
token                hvs.q7y...REDACTED...BZb
token_accessor       tBKVb1No3UIk8tbkRTAf2EUI
token_duration       ∞
token_renewable      false
token_policies       ["root"]
identity_policies    []
policies             ["root"]

 

Vault 클러스터의 피어 목록을 확인합니다. Cluster root token을 사용해서 vault login을 한 상태에서 확인할 수 있습니다.

kubectl exec vault-0 -n vault -c vault \
  -- vault operator raft list-peers
Node       Address                        State     Voter
----       -------                        -----     -----
vault-0    vault-0.vault-internal:8201    leader    true

위 Vault 클러스터 상태는 vault-0 파드가 리더(leader)로 동작하고 있음을 의미합니다.

 

vault-1 파드에 접속해서 Vault 클러스터에 조인합니다.

kubectl exec vault-1 -n vault -c vault \
  -- vault operator raft join http://vault-0.vault-internal:8200
Key       Value
---       -----
Joined    true

 

vault-1 파드에 접속해서 Vault 클러스터에 조인한 후 봉인 해제 키를 사용해서 봉인 해제(Unseal) 합니다.

kubectl exec vault-1 -n vault -c vault \
  -- vault operator unseal $VAULT_UNSEAL_KEY
Key                Value
---                -----
Seal Type          shamir
Initialized        true
Sealed             true
Total Shares       1
Threshold          1
Unseal Progress    0/1
Unseal Nonce       n/a
Version            1.18.1
Build Date         2024-10-29T14:21:31Z
Storage Type       raft
HA Enabled         true

 

vault-2 파드에 접속해서 Vault 클러스터에 조인합니다.

kubectl exec vault-2 -n vault -c vault \
  -- vault operator raft join http://vault-0.vault-internal:8200
Key       Value
---       -----
Joined    true

 

vault-2 파드에 접속해서 Vault 클러스터에 조인한 후 봉인 해제 키를 사용해서 봉인 해제(Unseal) 합니다.

kubectl exec vault-2 -n vault -c vault \
  -- vault operator unseal $VAULT_UNSEAL_KEY
Key                Value
---                -----
Seal Type          shamir
Initialized        true
Sealed             true
Total Shares       1
Threshold          1
Unseal Progress    0/1
Unseal Nonce       n/a
Version            1.18.1
Build Date         2024-10-29T14:21:31Z
Storage Type       raft
HA Enabled         true

 

Vault 클러스터의 피어 목록을 확인합니다. Cluster root token을 사용해서 vault login을 한 상태에서 확인할 수 있습니다.

kubectl exec vault-0 -n vault -c vault \
  -- vault operator raft list-peers
Node       Address                        State       Voter
----       -------                        -----       -----
vault-0    vault-0.vault-internal:8201    leader      true
vault-1    vault-1.vault-internal:8201    follower    true
vault-2    vault-2.vault-internal:8201    follower    true

 

Vault 클러스터의 파드 목록을 확인합니다. 모든 Vault 파드가 정상적으로 실행되고 있음을 확인할 수 있습니다.

$ kubectl get pods -n vault
NAME                                    READY   STATUS    RESTARTS   AGE
vault-0                                 2/2     Running   0          106m
vault-1                                 2/2     Running   0          106m
vault-2                                 2/2     Running   0          106m
vault-agent-injector-7c59f6dc9f-xxfwk   2/2     Running   0          106m

 

Auto unseal 설정

Vault 서버는 보안 강화를 위해 설치 후 초기 시작 시 봉인된(Sealed) 상태로 시작된다. 이 상태에서 Vault를 사용하기 위해서는 마스터 키를 이용한 봉인 해제, 즉 “Unseal” 과정이 필수적입니다.

일반적으로, 이 과정은 보안을 위해 수동으로 진행되며, 이는 특히 Vault가 재시작될 때마다 반복되어야 하므로, 특히 대규모 클러스터 환경에서는 운영 부담을 크게 증가시킵니다. 이러한 문제를 해결하기 위해 Vault는 자동 봉인해제(Auto Unseal) 기능을 제공합니다.

 

Auto Unseal을 지원하는 서비스 목록은 다음과 같습니다.

  • AWS KMS: vault가 AWS KMS에 저장된 키를 참조해서 Auto Unseal 하는 방식입니다.
  • GCP Cloud KMS
  • Azure Key Vault
  • Transit Secret Engine: vault가 다른 vault에 저장된 키를 참조해서 Auto Unseal 하는 방식입니다.

이 시나리오에서는 AWS KMS Key를 사용해서 Auto Unseal을 실행하는 시나리오입니다.

 

vault 파드의 KMS Key 획득을 위해서는 IAM Role for Service Accounts (IRSA) 구성이 필요합니다. IRSA는 특정 서비스 어카운트에 IAM Role을 부여해서 권한을 획득하는 방식입니다.

Vault 파드는 다음과 같은 방식으로 권한 획득 및 KMS Key 획득을 진행해서 Auto Unseal을 실행합니다.

vault autounseal with AWS KMS key by IRSA

 

테라폼을 사용해서 KMS Auto Unseal을 위해 필요한 리소스들을 생성합니다.

  • IAM Role (Vault 파드가 IRSA에서 사용)
  • IAM Policy
  • Customer Managed KMS Key

 

IRSA 관련 리소스들을 생성하기 위한 terraform 코드를 다운로드 받습니다.

mkdir vault-irsa
wget -O vault-irsa/main.tf https://raw.githubusercontent.com/younsl/box/main/box/asset/vault/irsa/main.tf
wget -O vault-irsa/outputs.tf https://raw.githubusercontent.com/younsl/box/main/box/asset/vault/irsa/outputs.tf
wget -O vault-irsa/versions.tf https://raw.githubusercontent.com/younsl/box/main/box/asset/vault/irsa/versions.tf

 

main.tf 파일에는 클러스터 이름을 지정하는 부분이 있습니다. data.aws_eks_cluster.this.name에는 vault를 설치할 EKS 클러스터 이름으로 변경합니다.

#===============================================================================
# External Data (Account ID, OIDC Provider ARN)
#===============================================================================
data "aws_caller_identity" "current" {}

data "aws_eks_cluster" "this" {
  name = "<CHANGE_YOUR_CLUSTER_NAME_HERE>"
}

data "aws_iam_openid_connect_provider" "this" {
  url = data.aws_eks_cluster.this.identity[0].oidc[0].issuer
}

# ... omitted for brevity ...

 

terraform 코드를 실행합니다. 로컬 환경에 AWS CLI 프로필을 설정한 후 실행합니다.

export AWS_PROFILE=<YOUR_PROFILE>
terraform init
terraform plan
terraform apply
module.iam_policy.aws_iam_policy.policy[0]: Creating...
module.irsa_role.aws_iam_role.this[0]: Creating...
module.iam_policy.aws_iam_policy.policy[0]: Creation complete after 1s [id=arn:aws:iam::111122223333:policy/vault-auto-unseal-policy]
module.irsa_role.aws_iam_role.this[0]: Creation complete after 1s [id=vault-irsa-role]
module.irsa_role.aws_iam_role_policy_attachment.this["policy"]: Creating...
module.irsa_role.aws_iam_role_policy_attachment.this["policy"]: Creation complete after 1s [id=vault-irsa-role-20241120095622455100000001]

Apply complete! Resources: 3 added, 0 changed, 0 destroyed.

Outputs:

iam_policy_arn = "arn:aws:iam::111122223333:policy/vault-auto-unseal-policy"
iam_role_arn = "arn:aws:iam::111122223333:role/vault-irsa-role"
iam_role_name = "vault-irsa-role"
iam_role_path = "/"
iam_role_unique_id = "XXXXZMNKXXFD6BVW32XYZ"

생성된 IAM Role ARN을 확인합니다. 쿠버네티스 serviceAccount에 어노테이션으로 이 Role ARN을 추가할 예정입니다.

 

terraform apply를 실행한 후 생성된 리소스들을 확인합니다.

terraform state list

 

이 명령어는 Terraform 상태 파일에 정의된 모든 리소스를 나열합니다. 각 리소스는 Terraform이 관리하는 AWS 리소스와 관련된 정보를 포함합니다.

data.aws_caller_identity.current
data.aws_eks_cluster.this
data.aws_iam_openid_connect_provider.this
aws_kms_alias.vault_auto_unseal
aws_kms_key.vault_auto_unseal
module.iam_policy.aws_iam_policy.policy[0]
module.irsa_role.data.aws_caller_identity.current
module.irsa_role.data.aws_iam_policy_document.this[0]
module.irsa_role.data.aws_partition.current
module.irsa_role.data.aws_region.current
module.irsa_role.aws_iam_role.this[0]
module.irsa_role.aws_iam_role_policy_attachment.this["policy"]

 

IRSA 설정:

eks.amazonaws.com/role-arn 어노테이션에는 이전에 생성한 IAM Role ARN을 추가합니다.

# charts/vault/values.yaml
server:
  serviceAccount:
    # Specifies whether a service account should be created
    create: true
    # The name of the service account to use.
    # If not set and create is true, a name is generated using the fullname template
    name: ""
    # Create a Secret API object to store a non-expiring token for the service account.
    # Prior to v1.24.0, Kubernetes used to generate this secret for each service account by default.
    # Kubernetes now recommends using short-lived tokens from the TokenRequest API or projected volumes instead if possible.
    # For more details, see https://kubernetes.io/docs/concepts/configuration/secret/#service-account-token-secrets
    # serviceAccount.create must be equal to 'true' in order to use this feature.
    createSecret: false
    # Extra annotations for the serviceAccount definition. This can either be
    # YAML or a YAML-formatted multi-line templated string map of the
    # annotations to apply to the serviceAccount.
    annotations:
      eks.amazonaws.com/role-arn: "arn:aws:iam::111122223333:role/vault-irsa-role"

server.serviceAccount.name을 지정하지 않으면 기본적으로 vault라는 이름으로 서비스 어카운트가 생성됩니다.

 

vault serviceAccount에 추가한 IAM Role ARN을 확인합니다.

$ kubectl describe sa vault -n vault
Name:                vault
Namespace:           vault
Labels:              app.kubernetes.io/instance=vault
                     app.kubernetes.io/managed-by=Helm
                     app.kubernetes.io/name=vault
                     helm.sh/chart=vault-0.29.0
Annotations:         eks.amazonaws.com/role-arn: arn:aws:iam::111122223333:role/vault-irsa-role
                     meta.helm.sh/release-name: vault
                     meta.helm.sh/release-namespace: vault
Image pull secrets:  <none>
Mountable secrets:   <none>
Tokens:              <none>
Events:              <none>

eks.amazonaws.com/role-arn 어노테이션이 정상적으로 붙었습니다.

 

Auto unseal 설정시 주의할 점은 server.ha.raft.config에 설정을 추가해야합니다. server.ha.config에 넣으면 Vault의 Auto unseal이 정상 동작하지 않습니다. vault는 HA 구성시 Kubernetes의 etcd처럼 Raft Consensus Algorithm을 사용해서 클러스터로서 동작하기 때문에 반드시 raft에 Auto Unseal 설정을 추가해야합니다.

# charts/vault/values.yaml
server:
  ha:
    raft:
      config: |
        seal "awskms" {
          region     = "ap-northeast-2"
          kms_key_id = "<YOUR_KMS_KEY_ID_NOT_ARN>"
        }        

 

Auto Unseal이 추가된 Vault configuration은 다음과 같습니다.

# charts/vault/values.yaml
server:
  ha:
    enabled: true
    replicas: 3

    # Set the api_addr configuration for Vault HA
    # See https://developer.hashicorp.com/vault/docs/configuration#api_addr
    # If set to null, this will be set to the Pod IP Address
    apiAddr: null

    # Set the cluster_addr configuration for Vault HA
    # See https://developer.hashicorp.com/vault/docs/configuration#cluster_addr
    # If set to null, this will be set to https://$(HOSTNAME).{{ template "vault.fullname" . }}-internal:8201
    clusterAddr: null

    # Enables Vault's integrated Raft storage.  Unlike the typical HA modes where
    # Vault's persistence is external (such as Consul), enabling Raft mode will create
    # persistent volumes for Vault to store data according to the configuration under server.dataStorage.
    # The Vault cluster will coordinate leader elections and failovers internally.
    raft:

      # Enables Raft integrated storage
      enabled: true
      # Set the Node Raft ID to the name of the pod
      setNodeId: true

      # Note: Configuration files are stored in ConfigMaps so sensitive data
      # such as passwords should be either mounted through extraSecretEnvironmentVars
      # or through a Kube secret.  For more information see:
      # https://developer.hashicorp.com/vault/docs/platform/k8s/helm/run#protecting-sensitive-vault-configurations
      # Supported formats are HCL and JSON.

      config: |
        ui = true

        listener "tcp" {
          tls_disable = 1
          address = "[::]:8200"
          cluster_address = "[::]:8201"
          # Enable unauthenticated metrics access (necessary for Prometheus Operator)
          #telemetry {
          #  unauthenticated_metrics_access = "true"
          #}
        }

        storage "raft" {
          path = "/vault/data"
        }

        seal "awskms" {
          region     = "ap-northeast-2"
          kms_key_id = "<YOUR_KMS_KEY_ID_NOT_ARN>"
        }

        service_registration "kubernetes" {}        

완성된 vault 차트의 Auto Unseal 설정은 위와 같습니다. kms_key_id에는 이전에 테라폼을 사용해서 생성한 KMS Key ID를 사용합니다.

 

serviceAccount와 Auto Unseal 설정을 반영하기 위해 vault 차트를 업그레이드합니다.

helm upgrade \
    --install \
    --values values.yaml \
    --namespace vault \
    --create-namespace \
    vault . \
    --wait

 

vault 파드가 정상적으로 올라온 것을 확인합니다. 아직 Initialized 된 상태가 아니므로 아래와 같이 파드들이 Ready 되지 않습니다.

$ kubectl get pod -n vault
NAME                                    READY   STATUS    RESTARTS   AGE
vault-0                                 1/2     Running   0          35s
vault-1                                 1/2     Running   0          35s
vault-2                                 1/2     Running   0          35s
vault-agent-injector-7c59f6dc9f-fkhdf   2/2     Running   0          35s

 

vault-0 파드에 접속해서 Vault를 초기화합니다. cluster-keys.json 파일에 Vault 클러스터의 루트 토큰과 초기 마스터 키를 저장합니다.

kubectl exec vault-0 -n vault -c vault \
  -- vault operator init -format=json > cluster-keys.json

주의사항: AWS KMS 키를 사용해서 Auto unseal을 구성하면 vault operator init 명령어에서 -key-shares=1-key-threshold=1 옵션을 사용할 수 없습니다.

# Wrong `vault operator init` command
kubectl exec vault-0 -n vault -c vault \
  -- vault operator init \
  -key-shares=1 \
  -key-threshold=1 \
  -format=json > cluster-keys.json

 

즉 KMS Auto Unseal 구성에서 위와 같은 명령어를 실행할 경우 아래와 같은 parameters secret_shares,secret_threshold not applicable to seal type awskms 에러가 발생합니다.

Error initializing: Error making API request.

URL: PUT http://127.0.0.1:8200/v1/sys/init
Code: 400. Errors:

* parameters secret_shares,secret_threshold not applicable to seal type awskms
command terminated with exit code 2

 

Vault의 봉인 타입(Seal Type), 봉인(Sealed) 및 초기화 상태(Initialized)를 확인합니다.

kubectl exec vault-0 -n vault -c vault \
  -- vault status
Key                      Value
---                      -----
Seal Type                awskms
Recovery Seal Type       shamir
Initialized              true
Sealed                   false
Total Recovery Shares    5
Threshold                3
Version                  1.18.1
Build Date               2024-10-29T14:21:31Z
Storage Type             raft
Cluster Name             vault-cluster-0c3c3c82
Cluster ID               8ae9c0f8-212f-2fcd-f2fb-a12819e5ac36
HA Enabled               true
HA Cluster               https://vault-0.vault-internal:8201
HA Mode                  active
Active Since             2024-11-20T23:51:46.98699563Z
Raft Committed Index     62
Raft Applied Index       62

Seal Typeawskms이어야 합니다.

 

vault 파드 상태를 확인합니다. vault-0 파드만 정상적으로 올라온 것을 확인할 수 있습니다.

kubectl get pod -n vault
NAME                                    READY   STATUS    RESTARTS   AGE
vault-0                                 2/2     Running   0          3m2s
vault-1                                 1/2     Running   0          3m2s
vault-2                                 1/2     Running   0          3m2s
vault-agent-injector-7c59f6dc9f-cb4xw   2/2     Running   0          3m2s

 

파드 로그를 확인해보면 vault-0 파드가 KMS를 통해 자동으로 Auto unseal 된 것을 확인할 수 있습니다.

$ kubectl logs -n vault -c vault vault-0 | grep unsealed
2024-11-20T23:51:46.750Z [INFO]  core: vault is unsealed
2024-11-20T23:51:46.828Z [INFO]  core: unsealed with stored key
2024-11-20T23:51:46.828Z [WARN]  core: attempted unseal with stored keys, but vault is already unsealed

 

vault-1vault-2 파드도 Vault 클러스터에 조인합니다.

# Join vault-1 pod to vault cluster
kubectl exec -n vault vault-1 -c vault \
  -- vault operator raft join http://vault-0.vault-internal:8200

# Join vault-2 pod to vault cluster
kubectl exec -n vault vault-2 -c vault \
    -- vault operator raft join http://vault-0.vault-internal:8200

 

vault-0에 Root Token을 사용해서 로그인하여 vault 클러스터에 접속합니다.

$ cat cluster-keys.json | jq -r ".root_token"
hvs.q7y...REDACTED...BZb

cluster-keys.json 파일에서 Root Token을 획득합니다. cluster-keys.json 파일은 이전에 vault operator init 명령어를 실행해서 생성한 파일입니다.

$ CLUSTER_ROOT_TOKEN=$(cat cluster-keys.json | jq -r ".root_token")

vault-0 파드에 접속해서 Vault에 로그인합니다.

$ kubectl exec vault-0 -n vault -c vault \
  -- vault login $CLUSTER_ROOT_TOKEN

 

Vault 클러스터에 조인된 피어들을 확인합니다.

kubectl exec vault-0 -n vault -c vault \
  -- vault operator raft list-peers

 

Vault 클러스터에 조인된 피어(파드)들 목록과 상태가 출력됩니다. 여기서 Voter의 의미는 클러스터에 참여하는 노드가 투표권을 가지고 있는 노드임을 의미합니다.

Node       Address                        State       Voter
----       -------                        -----       -----
vault-0    vault-0.vault-internal:8201    leader      true
vault-1    vault-1.vault-internal:8201    follower    true
vault-2    vault-2.vault-internal:8201    follower    true

 

Vault 파드가 조인되는 즉시 KMS를 사용해서 Auto Unseal 된 것을 READY 상태를 통해 확인할 수 있습니다.

$ kubectl get pod -n vault
NAME                                    READY   STATUS    RESTARTS   AGE
vault-0                                 2/2     Running   0          7m50s
vault-1                                 2/2     Running   0          7m50s
vault-2                                 2/2     Running   0          7m50s
vault-agent-injector-7c59f6dc9f-cb4xw   2/2     Running   0          7m50s

 

AWS KMS를 사용해서 자동 봉인해제(Auto Unseal)가 되는지 테스트하기 위해, vault 네임스페이스에 있는 모든 vault 파드를 삭제합니다.

kubectl delete --all pods -n vault
pod "vault-0" deleted
pod "vault-1" deleted
pod "vault-2" deleted
pod "vault-agent-injector-7c59f6dc9f-z2c9n" deleted

vault 네임스페이스의 모든 파드들이 삭제되었습니다.

 

재시작된 모든 vault 파드가 KMS Key를 사용해서 Auto Unseal 된 것을 확인할 수 있습니다.

$ kubectl get pod -n vault
NAME                                    READY   STATUS    RESTARTS   AGE
vault-0                                 2/2     Running   0          57s
vault-1                                 2/2     Running   0          57s
vault-2                                 2/2     Running   0          56s
vault-agent-injector-7c59f6dc9f-4xdbk   2/2     Running   0          68s

 

결론

이 문서에서는 EKS 클러스터에 Vault를 설치하고 AWS KMS를 사용한 Auto Unseal을 설정하는 방법을 다루었습니다. Vault 클러스터의 고가용성(HA) 구성과 PersistentVolume을 통한 시크릿 데이터 저장소 설정을 통해 안정적인 운영을 보장하며, Auto Unseal 기능을 통해 운영 부담을 줄일 수 있습니다. 이 과정을 통해 Vault의 보안성과 가용성을 높일 수 있습니다.

 

관련자료