Notice
Recent Posts
Recent Comments
Link
«   2026/04   »
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
Archives
Today
Total
관리 메뉴

근묵자흑

Kubernetes Pattern: Secure Configuration 본문

k8s/kubernetes-pattern

Kubernetes Pattern: Secure Configuration

Luuuuu 2026. 1. 17. 19:59

 

쿠버네티스에서 실행되는 애플리케이션은 데이터베이스 연결 정보, API 키, 인증 토큰 등 민감한 설정 데이터를 필요로 합니다. 이러한 기밀 정보를 안전하게 저장하고 사용하는 것은 보안의 핵심 과제입니다.

 

이 글에서는 "Kubernetes Patterns" 책의 Chapter 25 "Secure Configuration" 패턴을 기반으로, 쿠버네티스 환경에서 민감한 설정 데이터를 안전하게 관리하는 5가지 솔루션을 테스트 결과와 함께 살펴봅겠니다.


1. 문제 정의: Kubernetes Secret은 정말 안전한가?

1.1 Kubernetes Secret의 구조적 한계

Kubernetes Secret은 민감한 정보를 저장하기 위한 기본 리소스입니다. 하지만 이름과 달리 Secret은 기본적으로 암호화되지 않고 Base64 인코딩만 적용됩니다. Base64는 암호화가 아닌 단순한 인코딩 방식이므로, 보안 관점에서는 평문과 동일하게 취급해야 합니다.

다음은 일반적인 Secret 리소스의 예시입니다. data 필드의 값들은 Base64로 인코딩되어 있지만, 이는 단순히 바이너리 데이터를 텍스트로 표현하기 위한 것일 뿐 보안을 제공하지 않습니다.

apiVersion: v1
kind: Secret
metadata:
  name: db-credentials
type: Opaque
data:
  username: YWRtaW4=           # echo -n "admin" | base64
  password: c3VwZXJzZWNyZXQ=   # echo -n "supersecret" | base64

Base64 디코딩은 누구나 수행할 수 있습니다. 터미널에서 echo "YWRtaW4=" | base64 -d 명령을 실행하면 즉시 "admin"이라는 원본 값을 확인할 수 있습니다. 이것이 Secret이 본질적으로 안전하지 않은 이유입니다.

Kubernetes는 Secret의 보안을 위해 몇 가지 보호 메커니즘을 제공합니다. 첫째, Secret은 해당 Secret을 필요로 하는 Pod가 실행되는 노드에만 배포됩니다. 둘째, 노드에서 Secret은 tmpfs(메모리)에 저장되며 디스크에 기록되지 않습니다. 셋째, Pod가 삭제되면 해당 Secret도 노드에서 제거됩니다. 넷째, etcd 저장 시 암호화 설정을 활성화할 수 있습니다.

하지만 이러한 보호에도 불구하고 cluster-admin 권한을 가진 사용자는 모든 Secret에 접근할 수 있으며, etcd 암호화가 활성화되지 않은 경우 etcd 백업에서 Secret이 평문으로 노출될 수 있습니다. 이러한 구조적 한계를 이해하는 것이 Secure Configuration 패턴을 적용하는 첫 번째 단계입니다.

1.2 GitOps 환경에서의 보안 과제

GitOps 패러다임이 보편화되면서 모든 쿠버네티스 리소스를 Git 저장소에서 관리하게 되었습니다. 이로 인해 새로운 보안 과제가 발생합니다. 개발자가 Secret을 포함한 매니페스트를 Git에 커밋하면, 이 파일이 CI/CD 파이프라인을 통해 클러스터에 배포됩니다. 이 과정에서 Secret이 평문으로 Git에 저장될 수 있다는 점이 문제입니다.

flowchart LR
    subgraph "GitOps 워크플로우"
        A[개발자] --> B[Git Repository]
        B --> C[CI/CD Pipeline]
        C --> D[Kubernetes Cluster]
    end

    subgraph "보안 문제점"
        B --> E["Secret이 평문으로<br/>저장될 수 있음"]
        C --> F["어디서 복호화할 것인가?"]
    end

    style E fill:#ffcccc
    style F fill:#fff3cd

GitOps 환경에서 고려해야 할 핵심 질문은 세 가지입니다. 첫째, Git에 Secret을 저장해야 하는가에 대한 질문인데, 저장한다면 반드시 암호화가 필요합니다. 둘째, 암호화된 Secret은 어디서 복호화되는가에 대한 질문으로, 클러스터 진입 전인지 클러스터 내부인지 결정해야 합니다. 셋째, 클러스터 관리자를 신뢰할 수 있는가에 대한 질문인데, cluster-admin 권한을 가진 사용자는 모든 Secret에 접근할 수 있다는 점을 고려해야 합니다.


2. 솔루션 개요

Secure Configuration 패턴은 크게 세 가지 접근 방식으로 분류할 수 있습니다.

flowchart TB
    subgraph "Secure Configuration 패턴"
        direction TB
        A[Secure Configuration]

        subgraph "클러스터 외부 암호화"
            B1[Sealed Secrets]
            B2[sops + age]
        end

        subgraph "외부 SMS 연동"
            C1[External Secrets Operator]
        end

        subgraph "중앙 집중식 Secret 관리"
            D1[Secrets Store CSI Driver]
            D2[Vault Sidecar Injector]
        end

        A --> B1
        A --> B2
        A --> C1
        A --> D1
        A --> D2
    end

첫 번째 접근 방식인 클러스터 외부 암호화(Out-of-Cluster Encryption)는 클러스터 외부에서 암호화된 설정 정보를 Git에 저장하고, 클러스터 진입 시점 또는 클러스터 내부에서 복호화하여 Kubernetes Secret으로 변환하는 방식입니다. Sealed Secrets는 클러스터 내 Controller가 복호화를 담당하고, sops + age는 CI/CD 파이프라인 또는 로컬에서 복호화합니다.

