Secure Configuration 패턴은 Kubernetes에서 자격 증명(credentials)을 최대한 안전하게 유지하기 위한 모범 사례를 다룹니다.
어떤 실제 애플리케이션도 고립되어 존재하지 않습니다. 클라우드 제공업체의 부가 서비스, 다른 마이크로서비스, 데이터베이스 등 외부 시스템과 연결해야 합니다.
이러한 연결에는 대부분 인증(Authentication)이 필요하며, 사용자 이름과 비밀번호 또는 보안 토큰과 같은 자격 증명을 전송해야 합니다.
이 민감한 정보는 애플리케이션 가까이에 안전하게(securely) 저장되어야 합니다.
"Kubernetes Secret은 정말 안전한가? 어떤 상황에서 문제가 되는가?"
20장 "Configuration Resource"에서 배웠듯이, Secret 리소스는 이름과 달리 암호화되지 않고 Base64 인코딩만 적용됩니다.
Kubernetes는 Secret 내용에 대한 접근을 제한하기 위해 최선을 다하지만,
여전히 여러 보안 과제가 존재합니다.

| 과제 | 설명 | 위험도 |
|---|---|---|
| GitOps 환경 | Secret을 원격 Git 저장소에 저장해야 하는가? 저장한다면 암호화 없이 저장할 수 없음 | [높음] |
| 복호화 시점 | Git에 암호화되어 커밋된 경우, 클러스터로 들어가는 과정에서 어디서 복호화되는가? | [중간] |
| 클러스터 관리자 신뢰 | 클러스터 내부에 암호화되어 저장되어도, 클러스터 관리자는 모든 데이터에 접근 가능 | [높음] |
| 멀티테넌시 | 여러 팀이 같은 클러스터를 사용할 때 Secret 격리 문제 | [중간] |
RBAC 규칙으로 Kubernetes 리소스에 대한 접근을 세밀하게 제어할 수 있습니다.
하지만 최소한 한 명은 클러스터의 모든 데이터에 접근할 수 있습니다
바로 클러스터 관리자입니다.

실제 예시: 금융 서비스 회사에서 AWS EKS를 사용하는 경우, AWS 계정 관리자와 Kubernetes 클러스터 관리자 모두 민감한 데이터베이스 자격 증명에 접근할 수 있습니다.
PCI-DSS 규정 준수를 위해서는 이러한 접근을 최소화해야 합니다.
"Secret을 애플리케이션 코드 변경 없이 안전하게 관리하려면 어떤 접근 방식이 있는가?"
가장 단순한 해결책은 애플리케이션 내부에서 직접 암호화된 정보를 복호화하는 것입니다.
하지만 이 방식은 비즈니스 로직과 보안 로직을 결합시키는 문제가 있습니다.
Kubernetes에서는 더 투명한 방법들이 있습니다.

| 접근 방식 | 설명 | 결과물 |
|---|---|---|
| Out-of-Cluster Encryption | Kubernetes 외부에 암호화된 설정 정보를 저장하고, 클러스터 진입 시점 또는 Operator에 의해 Secret으로 변환 | Kubernetes Secret 생성 |
| Centralized Secret Management | 클라우드 제공업체(AWS, Azure, GCP) 또는 자체 Vault 서비스를 활용하여 민감한 데이터 저장 | 다양한 방식으로 Pod에 전달 |
2026년 현재, Secret 관리의 정의가 바뀌고 있습니다.
단순히 데이터베이스 비밀번호를 암호화하는 것이 아니라,
멀티 클라우드 환경에서 수천 개의 임시 자격 증명 라이프사이클을 관리하는 것으로 확장되었습니다.
| 관점 | 과거 (2020-2023) | 현재 (2024-2026) |
|---|---|---|
| 자격 증명 수명 | Static Secrets (장기 자격 증명) | Dynamic Secrets / JIT Access (단기, 필요 시 발급) |
| 인증 질문 | "What is the secret?" | "Who is asking?" (Identity-Based Access) |
| 표준 도구 | 다양한 도구 혼재 | External Secrets Operator가 사실상 표준 |
| SOPS 활용 | CLI 도구만 | sops-secrets-operator CRD 지원 |

