K8s Secret의 한계
Kubernetes Secret은 민감 데이터를 관리하는 기본 리소스이지만, 실제로는 Base64 인코딩일 뿐 암호화가 아닙니다. etcd에 평문으로 저장되고, RBAC만으로 접근을 통제합니다. 프로덕션에서는 AWS Secrets Manager, HashiCorp Vault 같은 외부 비밀 저장소와 연동하는 것이 보안 표준입니다. External Secrets Operator(ESO)는 이 연동을 자동화하는 K8s 네이티브 솔루션입니다.
기본 Secret 유형과 생성
K8s Secret의 기본 유형을 먼저 이해해야 합니다.
| 타입 | 용도 | 자동 생성 |
|---|---|---|
Opaque |
범용 키-값 | 아니오 |
kubernetes.io/tls |
TLS 인증서 | cert-manager |
kubernetes.io/dockerconfigjson |
레지스트리 인증 | 아니오 |
kubernetes.io/service-account-token |
SA 토큰 | 예 |
# Opaque Secret 생성
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
namespace: production
type: Opaque
stringData: # stringData는 평문으로 작성 가능
DB_HOST: postgres.internal
DB_USER: app_user
DB_PASSWORD: "s3cret!Pa$$w0rd"
---
# TLS Secret
apiVersion: v1
kind: Secret
metadata:
name: tls-secret
type: kubernetes.io/tls
data:
tls.crt: LS0tLS1CRU... # Base64 인코딩
tls.key: LS0tLS1CRU...
stringData는 평문으로 작성하면 K8s가 자동으로 Base64 인코딩합니다. 하지만 이 YAML을 Git에 커밋하면 비밀이 노출됩니다. 이것이 외부 비밀 관리가 필요한 근본 이유입니다.
etcd 암호화 설정
Secret을 etcd에 저장할 때 암호화하는 기본 보안 조치입니다.
# /etc/kubernetes/encryption-config.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- aescbc:
keys:
- name: key1
secret: dGhpcyBpcyBhIDMyIGJ5dGUga2V5... # 32바이트 Base64 키
- identity: {} # 폴백: 암호화 없이 읽기
# kube-apiserver 설정에 추가
# --encryption-provider-config=/etc/kubernetes/encryption-config.yaml
설정 후 기존 Secret을 다시 쓰기해야 암호화가 적용됩니다:
kubectl get secrets -A -o json | kubectl replace -f -
External Secrets Operator 아키텍처
ESO는 외부 비밀 저장소의 값을 K8s Secret으로 자동 동기화하는 오퍼레이터입니다.
# 1. 설치
helm install external-secrets external-secrets/external-secrets
-n external-secrets --create-namespace
# 2. SecretStore 정의 (AWS Secrets Manager 예시)
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: aws-secretsmanager
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
# 3. 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"
SecretStore는 네임스페이스 범위, ClusterSecretStore는 클러스터 전역입니다. 프로덕션에서는 환경별로 SecretStore를 분리하고, ClusterSecretStore는 공통 인프라 비밀에 사용합니다.
ExternalSecret으로 동기화
ExternalSecret 리소스가 외부 저장소의 값을 K8s Secret으로 매핑합니다.
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: db-credentials
namespace: production
spec:
refreshInterval: 1h # 동기화 주기
secretStoreRef:
name: aws-secretsmanager
kind: SecretStore
target:
name: db-credentials # 생성될 K8s Secret 이름
creationPolicy: Owner # ExternalSecret 삭제 시 Secret도 삭제
template: # Secret 데이터 커스텀 포맷
type: Opaque
data:
DATABASE_URL: "postgresql://{{ .DB_USER }}:{{ .DB_PASSWORD }}@{{ .DB_HOST }}:5432/{{ .DB_NAME }}"
data:
- secretKey: DB_USER
remoteRef:
key: production/database # AWS Secrets Manager 키
property: username # JSON 내 필드
- secretKey: DB_PASSWORD
remoteRef:
key: production/database
property: password
- secretKey: DB_HOST
remoteRef:
key: production/database
property: host
- secretKey: DB_NAME
remoteRef:
key: production/database
property: dbname
target.template을 사용하면 여러 비밀 값을 조합하여 DATABASE_URL 같은 커넥션 스트링을 자동 생성할 수 있습니다. Go 템플릿 문법을 지원합니다.
Pod에서 Secret 사용 패턴
생성된 Secret을 Pod에서 사용하는 방법입니다.
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-server
spec:
template:
spec:
containers:
- name: api
image: api-server:latest
# 패턴 1: 환경 변수로 주입
envFrom:
- secretRef:
name: db-credentials
# 패턴 2: 개별 키 매핑
env:
- name: JWT_SECRET
valueFrom:
secretKeyRef:
name: app-secrets
key: jwt-secret
# 패턴 3: 볼륨 마운트 (파일로)
volumeMounts:
- name: tls-certs
mountPath: /etc/tls
readOnly: true
volumes:
- name: tls-certs
secret:
secretName: tls-secret
defaultMode: 0400 # 읽기 전용 권한
볼륨 마운트 방식의 장점: kubelet이 Secret 변경을 감지하면 파일을 자동 업데이트합니다(기본 60초 주기). 환경 변수 방식은 Pod 재시작 없이는 갱신되지 않습니다. 인증서 로테이션이 필요한 경우 볼륨 마운트가 필수입니다.
Sealed Secrets: GitOps 친화적 암호화
Secret을 Git에 안전하게 커밋하려면 Sealed Secrets를 사용합니다.
# 설치
helm install sealed-secrets sealed-secrets/sealed-secrets
-n kube-system
# Secret을 SealedSecret으로 암호화
kubeseal --format yaml
--controller-name=sealed-secrets
--controller-namespace=kube-system
< db-secret.yaml > db-sealed-secret.yaml
# 결과: Git에 안전하게 커밋 가능
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
name: db-credentials
namespace: production
spec:
encryptedData:
DB_PASSWORD: AgBy3i4OJSWK+PiTySYZZA9rO... # 공개키로 암호화됨
DB_USER: AgCtr8HNQO+GMFj2zBNSGI...
SealedSecret은 컨트롤러의 공개키로 암호화되어 해당 클러스터에서만 복호화됩니다. K8s FluxCD GitOps와 결합하면 비밀 관리까지 완전한 GitOps 파이프라인을 구축할 수 있습니다.
Secret 로테이션 자동화
# ESO 자동 로테이션: refreshInterval로 주기적 동기화
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
spec:
refreshInterval: 15m # 15분마다 외부 저장소 확인
# Stakater Reloader로 Secret 변경 시 Pod 자동 재시작
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
reloader.stakater.com/auto: "true" # Secret 변경 감지 시 롤링 재시작
Stakater Reloader는 Secret/ConfigMap 변경을 감지하여 관련 Deployment를 자동 롤링 재시작합니다. ESO의 refreshInterval과 함께 사용하면 비밀 로테이션이 완전 자동화됩니다. K8s ServiceAccount 토큰 보안과 결합하면 인증 체계 전체를 자동 로테이션할 수 있습니다.
보안 체크리스트
| 항목 | 조치 |
|---|---|
| etcd 암호화 | EncryptionConfiguration으로 aescbc/aesgcm 적용 |
| RBAC | Secret 접근 최소 권한, get/list 분리 |
| Git 노출 방지 | SealedSecrets 또는 ESO 사용 |
| 감사 로그 | Secret 접근 audit logging 활성화 |
| 로테이션 | ESO refreshInterval + Reloader 자동화 |
| 네트워크 | NetworkPolicy로 Secret 접근 Pod 제한 |
정리
K8s Secret은 Base64 인코딩일 뿐 암호화가 아닙니다. 프로덕션에서는 etcd 암호화를 기본 적용하고, External Secrets Operator로 AWS Secrets Manager나 Vault의 비밀을 자동 동기화하세요. GitOps 환경에서는 Sealed Secrets로 암호화된 비밀을 안전하게 커밋하고, Stakater Reloader로 비밀 변경 시 Pod를 자동 재시작하는 것이 운영 표준입니다.