두 번째 접근 방식인 외부 SMS 연동은 AWS Secrets Manager, Azure Key Vault, HashiCorp Vault와 같은 전문 Secret Management System(SMS)을 활용하여 기밀 정보를 클러스터 외부에서 관리하고, 필요시 안전한 채널을 통해 가져오는 방식입니다. External Secrets Operator가 대표적인 솔루션입니다.

세 번째 접근 방식인 중앙 집중식 Secret 관리는 Secret을 Kubernetes etcd에 저장하지 않고, 외부 SMS에서 직접 Pod로 전달하는 방식입니다. Secrets Store CSI Driver는 CSI 볼륨으로 Secret을 마운트하고, Vault Sidecar Injector는 Sidecar 컨테이너가 Vault에서 Secret을 가져와 파일로 제공합니다.


3. Sealed Secrets

3.1 개요 및 동작 원리

Sealed Secrets는 Bitnami가 2017년에 개발한 쿠버네티스 애드온으로, Git에 안전하게 저장할 수 있는 암호화된 Secret을 생성합니다. 핵심 원리는 비대칭 암호화를 사용한다는 점입니다. RSA-OAEP와 AES-256-GCM 알고리즘을 조합하여 사용하며, 공개키로 암호화하고 개인키로 복호화합니다. 개인키는 클러스터 내 Controller만 보유하므로, 암호화된 SealedSecret 리소스는 Git에 안전하게 저장할 수 있습니다.

sequenceDiagram
    participant Dev as 개발자
    participant KS as kubeseal CLI
    participant Git as Git Repository
    participant SO as Sealed Secrets<br/>Controller
    participant K8s as Kubernetes API

    Dev->>KS: Secret YAML 입력
    KS->>SO: 공개키 요청
    SO-->>KS: 공개키 반환
    KS->>KS: 공개키로 암호화
    KS->>Git: SealedSecret 저장
    Git->>K8s: GitOps 배포
    K8s->>SO: SealedSecret 감지
    SO->>SO: 개인키로 복호화
    SO->>K8s: Kubernetes Secret 생성

이 다이어그램에서 주목할 점은 개인키가 클러스터를 떠나지 않는다는 것입니다. kubeseal CLI는 클러스터에서 공개키를 가져와 로컬에서 암호화를 수행합니다. 암호화된 SealedSecret은 Git에 저장되고, GitOps 도구(ArgoCD, Flux 등)에 의해 클러스터에 배포됩니다. Sealed Secrets Controller는 SealedSecret 리소스를 감지하면 개인키로 복호화하여 일반 Kubernetes Secret을 생성합니다.

3.2 설치 및 기본 사용법

Sealed Secrets를 설치하려면 Helm을 사용하는 것이 가장 간편합니다. 다음 명령으로 sealed-secrets 네임스페이스에 Controller를 설치합니다.

helm repo add sealed-secrets https://bitnami-labs.github.io/sealed-secrets
helm install sealed-secrets sealed-secrets/sealed-secrets \
  --namespace sealed-secrets \
  --create-namespace \
  --set-string fullnameOverride=sealed-secrets-controller

설치가 완료되면 kubeseal CLI를 사용하여 Secret을 암호화할 수 있습니다. macOS에서는 brew install kubeseal 명령으로 설치할 수 있고, 다른 플랫폼에서는 GitHub releases 페이지에서 바이너리를 다운로드합니다.

실제 테스트 과정을 살펴보겠습니다. 먼저 원본 Secret을 생성합니다.

# secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: db-credentials
  namespace: test
type: Opaque
stringData:
  username: admin
  password: supersecretpassword123

이 Secret을 SealedSecret으로 변환합니다.

kubeseal --format=yaml \
  --controller-name=sealed-secrets-controller \
  --controller-namespace=sealed-secrets \
  < secret.yaml > sealed-secret.yaml

생성된 SealedSecret은 다음과 같은 형태입니다. encryptedData 필드에 암호화된 값이 저장되며, 이 파일은 Git에 안전하게 커밋할 수 있습니다.

apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
  name: db-credentials
  namespace: test
spec:
  encryptedData:
    username: AgCgMs1HJFe3gWyokSpNoWS5ERa+WHNPqp...
    password: AgBMY0AMmiQ0urMd9NpjyzfMhWCVPoID31...
  template:
    metadata:
      name: db-credentials
      namespace: test
    type: Opaque

테스트 결과, Controller 설치, Secret 암호화, SealedSecret 적용, Secret 자동 생성, 값 검증 모두 성공적으로 완료되었습니다. SealedSecret을 클러스터에 적용하면 Controller가 자동으로 복호화하여 동일한 이름의 Kubernetes Secret을 생성합니다.

3.3 Scope 설정의 이해

Sealed Secrets는 세 가지 스코프를 지원하며, 이는 SealedSecret의 재사용 범위를 결정합니다.

strict 스코프는 기본값으로, Secret의 이름과 네임스페이스가 암호화 시점에 고정됩니다. 이 SealedSecret은 정확히 같은 이름과 네임스페이스로만 복호화될 수 있습니다. 프로덕션 환경에서 실수로 잘못된 위치에 Secret이 생성되는 것을 방지하므로 가장 안전한 옵션입니다.

namespace-wide 스코프는 동일한 네임스페이스 내에서 Secret 이름을 변경할 수 있습니다. 같은 네임스페이스 내에서 여러 애플리케이션이 동일한 Secret을 다른 이름으로 사용해야 할 때 유용합니다.

cluster-wide 스코프는 이름과 네임스페이스 모두 변경할 수 있습니다. 개발, 스테이징, 프로덕션 환경에서 동일한 Secret 값을 공유해야 할 때 사용할 수 있지만, 보안상 주의가 필요합니다.

cluster-wide 스코프로 암호화하려면 다음과 같이 명령을 실행합니다.

kubeseal --scope cluster-wide -f secret.yaml -o yaml > sealed-secret.yaml

3.4 운영 시 고려사항

Sealed Secrets를 프로덕션에서 운영할 때 반드시 고려해야 할 사항이 있습니다.