핵심 변화: 애플리케이션이 Kubernetes ServiceAccount 또는 클라우드 IAM Identity로 인증하면, SMS가 해당 워크로드에 맞는 최소 권한의 단기 자격 증명을 발급합니다.
[요약]
- 핵심 아이디어: 암호화된 Secret → Git 저장 → 클러스터에서 복호화 → Kubernetes Secret 생성
- 적용 대상: GitOps 환경, CI/CD 파이프라인, 소스 코드와 Secret 함께 관리
- 대표 도구: Sealed Secrets, External Secrets Operator, SOPS
클러스터 외부에서 비밀 데이터를 가져와 Kubernetes Secret으로 변환하는 방식입니다.
2026년 기준 가장 널리 사용되는 세 가지 프로젝트를 살펴보겠습니다.
Sealed Secrets는 Bitnami에서 2017년에 소개한 Kubernetes 애드온으로,
암호화된 데이터를 CRD(CustomResourceDefinition) SealedSecret에 저장합니다.
백그라운드에서 Operator가 이러한 리소스를 모니터링하고,
각 SealedSecret에 대해 복호화된 내용으로 Kubernetes Secret을 생성합니다.

Secret은 AES-256-GCM으로 대칭 암호화되어 세션 키로 사용되고,
세션 키는 RSA-OAEP로 비대칭 암호화됩니다.
이는 TLS가 사용하는 것과 동일한 설정입니다.
┌─────────────────────────────────────────────────────────┐
│ 암호화 계층 구조 │
├─────────────────────────────────────────────────────────┤
│ Secret Data ──▶ AES-256-GCM (세션키) ──▶ 암호문 │
│ Session Key ──▶ RSA-OAEP (공개키) ──▶ 암호화된 키 │
└─────────────────────────────────────────────────────────┘
SealedSecret을 생성할 때 선택할 수 있는 세 가지 범위가 있습니다
| Scope | 설명 | 사용 사례 |
|---|---|---|
| Strict (기본값) | namespace와 name이 고정됨. 원본 Secret과 동일한 namespace, 동일한 name으로만 생성 가능 | 단일 환경, 엄격한 보안 요구 |
| Namespace-wide | 동일 namespace 내에서 다른 name으로 적용 가능 | 같은 namespace에서 여러 배포 |
| Cluster-wide | namespace와 name 모두 변경 가능 | 멀티 환경 배포, 재사용 |
| Annotation | Value | 설명 |
|---|---|---|
sealedsecrets.bitnami.com/namespace-wide | "true" | namespace-wide scope 활성화 |
sealedsecrets.bitnami.com/cluster-wide | "true" | cluster-wide scope 활성화 |
Step 1. 원본 Secret 생성
# mysecret.yaml
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
namespace: production
type: Opaque
data:
username: YWRtaW4= # admin
password: czNjcjN0cEBzcw== # s3cr3tp@ss
Step 2. kubeseal로 암호화
# cluster-wide scope로 SealedSecret 생성
kubeseal --scope cluster-wide -f mysecret.yaml -o yaml > sealedsecret.yaml
Step 3. 생성된 SealedSecret
# sealedsecret.yaml - Git에 안전하게 저장 가능
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
annotations:
sealedsecrets.bitnami.com/cluster-wide: "true" # (1) cluster-wide scope
name: db-credentials
spec:
encryptedData:
password: AgCrKIF2gA7tSR/gqw+FH6cEV...wPWWkHJbo= # (2) 암호화된 값
username: AgAmvgFQBBNPlt9Gmx...0DNHJpDIMUGgwaQroXT+o=
cluster-wide annotation으로 어떤 namespace, 어떤 name으로도 적용 가능Step 4. 클러스터에 적용
kubectl apply -f sealedsecret.yaml
# Operator가 자동으로 Secret 생성
kubectl get secrets
# NAME TYPE DATA AGE
# db-credentials Opaque 2 5s
시나리오: DevOps 팀이 GitOps를 사용하여 여러 환경(dev, staging, prod)에 동일한 애플리케이션을 배포합니다.
# 1. 각 환경별 Secret 준비
cat > dev-secret.yaml << EOF
apiVersion: v1
kind: Secret
metadata:
name: api-keys
namespace: dev
data:
API_KEY: ZGV2LWFwaS1rZXk=
EOF
# 2. namespace-wide scope로 암호화 (같은 namespace 내 다른 이름 허용)
kubeseal --scope namespace-wide \\
--controller-namespace kube-system \\
-f dev-secret.yaml > dev-sealedsecret.yaml
# 3. Git에 커밋
git add dev-sealedsecret.yaml
git commit -m "Add encrypted API keys for dev environment"
git push
# Private Key 백업
kubectl get secret -n kube-system -l sealedsecrets.bitnami.com/sealed-secrets-key \\
-o yaml > sealed-secrets-master-key.yaml
External Secrets Operator는 다양한 외부 SMS(Secret Management System)와 통합되는 Kubernetes Operator입니다.
Sealed Secrets와의 핵심 차이점은 암호화된 데이터 저장소를 직접 관리하지 않고 외부 SMS에 의존한다는 점입니다.
이를 통해 클라우드 SMS의 모든 기능(키 자동 교체, 전용 UI, 감사 로그 등)을 활용할 수 있습니다.

