Kubernetes

Vault로 K8S Secret 관리하기

mokpolar 2025. 4. 10. 20:28
반응형

안녕하세요?
AEWS 10주차 과제로 Vault를 가지고 하는 K8S Secret 관리를 작성해보겠습니다.

 

Vault

Vault가 뭔가요?

HashiCorp Vault는 신원 기반(identity-based)의 시크릿 및 암호화 관리 시스템입니다. 이 시스템은 인증(authentication)인가(authorization) 방법을 통해 암호화 서비스를 제공하여 비밀에 대한 안전하고 감사 가능하며 제한된 접근을 보장합니다.

 

시크릿(Secret)이란 접근을 철저히 통제하고자 하는 모든 것을 의미하며, 예를 들어 토큰, API 키, 비밀번호, 암호화 키 또는 인증서 등이 이에 해당합니다. Vault는 모든 시크릿에 대해 통합된 인터페이스를 제공하면서, 엄격한 접근 제어와 상세한 감사 로그 기록 기능을 제공합니다.

 

외부 서비스용 API 키, 서비스 지향 아키텍처 간 통신을 위한 자격 증명 등은 플랫폼에 따라 누가 어떤 비밀에 접근했는지를 파악하기 어려울 수 있습니다. 여기에 키 롤링(교체), 안전한 저장, 상세한 감사 로그까지 추가하려면 별도의 커스텀 솔루션 없이는 거의 불가능합니다. Vault는 바로 이 지점에서 해결책을 제공합니다.

 

Vault는 클라이언트(사용자, 기계, 애플리케이션 등)를 검증하고 인가한 후에만 비밀이나 저장된 민감한 데이터에 접근할 수 있도록 합니다.

 

Vault의 동작 방식

Vault의 핵심 워크플로우는 다음 네 단계로 구성됩니다:

  1. 인증 (Authenticate): Vault에서 인증은 클라이언트가 Vault에 자신이 누구인지 증명할 수 있는 정보를 제공하는 과정입니다. 클라이언트가 인증 메서드를 통해 인증되면, 토큰이 생성되고 정책과 연결됩니다.
  2. 검증 (Validation): Vault는 Github, LDAP, AppRole 등과 같은 신뢰할 수 있는 외부 소스를 통해 클라이언트를 검증합니다.
  3. 인가 (Authorize): 클라이언트는 Vault의 보안 정책과 비교됩니다. 이 정책은 Vault 토큰을 사용하여 클라이언트가 접근할 수 있는 API 엔드포인트를 정의하는 규칙의 집합입니다. 정책은 Vault 내 특정 경로나 작업에 대한 접근을 허용하거나 거부하는 선언적 방식으로 권한을 제어합니다.
  4. 접근 (Access): Vault는 클라이언트의 신원에 연관된 정책을 기반으로 토큰을 발급하여 비밀, 키, 암호화 기능 등에 대한 접근을 허용합니다. 클라이언트는 이후 작업에서 해당 Vault 토큰을 사용할 수 있습니다.

Vault의 기본 구조는 아래와 같이 호텔 체크인에 비유할 수 있습니다.

테스트 환경 구성

이 실습은 로컬 MacOS 환경에서 Kind로 생성한 K8S 클러스터에서 진행합니다.

 

Kubernetes 세팅

kind로 배포할 config를 준비합니다.

cat > kind-3node.yaml <<EOF
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
networking:
  apiServerAddress: "127.0.0.1" # $MyIP로 설정하셔도 됩니다.
nodes:
- role: control-plane
  extraPortMappings:
  - containerPort: 30000
    hostPort: 30000
  - containerPort: 30001
    hostPort: 30001
  - containerPort: 30002
    hostPort: 30002
  - containerPort: 30003
    hostPort: 30003
  - containerPort: 30004
    hostPort: 30004
  - containerPort: 30005
    hostPort: 30005
  - containerPort: 30006
    hostPort: 30006
- role: worker
- role: worker
EOF

 

클러스터를 생성하겠습니다.

kind create cluster --config kind-3node.yaml --name myk8s --image kindest/node:v1.32.2

 

노드를 확인하면 control-plane과 2개의 worker가 생성되어 있는 것을 확인할 수 있습니다.

kind get nodes --name myk8s

myk8s-worker2
myk8s-worker
myk8s-control-plane

kubens default