첫째, 키 백업이 필수입니다. Controller의 개인키가 삭제되면 기존 SealedSecret을 복호화할 수 없습니다. 다음 명령으로 키를 백업하고 안전한 장소에 보관해야 합니다.

kubectl get secret -n sealed-secrets \
  -l sealedsecrets.bitnami.com/sealed-secrets-key \
  -o yaml > sealed-secrets-keys-backup.yaml

둘째, 키 로테이션을 주기적으로 수행해야 합니다. 보안 정책에 따라 주기적으로 키를 로테이션하고, 새 키 생성 후 기존 SealedSecret을 재암호화해야 합니다.

셋째, Controller의 가용성을 보장해야 합니다. Controller가 다운되면 새로운 SealedSecret이 복호화되지 않으므로, 적절한 복제본 수와 리소스를 할당해야 합니다.


4. sops + age

4.1 개요 및 동작 원리

Mozilla가 개발한 sops(Secrets OPerationS)는 클라이언트 측에서 완전히 동작하는 암호화 도구입니다. Sealed Secrets와 달리 서버 측 컴포넌트가 필요 없어 가장 간단한 설정으로 GitOps 환경에서 Secret을 안전하게 관리할 수 있습니다.

sops는 다양한 암호화 백엔드를 지원합니다. age는 로컬 비대칭 암호화로 가장 간단합니다. AWS KMS, Google Cloud KMS, Azure Key Vault는 클라우드 키 관리 서비스와 연동합니다. HashiCorp Vault는 자체 호스팅 가능한 Vault와 연동합니다. 이 글에서는 가장 간단한 age를 사용한 방법을 다룹니다.

flowchart LR
    subgraph "개발자 환경"
        A[원본 YAML] --> B[sops CLI]
        B --> C[암호화된 YAML]
        KEY[(age 공개키)]
        KEY --> B
    end

    subgraph "Git Repository"
        C --> D[안전하게 저장<br/>metadata는 평문]
    end

    subgraph "배포 시점"
        D --> E[sops decrypt]
        PKEY[(age 개인키)]
        PKEY --> E
        E --> F[kubectl apply]
        F --> G[Kubernetes Secret]
    end

    style A fill:#ffcccb
    style C fill:#90EE90
    style D fill:#90EE90

sops의 핵심 특징은 YAML 파일의 구조를 유지하면서 값만 암호화한다는 점입니다. 이는 Git에서 파일을 검색하거나 diff를 확인할 때 매우 유용합니다. metadata(name, namespace 등)는 평문으로 유지되고, data나 stringData 필드의 값만 암호화됩니다.

4.2 설치 및 기본 사용법

sops와 age를 설치합니다. macOS에서는 Homebrew를 사용합니다.

brew install sops age

age 키 쌍을 생성합니다. 이 명령은 keys.txt 파일에 개인키와 공개키를 모두 저장합니다.

age-keygen -o keys.txt
# 출력 예시:
# Public key: age1zs4kgau8386ejx8mrt2kdh0wula39l3mscz9nl6c00xenc835v0qgspnma

원본 Secret을 준비합니다.

# secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: sops-test-secret
  namespace: test
type: Opaque
stringData:
  username: admin
  password: my-super-secret-password
  api-key: sk-1234567890abcdef

sops로 암호화합니다. --encrypted-regex 옵션을 사용하면 특정 필드만 암호화할 수 있습니다. 여기서는 data와 stringData 필드만 암호화하고 metadata는 평문으로 유지합니다.

export SOPS_AGE_RECIPIENTS="age1zs4kgau8386ejx8mrt2kdh0wula39l3mscz9nl6c00xenc835v0qgspnma"
sops --encrypt --encrypted-regex '^(data|stringData)$' secret.yaml > secret.enc.yaml

암호화된 파일은 다음과 같은 형태입니다. metadata의 name과 namespace는 평문으로 유지되어 Git에서 검색이 가능하고, stringData의 값들만 ENC[...]로 암호화되어 있습니다.

apiVersion: v1
kind: Secret
metadata:
    name: sops-test-secret
    namespace: test
type: Opaque
stringData:
    username: ENC[AES256_GCM,data:b3H7go4=,iv:1FR2V8...,tag:gQEo...,type:str]
    password: ENC[AES256_GCM,data:8E2nyH+W7GL6mno/...,tag:hITk...,type:str]
    api-key: ENC[AES256_GCM,data:nCpqdKrCRn403n8/...,tag:G8ro...,type:str]
sops:
    age:
        - recipient: age1zs4kgau8386ejx8mrt2kdh0wula39l3mscz9nl6c00xenc835v0qgspnma
          enc: |
            -----BEGIN AGE ENCRYPTED FILE-----
            YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBDTHFqWmpIUkpBd1Z4MEQ2
            ...
            -----END AGE ENCRYPTED FILE-----
    encrypted_regex: ^(data|stringData)$
    version: 3.11.0

복호화 후 클러스터에 적용합니다.

export SOPS_AGE_KEY_FILE=keys.txt
sops --decrypt secret.enc.yaml | kubectl apply -f -

테스트 결과, age 키 생성, Secret 암호화, metadata 평문 유지, 복호화, 클러스터 적용 모두 성공적으로 완료되었습니다.

4.3 프로젝트 설정 파일 활용

프로젝트 루트에 .sops.yaml 파일을 생성하면 자동으로 암호화 규칙이 적용됩니다. 이를 통해 팀원들이 일관된 암호화 설정을 사용할 수 있습니다.

creation_rules:
  # .enc.yaml로 끝나는 파일에 적용
  - path_regex: .*\.enc\.yaml$
    encrypted_regex: ^(data|stringData)$
    age: age1zs4kgau8386ejx8mrt2kdh0wula39l3mscz9nl6c00xenc835v0qgspnma

  # secrets/ 디렉토리 내 모든 파일에 적용
  - path_regex: secrets/.*\.yaml$
    encrypted_regex: ^(data|stringData)$
    age: age1zs4kgau8386ejx8mrt2kdh0wula39l3mscz9nl6c00xenc835v0qgspnma

