K8s 시크릿 관리의 문제
Kubernetes Secret은 Base64 인코딩일 뿐 암호화가 아닙니다. Git에 Secret 매니페스트를 커밋하면 민감 정보가 평문으로 노출됩니다. ArgoCD GitOps 환경에서는 이 문제가 더 심각합니다. 이 글에서는 Sealed Secrets, External Secrets Operator(ESO), SOPS 3가지 시크릿 관리 도구의 아키텍처, 설정, 실전 패턴을 비교합니다.
3가지 도구 비교
| 구분 | Sealed Secrets | External Secrets (ESO) | SOPS + Age |
|---|---|---|---|
| 원리 | 클러스터 키로 암호화 | 외부 저장소에서 런타임 동기화 | 파일 단위 암호화 |
| 시크릿 저장 위치 | Git (암호화됨) | AWS SM, Vault, GCP SM 등 | Git (암호화됨) |
| 외부 의존성 | 없음 (클러스터 자체) | 외부 시크릿 매니저 필수 | 없음 |
| 자동 로테이션 | 수동 | 자동 (refreshInterval) | 수동 |
| 복잡도 | 낮음 | 중간 | 낮음 |
| 적합 환경 | 소규모, 단일 클러스터 | 엔터프라이즈, 멀티 클러스터 | 소규모, Helm/Kustomize |
Sealed Secrets
Bitnami의 Sealed Secrets는 클러스터에 설치된 컨트롤러의 공개키로 시크릿을 암호화합니다. 암호화된 SealedSecret 리소스를 Git에 안전하게 커밋할 수 있습니다.
# 설치
helm repo add sealed-secrets https://bitnami-labs.github.io/sealed-secrets
helm install sealed-secrets sealed-secrets/sealed-secrets
--namespace kube-system
# kubeseal CLI 설치
brew install kubeseal # macOS
# 또는
wget https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.25.0/kubeseal-0.25.0-linux-amd64.tar.gz
# 1. 일반 Secret 생성 (적용하지 않음)
kubectl create secret generic db-credentials
--namespace production
--from-literal=username=admin
--from-literal=password='S3cur3P@ss!'
--dry-run=client -o yaml > secret.yaml
# 2. SealedSecret으로 암호화
kubeseal --format yaml
--controller-name sealed-secrets
--controller-namespace kube-system
< secret.yaml > sealed-secret.yaml
# 3. 결과: Git에 커밋 가능한 암호화된 매니페스트
cat sealed-secret.yaml
# sealed-secret.yaml — Git에 커밋 가능
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
name: db-credentials
namespace: production
spec:
encryptedData:
username: AgBY7k...암호화된_데이터...==
password: AgCx9m...암호화된_데이터...==
template:
metadata:
name: db-credentials
namespace: production
type: Opaque
# 클러스터에 적용하면 컨트롤러가 복호화 → 일반 Secret 생성
kubectl apply -f sealed-secret.yaml
kubectl get secret db-credentials -n production # 자동 생성됨
Sealed Secrets 키 백업
# 키 분실 = 모든 SealedSecret 복호화 불가 → 반드시 백업
kubectl get secret -n kube-system
-l sealedsecrets.bitnami.com/sealed-secrets-key
-o yaml > sealed-secrets-master-key.yaml
# 키 복원 (클러스터 재구축 시)
kubectl apply -f sealed-secrets-master-key.yaml
kubectl delete pod -n kube-system -l name=sealed-secrets-controller
# 스코프 설정: strict (기본) vs namespace-wide vs cluster-wide
kubeseal --scope strict # 같은 이름+네임스페이스에서만 복호화
kubeseal --scope namespace-wide # 같은 네임스페이스의 다른 이름도 가능
kubeseal --scope cluster-wide # 클러스터 어디서든 복호화
External Secrets Operator (ESO)
ESO는 AWS Secrets Manager, HashiCorp Vault, GCP Secret Manager 등 외부 시크릿 저장소에서 값을 가져와 K8s Secret을 자동 생성·동기화합니다.
# 설치
helm repo add external-secrets https://charts.external-secrets.io
helm install external-secrets external-secrets/external-secrets
--namespace external-secrets
--create-namespace
# 1단계: SecretStore — 외부 저장소 연결 정보
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: aws-secrets-manager
namespace: production
spec:
provider:
aws:
service: SecretsManager
region: ap-northeast-2
auth:
secretRef:
accessKeyIDSecretRef:
name: aws-credentials
key: access-key-id
secretAccessKeySecretRef:
name: aws-credentials
key: secret-access-key
# ClusterSecretStore: 모든 네임스페이스에서 공유
---
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
name: vault-store
spec:
provider:
vault:
server: "https://vault.internal:8200"
path: "secret"
version: "v2"
auth:
kubernetes:
mountPath: "kubernetes"
role: "external-secrets"
# 2단계: ExternalSecret — 어떤 시크릿을 가져올지 정의
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: db-credentials
namespace: production
spec:
refreshInterval: 1h # 자동 동기화 주기
secretStoreRef:
name: aws-secrets-manager
kind: SecretStore
target:
name: db-credentials # 생성될 K8s Secret 이름
creationPolicy: Owner # ExternalSecret 삭제 시 Secret도 삭제
deletionPolicy: Retain # ExternalSecret 삭제 시 Secret 유지
# 개별 키 매핑
data:
- secretKey: username # K8s Secret의 키
remoteRef:
key: production/db-creds # AWS SM의 시크릿 이름
property: username # JSON 내 필드
- secretKey: password
remoteRef:
key: production/db-creds
property: password
# 또는 전체 JSON을 한 번에 매핑
dataFrom:
- extract:
key: production/db-creds # JSON 전체를 K8s Secret 키로 매핑
ESO 템플릿과 변환
# 커넥션 스트링 조합 등 템플릿 활용
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: app-secrets
namespace: production
spec:
refreshInterval: 30m
secretStoreRef:
name: aws-secrets-manager
kind: SecretStore
target:
name: app-secrets
template:
engineVersion: v2
data:
# 여러 시크릿 값을 조합하여 커넥션 스트링 생성
DATABASE_URL: "postgresql://{{ .username }}:{{ .password }}@{{ .host }}:5432/{{ .dbname }}"
REDIS_URL: "redis://:{{ .redis_password }}@redis:6379"
data:
- secretKey: username
remoteRef:
key: production/db-creds
property: username
- secretKey: password
remoteRef:
key: production/db-creds
property: password
- secretKey: host
remoteRef:
key: production/db-creds
property: host
- secretKey: dbname
remoteRef:
key: production/db-creds
property: dbname
- secretKey: redis_password
remoteRef:
key: production/redis
property: password
SOPS + Age 암호화
Mozilla SOPS는 YAML/JSON 파일의 값만 선택적으로 암호화합니다. Age(현대적 암호화 도구)와 조합하면 가볍게 시크릿을 관리할 수 있습니다.
# Age 키 생성
age-keygen -o age-key.txt
# Public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
# .sops.yaml — 프로젝트 루트에 규칙 정의
creation_rules:
- path_regex: secrets/production/.*.yaml$
age: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
- path_regex: secrets/staging/.*.yaml$
age: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
# 시크릿 파일 작성
cat > secrets/production/db.yaml << 'EOF'
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
namespace: production
type: Opaque
stringData:
username: admin
password: S3cur3P@ss!
host: db.internal
EOF
# SOPS로 암호화
sops --encrypt --in-place secrets/production/db.yaml
# 암호화된 파일 — Git에 안전하게 커밋
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
namespace: production
type: Opaque
stringData:
username: ENC[AES256_GCM,data:8dK3nQ==,iv:...,tag:...,type:str]
password: ENC[AES256_GCM,data:Vk9x2mPqRw==,iv:...,tag:...,type:str]
host: ENC[AES256_GCM,data:ZGIuaW50ZXJuYWw=,iv:...,tag:...,type:str]
sops:
age:
- recipient: age1ql3z7hjy54...
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
...
-----END AGE ENCRYPTED FILE-----
# 복호화하여 적용
sops --decrypt secrets/production/db.yaml | kubectl apply -f -
# 편집 (자동 복호화 → 편집 → 재암호화)
sops secrets/production/db.yaml
# ArgoCD + SOPS 연동: ksops 플러그인
# ArgoCD repo-server에 SOPS 복호화 플러그인 설치 필요
ArgoCD 연동 패턴
각 도구의 GitOps 워크플로우에서의 역할입니다.
# Sealed Secrets + ArgoCD: 가장 간단
# SealedSecret 매니페스트를 Git에 커밋 → ArgoCD가 적용 → 컨트롤러가 복호화
# 추가 설정 불필요
# External Secrets + ArgoCD: 권장 패턴
# ExternalSecret 매니페스트를 Git에 커밋 (시크릿 값 없음)
# ArgoCD가 ExternalSecret 적용 → ESO가 외부 저장소에서 동기화
# health check 설정 추가 권장:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-app
spec:
# ... source, destination ...
ignoreDifferences:
- group: ""
kind: Secret
jsonPointers:
- /data # ESO가 관리하는 Secret의 data 변경 무시
# SOPS + ArgoCD: ksops 플러그인 필요
# ArgoCD repo-server ConfigMap에 플러그인 등록
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-cm
namespace: argocd
data:
kustomize.buildOptions: "--enable-alpha-plugins"
시크릿 로테이션 전략
K8s 운영에서 시크릿 로테이션은 보안의 핵심입니다.
# ESO: refreshInterval로 자동 로테이션
spec:
refreshInterval: 15m # 15분마다 외부 저장소와 동기화
# AWS SM에서 값을 변경하면 15분 내 K8s Secret에 반영
# Pod가 시크릿 변경을 감지하려면:
# 1. Volume mount: kubelet이 주기적으로 갱신 (기본 1분)
# 2. 환경 변수: Pod 재시작 필요 → Reloader 사용
# stakater/Reloader: Secret 변경 시 자동 롤아웃
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
reloader.stakater.com/auto: "true" # 참조하는 Secret 변경 시 자동 재시작
spec:
template:
spec:
containers:
- name: app
envFrom:
- secretRef:
name: db-credentials
선택 가이드
- 소규모 팀, 단일 클러스터: Sealed Secrets — 설치 간단, 외부 의존성 없음
- 이미 AWS SM/Vault 사용 중: External Secrets Operator — 기존 인프라 활용, 자동 로테이션
- Helm/Kustomize 중심: SOPS + Age — 기존 워크플로우에 자연스럽게 통합
- 엔터프라이즈, 멀티 클러스터: ESO + Vault — 중앙 집중 관리, 감사 로그, 동적 시크릿
정리
K8s Secret을 Git에 평문으로 커밋하는 것은 보안 사고의 시작입니다. Sealed Secrets로 클러스터 키 암호화, ESO로 외부 저장소 동기화, SOPS로 파일 단위 암호화 중 팀의 규모와 인프라에 맞는 도구를 선택하세요. GitOps 환경에서는 ESO가 자동 로테이션과 중앙 관리 면에서 가장 유연하지만, 간단한 프로젝트에서는 Sealed Secrets만으로도 충분합니다. 중요한 것은 어떤 도구든 사용하는 것입니다.