Context "kind-myk8s" modified.
Active namespace is "default".

 

생성된 클러스터의 네트워크도 확인해보겠습니다.

docker network ls
docker inspect kind | jq

[
  {
    "Name": "kind",
    "Id": "7a90578a3a56ec00898c824fa6b45706e85d8e5e64a9d489d2742e9c7bb580b7",
    "Created": "2024-09-01T20:16:36.120864157+09:00",
    "Scope": "local",
    "Driver": "bridge",
    "EnableIPv6": true,
    "IPAM": {
      "Driver": "default",
      "Options": {},
      "Config": [
        {
          "Subnet": "192.168.247.0/24",
          "Gateway": "192.168.247.1"
        },
        {
          "Subnet": "fc00:f853:ccd:e793::/64",
          "Gateway": "fc00:f853:ccd:e793::1"
        }
      ]
    },
    "Internal": false,
    "Attachable": false,
    "Ingress": false,
    "ConfigFrom": {
      "Network": ""
    },
    "ConfigOnly": false,
    "Containers": {
      "1ab01ce58e995b550c89405fde98a5bf9a5625d64dee96781dd81856133de716": {
        "Name": "myk8s-worker",
        "EndpointID": "7031779b432353a69779cee1231afc3ad7477faf471073c33faa019094448f3d",
        "MacAddress": "02:42:c0:a8:f7:04",
        "IPv4Address": "192.168.247.4/24",
        "IPv6Address": "fc00:f853:ccd:e793::4/64"
      },
...
      }
    },
    "Options": {
      "com.docker.network.bridge.enable_ip_masquerade": "true",
      "com.docker.network.driver.mtu": "1500"
    },
    "Labels": {}
  }
]
kubectl get node -o wide

NAME                  STATUS   ROLES           AGE   VERSION   INTERNAL-IP     EXTERNAL-IP   OS-IMAGE                         KERNEL-VERSION                         CONTAINER-RUNTIME
myk8s-control-plane   Ready    control-plane   15m   v1.32.2   192.168.247.2   <none>        Debian GNU/Linux 12 (bookworm)   6.12.15-orbstack-00304-gd0ddcf70447d   containerd://2.0.3
myk8s-worker          Ready    <none>          15m   v1.32.2   192.168.247.4   <none>        Debian GNU/Linux 12 (bookworm)   6.12.15-orbstack-00304-gd0ddcf70447d   containerd://2.0.3
myk8s-worker2         Ready    <none>          15m   v1.32.2   192.168.247.3   <none>        Debian GNU/Linux 12 (bookworm)   6.12.15-orbstack-00304-gd0ddcf70447d   containerd://2.0.3

 

Vault 세팅

 

이제 Vault를 설치해보겠습니다.

kubectl create namespace vault
helm repo add hashicorp https://helm.releases.hashicorp.com

helm search repo hashicorp/vault

NAME                                CHART VERSION    APP VERSION    DESCRIPTION
hashicorp/vault                     0.30.0           1.19.0         Official HashiCorp Vault Chart
hashicorp/vault-secrets-gateway     0.0.2            0.1.0          A Helm chart for Kubernetes
hashicorp/vault-secrets-operator    0.10.0           0.10.0         Official Vault Secrets Operator Chart

 

Valult의 helm values 설정을 아래와 같이 미리 준비합니다.

cat <<EOF > override-values.yaml
global:
  enabled: true
  tlsDisable: true  # Disable TLS for demo purposes

server:
  image:
    repository: "hashicorp/vault"
    tag: "1.19.0"
  standalone:
    enabled: true
    replicas: 1
    config: |
      ui = true

      listener "tcp" {
        address = "[::]:8200"
        cluster_address = "[::]:8201"
        tls_disable = 1
      }

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

  service:
    enabled: true
    type: NodePort
    port: 8200
    targetPort: 8200
    nodePort: 30000   # 🔥 Kind에서 열어둔 포트 중 하나 사용

injector:
  enabled: true
EOF

 

준비한 설정대로 Vault를 Helm으로 설치합니다.

helm upgrade vault hashicorp/vault -n vault -f override-values.yaml --install

k get pods,svc,pvc -n vault

NAME                                        READY   STATUS    RESTARTS   AGE
pod/vault-0                                 0/1     Running   0          30s
pod/vault-agent-injector-56459c7545-nrk4t   1/1     Running   0          30s