이 설정 파일이 있으면 sops --encrypt secret.yaml 명령만으로도 적절한 규칙이 자동 적용됩니다.

4.4 CI/CD 통합

ArgoCD와 Flux 모두 sops를 네이티브로 지원합니다.

ArgoCD에서는 kustomize와 함께 sops를 사용하려면 argocd-cm ConfigMap에 다음 설정을 추가합니다.

data:
  kustomize.buildOptions: --enable-alpha-plugins --enable-exec

Flux에서는 Kustomization 리소스에서 직접 sops 복호화를 설정할 수 있습니다.

apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
spec:
  decryption:
    provider: sops
    secretRef:
      name: sops-age-key  # age 개인키가 저장된 Secret

4.5 Sealed Secrets와의 비교

Sealed Secrets와 sops + age는 모두 GitOps 환경에서 Secret을 안전하게 관리하는 솔루션이지만, 중요한 차이점이 있습니다.

서버 컴포넌트 측면에서 Sealed Secrets는 클러스터 내에 Controller가 필요하지만, sops는 클라이언트 측 도구이므로 서버 컴포넌트가 불필요합니다. 키 관리 위치 측면에서 Sealed Secrets는 개인키가 클러스터 내부에 저장되지만, sops는 개인키가 클러스터 외부(CI/CD 시스템, 개발자 머신 등)에 저장됩니다. Git 검색 측면에서 Sealed Secrets는 전체 리소스가 암호화되어 검색이 어렵지만, sops는 metadata가 평문으로 유지되어 검색이 가능합니다. 복호화 시점 측면에서 Sealed Secrets는 클러스터 내부에서 복호화되지만, sops는 CI/CD 파이프라인 또는 로컬에서 복호화됩니다.


5. External Secrets Operator (ESO)

5.1 개요 및 아키텍처

External Secrets Operator는 외부 Secret Management System과 쿠버네티스를 연결하는 가장 널리 사용되는 솔루션입니다. 2023년 이후 여러 프로젝트가 통합되어 사실상 표준으로 자리잡았습니다.

ESO는 AWS Secrets Manager, Azure Key Vault, Google Cloud Secret Manager, HashiCorp Vault, CyberArk Conjur, Doppler 등 20개 이상의 외부 SMS를 지원합니다. 조직에서 이미 사용 중인 SMS가 있다면 ESO를 통해 쿠버네티스와 자연스럽게 연동할 수 있습니다.

flowchart LR
    subgraph "External Secret Management"
        SMS[(AWS Secrets Manager<br/>Azure Key Vault<br/>HashiCorp Vault)]
    end

    subgraph "Kubernetes Cluster"
        ESO[External Secrets<br/>Operator]
        SS[SecretStore]
        ES[ExternalSecret]
        SEC[Kubernetes Secret]
        POD[Pod]

        SS --> ESO
        ES --> ESO
        ESO --> SEC
        SEC --> POD
    end

    SMS <--> ESO

    style SMS fill:#e1f5fe
    style ESO fill:#fff3e0
    style SEC fill:#e8f5e9

이 아키텍처에서 SecretStore는 외부 SMS와의 연결 정보를 정의합니다. ExternalSecret은 어떤 Secret을 가져올지, 어떻게 매핑할지를 정의합니다. ESO Controller는 ExternalSecret을 감시하고, SecretStore를 통해 외부 SMS에서 값을 가져와 Kubernetes Secret을 생성합니다.

5.2 핵심 리소스 이해

SecretStore는 외부 SMS 연결 정보를 정의하는 리소스입니다. 네임스페이스 범위로 동작하며, 해당 네임스페이스 내의 ExternalSecret만 이 SecretStore를 참조할 수 있습니다.

apiVersion: external-secrets.io/v1
kind: SecretStore
metadata:
  name: aws-secrets-manager
  namespace: myapp
spec:
  provider:
    aws:
      service: SecretsManager
      region: ap-northeast-2
      auth:
        secretRef:
          accessKeyIDSecretRef:
            name: aws-credentials
            key: access-key
          secretAccessKeySecretRef:
            name: aws-credentials
            key: secret-access-key

ExternalSecret은 가져올 Secret과 매핑 정보를 정의하는 리소스입니다. refreshInterval을 설정하면 ESO가 주기적으로 외부 SMS와 동기화하여 Secret 값을 최신 상태로 유지합니다.

apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
  name: db-credentials
  namespace: myapp
spec:
  refreshInterval: 1h  # 자동 갱신 주기
  secretStoreRef:
    name: aws-secrets-manager
    kind: SecretStore
  target:
    name: db-credentials-secret  # 생성될 K8s Secret 이름
    creationPolicy: Owner
  data:
    - secretKey: username
      remoteRef:
        key: prod/database
        property: username
    - secretKey: password
      remoteRef:
        key: prod/database
        property: password

5.3 설치 및 테스트

External Secrets Operator를 설치합니다.

helm repo add external-secrets https://charts.external-secrets.io
helm install external-secrets external-secrets/external-secrets \
  --namespace external-secrets \
  --create-namespace \
  --set installCRDs=true

외부 SMS가 없는 테스트 환경에서는 Fake Provider를 사용할 수 있습니다. Fake Provider는 정적인 값을 제공하는 테스트용 제공자입니다.

apiVersion: external-secrets.io/v1
kind: SecretStore
metadata:
  name: fake-store
  namespace: test
spec:
  provider:
    fake:
      data:
        - key: "/db/username"
          value: "fake-db-user"
        - key: "/db/password"
          value: "fake-password-123"
        - key: "/api/key"
          value: "fake-api-key-xyz789"

이 SecretStore를 참조하는 ExternalSecret을 생성합니다.

apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
  name: db-credentials
  namespace: test
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: fake-store
    kind: SecretStore
  target:
    name: db-credentials-secret
    creationPolicy: Owner
  data:
    - secretKey: username
      remoteRef:
        key: /db/username
    - secretKey: password
      remoteRef:
        key: /db/password

테스트 결과, Operator 설치, SecretStore 생성, ExternalSecret 동기화, K8s Secret 생성, 값 검증 모두 성공적으로 완료되었습니다. ExternalSecret의 status를 확인하면 SecretSynced 상태를 볼 수 있습니다.

5.4 refreshInterval과 Secret 로테이션

ESO의 핵심 기능 중 하나는 자동 Secret 갱신입니다. refreshInterval을 설정하면 ESO가 주기적으로 외부 SMS에서 값을 가져와 Kubernetes Secret을 업데이트합니다.

그러나 중요한 점은 Kubernetes Secret이 업데이트되어도 이를 사용하는 Pod는 자동으로 재시작되지 않는다는 것입니다. 환경변수로 Secret을 사용하는 경우 Pod 재시작 없이는 새 값을 읽을 수 없습니다.

이 문제를 해결하려면 Reloader와 같은 도구를 사용하여 Secret 변경 시 자동으로 Deployment를 재시작할 수 있습니다.

helm install reloader stakater/reloader -n kube-system

Deployment에 annotation을 추가하면 해당 Deployment가 참조하는 Secret이 변경될 때 자동으로 롤링 업데이트가 수행됩니다.

apiVersion: apps/v1
kind: Deployment
metadata:
  annotations:
    reloader.stakater.com/auto: "true"

5.5 ClusterSecretStore vs SecretStore

SecretStore는 네임스페이스 범위로 동작하여 해당 네임스페이스 내의 ExternalSecret만 참조할 수 있습니다. 반면 ClusterSecretStore는 클러스터 전체 범위로 동작하여 모든 네임스페이스의 ExternalSecret이 참조할 수 있습니다.

보안 관점에서 SecretStore를 사용하는 것이 권장됩니다. 팀별로 독립적인 SMS 연결을 관리할 수 있고, 한 팀의 설정 변경이 다른 팀에 영향을 미치지 않습니다. ClusterSecretStore는 개발/테스트 환경이나 모든 팀이 동일한 SMS를 공유하는 경우에 적합합니다.


6. Secrets Store CSI Driver

6.1 개요 및 아키텍처

Container Storage Interface(CSI)를 활용하여 외부 SMS의 Secret을 Pod에 볼륨으로 마운트하는 방식입니다. 이 솔루션의 핵심 장점은 쿠버네티스 etcd에 Secret이 저장되지 않는다는 것입니다. 클러스터 관리자조차 Secret 값에 직접 접근할 수 없으므로, 가장 엄격한 보안 요구사항을 충족할 수 있습니다.

flowchart TB
    subgraph "외부 SMS"
        VAULT[(HashiCorp Vault)]
    end

    subgraph "Kubernetes Node"
        CSI[CSI Driver<br/>DaemonSet]
        PROV[Vault CSI<br/>Provider]

        subgraph "Pod"
            APP[Application<br/>Container]
            VOL["/mnt/secrets-store<br/>tmpfs Volume"]
        end

        CSI --> PROV
        PROV <--> VAULT
        CSI --> VOL
        APP --> VOL
    end

    style VOL fill:#e8f5e9
    style VAULT fill:#e1f5fe

동작 흐름은 다음과 같습니다. 첫째, Pod가 생성될 때 CSI Driver가 Pod에 정의된 SecretProviderClass를 확인합니다. 둘째, CSI Driver는 해당 Provider Plugin(예: Vault Provider)을 호출합니다. 셋째, Provider Plugin이 외부 SMS에서 Secret을 조회합니다. 넷째, 조회된 Secret은 tmpfs 볼륨으로 Pod에 마운트됩니다. 다섯째, 선택적으로 Kubernetes Secret으로도 동기화할 수 있습니다.

6.2 설치

Secrets Store CSI Driver와 Provider를 설치합니다. Provider는 사용하는 SMS에 따라 다릅니다. 여기서는 HashiCorp Vault를 예로 듭니다.

# Secrets Store CSI Driver 설치
helm repo add secrets-store-csi-driver \
  https://kubernetes-sigs.github.io/secrets-store-csi-driver/charts
helm install csi-secrets-store secrets-store-csi-driver/secrets-store-csi-driver \
  --namespace kube-system \
  --set syncSecret.enabled=true \
  --set enableSecretRotation=true

# Vault Provider 설치 (Vault 사용 시)
helm install vault hashicorp/vault \
  --namespace vault \
  --create-namespace \
  --set "server.dev.enabled=true" \
  --set "csi.enabled=true"

syncSecret.enabled=true 옵션을 설정하면 CSI 볼륨의 내용을 Kubernetes Secret으로도 동기화할 수 있습니다. enableSecretRotation=true는 Secret 값이 변경되었을 때 자동으로 파일을 업데이트합니다.

6.3 SecretProviderClass 정의

SecretProviderClass는 어떤 Provider를 사용하고, 어떤 Secret을 가져올지 정의하는 리소스입니다.

apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
  name: vault-database
  namespace: test
spec:
  provider: vault
  parameters:
    vaultAddress: "http://vault.vault:8200"
    roleName: "csi-test"
    objects: |
      - objectName: "db-username"
        secretPath: "secret/data/database"
        secretKey: "username"
      - objectName: "db-password"
        secretPath: "secret/data/database"
        secretKey: "password"
  # K8s Secret으로도 동기화 (선택)
  secretObjects:
    - secretName: database-creds-synced
      type: Opaque
      data:
        - objectName: db-username
          key: username
        - objectName: db-password
          key: password

parameters 섹션에서 Vault 연결 정보와 가져올 Secret 경로를 정의합니다. secretObjects 섹션은 선택적으로 Kubernetes Secret을 생성하도록 설정합니다. 이를 통해 볼륨 마운트와 환경변수 방식을 모두 사용할 수 있습니다.

