본문 바로가기
클라우드/쿠버네티스 보안

🔓 쿠버네티스 etcd의 세 가지 치명적 약점 — 당신의 Secret은 정말 안전한가?

by gasbugs 2026. 4. 2.

Secret이라는 이름이 무색하게, 쿠버네티스의 기밀 데이터 관리는 구멍투성이다.

 

🎯 이 글에서 다루는 것

  • etcd가 Secret을 기본적으로 암호화하지 않고 평문(Base64)으로 저장하는 이유와 위험성
  • 파드를 생성할 수 있는 권한 하나가 Secret RBAC 통제 전체를 우회하는 공격 경로
  • 파드가 Secret을 매번 읽어도 감사 로그(Audit Log)가 사실상 공백인 구조적 이유
  • 각 취약점에 대한 실질적 대응 방법

 

📌 도입 — etcd는 쿠버네티스의 심장이자 아킬레스건

쿠버네티스 클러스터의 모든 상태 정보는 etcd라는 분산 키-값 저장소에 담긴다. Deployment 정보, ConfigMap, 그리고 우리가 오늘 주목할 Secret까지 전부 이곳에 있다.

 

etcd를 탈취하면 클러스터 전체를 손에 넣은 것이나 다름없다. 그런데 이 중요한 저장소에는 세 가지 구조적 취약점이 존재한다. 단순한 설정 실수가 아니라, 설계 결정과 운영 편의성이 낳은 구조적 문제다.


🔍 취약점 1 — etcd 암호화, 왜 기본값이 아닌가?

문제의 핵심

쿠버네티스 Secret의 값은 기본적으로 Base64로 인코딩되어 암호화 없이 etcd에 저장된다. Base64는 암호화가 아니다. 그냥 다른 형태로 변환한 것이다. echo "bXlwYXNzd29yZA==" | base64 -d 한 줄이면 원문이 나온다.

etcd에 직접 접근할 수 있는 사람은 누구든지 이 데이터를 쉽게 복호화해서 읽을 수 있다. 이것이 at-rest 암호화를 반드시 활성화해야 하는 이유다.

 

왜 기본값이 아닌가?

쿠버네티스가 암호화를 기본 활성화하지 않는 데는 현실적인 이유가 있다.

① 키 관리의 복잡성: 암호화 키를 어디에 저장할 것인가? 로컬 키로 Secret 데이터를 암호화하면 etcd 탈취에는 대응할 수 있지만, 호스트 자체가 침해될 경우에는 보호되지 않는다. 암호화 키가 EncryptionConfiguration YAML 파일에 저장되어 있고, 실력 있는 공격자라면 그 파일에 접근할 수 있기 때문이다.

② 기존 데이터 마이그레이션: 암호화를 활성화해도 이미 저장된 Secret은 자동으로 암호화되지 않는다. 기존 Secret을 모두 다시 write해야 한다.

③ 성능 오버헤드: 모든 읽기/쓰기에 암호화/복호화가 추가된다.

 

현재 제공되는 암호화 방식

쿠버네티스는 여러 암호화 프로바이더를 제공한다. identity는 기본값으로 암호화를 전혀 하지 않는다. AES-CBC는 패딩 오라클 취약점 때문에 더 이상 안전하지 않으며 키를 컨트롤 플레인 노드에 저장하므로 호스트 침해에는 무력하다. AES-GCM은 AES-CBC보다 개선됐지만 역시 키가 호스트에 있다는 한계가 있다.

 

올바른 대응

# /etc/kubernetes/enc.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
  - resources:
      - secrets
    providers:
      - kms:               # ✅ 프로덕션 권장: KMS 연동으로 키를 외부 관리
          name: myKmsPlugin
          endpoint: unix:///tmp/socketfile.sock
          apiVersion: v2
      - identity: {}       # fallback — 복호화 전용으로만 사용

 

kube-apiserver 설정 추가:

# /etc/kubernetes/manifests/kube-apiserver.yaml
spec:
  containers:
  - command:
    - kube-apiserver
    - --encryption-provider-config=/etc/kubernetes/enc.yaml  # 이 줄 추가

 

프로덕션에서는 AWS KMS, GCP KMS, Azure Key Vault 등 외부 KMS와 연동하여 키 자체를 클러스터 외부에서 관리하는 것이 정답이다.