| CRD | 역할 | 관리 주체 |
|---|---|---|
| SecretStore | 외부 SMS 연결 설정 (타입, 인증 정보) | Platform/Security 팀 |
| ExternalSecret | 어떤 Secret을 가져올지 정의, SecretStore 참조 | Application 팀 |
Step 1. SecretStore 설정
# secret-store-aws.yaml
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: secret-store-aws
namespace: production
spec:
provider:
aws: # (1) AWS Secrets Manager 사용
service: SecretsManager
region: us-east-1
auth:
secretRef:
accessKeyIDSecretRef: # (2) AWS 접근 키 참조
name: awssm-secret
key: access-key
secretAccessKeySecretRef:
name: awssm-secret
key: secret-access-key
Step 2. ExternalSecret 정의
# external-secret-db.yaml
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: db-credentials
namespace: production
spec:
refreshInterval: 1h # (1) 1시간마다 동기화
secretStoreRef: # (2) SecretStore 참조
name: secret-store-aws
kind: SecretStore
target:
name: db-credentials-secrets # (3) 생성될 Secret 이름
creationPolicy: Owner
data:
- secretKey: username # Secret의 key 이름
remoteRef:
key: cluster/db-username # (4) AWS Secrets Manager의 경로
- secretKey: password
remoteRef:
key: cluster/db-password
Step 3. 결과 확인
# ExternalSecret 상태 확인
kubectl get externalsecret db-credentials -n production
# NAME STORE REFRESH INTERVAL STATUS
# db-credentials secret-store-aws 1h SecretSynced
# 생성된 Secret 확인
kubectl get secret db-credentials-secrets -n production -o yaml
| 프로바이더 | 서비스 |
|---|---|
| AWS | Secrets Manager, Parameter Store |
| Azure | Key Vault |
| GCP | Secret Manager |
| HashiCorp | Vault |
| IBM | Secrets Manager |
| Oracle | Vault |
| Kubernetes | 다른 클러스터의 Secret |
시나리오: 마이크로서비스 아키텍처에서 각 서비스가 필요한 Secret만 접근하도록 제한합니다.
# ClusterSecretStore - 클러스터 전체에서 공유
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
name: vault-backend
spec:
provider:
vault:
server: "<https://vault.company.com:8200>"
path: "secret"
version: "v2"
auth:
kubernetes:
mountPath: "kubernetes"
role: "external-secrets"
---
# 각 팀은 자신의 namespace에서 ExternalSecret만 관리
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: payment-service-secrets
namespace: payments
spec:
refreshInterval: 30m
secretStoreRef:
name: vault-backend
kind: ClusterSecretStore
target:
name: payment-credentials
data:
- secretKey: STRIPE_API_KEY
remoteRef:
key: payments/stripe
property: api_key
- secretKey: STRIPE_WEBHOOK_SECRET
remoteRef:
key: payments/stripe
property: webhook_secret
SOPS는 Mozilla에서 개발한 순수 클라이언트 사이드 솔루션입니다.
Kubernetes에 특화되지 않았지만, YAML이나 JSON 파일을 암호화하고 복호화하여
소스 코드 저장소에 안전하게 저장할 수 있습니다.
값(value)만 암호화하고 키(key)는 그대로 유지하는 것이 특징입니다.
SOPS는 다양한 암호화 방식을 지원합니다:
| 방식 | 설명 | 사용 사례 |
|---|---|---|
| age | 로컬에 저장된 키로 비대칭 암호화 | 개인/소규모 팀 |
| AWS KMS | AWS의 키 관리 서비스 | AWS 환경 |
| GCP KMS | Google의 키 관리 서비스 | GCP 환경 |
| Azure Key Vault | Azure의 키 관리 서비스 | Azure 환경 |
| HashiCorp Vault | 자체 호스팅 가능한 SMS | 온프레미스/하이브리드 |