6.4 Pod에서 CSI 볼륨 사용

Pod에서 CSI 볼륨을 마운트하여 Secret에 접근합니다.

apiVersion: v1
kind: Pod
metadata:
  name: app
spec:
  serviceAccountName: csi-test-sa
  containers:
    - name: app
      image: busybox
      command: ["cat", "/mnt/secrets-store/db-password"]
      volumeMounts:
        - name: secrets-store
          mountPath: "/mnt/secrets-store"
          readOnly: true
      env:
        # syncSecret으로 생성된 Secret 사용 (선택)
        - name: DB_USERNAME
          valueFrom:
            secretKeyRef:
              name: database-creds-synced
              key: username
  volumes:
    - name: secrets-store
      csi:
        driver: secrets-store.csi.k8s.io
        readOnly: true
        volumeAttributes:
          secretProviderClass: "vault-database"

테스트 결과, CSI Driver 설치, Vault Provider 설치, SecretProviderClass 생성, Volume Mount, syncSecret, 값 검증 모두 성공적으로 완료되었습니다.

마운트된 파일을 확인하면 다음과 같습니다.

$ kubectl exec csi-test-pod -- ls -la /mnt/secrets-store/
total 4
drwxrwxrwt    3 root     root           140 Jan 17 10:38 .
drwxr-xr-x    3 root     root          4096 Jan 17 10:38 ..
lrwxrwxrwx    1 root     root            18 Jan 17 10:38 db-password -> ..data/db-password
lrwxrwxrwx    1 root     root            18 Jan 17 10:38 db-username -> ..data/db-username

$ kubectl exec csi-test-pod -- cat /mnt/secrets-store/db-username
db-admin

6.5 ESO와의 비교

External Secrets Operator와 Secrets Store CSI Driver는 모두 외부 SMS와 연동하지만 중요한 차이점이 있습니다.

etcd 저장 측면에서 ESO는 외부 SMS에서 가져온 값으로 Kubernetes Secret을 생성하므로 etcd에 저장됩니다. CSI Driver는 기본적으로 파일로만 마운트하므로 etcd에 저장되지 않습니다. syncSecret 옵션을 사용하면 ESO와 동일하게 Kubernetes Secret을 생성할 수 있습니다.

Secret 접근 방식 측면에서 ESO로 생성된 Secret은 환경변수, 볼륨 마운트 등 모든 방식으로 사용할 수 있습니다. CSI Driver는 기본적으로 파일 기반 접근만 지원합니다. 환경변수로 사용하려면 syncSecret을 활성화해야 합니다.

클러스터 관리자 접근 측면에서 ESO로 생성된 Secret은 클러스터 관리자가 kubectl로 조회할 수 있습니다. CSI Driver로 마운트된 파일은 해당 Pod 내부에서만 접근 가능하므로 클러스터 관리자도 직접 조회할 수 없습니다.

클러스터 관리자를 신뢰할 수 없는 환경, 예를 들어 금융이나 의료 분야의 규제 요구사항이 있는 경우 CSI Driver를 권장합니다.


7. Vault Sidecar Agent Injector

7.1 개요 및 아키텍처

HashiCorp Vault의 Sidecar Agent Injector는 Mutating Webhook을 활용하여 Pod에 자동으로 Vault Agent 컨테이너를 주입합니다. 개발자는 Pod에 annotation만 추가하면 되고, 애플리케이션 코드 수정 없이 Secret에 접근할 수 있습니다.

sequenceDiagram
    participant User as 사용자
    participant API as Kubernetes API
    participant VWH as Vault Mutating<br/>Webhook
    participant INIT as vault-agent-init
    participant SIDE as vault-agent<br/>(sidecar)
    participant VAULT as HashiCorp Vault
    participant APP as Application

    User->>API: Pod 생성 요청<br/>(annotations 포함)
    API->>VWH: Admission Review
    VWH->>VWH: Pod Spec 수정<br/>- Init Container 추가<br/>- Sidecar 추가<br/>- Volume 추가
    VWH-->>API: 수정된 Pod Spec
    API->>INIT: Init Container 실행
    INIT->>VAULT: K8s 인증 + Secret 요청
    VAULT-->>INIT: Secret 반환
    INIT->>INIT: /vault/secrets/에 파일 저장
    INIT-->>APP: Init 완료, App 시작

    loop 주기적 동기화
        SIDE->>VAULT: Secret 갱신 확인
        VAULT-->>SIDE: 변경된 Secret
        SIDE->>SIDE: 파일 업데이트
    end

    APP->>APP: /vault/secrets/ 파일 읽기

이 아키텍처의 핵심은 Mutating Webhook입니다. 사용자가 annotation이 포함된 Pod를 생성하면, Vault Mutating Webhook이 Pod Spec을 수정하여 Init Container, Sidecar Container, Volume을 자동으로 추가합니다. 개발자는 이 복잡한 설정을 직접 작성할 필요가 없습니다.

7.2 설치 및 Vault 설정

Vault with Injector를 설치합니다.

helm install vault hashicorp/vault \
  --namespace vault \
  --create-namespace \
  --set "server.dev.enabled=true" \
  --set "injector.enabled=true"

Vault에서 Kubernetes 인증을 설정합니다. 이 설정을 통해 Kubernetes ServiceAccount를 사용하여 Vault에 인증할 수 있습니다.

# Kubernetes 인증 활성화
kubectl exec -n vault vault-0 -- vault auth enable kubernetes

# 인증 설정
kubectl exec -n vault vault-0 -- sh -c '
vault write auth/kubernetes/config \
  kubernetes_host="https://$KUBERNETES_PORT_443_TCP_ADDR:443"
'

# 정책 생성 - myapp 경로의 Secret 읽기 권한
echo 'path "secret/data/myapp" { capabilities = ["read"] }' | \
  kubectl exec -i -n vault vault-0 -- vault policy write myapp -