NAME                               TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)                         AGE
service/vault                      NodePort    10.96.59.63     <none>        8200:30000/TCP,8201:31683/TCP   30s
service/vault-agent-injector-svc   ClusterIP   10.96.136.142   <none>        443/TCP                         30s
service/vault-internal             ClusterIP   None            <none>        8200/TCP,8201/TCP               30s

NAME                                 STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
persistentvolumeclaim/data-vault-0   Bound    pvc-27ffba74-3541-4170-ae77-3df544fac62a   10Gi       RWO            standard       <unset>                 30s

 

Vault의 상태를 확인해 봅니다.

ubectl -n vault exec -ti vault-0 -- 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.19.0
Build Date         2025-03-04T12:36:40Z
Storage Type       file
HA Enabled         false

 

init-unseal.sh 이라는 파일을 하나 만들어서 Vault Unseal 자동화할 수 있도록 해봅니다.

cat <<EOF > init-unseal.sh
#!/bin/bash

# Vault Pod 이름
VAULT_POD="vault-0"

# Vault 명령 실행
VAULT_CMD="kubectl exec -ti \$VAULT_POD -- vault"

# 출력 저장 파일
VAULT_KEYS_FILE="./vault-keys.txt"
UNSEAL_KEY_FILE="./vault-unseal-key.txt"
ROOT_TOKEN_FILE="./vault-root-token.txt"

# Vault 초기화 (Unseal Key 1개만 생성되도록 설정)
\$VAULT_CMD operator init -key-shares=1 -key-threshold=1 | sed \$'s/\\x1b\\[[0-9;]*m//g' | tr -d '\r' > "\$VAULT_KEYS_FILE"

# Unseal Key / Root Token 추출
grep 'Unseal Key 1:' "\$VAULT_KEYS_FILE" | awk -F': ' '{print \$2}' > "\$UNSEAL_KEY_FILE"
grep 'Initial Root Token:' "\$VAULT_KEYS_FILE" | awk -F': ' '{print \$2}' > "\$ROOT_TOKEN_FILE"

# Unseal 수행
UNSEAL_KEY=\$(cat "\$UNSEAL_KEY_FILE")
\$VAULT_CMD operator unseal "\$UNSEAL_KEY"

# 결과 출력
echo "[🔓] Vault Unsealed!"
echo "[🔐] Root Token: \$(cat \$ROOT_TOKEN_FILE)"
EOF

# 실행 권한 부여
chmod +x init-unseal.sh

# 실행
./init-unseal.sh

 

파일을 실행하면..

./init-unseal.sh
Key             Value
---             -----
Seal Type       shamir
Initialized     true
Sealed          false
Total Shares    1
Threshold       1
Version         1.19.0
Build Date      2025-03-04T12:36:40Z
Storage Type    file
Cluster Name    vault-cluster-3d859970
Cluster ID      ad7a59aa-f76e-aad0-cc4e-d98f7e192106
HA Enabled      false
[🔓] Vault Unsealed!
[🔐] Root Token: ---

그전에 Sealed라고 되어있던 부분이 true -> false로 바뀐 모습을 확인할 수 있습니다.
이제 Unseal이 마무리 되었으므로 Root Token 값을 사용하여 UI에 접속할 수 있습니다.

 

Vault CLI 설정

Vault를 CLI로 사용할 수 있도록 세팅해보겠습니다.

brew tap hashicorp/tap
brew install hashicorp/tap/vault
vault --version                 
Vault v1.19.1 (aa75903ec499b2236da9e7bbbfeb7fd16fa4fd9d), built 2025-04-02T15:43:01Z
# NodePort로 공개한 30000 Port로 설정
export VAULT_ADDR='http://localhost:30000'
vault status

Key             Value
---             -----
Seal Type       shamir
Initialized     true
Sealed          false
Total Shares    1
Threshold       1
Version         1.19.0
Build Date      2025-03-04T12:36:40Z
Storage Type    file
Cluster Name    vault-cluster-3d859970
Cluster ID      ad7a59aa-f76e-aad0-cc4e-d98f7e192106
HA Enabled      false

vault login

Token (will be hidden): 
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                ---
token_accessor       ---
token_duration       ∞
token_renewable      false
token_policies       ["root"]
identity_policies    []
policies             ["root"]

 

테스트

