개요

이전 글에서는 홈랩 쿠버네티스 클러스터에 Traefik 인그레스 컨트롤러를 설치하고 외부 접근을 구성했다. 이번 글에서는 쿠버네티스 클러스터에서 민감한 정보(비밀번호, API 키, 인증서 등)를 안전하게 관리하기 위한 HashiCorp Vault 설치와 구성 방법을 다룬다.

Vault Logo

왜 기본 쿠버네티스 시크릿으로는 부족했나?

GitOps 방식으로 홈랩 환경을 구성하면서 시크릿 관리가 난제였다. 기본 쿠버네티스 Secret을 사용해보니 몇 가지 한계가 명확했다.

첫째, GitOps와의 통합 문제다. Git에 시크릿을 그대로 저장할 수 없고, base64로 인코딩해도 쉽게 복원 가능해 보안에 취약하다. Sealed Secrets나 SOPS 같은 도구도 검토했으나 단순 암호화를 넘어선 종합적인 시크릿 관리 솔루션이 필요했다.

둘째, 시크릿 갱신 문제다. 외부 API 토큰이나 인증서는 주기적으로 갱신이 필요한데, 매번 수동으로 처리하는 건 비효율적이다. 자동화된 방식의 시크릿 로테이션 관리가 필요했다.

HashiCorp Vault는 이런 문제들을 해결해주는 솔루션이다. 시크릿 암호화, 접근 제어, 자동 갱신 기능과 함께 쿠버네티스와의 통합도 지원한다. GitOps 워크플로우에 통합할 수 있는 방법도 제공해 선택하게 되었다.

홈랩 환경에 Vault 설치하기

1. GitOps 구성을 위한 디렉토리 준비

홈랩 환경의 모든 것을 GitOps로 관리하므로 Vault 설치도 같은 방식으로 진행했다. 먼저 필요한 디렉토리 구조를 생성한다.

1
2
mkdir -p k8s-resources/apps/vault/templates
cd k8s-resources/apps/vault

2. Helm 차트 구성

Chart.yaml 파일을 다음과 같이 작성했다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
apiVersion: v2
name: vault
description: HashiCorp Vault installation
type: application
version: 1.0.0
appVersion: "1.15.2"
dependencies:
    - name: vault
      version: "0.27.0"
      repository: "https://helm.releases.hashicorp.com"

values.yaml 파일에는 Vault 설정을 추가했다.

1
2
3
4
5
6
vault:
    server:
        enabled: true

    ui:
        enabled: true # 웹 기반 관리 UI 활성화

고가용성 설정도 고려했으나, 홈랩 환경에서는 자원 낭비라고 판단해 간소화했다. 필요시 후에 업그레이드하는 방식으로 접근했다.

3. 인그레스 구성

Vault UI에 접근하기 위한 인그레스 라우트를 구성했다. 이전에 설정한 Traefik 인그레스 컨트롤러를 활용했다.

templates/ingressroute.yaml 파일:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
    name: vault-ui
    namespace: vault
spec:
    entryPoints:
        - intweb
        - intwebsec
    routes:
        - kind: Rule
          match: Host(`vault.injunweb.com`)
          services:
              - name: vault-ui
                port: 8200

entryPointsintwebintwebsec로 설정해 Vault UI는 내부 네트워크에서만 접근 가능하도록 했다. 시크릿 관리 인터페이스인 만큼 외부 노출은 보안 위험이기 때문이다.

4. GitHub에 변경사항 추가 및 ArgoCD로 배포

1
2
3
git add .
git commit -m "Add Vault Helm chart configuration"
git push origin main

Vault 초기화 및 언실링

Vault 설치 후에는 두 가지 중요한 단계가 필요하다: 초기화와 언실링. 이 과정은 암호화 키 생성과 활성화 과정으로, 보안을 위해 자동화하지 않고 수동으로 진행한다.

1. 초기화 수행

Vault 파드에 접속하여 초기화를 수행한다.

1
2
3
4
5
# Vault 서버에 접속
kubectl -n vault exec -it vault-0 -- /bin/sh

# 초기화 수행 (기본 5개 키, 3개 필요)
vault operator init

실행 결과:

Unseal Key 1: wO14Gu9jIfGtae33/8U3l9mFv9QERnQS/IMoA1jJZ0vF
Unseal Key 2: FfL8J4QoIP/7fRrKJ7NN/5W8zG2ODzL9MiCJV5UcQmjx
Unseal Key 3: IgNkd4APfXmJywTqh+JjWbkiVgEHBTS+wjUGy/mtQ1pL
Unseal Key 4: +3Q0TUmCtw91/TNjdg7+dIh/8tHmfkoMykMTB9BPkMKn
Unseal Key 5: tJGLuUEYjpXc+K2jjxnMZ2JW7BUQ0KVYq7pGGBhEFLvG