| 구분 | SMS (Secret Management System) | KMS (Key Management System) |
|---|---|---|
| 역할 | Secret을 저장하고 관리하는 서비스 | 암호화 키를 관리하는 서비스 |
| 기능 | 저장, 접근 제어, 자동 암호화, UI 제공 | 키 생성, 저장, 교체, 접근 제어 |
| 데이터 | 실제 민감한 데이터를 저장 | 암호화 키만 저장 (데이터는 외부) |
| 예시 | AWS Secrets Manager, Vault | AWS KMS, GCP KMS, GnuPG keyserver |
| 사용 패턴 | API로 Secret 조회 | 키를 받아서 직접 암호화/복호화 |

Step 1. age 키 생성
# age 키 페어 생성
$ age-keygen -o keys.txt
Public key: age1j49ugcg2rzyye07ksyvj5688m6hmv...
# 키 파일 확인
$ cat keys.txt
# created: 2024-01-15T10:30:00+09:00
# public key: age1j49ugcg2rzyye07ksyvj5688m6hmv...
AGE-SECRET-KEY-1QFNZ7...
Step 2. 원본 ConfigMap/Secret 준비
# configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name_unencrypted: db-auth # (1) _unencrypted 접미사로 암호화 제외
data:
# User and Password
USER: "batman"
PASSWORD: "r0b1n"
_unencrypted 접미사를 붙이면 해당 필드는 암호화되지 않음 (복호화 시 접미사 자동 제거)Step 3. SOPS로 암호화
$ sops --encrypt \\
--age age1j49ugcg2rzyye07ksyvj5688m6hmv... \\
configmap.yaml > configmap_encrypted.yaml
Step 4. 암호화된 결과
# configmap_encrypted.yaml
apiVersion: ENC[AES256_GCM,data:...,iv:...,tag:...,type:str] # (1) 값 암호화
kind: ENC[AES256_GCM,data:...,iv:...,tag:...,type:str]
metadata:
name_unencrypted: db-auth # (2) 암호화 제외됨
data:
#ENC[AES256_GCM,data:...,iv:...,tag:...,type:comment] # 주석도 암호화!
USER: ENC[AES256_GCM,data:...,iv:...,tag:...,type:str]
PASSWORD: ENC[AES256_GCM,data:...,iv:...,tag:...,type:str]
sops: # (3) 복호화에 필요한 메타데이터
age:
- recipient: age1j49ugcg2rzyye07ksyvj5688m6hmv...
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0...
-----END AGE ENCRYPTED FILE-----
lastmodified: "2024-01-15T10:35:00Z"
mac: ENC[AES256_GCM,data:...,iv:...,tag:...,type:str]
unencrypted_suffix: _unencrypted
ENC[...] 형태로 암호화됨_unencrypted 접미사가 붙은 필드는 평문 유지sops 섹션에 복호화에 필요한 메타데이터 포함# 환경 변수로 키 파일 지정
$ export SOPS_AGE_KEY_FILE=keys.txt
# 복호화 후 바로 적용 (파이프라인)
$ sops --decrypt configmap_encrypted.yaml | kubectl apply -f -
configmap/db-auth created
# 또는 복호화된 내용 확인
$ sops --decrypt configmap_encrypted.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: db-auth # _unencrypted 접미사 자동 제거
data:
USER: "batman"
PASSWORD: "r0b1n"
# .github/workflows/deploy.yaml
name: Deploy to Kubernetes
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install sops
run: |
curl -LO <https://github.com/getsops/sops/releases/download/v3.8.1/sops-v3.8.1.linux.amd64>
chmod +x sops-v3.8.1.linux.amd64
sudo mv sops-v3.8.1.linux.amd64 /usr/local/bin/sops
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Deploy secrets
run: |
# AWS KMS를 사용하는 경우 자동으로 권한 인증
sops --decrypt k8s/secrets.enc.yaml | kubectl apply -f -
kubectl apply -f k8s/deployment.yaml
| 장점 | 한계 |
|---|---|
| 서버 사이드 컴포넌트 불필요 | 클러스터에 적용 후에는 평문으로 저장됨 |
| GitOps와 완벽 호환 | 키 관리 책임이 사용자에게 있음 |
| 다양한 KMS 지원 | 복호화 권한이 있는 곳에서만 적용 가능 |
| 설치/유지보수 간단 | 자동 키 rotation 미지원 |
[요약]
- 핵심 아이디어: 외부 SMS → 실시간 요청 → Pod 볼륨/환경변수로 주입 (etcd 저장 없음)
- 적용 대상: 최고 수준 보안 요구, 자동 rotation 필요, 감사/규정 준수 환경
- 대표 도구: Secrets Store CSI Driver, Vault Sidecar Agent Injection
클러스터 외부의 전문 SMS에 민감한 정보를 저장하고,
필요할 때 안전한 채널을 통해 요청하는 방식입니다.
클러스터 내부에 Secret이 영구 저장되지 않는 것이 핵심입니다.
Secrets Store CSI Driver는 다양한 중앙 집중식 SMS에 접근하여 일반 Kubernetes 볼륨처럼 마운트할 수 있게 해줍니다.
핵심 차이점: 일반 Secret 볼륨과 달리 etcd에 아무것도 저장되지 않고, 외부 Vault에서 직접 가져옵니다.