KV 시크릿 엔진 활성화 및 샘플 구성

Vault KV version 2 엔진을 활성화하고 샘플 데이터를 저장합니다.
Version1 : KV 버전관리 불가 / Version2 : KV 버전관리 가능

KV v2 형태로 엔진 활성화합니다.

vault secrets enable -path=secret kv-v2
Success! Enabled the kv-v2 secrets engine at: secret/

 

그리고 샘플 시크릿을 저장해봅니다.

vault kv put secret/sampleapp/config \
  username="demo" \
  password="p@ssw0rd"
======== Secret Path ========
secret/data/sampleapp/config

======= Metadata =======
Key                Value
---                -----
created_time       2025-04-10T11:02:57.027681868Z
custom_metadata    <nil>
deletion_time      n/a
destroyed          false
version            1

 

입력된 데이터를 확인해봅니다.

vault kv get secret/sampleapp/config

======== Secret Path ========
secret/data/sampleapp/config

======= Metadata =======
Key                Value
---                -----
created_time       2025-04-10T11:02:57.027681868Z
custom_metadata    <nil>
deletion_time      n/a
destroyed          false
version            1

====== Data ======
Key         Value
---         -----
password    p@ssw0rd
username    demo

 

실제 Vault의 UI에서도 해당 시크릿을 확인할 수 있습니다.

Vault Sidecar (Vault Agent)연동

Vault Agent Sidecar 패턴을 이용해서 Pod가 Secret을 Volume으로 해서 마운트하도록 구성합니다.

Vault Sidecar 아키텍쳐는 아래와 같습니다.

Vault Agent Injector는 Kubernetes Pod 내부에 Vault Agent를 자동으로 주입해주는 기능입니다.

이를 통해 어플리케이션이 Vault로부터 자동으로 시크릿를 받아올 수 있게 됩니다.

 

먼저 AppRole 인증 방식을 활성화합니다.

vault auth enable approle || echo "AppRole already enabled"
Success! Enabled approle auth method at: approle/

vault auth list
Path        Type       Accessor                 Description                Version
----        ----       --------                 -----------                -------
approle/    approle    auth_approle_0e9fad54    n/a                        n/a
token/      token      auth_token_8a73a32a      token based credentials    n/a

 