Initial Root Token: hvs.6xu4j8TSoFBJ3EFNpW791e0I

주의: 이 키들은 예시일 뿐이며, 실제 환경에서는 절대로 이런 정보를 공개해서는 안 된다. 패스워드 관리자에 안전하게 저장해야 한다.

2. 언실링 수행

초기화 후에는 언실링 과정이 필요하다. 5개 중 3개의 키를 사용하여 Vault를 언실링한다.

1
2
3
4
# 언실링 수행 (3개 키 필요)
vault operator unseal wO14Gu9jIfGtae33/8U3l9mFv9QERnQS/IMoA1jJZ0vF
vault operator unseal FfL8J4QoIP/7fRrKJ7NN/5W8zG2ODzL9MiCJV5UcQmjx
vault operator unseal IgNkd4APfXmJywTqh+JjWbkiVgEHBTS+wjUGy/mtQ1pL

세 번째 키를 입력하면 Vault가 활성화된다. 상태 확인:

1
vault status
Key             Value
---             -----
Seal Type       shamir
Initialized     true
Sealed          false
Total Shares    5
Threshold       3
...

Sealed: false 표시는 정상적으로 언실링되었음을 의미한다.

Shamir의 비밀 공유 알고리즘을 활용하는 점이 주목할 만하다. 기업 환경에서는 이 5개의 키를 서로 다른 관리자에게 분산시켜, 최소 3명이 동의해야만 Vault를 열 수 있게 하는 4-eyes principle을 구현한다. 홈랩에서는 한 사람이 관리하지만, 엔터프라이즈 보안 원칙을 경험해볼 수 있는 부분이다.

Vault 웹 UI 접근

Vault가 활성화되면 웹 UI를 통해 관리할 수 있다. 호스트 파일에 다음 항목을 추가한다.

192.168.0.200 vault.injunweb.com

브라우저에서 http://vault.injunweb.com으로 접속하면 Vault UI를 볼 수 있다. 로그인에는 초기화 과정에서 받은 루트 토큰을 사용한다.

Vault UI Login

로그인 후 표시되는 대시보드:

Vault Dashboard

UI는 직관적으로 구성되어 있어 복잡한 정책 설정이나 시크릿 관리 작업도 효율적으로 수행할 수 있다.

Vault 기본 설정하기

Vault 설치와 초기화가 완료되었으면 쿠버네티스와의 통합을 위한 기본 설정을 진행한다.

1. 쿠버네티스 인증 설정

쿠버네티스 인증 방식을 사용하면 파드가 자신의 서비스 계정 토큰으로 Vault에 인증할 수 있다. Vault 파드에 접속하여 다음 명령을 실행한다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# Vault 로그인 (루트 토큰 사용)
vault login hvs.6xu4j8TSoFBJ3EFNpW791e0I

# Kubernetes 인증 활성화
vault auth enable kubernetes

# Kubernetes 인증 구성
vault write auth/kubernetes/config \
  kubernetes_host="https://$KUBERNETES_PORT_443_TCP_ADDR:443" \
  token_reviewer_jwt="$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" \
  kubernetes_ca_cert="$(cat /var/run/secrets/kubernetes.io/serviceaccount/ca.crt)" \
  issuer="https://kubernetes.default.svc.cluster.local"

이 설정은 Vault가 쿠버네티스 서비스 계정 토큰의 유효성을 검증할 수 있게 해준다.

2. KV 시크릿 엔진 활성화

Key-Value(KV) 엔진은 가장 기본적인 시크릿 저장 방식이다. 버전 2 엔진을 활성화한다.

1
vault secrets enable -path=secret kv-v2

KV 버전 2는 시크릿 버전 관리, 소프트 삭제 등 유용한 기능을 제공한다.

3. 정책 및 역할 생성

Vault에서 접근 제어의 핵심은 정책(Policy)이다. 애플리케이션에서 사용할 샘플 정책을 생성한다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# 샘플 정책 생성
cat <<EOF > app-policy.hcl
# app/* 경로의 모든 시크릿에 대한 읽기 권한
path "secret/data/app/*" {
  capabilities = ["read"]
}

# app/* 경로의 메타데이터 읽기 권한
path "secret/metadata/app/*" {
  capabilities = ["read", "list"]
}
EOF

# 정책 등록
vault policy write app-policy app-policy.hcl