관리자 작업
개발자 작업
Step 1: SecretProviderClass 정의
# secret-provider-class.yaml
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: vault-database
namespace: production
spec:
provider: vault # (1) 프로바이더 타입
parameters:
vaultAddress: "<http://vault.default:8200>" # (2) Vault 주소
roleName: "database" # (3) Vault 인증 역할
objects: |
- objectName: "database-password" # (4) 마운트될 파일 이름
secretPath: "secret/data/database-creds" # (5) Vault 경로
secretKey: "password" # (6) Vault Secret의 키
- objectName: "database-username"
secretPath: "secret/data/database-creds"
secretKey: "username"
azure, gcp, aws, vault 중 선택Step 2. Pod에서 CSI 볼륨 마운트
# pod-with-vault-secrets.yaml
apiVersion: v1
kind: Pod
metadata:
name: webapp
namespace: production
spec:
serviceAccountName: vault-access-sa # (1) Vault 인증에 사용될 SA
containers:
- name: app
image: myapp:latest
volumeMounts:
- name: secrets-store
mountPath: "/secrets-store" # (2) Secret이 마운트될 경로
readOnly: true
env:
- name: DB_PASSWORD_FILE
value: "/secrets-store/database-password"
volumes:
- name: secrets-store
csi: # (3) CSI 드라이버 사용
driver: secrets-store.csi.k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: "vault-database" # (4) SecretProviderClass 참조
database 역할에 매핑되어 있어야 함Step 3. Vault 측 설정 (참고)
# Vault에서 Kubernetes 인증 활성화
vault auth enable kubernetes
# Kubernetes 인증 설정
vault write auth/kubernetes/config \\
kubernetes_host="https://$KUBERNETES_SERVICE_HOST:$KUBERNETES_SERVICE_PORT"
# 정책 생성
vault policy write database-policy - <<EOF
path "secret/data/database-creds" {
capabilities = ["read"]
}
EOF
# 역할 생성 (ServiceAccount와 매핑)
vault write auth/kubernetes/role/database \\
bound_service_account_names=vault-access-sa \\
bound_service_account_namespaces=production \\
policies=database-policy \\
ttl=1h
Step 4. 결과 확인
# Pod 내부에서 Secret 파일 확인
kubectl exec -it webapp -n production -- ls -la /secrets-store/
# -r--r--r-- 1 root root 16 Jan 15 10:00 database-password
# -r--r--r-- 1 root root 8 Jan 15 10:00 database-username
kubectl exec -it webapp -n production -- cat /secrets-store/database-password
# s3cr3tp@ss
| 장점 | 단점 |
|---|---|
| etcd에 민감 정보 저장 안 함 | 설정이 복잡함 |
| SMS의 모든 기능 활용 가능 | 더 많은 컴포넌트 = 더 많은 장애 포인트 |
| 볼륨 마운트로 투명한 접근 | 트러블슈팅 어려움 |
| ServiceAccount 기반 세밀한 접근 제어 | SMS 장애 시 Pod 시작 불가 |
애플리케이션이 proprietary 클라이언트 라이브러리 없이 외부 SMS에 접근할 수 있는 또 다른 방법입니다.
Mutating Webhook을 활용하여 특정 annotation이 있는 Pod에 자동으로 Vault Sidecar를 주입합니다.
두 가지 패턴을 활용합니다