샘플용 정책을 생성합니다.

 

  • secret/data/sampleapp/* 경로에 대해 read 권한만 부여합니다.
  • 이 정책은 특정 경로의 시크릿만 읽을 수 있도록 제한합니다

 

vault policy write sampleapp-policy - <<EOF
path "secret/data/sampleapp/*" {
  capabilities = ["read"]
}
EOF
Success! Uploaded policy: sampleapp-policy

 

 

  • sampleapp-role이라는 AppRole을 생성합니다.
  • 해당 역할로 발급된 토큰은 sampleapp-policy 권한을 갖습니다.
  • secret_id는 1시간만 유효, 발급된 토큰은 1시간 TTL, 최대 4시간 TTL.

 

vault write auth/approle/role/sampleapp-role \
  token_policies="sampleapp-policy" \
  secret_id_ttl="1h" \
  token_ttl="1h" \
  token_max_ttl="4h"
Success! Data written to: auth/approle/role/sampleapp-role

 

 

  • 위에서 만든 AppRole에 대한 ROLE_ID와 SECRET_ID를 추출합니다.
  • 이는 Vault Agent가 인증할 때 사용할 자격 정보입니다.

 

ROLE_ID=$(vault read -field=role_id auth/approle/role/sampleapp-role/role-id)
SECRET_ID=$(vault write -f -field=secret_id auth/approle/role/sampleapp-role/secret-id)

echo "ROLE_ID: $ROLE_ID"
echo "SECRET_ID: $SECRET_ID"
ROLE_ID: c2d5513f-2a53-ceea-6b43-0afbbc389315
SECRET_ID: 6475069b-7de1-93fc-ce4f-4cb236060a58

 

파일로 저장합니다.

mkdir -p approle-creds
echo "$ROLE_ID" > approle-creds/role_id.txt
echo "$SECRET_ID" > approle-creds/secret_id.txt

 

그리고 Kubernetes Secret으로 저장합니다.

kubectl create secret generic vault-approle -n vault \
  --from-literal=role_id="${ROLE_ID}" \
  --from-literal=secret_id="${SECRET_ID}" \
  --save-config \
  --dry-run=client -o yaml | kubectl apply -f -

제 Vault Agent를 설정할 준비를 합니다.
vault-agent-config.hcl 설정을 통해 연결할 Vault의 정보와 Template 구성, 렌더링 주기, 참조할 Vault KV 위치정보 등을 정의합니다.

Vault Agent의 구성 파일(agent-config.hcl)을 Kubernetes ConfigMap으로 생성하고 적용합니다.

 

해당 내용 중에는

  • Vault 서버 주소를 지정합니다. (vault 네임스페이스의 서비스 "http://vault.vault.svc:8200")
  • Vault Agent가 /etc/vault/approle 경로에서 RoleID/SecretID 파일을 읽어 자동 인증합니다.
  • 인증된 결과 토큰은 /etc/vault-agent-token/token에 저장됩니다.
  • Vault에서 시크릿을 주기적으로 조회하고 템플릿을 렌더링합니다.
  • 이 경우 HTML 파일 /etc/secrets/index.html을 만들어서 시크릿 내용을 넣습니다.
cat <<EOF | kubectl create configmap vault-agent-config -n vault --from-file=agent-config.hcl=/dev/stdin --dry-run=client -o yaml | kubectl apply -f -
vault {
  address = "http://vault.vault.svc:8200"
}

auto_auth {
  method "approle" {
    config = {
      role_id_file_path = "/etc/vault/approle/role_id"
      secret_id_file_path = "/etc/vault/approle/secret_id"
      remove_secret_id_file_after_reading = false
    }
  }

  sink "file" {
    config = {
      path = "/etc/vault-agent-token/token"
    }
  }
}

template_config {
  static_secret_render_interval = "20s"
}

template {
  destination = "/etc/secrets/index.html"
  contents = <<EOH
  <html>
  <body>
    <p>username: {{ with secret "secret/data/sampleapp/config" }}{{ .Data.data.username }}{{ end }}</p>
    <p>password: {{ with secret "secret/data/sampleapp/config" }}{{ .Data.data.password }}{{ end }}</p>
  </body>
  </html>
EOH
}
EOF

 

샘플 애플리케이션으로서 Nginx와 Sidecar로서 vault를 배포합니다.

kubectl apply -n vault -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-vault-demo
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx-vault-demo
  template:
    metadata:
      labels:
        app: nginx-vault-demo
    spec:
      containers:
      - name: nginx
        image: nginx:latest
        ports:
        - containerPort: 80
        volumeMounts:
        - name: html-volume
          mountPath: /usr/share/nginx/html
      - name: vault-agent-sidecar
        image: hashicorp/vault:latest
        args:
          - "agent"
          - "-config=/etc/vault/agent-config.hcl"
        volumeMounts:
        - name: vault-agent-config
          mountPath: /etc/vault
        - name: vault-approle
          mountPath: /etc/vault/approle
        - name: vault-token
          mountPath: /etc/vault-agent-token
        - name: html-volume
          mountPath: /etc/secrets
      volumes:
      - name: vault-agent-config
        configMap:
          name: vault-agent-config
      - name: vault-approle
        secret:
          secretName: vault-approle
      - name: vault-token
        emptyDir: {}
      - name: html-volume
        emptyDir: {}
EOF

해당 Nginx Pod를 확인해보면 Vault Sidecar가 들어있는 모습을 볼 수 있습니다.

 

 

볼륨 마운트가 되어있는지 확인합니다.

kubectl exec -it deploy/nginx-vault-demo -c vault-agent-sidecar -- ls -l /etc/vault-agent-token

total 4
-rw-r-----    1 vault    vault           95 Apr 10 11:22 token

kubectl exec -it deploy/nginx-vault-demo -c vault-agent-sidecar -- cat /etc/vault-agent-token/token ; echo

hvs.CAESID7SBntL_0-7sJamhidUyA50aexVjFwzeqpPxO0zu0zyGh4KHGh2cy5uOXBQQzZDdjhVVzJZbWhYZXNZU3pzMlQ

kubectl exec -it deploy/nginx-vault-demo -c vault-agent-sidecar -- cat /etc/secrets/index.html

  <html>
  <body>
    <p>username: demo</p>
    <p>password: p@ssw0rd</p>
  </body>
  </html>

실제 배포된 화면을 확인해보겠습니다.

반응형