쿠버네티스 Secrets의 한계를 이해하고, HashiCorp Vault, External Secrets Operator, Sealed Secrets로 안전한 시크릿 관리 체계를 구축합니다.
쿠버네티스 Secret은 민감한 데이터를 저장하기 위한 기본 리소스입니다. 그러나 기본 설정에서는 여러 보안 한계가 있습니다.
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
namespace: production
type: Opaque
data:
username: YWRtaW4= # base64("admin")
password: cDRzc3cwcmQ= # base64("p4ssw0rd")echo "cDRzc3cwcmQ=" | base64 --decode
# 출력: p4ssw0rdBase64 인코딩은 암호화가 아닙니다. 누구든 디코딩할 수 있습니다. kubectl get secret -o yaml 명령으로 클러스터에 접근할 수 있는 사람이라면 모든 시크릿을 읽을 수 있습니다.
기본 설정에서 시크릿은 etcd에 평문(또는 Base64)으로 저장됩니다. etcd 백업 파일이 유출되면 모든 시크릿이 노출됩니다.
RBAC이 충분히 세밀하게 설정되지 않으면, 하나의 네임스페이스에 접근 권한을 가진 사용자가 해당 네임스페이스의 모든 시크릿을 읽을 수 있습니다.
쿠버네티스 기본 Secrets를 "안전한 시크릿 관리 도구"로 생각해서는 안 됩니다. 기본 Secrets는 민감 데이터와 일반 설정을 분리하는 논리적 구분일 뿐, 암호화나 접근 제어가 충분하지 않습니다.
가장 기본적인 조치로, etcd에 저장되는 시크릿을 **Encryption at Rest(저장 시 암호화)**로 보호할 수 있습니다.
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- aescbc:
keys:
- name: key1
secret: <base64-encoded-32-byte-key>
- identity: {} # 복호화 폴백이 설정을 API 서버에 적용하면 etcd에 저장되는 시크릿이 AES-CBC로 암호화됩니다. 그러나 이것만으로는 불충분합니다. 암호화 키 자체를 누가 관리하는가의 문제가 남습니다.
클라우드 환경에서는 KMS(Key Management Service) 제공자를 사용하는 것이 권장됩니다.
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- kms:
apiVersion: v2
name: aws-kms
endpoint: unix:///var/run/kmsplugin/socket.sock
- identity: {}HashiCorp Vault는 시크릿 관리의 사실상 표준입니다. 중앙화된 시크릿 저장소, 동적 시크릿 생성, 감사 로그, 세밀한 접근 제어를 제공합니다.
Vault는 쿠버네티스 서비스 계정을 기반으로 인증할 수 있습니다.
# 쿠버네티스 인증 백엔드 활성화
vault auth enable kubernetes
# 쿠버네티스 API 서버 연결 설정
vault write auth/kubernetes/config \
kubernetes_host="https://kubernetes.default.svc:443"
# 역할 생성 (특정 서비스 계정 + 네임스페이스에 바인딩)
vault write auth/kubernetes/role/webapp \
bound_service_account_names=webapp-sa \
bound_service_account_namespaces=production \
policies=webapp-policy \
ttl=1h# webapp 서비스가 접근할 수 있는 시크릿 경로
path "secret/data/production/webapp/*" {
capabilities = ["read"]
}
# 데이터베이스 동적 자격증명
path "database/creds/webapp-role" {
capabilities = ["read"]
}
# 다른 경로는 모두 차단 (암묵적 거부)Vault Agent Injector는 파드에 사이드카를 자동 주입하여 Vault에서 시크릿을 가져옵니다.
apiVersion: v1
kind: Pod
metadata:
name: webapp
annotations:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: "webapp"
vault.hashicorp.com/agent-inject-secret-db-creds: "secret/data/production/webapp/db"
vault.hashicorp.com/agent-inject-template-db-creds: |
{{- with secret "secret/data/production/webapp/db" -}}
export DB_HOST="{{ .Data.data.host }}"
export DB_USER="{{ .Data.data.username }}"
export DB_PASS="{{ .Data.data.password }}"
{{- end -}}
spec:
serviceAccountName: webapp-sa
containers:
- name: webapp
image: myapp:latest
command: ["/bin/sh", "-c", "source /vault/secrets/db-creds && ./start.sh"]Vault Agent가 사이드카로 주입되어 /vault/secrets/db-creds 파일에 시크릿을 렌더링합니다. 애플리케이션은 이 파일을 읽어 사용합니다.
Vault의 동적 시크릿(Dynamic Secrets) 기능을 사용하면, 데이터베이스 자격증명을 요청할 때마다 새로운 임시 계정이 생성되고 TTL 만료 시 자동 삭제됩니다. 자격증명 유출의 영향 범위를 최소화하는 강력한 기능입니다.
**External Secrets Operator(ESO)**는 외부 시크릿 관리 도구(Vault, AWS Secrets Manager, GCP Secret Manager 등)의 시크릿을 쿠버네티스 Secret으로 자동 동기화하는 오퍼레이터입니다.
helm repo add external-secrets https://charts.external-secrets.io
helm install external-secrets external-secrets/external-secrets \
--namespace external-secrets \
--create-namespaceapiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: aws-secret-store
namespace: production
spec:
provider:
aws:
service: SecretsManager
region: ap-northeast-2
auth:
jwt:
serviceAccountRef:
name: eso-service-accountapiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: db-credentials
namespace: production
spec:
refreshInterval: 5m
secretStoreRef:
name: aws-secret-store
kind: SecretStore
target:
name: db-credentials
creationPolicy: Owner
data:
- secretKey: username
remoteRef:
key: production/webapp/db
property: username
- secretKey: password
remoteRef:
key: production/webapp/db
property: passwordESO가 5분마다 AWS Secrets Manager에서 값을 가져와 쿠버네티스 Secret을 업데이트합니다. 외부에서 시크릿을 변경하면 자동으로 클러스터에 반영됩니다.
Sealed Secrets는 Bitnami에서 개발한 도구로, 시크릿을 암호화하여 Git 저장소에 안전하게 저장할 수 있게 합니다.
# 시크릿 매니페스트 생성
kubectl create secret generic db-creds \
--from-literal=username=admin \
--from-literal=password=s3cr3t \
--dry-run=client -o yaml > secret.yaml
# SealedSecret으로 암호화
kubeseal --format=yaml < secret.yaml > sealed-secret.yaml
# Git에 커밋 (암호화되어 안전)
git add sealed-secret.yaml
git commit -m "feat: DB 시크릿 추가"apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
name: db-creds
namespace: production
spec:
encryptedData:
username: AgBy3i4OJSWK+PiTySYZZA9r...
password: AgCtr85w2GOjP4sHLiS3D2O...SealedSecret 리소스는 암호화되어 있으므로 Git에 안전하게 저장할 수 있습니다. 클러스터의 Sealed Secrets Controller만이 이를 복호화할 수 있습니다.
GitOps 워크플로우를 사용하는 팀에게 Sealed Secrets는 가장 간단한 시크릿 관리 방법입니다. 별도의 외부 시크릿 관리 도구가 필요 없고, Git이 시크릿의 단일 진실 공급원(Single Source of Truth)이 됩니다.
Secrets Store CSI Driver는 시크릿을 파드의 볼륨으로 직접 마운트하는 CSI(Container Storage Interface) 드라이버입니다.
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: vault-db-creds
namespace: production
spec:
provider: vault
parameters:
vaultAddress: "https://vault.example.com:8200"
roleName: "webapp"
objects: |
- objectName: "db-username"
secretPath: "secret/data/production/webapp/db"
secretKey: "username"
- objectName: "db-password"
secretPath: "secret/data/production/webapp/db"
secretKey: "password"
secretObjects:
- secretName: db-creds-synced
type: Opaque
data:
- objectName: db-username
key: username
- objectName: db-password
key: passwordapiVersion: v1
kind: Pod
metadata:
name: webapp
spec:
containers:
- name: webapp
image: myapp:latest
volumeMounts:
- name: secrets
mountPath: /mnt/secrets
readOnly: true
volumes:
- name: secrets
csi:
driver: secrets-store.csi.k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: vault-db-creds| 항목 | 환경 변수 | 볼륨 마운트 |
|---|---|---|
| 접근 방식 | process.env.DB_PASS | 파일 읽기 |
| 로테이션 | 파드 재시작 필요 | 파일 변경으로 자동 반영 가능 |
| 메모리 노출 | /proc/[pid]/environ에 노출 | 파일시스템만 접근 가능 |
| 디버깅 | env 명령으로 확인 가능 | cat 명령으로 확인 가능 |
| 보안 수준 | 낮음 (프로세스 환경 노출) | 높음 (파일 권한 제어 가능) |
환경 변수로 주입된 시크릿은 /proc/[pid]/environ 파일을 통해 노출될 수 있고, 크래시 덤프나 로그에 포함될 위험이 있습니다. 가능하면 볼륨 마운트 방식을 사용하세요.
시크릿은 주기적으로 교체(로테이션)해야 합니다. 유출 시 피해를 최소화하기 위함입니다.
Vault의 동적 시크릿은 TTL이 만료되면 자동으로 새 자격증명이 생성됩니다. ESO는 refreshInterval에 따라 주기적으로 외부 시크릿을 동기화합니다.
애플리케이션 측에서는 시크릿 파일의 변경을 감지하여 자동으로 연결을 갱신하는 로직이 필요합니다.
package main
import (
"os"
"time"
"log"
)
func watchSecret(path string, onChange func([]byte)) {
var lastMod time.Time
for {
info, err := os.Stat(path)
if err != nil {
log.Printf("시크릿 파일 확인 실패: %v", err)
time.Sleep(5 * time.Second)
continue
}
if info.ModTime().After(lastMod) {
data, _ := os.ReadFile(path)
onChange(data)
lastMod = info.ModTime()
}
time.Sleep(5 * time.Second)
}
}이번 장에서는 컨테이너 환경의 시크릿 관리를 다루었습니다.
다음 장에서는 이 시리즈의 핵심 주제인 공급망 공격 방어와 제로 트러스트를 다룹니다. 의존성 혼동, 타이포스쿼팅 등 공급망 공격 유형과 SLSA 프레임워크 기반의 방어 전략을 실습합니다.
이 글이 도움이 되셨나요?
의존성 혼동, 타이포스쿼팅, CI 침투 등 공급망 공격 유형을 분석하고, SLSA 프레임워크와 어드미션 컨트롤러 기반의 제로 트러스트 방어 전략을 구축합니다.
쿠버네티스 NetworkPolicy로 기본 거부 정책을 구현하고, Calico/Cilium 네트워크 정책과 Istio mTLS로 컨테이너 간 통신을 안전하게 제어합니다.
빌드, 스캔, 서명, 배포, 런타임 모니터링까지 전체 컨테이너 보안 파이프라인을 GitHub Actions와 쿠버네티스 기반으로 통합 구축하는 실전 프로젝트입니다.