시크릿 운영
5부 세 번째 챕터입니다. K8s Secret의 base64 한계와 etcd encryption-at-rest의 의미부터 시작해, 저장 · 회전 · 주입 · 감사의 네 축으로 시크릿 라이프사이클을 다룹니다. sealed-secrets · external-secrets · SOPS 세 옵션의 비교, IRSA와 결합한 비밀번호 0 운영 (AWS API는 IRSA, DB는 RDS IAM auth), envFrom vs mount의 회전 차이, RBAC로 네임스페이스 단위 분리, Audit log와 GuardDuty의 감사 관점까지 본격적인 운영 매뉴얼로 묶습니다.
5부 (운영 · 디버깅 · 비용)의 세 번째 챕터입니다. 본 책의 여러 챕터 (6장 ConfigMap · Secret, 14장 RBAC / NetworkPolicy / ResourceQuota, 16장 RBAC / ServiceAccount 깊이, 18장 CRD와 Operator, 20장 GitOps, 23장 DB 연동)에서 시크릿이 단편적으로 등장했습니다. 이번 챕터는 그 단편들을 한 운영 매뉴얼로 묶습니다. “secret YAML을 그대로 git에 커밋하지 마라"의 다음 단계 — 프로덕션의 시크릿 라이프사이클 전체를 다룹니다.
이번 챕터의 목표는 저장 · 회전 · 주입 · 감사의 네 축이 하나의 운영 모델로 정리된 상태입니다. sealed-secrets / external-secrets / SOPS 세 도구의 차이를 비교하고, IRSA와 결합한 “비밀번호 0” 운영을 본격적으로 다뤄 시크릿 거버넌스의 기준선을 잡습니다.
K8s Secret의 한계 — base64는 암호화가 아니다 #
6장 ConfigMap · Secret §“Secret의 본질적 한계"에서 짚었던 한 줄이 본 챕터의 출발점입니다.
apiVersion: v1
kind: Secret
metadata:
name: myshop-api
type: Opaque
data:
DATABASE_PASSWORD: cG9zdGdyZXNAcHJvZA== # postgres@proddata의 값은 base64 인코딩일 뿐 암호화가 아닙니다. base64 -d 한 줄로 누구나 원본을 볼 수 있습니다. 이 매니페스트가 git에 커밋되면 비밀이 그대로 외부에 노출됩니다.
etcd encryption-at-rest의 의미와 한계 #
K8s API Server는 etcd에 객체를 저장합니다. EKS는 기본적으로 etcd의 디스크가 KMS로 암호화되어 있지만, etcd 안의 객체 자체는 평문입니다. 노드의 etcd 데이터에 접근할 수 있는 누군가는 Secret의 값을 볼 수 있다는 뜻입니다.
이를 막는 것이 encryption-at-rest입니다.
module "eks" {
# ...
encryption_config = [{
provider_key_arn = aws_kms_key.eks.arn
resources = ["secrets"]
}]
}이 설정이 켜져 있으면 etcd에 저장되기 전에 Secret 객체 자체가 KMS 키로 암호화됩니다. 운영 클러스터의 기본 셋업입니다. 다만 이게 켜져 있어도 매니페스트의 git 커밋 문제는 풀리지 않습니다 — etcd 안의 보호이지, 매니페스트 단계의 보호가 아닙니다.
이 둘을 분리해 두는 게 시크릿 운영의 첫 멘탈 모델입니다.
| 위치 | 보호 도구 |
|---|---|
| git repo 안의 매니페스트 | sealed-secrets / external-secrets / SOPS |
| 클러스터 안의 etcd | encryption-at-rest (KMS) |
| Pod 안의 환경변수 / 파일 | Pod 격리, RBAC, 감사 |
시크릿 운영의 네 축 #
production 시크릿 라이프사이클은 네 결로 분해됩니다.
| 축 | 질문 |
|---|---|
| 저장 (Storage) | 진짜 비밀값이 어디에 있는가 |
| 회전 (Rotation) | 비밀번호가 어떻게 갱신되는가 |
| 주입 (Injection) | Pod가 그 값을 어떻게 받는가 |
| 감사 (Audit) | 누가 언제 그 값에 접근했는가 |
대부분의 시크릿 사고는 한 축의 결함이 아니라 여러 축의 누수가 합쳐진 결과입니다. 한 도구만 도입하면 한두 축은 해결되지만 다른 축이 비어 있으면 결국 보안이 깨집니다. 네 축을 한 묶음으로 보는 게 운영의 출발점입니다.
주입 패턴 — envFrom vs mount #
비밀을 Pod에 주입하는 두 표준 패턴이 있습니다.
spec:
containers:
- name: api
envFrom:
- secretRef:
name: myshop-api-dbspec:
containers:
- name: api
volumeMounts:
- name: db-secret
mountPath: /var/secrets/db
readOnly: true
volumes:
- name: db-secret
secret:
secretName: myshop-api-db두 패턴의 결정적 차이는 회전 시 동작입니다.
| 결 | envFrom | volumeMount |
|---|---|---|
| Secret 갱신 시 | Pod 안의 환경변수는 그대로 (Pod 시작 시점에만 고정) | 약 1분 안에 파일이 자동 갱신 |
| 회전 대응 | Pod 재시작 필요 | 애플리케이션이 파일을 다시 읽기만 하면 됨 |
| 디버깅 | env 명령으로 즉시 확인 | 파일 경로 확인 + cat |
23장 DB 연동에서 짚었던 “Secret 갱신 시 kubectl rollout restart 필요"의 함정이 envFrom의 결입니다. 회전이 잦은 시크릿은 volumeMount가 운영적으로 더 자연스럽습니다. 애플리케이션 코드 측에서 파일을 주기적으로 다시 읽도록 만들면, Pod 재시작 없이 새 비밀이 적용됩니다.
다만 환경변수 모델이 더 단순하고, 대부분의 12 factor app이 환경변수를 기대합니다. 회전 빈도와 애플리케이션의 결을 고려해 둘 사이에서 골라잡는 게 표준입니다.
sealed-secrets — git에 안전하게 커밋 #
Bitnami의 sealed-secrets는 비밀을 git에 커밋해도 안전한 형태로 봉인 하는 도구입니다.
helm repo add sealed-secrets https://bitnami-labs.github.io/sealed-secrets
helm install sealed-secrets sealed-secrets/sealed-secrets \
-n kube-system설치 후 컨트롤러가 클러스터 안에서 RSA 키 쌍을 생성합니다. 사용자는 공개키로 비밀을 봉인하고, 컨트롤러가 개인키로 복호화합니다.
echo -n "postgres@prod" | kubectl create secret generic myshop-db \
--dry-run=client --from-file=password=/dev/stdin -o yaml \
| kubeseal --controller-namespace kube-system \
--controller-name sealed-secrets \
--format yaml \
> sealedsecret.yamlapiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
name: myshop-db
spec:
encryptedData:
password: AgB7K8x... # 클러스터의 컨트롤러만 복호화 가능
template:
type: Opaque이 매니페스트를 git에 커밋하고 ArgoCD로 클러스터에 동기화하면, 컨트롤러가 자동으로 복호화해 일반 Secret을 만듭니다. 20장 GitOps §“비밀의 단일 소스 모델"에서 짚었던 세 옵션 중 첫 번째가 본 절의 도구입니다.
sealed-secrets의 트레이드오프 #
- 장점 — 외부 의존성이 없습니다. 클러스터 안의 컨트롤러 한 개로 끝납니다.
- 단점 — 컨트롤러의 개인키가 클러스터 안에 있어 클러스터 백업이 곧 키 백업입니다. dev / staging / prod의 키가 다르므로 환경 간 SealedSecret 매니페스트는 호환되지 않습니다.
- 회전 — 비밀 회전 시 새로 봉인해 git에 커밋해야 합니다 — 수동 단계입니다.
작은 팀 + 단일 클러스터 환경에서는 가장 단순한 옵션입니다. 환경 간 매니페스트 공유 / 자동 회전이 필요해지면 다음 도구가 적합합니다.
external-secrets — 외부 비밀 저장소와 동기화 #
18장 CRD와 Operator의 Operator 패턴이고, 23장 DB 연동에서 본격적으로 다룬 도구입니다. AWS Secrets Manager / HashiCorp Vault / GCP Secret Manager의 비밀을 K8s Secret으로 자동 동기화합니다.
핵심 결의 차이를 sealed-secrets와 비교하면 다음과 같습니다.
| 결 | sealed-secrets | external-secrets |
|---|---|---|
| 비밀의 source of truth | git repo | 외부 비밀 저장소 (AWS SM 등) |
| 매니페스트의 내용 | 봉인된 값 (암호문) | 비밀의 참조 (path / key) |
| 회전 | 수동 (새 봉인 + git push) | 자동 (외부 저장소에서 갱신, ESO가 자동 동기화) |
| 환경 간 매니페스트 | 환경마다 다름 (키 다름) | 환경마다 동일 (참조만 다름) |
| 외부 의존 | 없음 | AWS SM / Vault 비용 + 가용성 |
운영 환경에서 회전이 잦은 비밀 (DB 비밀번호, API 키 등)에는 external-secrets가 자연스럽습니다. ESO 자체의 매니페스트는 23장에서 다룬 그대로입니다 — ClusterSecretStore + ExternalSecret의 두 CRD입니다.
ESO의 회전 — 자동의 결 #
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: myshop-api-db
spec:
refreshInterval: 1h # 1시간마다 외부 저장소 확인
# ...refreshInterval이 핵심 — ESO가 외부 저장소를 주기적으로 polling 해 변경을 감지하고 K8s Secret을 갱신합니다. AWS Secrets Manager의 자동 회전 (Lambda 기반)과 결합하면, 비밀번호 회전이 완전 자동화됩니다.
1. AWS Secrets Manager 가 Lambda 호출 (30 일 주기 등)
2. Lambda 가 RDS 에 새 비밀번호 적용 + Secrets Manager 업데이트
3. ESO 가 1 시간 안에 변경 감지, K8s Secret 갱신
4. Reloader (별도 컴포넌트) 가 Secret 변경 감지, Pod rollout restart
5. myshop-api Pod 가 새 비밀번호로 RDS 연결이 사이클이 굴러가면 사람이 한 번도 비밀번호를 만지지 않고도 분기 회전이 자동화 됩니다. 운영 시크릿의 목표 중 하나입니다.
SOPS — 작은 팀의 단순 옵션 #
Mozilla의 SOPS (Secrets OPerationS)는 sealed-secrets / ESO와 다른 결의 도구입니다. 로컬에서 파일을 암호화 하고, 그 파일을 git에 커밋합니다.
# age 키 쌍 생성 (한 번)
age-keygen -o ~/.config/sops/age/keys.txt
# 평범한 secret YAML 작성
cat > secret.yaml <<EOF
apiVersion: v1
kind: Secret
metadata:
name: myshop-db
stringData:
password: postgres@prod
EOF
# 암호화
sops --age $(cat ~/.config/sops/age/keys.txt | grep public | cut -d: -f2) \
--encrypt --in-place secret.yaml암호화된 파일은 키가 없으면 평문이 보이지 않습니다. git에 커밋해도 안전합니다. 적용 시점에는 SOPS가 복호화해 평범한 매니페스트로 풀어 준 다음 kubectl apply 합니다.
ArgoCD와 결합하려면 helm-secrets 또는 argocd-vault-plugin 같은 보조 도구가 필요합니다. AWS KMS와 결합하면 키 관리를 AWS에 위임할 수 있어 운영 부담이 줄어듭니다.
SOPS의 위치 #
- 장점 — 가장 단순합니다. 한 파일 = 한 비밀 묶음이라 멘탈 모델이 직관적입니다.
- 단점 — 자동 회전 없음, 환경 간 키 관리가 손이 갑니다. 비밀이 늘어나면 파일 관리가 번거롭습니다.
작은 팀의 단일 환경 + 비밀이 10개 미만인 경우에 자연스럽습니다. 비밀이 늘어나거나 회전 자동화가 필요해지면 ESO로 옮기는 게 자연스러운 흐름입니다.
세 도구의 결정 트리 #
- 외부 비밀 저장소를 쓰지 않고 클러스터 안에서 끝내고 싶음
-> sealed-secrets
- AWS / GCP / Vault 같은 외부 저장소가 이미 있음
+ 자동 회전이 운영 요구사항
-> external-secrets (ESO)
- 작은 팀 + 단일 환경 + 비밀 수가 적음
+ 한 파일 = 한 비밀의 단순함이 좋음
-> SOPS
- 위 셋의 조합 (인프라 비밀은 SOPS, 앱 비밀은 ESO 등)
-> 가능하지만 운영 부담 증가본 책의 표준 경로는 21~26장에서 다룬 그대로 — AWS Secrets Manager + External Secrets Operator입니다. EKS 환경의 운영 표준이고, 23장의 RDS 비밀 자동 동기화가 이 모델의 본격적인 응용입니다.
IRSA와 결합한 “비밀번호 0” 운영 #
가장 진보된 패턴은 비밀번호 자체를 없애는 것입니다. 16장 RBAC / ServiceAccount 깊이에서 다룬 IRSA가 이 패턴의 토대입니다.
두 결의 결합 #
운영 워크로드의 외부 자격 증명은 크게 둘로 나뉩니다.
[AWS API 호출 — S3, Secrets Manager, CloudWatch]
-> IRSA + projected token + STS AssumeRoleWithWebIdentity
-> 정적 키 없음, 토큰은 1시간 자동 회전
[DB 연결 — RDS PostgreSQL / MySQL]
-> RDS IAM auth + 15분짜리 IAM 토큰
-> DB 비밀번호 없음, IAM 권한으로 인증두 결을 결합하면 myshop-api의 어디에도 영구 비밀이 없는 운영 모델이 만들어집니다. 비밀번호 회전을 신경 쓸 필요가 없고, 모든 접근이 CloudTrail에 기록됩니다.
RDS IAM auth의 적용 #
import os
import boto3
import psycopg2
def get_db_connection():
rds_client = boto3.client("rds")
token = rds_client.generate_db_auth_token(
DBHostname=os.environ["DB_HOST"],
Port=5432,
DBUsername=os.environ["DB_USER"],
Region="ap-northeast-2",
)
return psycopg2.connect(
host=os.environ["DB_HOST"],
port=5432,
user=os.environ["DB_USER"],
password=token, # 비밀번호가 아니라 IAM 토큰
dbname=os.environ["DB_NAME"],
sslmode="require",
)여기서 boto3.client("rds")가 IRSA의 projected token으로 자동 인증되고, generate_db_auth_token이 15분짜리 토큰을 만듭니다. K8s Secret도, AWS Secrets Manager의 비밀도 필요하지 않습니다.
“비밀번호 0"의 한계 #
- 토큰 만료 — 15분마다 갱신해야 하므로 long-lived connection과 결합이 까다롭습니다. 풀러를 사이에 두면 풀러 자체가 토큰을 받아야 합니다.
- PostgreSQL 사용자 설정 필요 —
rds_iam그룹에 사용자를 추가하고 grants를 잡아 둬야 합니다. - 모든 DB가 지원하지 않음 — Aurora MySQL / PostgreSQL은 지원, 일부 옛 RDS 엔진은 미지원.
- PgBouncer transaction pooling과 함께 쓰기 어려움 — 23장 §“transaction pooling의 함정"에서 짚은 결.
이 한계로 인해, 본 책의 표준 경로는 전통 비밀번호 + Secrets Manager + ESO + IRSA의 결합입니다. “비밀번호 0"은 한층 보안이 엄격한 환경에서 일부 워크로드에 적용하는 옵션입니다. 운영 부담과 보안 강도의 균형을 한 사이클로 평가해야 합니다.
RBAC와의 결합 — 네임스페이스 단위 분리 #
시크릿 자체의 운영뿐 아니라 누가 그 시크릿을 읽을 수 있는가도 시크릿 보안의 핵심입니다. 14장 RBAC / NetworkPolicy / ResourceQuota의 RBAC 모델이 본 절의 키입니다.
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: secret-reader
namespace: myshop
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list", "watch"]이 Role을 myshop 네임스페이스의 ServiceAccount에만 부여하면, 다른 네임스페이스의 워크로드는 myshop의 비밀을 읽을 수 없습니다. 한 클러스터 안에서 팀별 격리의 기본 셋업입니다.
ServiceAccount 토큰 비활성화 #
14장 §“ServiceAccount 토큰의 자동 마운트 해제"의 패턴이 보안 결의 마지막 안전선입니다.
apiVersion: v1
kind: ServiceAccount
metadata:
name: myshop-api
namespace: myshop
automountServiceAccountToken: false이 한 줄을 넣어 두면 Pod가 침해되어도 K8s API에 직접 접근할 토큰이 없습니다. IRSA가 필요한 워크로드만 명시적으로 토큰을 받게 하고, 그 외는 모두 끄는 게 보안 가이드의 단골 권장입니다.
dev / staging / prod의 키 분리 #
sealed-secrets의 경우 환경마다 컨트롤러의 키가 분리되어 있어야 자연스럽고, ESO의 경우 환경별 IRSA Role의 trust policy가 분리되어 있어야 합니다. prod의 키가 dev 컨트롤러에서 복호화 가능하면 보안의 큰 구멍입니다.
20장 GitOps의 환경별 분리가 본 절의 키 분리와 자연스럽게 결합됩니다. 환경의 분리는 매니페스트 차원뿐 아니라 비밀 키 차원에서도 일관되게 잡혀 있어야 합니다.
감사 — 누가 언제 무엇에 접근했는가 #
시크릿 사고의 사후 분석에는 audit가 필수입니다. 세 결의 도구가 있습니다.
K8s Audit log #
module "eks" {
# ...
cluster_enabled_log_types = ["api", "audit", "authenticator"]
}이 설정이 켜져 있으면 모든 API 요청이 CloudWatch Logs의 audit log 그룹에 기록됩니다. “어느 ServiceAccount가 언제 myshop-api-db Secret을 읽었는가"가 추적 가능해집니다.
fields @timestamp, user.username, verb, objectRef.resource, objectRef.name
| filter objectRef.resource = "secrets"
| filter verb in ["get", "list"]
| sort @timestamp desc
| limit 100분기에 한 번씩 이 쿼리를 돌려 비정상적인 접근 패턴이 없는지 점검하는 게 표준입니다. 26장 운영 체크리스트의 분기 보안 점검 항목에 추가하기 좋습니다.
AWS CloudTrail — Secrets Manager 접근 #
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=EventName,AttributeValue=GetSecretValue \
--max-results 50AWS Secrets Manager의 모든 호출이 CloudTrail에 기록됩니다. 누가, 어느 IAM Role로, 어느 비밀에 접근했는지가 보입니다. 23장의 ESO IRSA Role이 일관되게 사용되는지 검증하는 도구입니다.
GuardDuty / Kubescape — 이상 탐지 #
GuardDuty의 EKS Protection이 켜져 있으면 비정상적인 시크릿 접근 패턴 (예: 새로 생성된 ServiceAccount가 갑자기 많은 비밀을 읽음)을 자동 탐지합니다. Kubescape는 매니페스트 단계의 보안 정책 위반 (Secret 평문 커밋, 토큰 자동 마운트 미비활성화 등)을 CI 단계에서 잡습니다.
시크릿 거버넌스 체크리스트 #
분기 점검의 한 페이지 체크리스트를 정리합니다.
[저장]
- EKS encryption-at-rest (KMS) 활성화 여부
- git repo 의 매니페스트에 평문 비밀 없는가 — gitleaks / trufflehog 스캔
- sealed-secrets / ESO / SOPS 중 선택된 도구의 일관성
[회전]
- 90 일 이상 회전 안 된 비밀 목록
- AWS Secrets Manager 자동 회전 활성화 여부 (RDS, API 키 등)
- 회전 실패 알람의 동작 여부
[주입]
- envFrom vs volumeMount 의 결정이 회전 빈도에 맞는가
- Reloader 와의 통합 — Secret 갱신 시 Pod 재시작 자동화
- IRSA + RDS IAM auth 로 비밀번호 0 으로 운영 가능한 워크로드 목록
[감사]
- EKS audit log 활성화 + CloudWatch Insights 분기 점검
- CloudTrail 의 Secrets Manager GetSecretValue 점검
- GuardDuty / Kubescape 의 alert 처리 상태
- automountServiceAccountToken: false 의 적용 비율이 체크리스트가 한 페이지에 들어가고, 매분기 정기적으로 채워지는 게 시크릿 운영의 목표입니다. 26장의 정기 운영 캘린더와 본 챕터의 체크리스트가 한 묶음으로 운영 클러스터의 보안 결을 받칩니다.
연습문제 #
- 본인의 dev 클러스터에 sealed-secrets와 external-secrets를 모두 설치하고, 같은 비밀 (예: dummy DB 비밀번호)을 두 도구로 각각 운영해 봅니다. 비밀 회전 시나리오 (값 변경)를 두 경로로 따라가며 sealed-secrets는 몇 단계가, ESO는 몇 단계가 필요한지 비교합니다. 매니페스트 git diff의 모양, ArgoCD UI의 변화, Pod 재시작 여부를 표 한 장으로 정리합니다.
- myshop-api의 한 워크로드를 골라 “비밀번호 0"으로 옮겨 봅니다. RDS의 한 데이터베이스 사용자를
rds_iam그룹에 추가하고, 본 챕터의 Python 예시처럼 IAM 토큰으로 연결하는 코드를 적용합니다. PgBouncer와의 결합에서 발생하는 토큰 만료 문제를 어떻게 풀지 — 풀러를 우회할지, 풀러 자체가 토큰을 받을지 — 본인 시나리오에 맞춰 한 단락으로 결정 근거를 정리합니다. - EKS audit log를 활성화하고, CloudWatch Insights에서 본 챕터의 secret 접근 쿼리를 돌려 봅니다. 한 주 동안의 결과에서 정상 / 비정상 패턴을 분류하고, 비정상으로 의심되는 항목 (예상 외의 ServiceAccount가 myshop의 Secret 접근, 새벽 시간대 다량 GetSecretValue 등)이 있는지 살펴봅니다. 발견한 패턴을 25장 모니터링 · 알람의 PrometheusRule 또는 GuardDuty의 룰로 자동 감지하는 매니페스트를 한 장 적어 봅니다.
한 줄 요약: K8s Secret의 base64는 암호화가 아니고, etcd encryption-at-rest는 클러스터 내부의 보호일 뿐 매니페스트 단계는 별도다. 시크릿 운영은 저장 · 회전 · 주입 · 감사의 네 축이고, 도구는 sealed-secrets (git 안에서 끝) / external-secrets (외부 저장소 동기화 + 자동 회전) / SOPS (작은 팀의 단순)의 세 결로 갈라진다. envFrom은 단순하지만 회전 시 Pod 재시작, volumeMount는 파일 자동 갱신. IRSA + RDS IAM auth의 “비밀번호 0"이 가장 진보된 모델이지만 토큰 만료 · PgBouncer 결합의 한계 때문에 일부 워크로드에 적용하는 옵션이다. RBAC + 환경별 키 분리 +
automountServiceAccountToken: false가 보안 마지막 안전선, EKS audit log + CloudTrail + GuardDuty가 감사 결의 도구다. 분기 시크릿 거버넌스 체크리스트가 한 페이지에 들어가는 게 운영의 목표이다.
다음 챕터 #
이번 챕터에서 시크릿 결을 다뤘다면, 다음 챕터는 시간의 결입니다. K8s는 분기마다 마이너 버전이 나오고, EKS 표준 지원 기간이 14개월입니다. 1년에 최소 한 번의 마이너 업그레이드가 운영의 필수 사이클이고, 그 사이클을 안전하게 굴리는 매뉴얼이 다음 챕터의 본문입니다.
30장 업그레이드 전략에서는 26장 운영 체크리스트에서 짧게 짚었던 EKS 업그레이드 흐름을 본격적으로 다룹니다. 컨트롤 플레인 → 데이터 플레인 → 애드온의 순서, deprecated API 검출 (pluto · kubent · apiserver_requested_deprecated_apis 메트릭), 노드 drain의 안전장치 (PDB · terminationGracePeriodSeconds), blast radius 최소화, 롤백 시나리오, 그리고 업그레이드 전 1주 / 당일 / 후 1주의 체크리스트까지를 한 사이클로 다룹니다.