Step 1. Vault Injector 설치 (Helm)
helm repo add hashicorp <https://helm.releases.hashicorp.com>
helm install vault hashicorp/vault \\
--set "injector.enabled=true" \\
--set "server.dev.enabled=true" # 개발용
Step 2. Pod에 Annotation 추가
apiVersion: v1
kind: Pod
metadata:
name: webapp
annotations:
# (1) Vault Agent 주입 활성화
vault.hashicorp.com/agent-inject: "true"
# (2) 사용할 Vault 역할
vault.hashicorp.com/role: "webapp"
# (3) Secret 마운트 경로와 Vault 경로 매핑
vault.hashicorp.com/agent-inject-secret-database: "secret/data/database"
# (4) 템플릿으로 원하는 형식 지정 (선택)
vault.hashicorp.com/agent-inject-template-database: |
{{- with secret "secret/data/database" -}}
DB_USER={{ .Data.data.username }}
DB_PASS={{ .Data.data.password }}
{{- end }}
spec:
serviceAccountName: webapp-sa
containers:
- name: app
image: myapp:latest
# Secret은 /vault/secrets/database에 자동 마운트됨
Step 3. 자동 주입 결과
# Pod 확인 - Sidecar가 자동 추가됨
kubectl get pod webapp -o jsonpath='{.spec.containers[*].name}'
# app vault-agent
# 주입된 볼륨 확인
kubectl get pod webapp -o jsonpath='{.spec.volumes[*].name}'
# vault-secrets
Step 4. 애플리케이션에서 사용
# Python 애플리케이션 예시
import os
def get_db_credentials():
# Vault Sidecar가 동기화한 파일 읽기
with open('/vault/secrets/database', 'r') as f:
content = f.read()
# 파싱 (템플릿 사용 시)
creds = {}
for line in content.strip().split('\\n'):
key, value = line.split('=')
creds[key] = value
return creds['DB_USER'], creds['DB_PASS']
| 장점 | 단점 |
|---|---|
| CSI Driver보다 설정 간단 | 보안 annotation이 앱 배포에 포함됨 (관심사 혼합) |
| SMS 직접 접근 차단 | Pod당 추가 컨테이너 리소스 소모 |
| 자동 Secret rotation | Vault 의존성 |
| 애플리케이션 코드 변경 불필요 |
[요약]
- 기존 기능: Encryption at Rest, Immutable Secrets, Audit Logging, Pod Security Standards
- v1.35 신규: Pod Certificates (mTLS), Image Pull Credentials Verification, CSI SA Tokens 분리
- 적용 효과: etcd 암호화, 변경 방지, 접근 추적, 런타임 보안 강화
- 권장 사항: 모든 프로덕션 클러스터에 기본 적용
Kubernetes 1.13+부터 지원되는 etcd 저장 데이터 암호화입니다.
# /etc/kubernetes/encryption-config.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
- configmaps
providers:
# 가장 먼저 나온 provider가 암호화에 사용됨
- aescbc:
keys:
- name: key1
secret: <BASE64_ENCODED_32_BYTE_KEY>
- identity: {} # 기존 암호화되지 않은 데이터 읽기용
# API Server 설정에 추가
kube-apiserver --encryption-provider-config=/etc/kubernetes/encryption-config.yaml
# 기존 Secret 재암호화
kubectl get secrets --all-namespaces -o json | kubectl replace -f -
Kubernetes 1.21+에서 GA된 변경 불가능한 Secret입니다.
apiVersion: v1
kind: Secret
metadata:
name: api-credentials
type: Opaque
data:
API_KEY: c2VjcmV0LWtleQ==
immutable: true # [O] 수정/삭제 시 새로 생성해야 함
장점:
Secret 접근을 추적하기 위한 감사 로그 설정입니다.
# audit-policy.yaml
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
# Secret 읽기 요청은 상세 로깅
- level: RequestResponse
resources:
- group: ""
resources: ["secrets"]
verbs: ["get", "list", "watch"]
# Secret 생성/수정/삭제는 메타데이터만
- level: Metadata
resources:
- group: ""
resources: ["secrets"]
verbs: ["create", "update", "delete", "patch"]
# API Server 설정
kube-apiserver \\
--audit-policy-file=/etc/kubernetes/audit-policy.yaml \\
--audit-log-path=/var/log/kubernetes/audit.log \\
--audit-log-maxage=30 \\
--audit-log-maxbackup=10