🔍 취약점 2 — 파드 생성 권한으로 Secret RBAC 우회하기

가장 교묘한 취약점

이것이 세 가지 중 가장 교묘하다. 어떤 사용자가 secrets 리소스에 대해 get 권한이 없더라도, 파드를 생성할 수 있다면 Secret에 접근할 수 있다.

 

RBAC 시스템은 권한 있는 사용자만 파드를 생성하도록 강제하지만, 그 사용자가 파드 정의 안에 무엇을 넣을지는 제어하지 않는다. 적절한 Admission Control 없이는, 파드 생성 권한이 있는 사용자가 임의의 이미지와 임의의 권한을 가진 컨테이너를 만들 수 있다.

 

공격 시나리오 3가지

시나리오 A: 환경변수로 Secret 빼내기

# Secret get 권한 없어도 이렇게 파드를 만들면...
apiVersion: v1
kind: Pod
metadata:
  name: secret-stealer
spec:
  containers:
  - name: attacker
    image: busybox
    command: ["sh", "-c", "echo $DB_PASSWORD && sleep 3600"]
    env:
    - name: DB_PASSWORD
      valueFrom:
        secretKeyRef:
          name: db-credentials  # 접근하고 싶은 Secret
          key: password

 

파드 로그를 보면 DB_PASSWORD 값이 그대로 출력된다.

 

시나리오 B: Service Account 토큰 탈취

파드가 높은 권한을 가진 Service Account의 토큰을 탈취하도록 설계된 악성 이미지를 사용하는 파드를 생성할 수 있다. 공격자는 이 토큰을 이용해 API 액션을 수행하고 권한을 상승시킬 수 있다.

# 파드 내부에서 마운트된 토큰 확인
cat /var/run/secrets/kubernetes.io/serviceaccount/token
# 이 토큰으로 API 서버에 요청
curl -H "Authorization: Bearer $(cat /var/run/secrets/...)" https://k8s-api/api/v1/secrets

 

시나리오 C: hostPath 마운트로 etcd 직접 접근

노드 셀렉터를 사용해 컨트롤 플레인 노드에 파드를 스케줄하고 호스트 파일시스템을 마운트할 수 있다면, 해당 노드에 chroot하여 루트 권한을 획득하고 etcd 데이터베이스를 직접 읽을 수 있다.

 

대응 방법

# Admission Controller로 특권 파드 차단
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
# 또는 Pod Security Admission 사용
---
# namespace에 PSA 레이블 적용
kubectl label namespace production \
  pod-security.kubernetes.io/enforce=restricted
  pod-security.kubernetes.io/enforce-version=latest

 

핵심 원칙:

  • automountServiceAccountToken: false 를 기본으로 설정
  • hostPath, hostPID, hostNetwork 마운트 금지 정책
  • Admission Controller(OPA Gatekeeper, Kyverno)로 이미지 출처 및 권한 검증

🔍 취약점 3 — Secret 접근 감사 로그의 구조적 공백

이 부분이 핵심이다. 파드가 마운트된 Secret을 읽을 때마다 감사 로그가 쌓이지 않는다. 왜?

 

쿠버네티스 감사 로그의 작동 방식

쿠버네티스 감사 로그는 kube-apiserver 컴포넌트 내부에서 시작된다. API 서버에 대한 각 요청(request)마다 감사 이벤트가 생성된다.

여기에 핵심 문제가 있다. 감사 로그는 API 서버를 통한 요청에 대해서만 기록된다.

 

Secret이 파드에 전달되는 방식

[파드 생성 시]
API Server → kubelet → 파드 볼륨 마운트
              ↑
        이 시점에 1번만 Secret GET 요청 발생 → 로그 기록됨

[파드 실행 중]
파드 컨테이너 → /var/run/secrets/... 파일 직접 읽기
                ↑
        이건 그냥 파일시스템 read() 시스템 콜
        → API 서버를 거치지 않음 → 감사 로그 없음

Secret이 환경변수로 주입된 경우도 동일하다. 컨테이너 런타임이 파드 시작 시 한 번 값을 가져와서 환경변수로 설정하고, 이후 접근은 전부 프로세스 레벨에서 일어난다. API 서버는 무관하다.