그리고 쿠버네티스 인증을 위한 역할을 생성한다.

1
2
3
4
5
6
# Kubernetes 인증 역할 생성
vault write auth/kubernetes/role/app \
  bound_service_account_names=app \
  bound_service_account_namespaces=default \
  policies=app-policy \
  ttl=1h

이 설정은 default 네임스페이스의 app 서비스 계정이 Vault에 인증할 때 app-policy 정책이 적용되며, 토큰은 1시간 후 만료됨을 의미한다.

4. 샘플 시크릿 생성

테스트를 위한 샘플 시크릿을 생성한다.

1
2
3
4
5
6
7
8
# KV 버전 2 시크릿 생성
vault kv put secret/app/config \
  db.username="dbuser" \
  db.password="supersecret" \
  api.key="api12345"

# 생성한 시크릿 확인
vault kv get secret/app/config

시크릿 확인 결과:

====== Metadata ======
Key              Value
---              -----
created_time     2025-02-26T07:45:22.123456789Z
deletion_time    n/a
destroyed        false
version          1

====== Data ======
Key            Value
---            -----
api.key        api12345
db.password    supersecret
db.username    dbuser

이제 Vault에 기본적인 시크릿이 저장되었다. 다음으로 이 시크릿을 쿠버네티스 애플리케이션에서 사용할 수 있는 방법 두 가지를 구현해본다.

Vault Secrets Operator 설치하기

첫 번째 접근법은 Vault Secrets Operator를 사용하는 것이다. 이 Operator는 Vault의 시크릿을 쿠버네티스 Secret으로 자동 동기화해준다. 기존 애플리케이션 코드 변경 없이 Vault 시크릿을 활용할 수 있는 장점이 있다.

1. Operator 설정 추가

k8s-resources/apps/vault-secrets-operator/Chart.yaml 파일:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
apiVersion: v2
name: vault-secrets-operator
description: Vault Secrets Operator installation
type: application
version: 1.0.0
appVersion: "0.4.1"
dependencies:
    - name: vault-secrets-operator
      version: "0.3.4"
      repository: "https://helm.releases.hashicorp.com"

k8s-resources/apps/vault-secrets-operator/values.yaml 파일:

1
2
3
4
vault-secrets-operator:
    defaultVaultConnection:
        enabled: true
        address: "http://vault.vault.svc.cluster.local:8200" # 클러스터 내부 Vault 주소

이 설정은 Operator가 Vault에 접근할 수 있는 기본 정보를 제공한다.

2. Operator용 Vault 역할 생성

Vault에 접속하여 Operator용 정책과 역할을 생성한다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 정책 파일 생성
cat <<EOF > operator-policy.hcl
# app/* 경로의 시크릿 읽기 권한
path "secret/data/app/*" {
  capabilities = ["read"]
}

# app/* 경로의 메타데이터 읽기 권한
path "secret/metadata/app/*" {
  capabilities = ["read", "list"]
}
EOF

# 정책 등록
vault policy write operator-policy operator-policy.hcl

# 역할 생성
vault write auth/kubernetes/role/vault-secrets-operator \
  bound_service_account_names=vault-secrets-operator \
  bound_service_account_namespaces=vault-secrets-operator \
  policies=operator-policy \
  ttl=1h

3. Git에 추가 및 배포

1
2
3
4
cd k8s-resources
git add apps/vault-secrets-operator
git commit -m "Add Vault Secrets Operator configuration"
git push origin main

배포 후 확인:

1
kubectl get pods -n vault-secrets-operator

결과:

NAME                                      READY   STATUS    RESTARTS   AGE
vault-secrets-operator-75bcd5b69d-x2jf9   2/2     Running   0          45s

시크릿 동기화 리소스 구성하기

Vault Secrets Operator를 통해 Vault의 시크릿을 쿠버네티스 Secret으로 동기화하는 설정을 한다.

1. VaultAuth 리소스 생성

VaultAuth 리소스는 Vault에 인증하는 방법을 정의한다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultAuth
metadata:
    name: default
    namespace: default
spec:
    method: kubernetes # 인증 방식 (쿠버네티스)
    mount: kubernetes # Vault 인증 마운트 경로
    kubernetes:
        role: vault-secrets-operator # Vault 쿠버네티스 인증 역할
        serviceAccount: default # 사용할 서비스 계정

2. VaultStaticSecret 리소스 생성

VaultStaticSecret 리소스는 Vault의 특정 시크릿을 쿠버네티스 Secret으로 동기화하도록 지정한다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
    name: app-config
    namespace: default