구현 요소:
apiVersion: v1
kind: Pod
metadata:
name: secure-app
spec:
securityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: myapp:latest
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
volumeMounts:
- name: secrets
mountPath: /secrets
readOnly: true # [O] Secret 볼륨은 항상 readOnly
Kubernetes 1.32~1.35 사이에 Secret 관리와 관련된 중요한 기능들이 안정화되거나 베타로 승격되었습니다.
| 기능 | 상태 | KEP | 설명 |
|---|---|---|---|
| Pod Certificates for mTLS | Beta | KEP-4317 | Pod 간 mTLS를 위한 X.509 인증서 자동 발급. PodCertificateRequest API 제공 |
| Image Pull Credentials Verification | Beta | KEP-2535 | 캐시된 이미지도 레지스트리 인증 재검증. imagePullCredentialsVerificationPolicy 설정 |
| CSI ServiceAccount Tokens via Secrets | Alpha | KEP-5538 | CSI 드라이버의 SA 토큰을 volumeContext에서 분리하여 별도 secrets 필드로 관리 |
Pod 간 통신에 mTLS를 적용하려면 각 Pod에 X.509 인증서가 필요합니다. 기존에는 cert-manager 같은 외부 도구가 필수였지만, v1.35부터 네이티브 지원이 가능해졌습니다.
# v1.35+ PodCertificate 볼륨 소스 예시 (Beta)
apiVersion: v1
kind: Pod
metadata:
name: mtls-app
spec:
containers:
- name: app
image: myapp:latest
volumeMounts:
- name: pod-cert
mountPath: /etc/certs
readOnly: true
volumes:
- name: pod-cert
podCertificate: # 신규 볼륨 타입
issuerRef:
name: cluster-issuer
kind: ClusterIssuer
dnsNames:
- myapp.default.svc
기존에는 이미지가 한 번 pull되어 캐시되면, 다른 Pod가 인증 없이 해당 이미지를 사용할 수 있었습니다. v1.35에서는 이를 방지합니다.
# kubelet 설정
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
imagePullCredentialsVerificationPolicy: Always # Never | IfNotPresent | Always
Always: 캐시된 이미지도 매번 인증 재검증IfNotPresent: 캐시에 없을 때만 인증Never: 인증 검증 안 함 (기본값, 하위 호환)"우리 조직과 환경에는 어떤 패턴이 가장 적합한가?"