쿠버네티스는 Secret 접근에 대한 상세 감사 로그를 기본적으로 제공하지 않는다. 이로 인해 누가 언제 Secret에 접근했는지 모니터링하기 어렵다.

왜 이걸 "방치"하는가?

설계 관점에서 보면 의도된 트레이드오프에 가깝다:

  1. 성능: Secret을 파일로 마운트하면 컨테이너가 수백 번 읽어도 API 서버 부하가 없다. 매번 API 호출로 처리하면 클러스터 전체 성능이 폭락한다.
  2. 볼륨 마운트의 아키텍처 한계: kubelet은 Secret을 tmpfs에 마운트한다. 이후 접근은 OS 커널 레벨 파일 I/O다. 쿠버네티스가 이를 가로챌 방법이 없다.
  3. 감사 로그 기본 비활성화: 기본적으로 쿠버네티스 클러스터는 감사 로그가 활성화되어 있지 않으며, 감사 정책을 만들고 kube-apiserver 설정에 명시해야 활성화된다.

감사 로그 설정 + 한계 인식

# /etc/kubernetes/audit-policy.yaml
apiVersion: audit.k8s.io/v1
kind: Policy
omitStages:
  - "RequestReceived"
rules:
  # Secret API 요청은 Metadata 레벨로 기록 (값 노출 방지)
  - level: Metadata
    resources:
      - group: ""
        resources: ["secrets"]
  # 파드 생성/삭제는 추적
  - level: RequestResponse
    verbs: ["create", "delete", "patch"]
    resources:
      - group: ""
        resources: ["pods"]

⚠️ 주의: RequestResponse 레벨로 Secret을 로깅하면 Secret 값 자체가 감사 로그에 기록된다. 감사 로그가 또 다른 유출 경로가 된다. Metadata 레벨이 현실적 타협안이다.

진짜 해결책 — 런타임 감사

파드의 Secret 파일 접근까지 감지하려면 OS 레벨 도구가 필요하다.

# Falco 룰 예시 — /var/run/secrets 접근 감지
- rule: Read Kubernetes Service Account Token
  desc: Detect any read of a Kubernetes service account token
  condition: >
    open_read
    and fd.name startswith /var/run/secrets/kubernetes.io/serviceaccount
    and not proc.name in (known_sa_readers)
  output: >
    "Service account token read (user=%user.name command=%proc.cmdline
    file=%fd.name container=%container.name)"
  priority: WARNING

Falco, Tetragon, eBPF 기반 런타임 보안 도구가 이 공백을 메운다.


⚠️ 주의사항 / 흔한 실수

  • AES-CBC는 더 이상 안전하지 않다 — 반드시 KMS 또는 AES-GCM 사용
  • 감사 로그에 Secret RequestResponse 레벨 설정 금지 — 로그에 Secret 값이 그대로 기록됨
  • pods/exec 권한도 Secret 노출 경로다 — exec로 컨테이너 진입하면 마운트된 Secret 파일에 접근 가능. 이 경로는 Kubelet API를 직접 사용하면 API 서버를 우회하므로 감사 로그조차 남지 않는다.
  • 암호화 키를 실수로 삭제하면 클러스터 복구가 불가능해진다. 키 관리는 Vault나 클라우드 KMS에 위임하라.

✅ 정리 — 세 취약점 대응 요약

취약점  핵심 문제 대응
1. 암호화 미적용 Base64만 적용, 암호화 없음 KMS 연동 + EncryptionConfiguration
2. 파드 생성으로 우회 RBAC이 파드 콘텐츠를 제어 못함 PSA Restricted + OPA Gatekeeper
3. 감사 공백 API 서버 우회 파일 접근은 미기록 Falco/Tetragon eBPF 런타임 감시

 

세 취약점 모두 "기능 결함"이 아니라 성능·유연성과 보안 사이의 트레이드오프에서 비롯된다. 이를 이해해야 올바른 대응이 가능하다.

 

다음 단계로는 HashiCorp Vault의 Dynamic Secret 또는 Kubernetes Secrets Store CSI Driver를 통해 Secret을 etcd에서 완전히 빼내는 아키텍처를 검토해볼 것을 권한다.