spec:
    type: kv-v2 # 시크릿 엔진 타입
    mount: secret # 시크릿 엔진 마운트 경로
    path: app/config # Vault 시크릿 경로
    destination:
        name: app-config # 생성할 쿠버네티스 Secret 이름
        create: true # Secret이 없으면 생성
    refreshAfter: 30s # 30초마다 동기화
    vaultAuthRef: default # 사용할 VaultAuth 리소스

refreshAfter: 30s 설정은 Vault에서 시크릿이 변경될 때 30초 내에 쿠버네티스 Secret도 자동으로 업데이트되도록 한다.

3. 배포 및 확인

1
2
kubectl apply -f vault-auth.yaml
kubectl apply -f static-secret.yaml

Secret 생성 확인:

1
kubectl get secret app-config

결과:

NAME        TYPE     DATA   AGE
app-config  Opaque   3      15s

시크릿 내용 확인:

1
kubectl get secret app-config -o jsonpath="{.data.db\.password}" | base64 -d

4. 시크릿 자동 갱신 테스트

Vault에서 시크릿을 변경했을 때 자동으로 쿠버네티스 Secret이 업데이트되는지 확인:

1
2
3
4
5
6
7
8
# Vault에서 시크릿 변경
vault kv put secret/app/config \
  db.username="dbuser" \
  db.password="newpassword" \
  api.key="newapi12345"

# 30초 후 쿠버네티스 Secret 확인
kubectl get secret app-config -o jsonpath="{.data.db\.password}" | base64 -d

결과가 newpassword로 변경되면 자동 갱신이 정상 작동하는 것이다.

ArgoCD Vault Plugin 설치하기

두 번째 접근법은 ArgoCD Vault Plugin을 구성하는 것이다. 이 플러그인은 GitOps 워크플로우에 깊게 통합되어, Git 저장소에는 시크릿 참조만 저장하고 ArgoCD가 배포할 때 실제 값으로 대체한다.

1. ArgoCD Helm 차트 값 파일 수정

k8s-resources/apps/argocd/values.yaml 파일에 다음 내용을 추가한다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
argo-cd:
    configs:
        params:
            server.disable.auth: true
            server.insecure: true
    server:
        extraArgs:
            - --insecure
        ingress:
            enabled: false
        ingressGrpc:
            enabled: false

    repoServer:
        rbac:
            - verbs: ["get", "list", "watch"]
              apiGroups: [""]
              resources: ["secrets", "configmaps"]

        initContainers:
            - name: download-tools
              image: alpine/curl
              env:
                  - name: AVP_VERSION
                    value: "1.18.1"
              command: [sh, -c]
              args:
                  - >-
                      curl -L https://github.com/argoproj-labs/argocd-vault-plugin/releases/download/v$(AVP_VERSION)/argocd-vault-plugin_$(AVP_VERSION)_linux_amd64 -o argocd-vault-plugin &&
                      chmod +x argocd-vault-plugin &&
                      mv argocd-vault-plugin /custom-tools/
              volumeMounts:
                  - mountPath: /custom-tools
                    name: custom-tools

        extraContainers:
            - name: avp-helm
              command: ["/var/run/argocd/argocd-cmp-server"]
              image: quay.io/argoproj/argocd:v2.13.2
              securityContext:
                  runAsNonRoot: true
                  runAsUser: 999
              volumeMounts:
                  - mountPath: /var/run/argocd
                    name: var-files
                  - mountPath: /home/argocd/cmp-server/plugins
                    name: plugins
                  - mountPath: /tmp
                    name: tmp-dir
                  - mountPath: /home/argocd/cmp-server/config
                    name: cmp-plugin
                  - name: custom-tools
                    subPath: argocd-vault-plugin
                    mountPath: /usr/local/bin/argocd-vault-plugin
        volumes:
            - configMap:
                  name: cmp-plugin
              name: cmp-plugin
            - name: custom-tools
              emptyDir: {}
            - name: tmp-dir
              emptyDir: {}

2. ArgoCD용 Vault 역할 생성

Vault에 접속하여 ArgoCD용 정책과 역할을 생성한다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# ArgoCD 정책 생성
cat <<EOF > argocd-policy.hcl
# app/* 경로의 시크릿 읽기 권한
path "secret/data/app/*" {
  capabilities = ["read"]
}

# app/* 경로의 메타데이터 읽기 권한
path "secret/metadata/app/*" {
  capabilities = ["read", "list"]
}
EOF

# 정책 등록
vault policy write argocd argocd-policy.hcl