| 상황 | 추천 솔루션 | 이유 |
|---|---|---|
| 소규모 팀, GitOps 시작 | SOPS | 설치 없음, 즉시 사용 가능 |
| 멀티 클러스터 환경 | External Secrets | 중앙 SMS로 일관된 관리 |
| 최고 수준 보안 요구 | CSI Driver | etcd에 저장 안 함 |
| 개발자 편의성 우선 | Vault Sidecar | annotation만 추가하면 됨 |
| 단일 클러스터, Git 저장 필요 | Sealed Secrets | 간단하고 검증됨 |
| 클라우드 네이티브 | External Secrets + 클라우드 SMS | 클라우드 기능 최대 활용 |
| 기능 | Sealed Secrets | External Secrets | SOPS | CSI Driver | Vault Sidecar |
|---|---|---|---|---|---|
| 복잡도 | 낮음 | 중간 | 낮음 | 높음 | 중간 |
| 서버 컴포넌트 | 필요 | 필요 | 불필요 | 필요 | 필요 |
| Git 친화적 | O | - | O | X | X |
| 자동 rotation | X | O | X | O | O |
| etcd 저장 | Secret 생성 | Secret 생성 | Secret 생성 | X | X |
| SMS 연동 | X | O | KMS만 | O | O |
| 멀티 클러스터 | 제한적 | O | O | - | - |
중요: 아무리 안전하게 Secret을 구성하더라도, 악의적인 의도를 가진 사람이 클러스터와 컨테이너에 대한 완전한 root 접근 권한을 가지고 있다면 해당 데이터에 접근할 방법은 항상 존재합니다. 이 패턴은 이러한 종류의 공격을 최대한 어렵게 만드는 것이 목표입니다.
Defense in Depth 원칙 적용
# 1. Sealed Secrets 설치
kubectl apply -f <https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.24.5/controller.yaml>
# 2. kubeseal CLI 설치 (macOS)
brew install kubeseal
# 3. 테스트 Secret 생성
kubectl create secret generic test-secret \\
--from-literal=username=admin \\
--from-literal=password=supersecret \\
--dry-run=client -o yaml > test-secret.yaml
# 4. SealedSecret으로 변환
kubeseal -f test-secret.yaml -o yaml > sealed-secret.yaml
# 5. Git에 커밋 (안전!)
cat sealed-secret.yaml
git add sealed-secret.yaml
git commit -m "Add encrypted test secret"
# 6. 클러스터에 적용
kubectl apply -f sealed-secret.yaml
# 7. 생성된 Secret 확인
kubectl get secret test-secret -o jsonpath='{.data.password}' | base64 -d
# supersecret
# 1. age 설치
# macOS
brew install age
# Linux
sudo apt install age # 또는 snap install age
# 2. age 키 페어 생성
age-keygen -o keys.txt
# Public key: age1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# 3. 공개키 확인 및 저장
AGE_PUBLIC_KEY=$(grep "public key:" keys.txt | cut -d: -f2 | tr -d ' ')
echo $AGE_PUBLIC_KEY
# 4. SOPS 설치
# macOS
brew install sops
# Linux
curl -LO <https://github.com/getsops/sops/releases/download/v3.8.1/sops-v3.8.1.linux.amd64>
chmod +x sops-v3.8.1.linux.amd64
sudo mv sops-v3.8.1.linux.amd64 /usr/local/bin/sops
# 5. Secret YAML 생성
cat > db-secret.yaml << 'EOF'
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
type: Opaque
stringData:
username: admin
password: supersecretpassword
EOF
# 6. SOPS로 암호화
sops --encrypt --age $AGE_PUBLIC_KEY db-secret.yaml > db-secret.enc.yaml
# 7. 암호화된 파일 확인 (값만 암호화됨)
cat db-secret.enc.yaml
# 8. Git에 안전하게 저장
git add db-secret.enc.yaml
git commit -m "Add encrypted database secret"
# 9. 복호화 후 클러스터에 적용
export SOPS_AGE_KEY_FILE=./keys.txt
sops --decrypt db-secret.enc.yaml | kubectl apply -f -
# 10. 적용 확인
kubectl get secret db-credentials -o jsonpath='{.data.password}' | base64 -d
# 1. Vault 설치 (개발 모드 - 테스트용)
# Helm으로 설치
helm repo add hashicorp <https://helm.releases.hashicorp.com>
helm install vault hashicorp/vault --set "server.dev.enabled=true"
# 2. Vault Pod 확인
kubectl get pods -l app.kubernetes.io/name=vault
# 3. Vault에 접속
kubectl exec -it vault-0 -- /bin/sh
# 4. (Vault 내부) Secret 저장
vault kv put secret/database username="admin" password="dbpassword123"
# 5. (Vault 내부) Secret 확인
vault kv get secret/database
# 6. (Vault 내부) Kubernetes 인증 활성화
vault auth enable kubernetes
vault write auth/kubernetes/config \\
kubernetes_host="https://$KUBERNETES_PORT_443_TCP_ADDR:443"
# 7. (Vault 내부) 정책 생성
vault policy write database-policy - <<EOF
path "secret/data/database" {
capabilities = ["read"]
}
EOF
# 8. (Vault 내부) Role 생성 (ServiceAccount 매핑)
vault write auth/kubernetes/role/database \\
bound_service_account_names=webapp-sa \\
bound_service_account_namespaces=default \\
policies=database-policy \\
ttl=1h
# 9. (Vault 외부) ServiceAccount 생성
kubectl create serviceaccount webapp-sa
# 10. (Vault 외부) Vault Agent Injector로 Secret 주입 테스트
kubectl apply -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
name: webapp
annotations:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: "database"
vault.hashicorp.com/agent-inject-secret-database: "secret/data/database"
spec:
serviceAccountName: webapp-sa
containers:
- name: app
image: nginx:alpine
command: ["sleep", "infinity"]
EOF
# 11. Secret 주입 확인
kubectl exec webapp -- cat /vault/secrets/database
# Sealed Secrets 정리
kubectl delete sealedsecret test-secret
kubectl delete -f <https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.24.5/controller.yaml>
# SOPS 정리
kubectl delete secret db-credentials
rm -f keys.txt db-secret.yaml db-secret.enc.yaml
# Vault 정리
kubectl delete pod webapp
kubectl delete serviceaccount webapp-sa
helm uninstall vault
[최종 요약]
- 핵심 메시지: Kubernetes Secret ≠ 암호화. 상황에 맞는 도구 선택과 Defense in Depth 적용이 핵심
- 선택 기준: Git 저장 필요 → Out-of-Cluster / 최고 보안 → CSI Driver / 편의성 → Vault Sidecar
- 필수 조치: Encryption at Rest + RBAC + Audit Logging + Pod Security Standards
Secure Configuration 패턴은 Secret 탈취의 난이도를 높이고, 보안 책임을 명확하게 분리하기 위한 현실적인 접근법입니다.
핵심 포인트
기술은 계속 발전하지만, 여기서 소개한 기법들(클라이언트 측 암호화, Secret 동기화, 볼륨 프로젝션, 사이드카 주입)은 보편적이며 앞으로도 새로운 솔루션의 기반이 될 것입니다.