# 역할 생성 - ServiceAccount와 정책 매핑
kubectl exec -n vault vault-0 -- vault write auth/kubernetes/role/myapp \
  bound_service_account_names=myapp-sa \
  bound_service_account_namespaces=myapp \
  policies=myapp \
  ttl=24h

7.3 Deployment에 annotation 추가

Pod에 annotation을 추가하면 Vault Agent가 자동으로 주입됩니다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  template:
    metadata:
      annotations:
        # Vault Agent Injection 활성화
        vault.hashicorp.com/agent-inject: "true"
        vault.hashicorp.com/role: "myapp"

        # Secret 파일 경로 지정
        vault.hashicorp.com/agent-inject-secret-database.txt: "secret/data/database"

        # 템플릿으로 원하는 형식 지정
        vault.hashicorp.com/agent-inject-template-database.txt: |
          {{- with secret "secret/data/database" -}}
          DB_USERNAME={{ .Data.data.username }}
          DB_PASSWORD={{ .Data.data.password }}
          {{- end -}}
    spec:
      serviceAccountName: myapp-sa
      containers:
        - name: app
          image: myapp:latest

agent-inject annotation은 Vault Agent Injection을 활성화합니다. role annotation은 Vault에서 사용할 역할을 지정합니다. agent-inject-secret-* annotation은 Secret 파일의 경로와 Vault의 Secret 경로를 매핑합니다. agent-inject-template-* annotation은 Go 템플릿을 사용하여 파일 형식을 커스터마이즈합니다.

테스트 결과, Injector 설치, Sidecar 주입, Init Container 실행, Secret 파일 생성, 템플릿 렌더링 모두 성공적으로 완료되었습니다.

주입된 Pod를 확인하면 컨테이너가 2개(app, vault-agent)이고, Init Container로 vault-agent-init이 추가된 것을 볼 수 있습니다.

$ kubectl get pod myapp-xxx -o jsonpath='{.spec.containers[*].name}'
app vault-agent

$ kubectl exec myapp-xxx -c app -- cat /vault/secrets/database.txt
DB_USERNAME=db-admin
DB_PASSWORD=supersecret123

7.4 템플릿 활용 예시

Vault Agent의 템플릿 기능을 사용하면 다양한 형식으로 Secret 파일을 생성할 수 있습니다.

환경 변수 형식으로 생성하려면 다음과 같이 설정합니다.

vault.hashicorp.com/agent-inject-template-database.txt: |
  {{- with secret "secret/data/database" -}}
  export DB_USERNAME={{ .Data.data.username }}
  export DB_PASSWORD={{ .Data.data.password }}
  {{- end -}}

JSON 형식으로 생성하려면 다음과 같이 설정합니다.

vault.hashicorp.com/agent-inject-template-config.json: |
  {{- with secret "secret/data/myapp" -}}
  {
    "database": {
      "username": "{{ .Data.data.username }}",
      "password": "{{ .Data.data.password }}"
    }
  }
  {{- end -}}

7.5 Init Container Only vs Sidecar

Vault Agent는 두 가지 모드로 동작할 수 있습니다.

기본 모드인 Sidecar 모드에서는 Init Container가 초기 Secret을 가져오고, Sidecar Container가 계속 실행되면서 주기적으로 Secret을 동기화합니다. Secret이 Vault에서 변경되면 파일이 자동으로 업데이트됩니다.

Init Container Only 모드에서는 Init Container만 실행되고 Sidecar는 주입되지 않습니다. Pod 시작 시 한 번만 Secret을 가져오므로 리소스를 절약할 수 있지만, Secret 변경 시 Pod를 재시작해야 합니다.

Init Container Only 모드를 사용하려면 다음 annotation을 추가합니다.

vault.hashicorp.com/agent-pre-populate-only: "true"

정적인 Secret(변경이 거의 없는 Secret)을 사용하는 경우 Init Container Only 모드가 적합합니다. 동적인 Secret(자주 로테이션되는 Secret)을 사용하는 경우 기본 Sidecar 모드가 적합합니다.


8. 솔루션 선택 가이드

8.1 종합 비교

각 솔루션의 특성을 종합적으로 비교하면 다음과 같습니다.

설치 복잡도 측면에서 sops + age가 가장 간단하고 서버 컴포넌트가 필요 없습니다. Sealed Secrets, ESO, Vault Sidecar는 중간 정도의 복잡도를 가지며, CSI Driver가 가장 복잡합니다.

etcd 저장 측면에서 Sealed Secrets, sops + age, ESO는 모두 최종적으로 Kubernetes Secret을 생성하므로 etcd에 저장됩니다. CSI Driver와 Vault Sidecar는 기본적으로 etcd에 저장하지 않고 파일로만 제공합니다.

외부 SMS 연동 측면에서 Sealed Secrets와 sops + age는 자체 암호화를 사용하므로 외부 SMS가 필요 없습니다. ESO, CSI Driver, Vault Sidecar는 외부 SMS와 연동합니다.

자동 Secret 로테이션 측면에서 Sealed Secrets와 sops + age는 수동으로 재암호화하고 재배포해야 합니다. ESO는 refreshInterval로 자동 동기화하지만 Pod 재시작이 필요합니다. CSI Driver와 Vault Sidecar는 파일이 자동으로 업데이트되므로 Pod 재시작 없이 새 값을 사용할 수 있습니다.

클러스터 관리자 접근 측면에서 Sealed Secrets, sops + age, ESO로 생성된 Secret은 클러스터 관리자가 kubectl로 조회할 수 있습니다. CSI Driver와 Vault Sidecar는 파일로만 제공되므로 Pod 내부에서만 접근 가능합니다.

8.2 의사결정 플로우차트

