근묵자흑
Kubernetes Pattern: Secure Configuration 본문
쿠버네티스에서 실행되는 애플리케이션은 데이터베이스 연결 정보, 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에 저장되지 않습니다.
다섯째, 어떤 솔루션을 선택하든 완벽한 보안은 없습니다. 이 패턴은 공격을 최대한 어렵게 만드는 추가적인 방어 계층입니다.
참고 자료
공식 문서
- Kubernetes Secrets Best Practices: https://kubernetes.io/docs/concepts/security/secrets-good-practices/
- Kubernetes Encrypting Confidential Data at Rest: https://kubernetes.io/docs/tasks/administer-cluster/encrypt-data/
솔루션별 문서
- Sealed Secrets GitHub: https://github.com/bitnami-labs/sealed-secrets
- sops GitHub: https://github.com/getsops/sops
- External Secrets Operator: https://external-secrets.io/
- Secrets Store CSI Driver: https://secrets-store-csi-driver.sigs.k8s.io/
- HashiCorp Vault Kubernetes Integration: https://developer.hashicorp.com/vault/docs/platform/k8s
클라우드 제공자별 Secret 관리
- AWS Secrets Manager: https://docs.aws.amazon.com/secretsmanager/
- Azure Key Vault: https://learn.microsoft.com/azure/key-vault/
- GCP Secret Manager: https://cloud.google.com/secret-manager/docs
'k8s > kubernetes-pattern' 카테고리의 다른 글
| Kubernetes Pattern: Controller (0) | 2026.01.31 |
|---|---|
| Kubernetes Pattern: Access Control (0) | 2026.01.24 |
| Kubernetes Pattern: Network Segmentation (0) | 2026.01.10 |
| Kubernetes Pattern: Process Containment (0) | 2026.01.03 |
| Kubernetes Pattern: Configuration Template (0) | 2025.12.27 |