# Kubernetes 인증 역할 생성
vault write auth/kubernetes/role/argocd \
  bound_service_account_names=argocd-repo-server \
  bound_service_account_namespaces=argocd \
  policies=argocd \
  ttl=1h

3. 인증 시크릿 생성

k8s-resources/apps/argocd/templates/avp-secret.yaml 파일을 생성한다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
apiVersion: v1
kind: Secret
metadata:
    name: argocd-vault-plugin-credentials
    namespace: argocd
type: Opaque
stringData:
    AVP_AUTH_TYPE: "k8s" # 쿠버네티스 인증 방식
    AVP_K8S_ROLE: "argocd" # Vault 역할 이름
    AVP_TYPE: "vault" # Vault 타입
    VAULT_ADDR: "http://vault.vault.svc.cluster.local:8200" # Vault 주소

4. ConfigMap 생성

k8s-resources/apps/argocd/templates/configmap.yaml 파일:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
apiVersion: v1
kind: ConfigMap
metadata:
    name: cmp-plugin
    namespace: argocd
data:
    plugin.yaml: |
        apiVersion: argoproj.io/v1alpha1
        kind: ConfigManagementPlugin
        metadata:
          name: argocd-vault-plugin-helm
        spec:
          allowConcurrency: true
          discover:
            find:
              command:
                - sh
                - "-c"
                - "find . -name 'Chart.yaml' && find . -name 'values.yaml'"
          init:
            command:
              - bash
              - "-c"
              - |
                helm repo add bitnami https://charts.bitnami.com/bitnami
                helm dependency build
          generate:
            command:
              - sh
              - "-c"
              - |
                helm template $ARGOCD_APP_NAME -n $ARGOCD_APP_NAMESPACE ${ARGOCD_ENV_HELM_ARGS} . --include-crds |
                argocd-vault-plugin generate -s argocd:argocd-vault-plugin-credentials -
          lockRepo: false

애플리케이션에서 시크릿 활용하기

이제 두 가지 방법으로 Vault 시크릿을 애플리케이션에서 사용할 수 있다.

1. Vault Secrets Operator로 동기화된 Secret 사용

간단한 테스트 Deployment:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
apiVersion: apps/v1
kind: Deployment
metadata:
    name: demo-app
    namespace: default
spec:
    replicas: 1
    selector:
        matchLabels:
            app: demo-app
    template:
        metadata:
            labels:
                app: demo-app
        spec:
            containers:
                - name: demo-app
                  image: nginx:alpine
                  env:
                      - name: DB_PASSWORD
                        valueFrom:
                            secretKeyRef:
                                name: app-config # Operator가 동기화한 Secret
                                key: db.password

이 방식의 장점은 기존 애플리케이션 코드를 전혀 수정할 필요가 없다는 것이다. 표준 쿠버네티스 Secret 참조 방식을 그대로 사용한다.

1
kubectl apply -f demo-app.yaml

2. ArgoCD Vault Plugin으로 대체되는 시크릿 사용

플러그인 참조를 사용하는 Deployment:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
apiVersion: apps/v1
kind: Deployment
metadata:
    name: demo-app-avp
    namespace: default
    annotations:
        avp.kubernetes.io/path: "secret/data/app/config" # Vault 경로
spec:
    replicas: 1
    selector:
        matchLabels:
            app: demo-app-avp
    template:
        metadata:
            labels:
                app: demo-app-avp
        spec:
            containers:
                - name: demo-app
                  image: nginx:alpine
                  env:
                      - name: DB_PASSWORD
                        value: <path:secret/data/app/config#db.password> # 플레이스홀더

이 방식의 장점은 시크릿 값이 Git에 저장되지 않는다는 점이다. <path:secret/data/app/config#db.password> 같은 플레이스홀더만 Git에 저장하고, 실제 값은 ArgoCD가 배포 시점에 Vault에서 가져온다.

ArgoCD에서 애플리케이션을 생성할 때 “argocd-vault-plugin"을 Config Management Plugin으로 선택해야 한다. 그러면 ArgoCD가 매니페스트를 클러스터에 적용하기 전에 <path:...> 형식의 참조를 실제 값으로 대체한다.

마치며

이번 글에서는 홈랩 쿠버네티스 클러스터에 Vault를 설치하고, 안전한 시크릿 관리 시스템을 구축하는 방법을 알아보았다. 또한 ArgoCD Vault Plugin과 Vault Secrets Operator를 통해 GitOps 방식으로 시크릿을 관리하는 방법도 살펴보았다.

다음 글에서는 CI/CD 파이프라인을 구축하는 방법을 알아볼 것이다.