flowchart TD
    START[Secret 관리 방식 선택] --> Q1{GitOps 환경인가?}

    Q1 -->|Yes| Q2{외부 SMS를<br/>이미 사용 중인가?}
    Q1 -->|No| Q5{클러스터 내<br/>Secret 저장 가능?}

    Q2 -->|Yes| ESO[External Secrets<br/>Operator]
    Q2 -->|No| Q3{추가 인프라<br/>운영 가능?}

    Q3 -->|Yes| SEALED[Sealed Secrets]
    Q3 -->|No| SOPS[sops + age]

    Q5 -->|Yes| Q6{외부 SMS<br/>사용?}
    Q5 -->|No| Q7{설정 복잡도<br/>감당 가능?}

    Q6 -->|Yes| ESO
    Q6 -->|No| SOPS

    Q7 -->|Yes| CSI[Secrets Store<br/>CSI Driver]
    Q7 -->|No| VAULT[Vault Sidecar<br/>Injector]

    style ESO fill:#e8f5e9
    style SOPS fill:#fff3e0
    style SEALED fill:#e3f2fd
    style CSI fill:#fce4ec
    style VAULT fill:#f3e5f5

8.3 시나리오별 권장

간단한 GitOps 시작 시나리오에서는 sops + age를 권장합니다. 추가 인프라 없이 5분 내에 설정을 완료할 수 있고, metadata가 평문으로 유지되어 Git에서 검색이 가능합니다.

AWS, Azure, GCP 클라우드 환경에서는 External Secrets Operator를 권장합니다. 이미 사용 중인 클라우드 SMS(AWS Secrets Manager, Azure Key Vault 등)와 자연스럽게 연동되며, refreshInterval로 자동 동기화가 가능합니다.

엄격한 보안 요구사항이 있는 환경에서는 Secrets Store CSI Driver를 권장합니다. etcd에 Secret이 저장되지 않으므로 클러스터 관리자도 Secret에 직접 접근할 수 없습니다. 금융, 의료 등 규제가 엄격한 산업에 적합합니다.

동적 Secret이 필요한 환경에서는 Vault Sidecar Injector를 권장합니다. Secret 로테이션 시 파일이 자동으로 업데이트되며, Pod 재시작 없이 새 값을 사용할 수 있습니다. 이미 HashiCorp Vault를 사용 중인 환경에서 자연스럽게 도입할 수 있습니다.

기존 Vault 인프라가 있는 환경에서는 CSI Driver 또는 Vault Sidecar를 권장합니다. 두 솔루션 모두 Vault와 잘 연동되며, 기존 인프라를 재사용할 수 있습니다.


9. 적용 포인트

9.1 etcd Encryption at Rest

어떤 솔루션을 사용하든 etcd에 Secret이 저장되는 경우(Sealed Secrets, sops, ESO) 암호화를 활성화해야 합니다.

자체 관리 클러스터에서는 EncryptionConfiguration을 설정합니다.

# /etc/kubernetes/enc/encryption-config.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
  - resources:
      - secrets
    providers:
      - aescbc:
          keys:
            - name: key1
              secret: <base64-encoded-32-byte-key>
      - identity: {}  # 기존 데이터 읽기용

관리형 쿠버네티스(AWS EKS, GCP GKE, Azure AKS)는 기본적으로 etcd 암호화가 활성화되어 있습니다. 추가로 고객 관리 키(CMK)를 사용하여 암호화 키를 직접 관리할 수도 있습니다.

9.2 Secret 로테이션 전략

각 솔루션별로 Secret 로테이션 방식이 다릅니다.

Sealed Secrets와 sops는 수동으로 새 SealedSecret 또는 암호화된 파일을 생성하고 재배포해야 합니다. Pod 재시작이 필요합니다.

ESO는 refreshInterval 설정으로 자동 동기화됩니다. 그러나 Kubernetes Secret이 업데이트되어도 Pod는 자동 재시작되지 않으므로 Reloader를 함께 사용해야 합니다.

CSI Driver와 Vault Sidecar는 rotationPollInterval 또는 Sidecar의 동기화 주기에 따라 파일이 자동 업데이트됩니다. 애플리케이션이 파일을 다시 읽으면 새 값을 사용할 수 있으므로 Pod 재시작이 불필요합니다. 다만 애플리케이션이 시작 시에만 파일을 읽는 경우에는 재시작이 필요할 수 있습니다.

9.3 RBAC 최소 권한 원칙

Secret에 대한 접근 권한은 최소화해야 합니다. 전체 Secret이 아닌 특정 Secret에만 접근을 허용하는 Role을 정의합니다.

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: secret-reader
rules:
  - apiGroups: [""]
    resources: ["secrets"]
    verbs: ["get"]
    resourceNames: ["specific-secret-name"]  # 특정 Secret만 허용

9.4 감사(Audit) 로깅

Secret 접근에 대한 감사 로깅을 활성화하여 보안 사고 발생 시 추적할 수 있어야 합니다.

apiVersion: audit.k8s.io/v1
kind: Policy
rules:
  - level: Metadata
    resources:
      - group: ""
        resources: ["secrets"]
    verbs: ["get", "list", "watch", "create", "update", "delete"]

10. 결론

10.1 요약

첫째, Kubernetes Secret은 기본적으로 안전하지 않습니다. Base64는 암호화가 아니며, etcd 암호화를 반드시 활성화해야 합니다.

둘째, GitOps 환경에서는 암호화된 형태로 Secret을 저장해야 합니다. Sealed Secrets, sops 중 상황에 맞는 솔루션을 선택합니다.

셋째, 외부 SMS를 이미 사용 중이라면 External Secrets Operator가 가장 효율적입니다. AWS Secrets Manager, Azure Key Vault 등과 자연스럽게 연동됩니다.

넷째, 클러스터 관리자를 신뢰할 수 없다면 CSI Driver 또는 Vault Sidecar를 사용해야 합니다. Secret이 etcd에 저장되지 않습니다.

다섯째, 어떤 솔루션을 선택하든 완벽한 보안은 없습니다. 이 패턴은 공격을 최대한 어렵게 만드는 추가적인 방어 계층입니다.


참고 자료

공식 문서

솔루션별 문서

클라우드 제공자별 Secret 관리