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

Kubernetes etcd 민감 데이터 암호화 튜토리얼 — Secret을 평문 저장에서 보호하기

by gasbugs 2026. 7. 23.
반응형

Kubernetes에서 Secret을 만들면 값이 Base64로 표시됩니다. 그래서 처음 접하는 사람은 Secret이 이미 암호화되어 있다고 오해하기 쉽습니다.

 

하지만 Base64는 암호화가 아니라 인코딩입니다. 누구든 다음 명령으로 원래 값을 확인할 수 있습니다.

echo 'bXlkYXRh' | base64 --decode

더 중요한 문제는 기본 설정의 kube-apiserver가 Secret을 etcd에 저장할 때 별도의 저장 암호화, 즉 encryption at rest를 적용하지 않는다는 점입니다. etcd 백업 파일이나 데이터 디렉터리, 디스크 스냅샷이 유출되면 RBAC를 거치지 않고 민감 데이터가 노출될 수 있습니다.

 

이 글에서는 kubeadm으로 구성한 Kubernetes 클러스터를 기준으로 다음 과정을 직접 확인합니다.

  • 암호화 적용 전 etcd에 저장된 Secret 확인
  • 32바이트 암호화 키 생성
  • EncryptionConfiguration 작성
  • kube-apiserver 정적 Pod에 설정 연결
  • 새 Secret이 암호화되었는지 etcd에서 검증
  • 기존 Secret을 다시 써서 모두 암호화
  • 키 교체와 운영 환경에서의 주의사항

이 튜토리얼은 Kubernetes 공식 문서의 Encrypting Confidential Data at Rest 절차를 바탕으로 작성했습니다.

API 서버가 Secret을 암호화해 etcd에 저장하는 흐름


먼저 이해해야 할 것: 무엇을 암호화하는가

Kubernetes 저장 암호화는 Pod에 마운트된 파일이나 애플리케이션 데이터베이스를 암호화하는 기능이 아닙니다.

 

암호화 대상은 Kubernetes API를 통해 생성되고 etcd에 영속 저장되는 API 리소스입니다. 대표적으로 다음 리소스를 지정할 수 있습니다.

  • secrets
  • configmaps
  • 사용자 정의 리소스인 CRD 객체
  • 그 밖에 etcd에 저장되는 Kubernetes API 리소스

데이터 흐름은 다음과 같습니다.

kubectl 또는 애플리케이션
        ↓
kube-apiserver
        ↓  EncryptionConfiguration에 따라 암호화
      etcd

클라이언트가 Kubernetes API로 Secret을 조회하면 kube-apiserver가 etcd의 암호문을 복호화해 정상적인 API 응답을 반환합니다. 따라서 사용자는 기존처럼 kubectl get secret을 사용할 수 있습니다.

 

저장 암호화는 etcd 디스크나 백업본 유출에 대비하는 방어 계층입니다. 다음 보안 통제를 대신하지는 않습니다.

보안 통제 보호하는 영역
Kubernetes RBAC API를 통해 누가 Secret을 읽을 수 있는지 통제
TLS API 서버와 etcd 사이의 전송 구간 보호
etcd 저장 암호화 etcd 데이터와 백업본에 저장된 값 보호
디스크 암호화 노드 디스크나 볼륨 전체 보호
KMS/HSM 암호화 키를 클러스터 외부에서 보호하고 수명주기 관리

하나만 적용한다고 다른 계층이 필요 없어지는 것은 아닙니다.

실습 환경과 사전 주의사항

이 글은 다음 환경을 가정합니다.

  • kubeadm으로 구성된 Kubernetes 클러스터
  • etcd 3.x
  • 각 컨트롤 플레인에서 kube-apiserver가 정적 Pod로 실행
  • 컨트롤 플레인 노드에 sudo로 접근 가능
  • kubectletcdctl 사용 가능

클러스터 버전은 다음 명령으로 확인합니다.

kubectl version

이 작업은 컨트롤 플레인의 핵심 설정을 변경합니다. 운영 환경에서는 반드시 etcd 스냅샷과 현재 정적 Pod 매니페스트를 백업하고, 복구 절차를 확인한 뒤 진행해야 합니다.

sudo cp -a /etc/kubernetes/manifests/kube-apiserver.yaml \
  /root/kube-apiserver.yaml.before-encryption

고가용성 클러스터라면 모든 컨트롤 플레인 노드가 같은 암호화 설정과 키를 가져야 합니다. 노드마다 설정이 다르면 어떤 API 서버는 데이터를 쓰고 다른 API 서버는 그 데이터를 복호화하지 못하는 상황이 발생할 수 있습니다.

1단계: 암호화가 이미 적용되어 있는지 확인한다

먼저 kube-apiserver--encryption-provider-config 인수가 있는지 확인합니다.

sudo grep -- '--encryption-provider-config' \
  /etc/kubernetes/manifests/kube-apiserver.yaml

아무 결과도 나오지 않으면 API 서버 수준의 저장 암호화가 활성화되지 않은 상태입니다.

 

인수가 있더라도 안심하면 안 됩니다. EncryptionConfiguration의 첫 번째 provider가 identity라면 새 데이터가 평문으로 저장됩니다. provider 순서가 읽기와 쓰기 동작을 결정하기 때문입니다.

  • 쓸 때: 목록의 첫 번째 provider 사용
  • 읽을 때: 위에서 아래로 시도하며 복호화 가능한 provider 사용

마이그레이션 중에는 암호화 provider를 첫 번째에 두고, 과거 평문 데이터를 읽기 위한 identity를 뒤에 둡니다.

2단계: 적용 전 Secret을 만들어 etcd 값을 확인한다

비교를 위해 테스트 Secret을 하나 만듭니다.

kubectl create secret generic secret-before \
  --namespace default \
  --from-literal=mykey=mydata

Kubernetes API로 조회하면 값은 Base64로 보입니다.

kubectl get secret secret-before -n default -o yaml

예상되는 데이터는 다음과 같습니다.

data:
  mykey: bXlkYXRh

이제 컨트롤 플레인 노드에서 etcd 원본 값을 확인합니다.

sudo ETCDCTL_API=3 etcdctl \
  --cacert=/etc/kubernetes/pki/etcd/ca.crt \
  --cert=/etc/kubernetes/pki/etcd/server.crt \
  --key=/etc/kubernetes/pki/etcd/server.key \
  get /registry/secrets/default/secret-before | hexdump -C

암호화를 적용하지 않은 상태라면 출력 안에서 Secret의 구조와 mydata에 해당하는 데이터를 식별할 수 있습니다. 이것이 Base64 표시만으로는 etcd 저장 데이터를 보호할 수 없는 이유입니다.

etcdctl 접속 인증서와 endpoint는 클러스터 구성에 따라 다를 수 있습니다. 외부 etcd를 사용한다면 실제 endpoint와 클라이언트 인증서를 지정해야 합니다.

3단계: 32바이트 암호화 키를 생성한다

공식 문서는 로컬 암호화 provider를 사용할 때 32바이트 난수 키를 만들고 Base64로 인코딩하도록 안내합니다.

KEY=$(head -c 32 /dev/urandom | base64)

화면에 키를 출력하거나 셸 기록, 메신저, 일반 문서에 복사하지 않는 것이 좋습니다. 이 키를 가진 사람은 etcd의 암호화된 데이터를 복호화할 수 있습니다.

 

암호화 설정을 저장할 디렉터리를 만듭니다.

sudo install -d -m 700 /etc/kubernetes/enc

4단계: EncryptionConfiguration을 작성한다

이번 실습에서는 Kubernetes 공식 튜토리얼의 저장 형식과 검증 흐름을 그대로 이해하기 위해 Secret과 ConfigMap을 aescbc provider로 암호화합니다.

주의: 현재 Kubernetes 공식 문서는 aescbc를 CBC 패딩 오라클 공격 가능성 때문에 약한 방식으로 분류하고 운영 환경에는 권장하지 않습니다. 아래 구성은 저장 암호화의 동작 원리와 마이그레이션 절차를 익히기 위한 실습용입니다. 새 운영 환경이라면 KMS v2를 우선 검토해야 합니다.

sudo tee /etc/kubernetes/enc/enc.yaml >/dev/null <<EOF
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
  - resources:
      - secrets
      - configmaps
    providers:
      - aescbc:
          keys:
            - name: key1
              secret: ${KEY}
      - identity: {}
EOF

unset KEY
sudo chmod 600 /etc/kubernetes/enc/enc.yaml
sudo chown root:root /etc/kubernetes/enc/enc.yaml

provider의 순서가 중요합니다.

providers:
  - aescbc: ...
  - identity: {}

이 구성에서 새로 생성하거나 갱신하는 데이터는 aescbc로 암호화됩니다. 기존에 평문으로 저장된 데이터는 뒤쪽의 identity provider 덕분에 계속 읽을 수 있습니다.

 

반대로 identity를 첫 번째에 놓으면 API 서버는 새 데이터를 계속 평문으로 저장합니다.

aescbc, aesgcm, secretbox, KMS 중 무엇을 선택할까

Kubernetes는 여러 암호화 provider를 지원하지만 운영 특성은 다릅니다.

Provider 특징 운영 시 고려사항
aescbc 설정이 단순한 AES-CBC 로컬 대칭키 방식 공식 문서가 약한 방식으로 분류하며 운영 환경에는 비권장
aesgcm 인증 암호화 제공 같은 키로 약 20만 건을 초과해 쓰지 않도록 키 회전 필요
secretbox XSalsa20과 Poly1305 기반 로컬 설정 파일에 키가 존재
kms v2 외부 KMS가 키 암호화 키를 관리하는 envelope encryption 강한 키 격리 제공, 별도 플러그인과 가용성 설계 필요

로컬 키를 EncryptionConfiguration에 직접 저장하는 방식은 암호화를 사용하지 않는 것보다 낫지만, 컨트롤 플레인 파일과 etcd 데이터가 함께 탈취되면 방어력이 제한됩니다. 중요한 운영 환경이라면 클라우드 KMS나 HSM과 연동하는 방식을 검토해야 합니다.

5단계: kube-apiserver에 암호화 설정을 연결한다

정적 Pod 매니페스트를 편집합니다.

sudo vi /etc/kubernetes/manifests/kube-apiserver.yaml

kube-apiservercommand에 다음 인수를 추가합니다.

- --encryption-provider-config=/etc/kubernetes/enc/enc.yaml

같은 컨테이너의 volumeMounts에 읽기 전용 마운트를 추가합니다.

volumeMounts:
  - name: enc
    mountPath: /etc/kubernetes/enc
    readOnly: true

Pod spec.volumes에는 hostPath 볼륨을 추가합니다.

volumes:
  - name: enc
    hostPath:
      path: /etc/kubernetes/enc
      type: DirectoryOrCreate

파일을 저장하면 kubelet이 정적 Pod 변경을 감지해 kube-apiserver를 다시 생성합니다.

kubectl -n kube-system get pod \
  -l component=kube-apiserver -w

API 서버가 정상 상태로 돌아오는지 확인합니다.

kubectl get --raw='/readyz?verbose'

오류가 발생하면 kubelet 로그와 컨테이너 런타임 로그를 확인하고, 미리 백업한 매니페스트로 복구합니다.

sudo journalctl -u kubelet -n 200 --no-pager

6단계: 새 Secret이 암호화되어 저장되는지 검증한다

암호화 설정을 적용한 뒤 새로운 Secret을 만듭니다.

kubectl create secret generic secret-after \
  --namespace default \
  --from-literal=mykey=mydata

etcd에서 값을 직접 읽습니다.

sudo ETCDCTL_API=3 etcdctl \
  --cacert=/etc/kubernetes/pki/etcd/ca.crt \
  --cert=/etc/kubernetes/pki/etcd/server.crt \
  --key=/etc/kubernetes/pki/etcd/server.key \
  get /registry/secrets/default/secret-after | hexdump -C

출력에는 다음 접두사가 보여야 합니다.

k8s:enc:aescbc:v1:key1:

각 항목의 의미는 다음과 같습니다.

의미
k8s:enc Kubernetes 저장 암호화 데이터
aescbc 사용한 provider
v1 저장 형식 버전
key1 암호화에 사용한 키 이름

이제 API를 통한 복호화도 정상인지 확인합니다.

kubectl get secret secret-after \
  --namespace default \
  -o jsonpath='{.data.mykey}' | base64 --decode
echo

다음 값이 출력되면 정상입니다.

mydata

즉 etcd에는 암호문이 저장되지만, 권한이 있는 사용자는 Kubernetes API를 통해 정상적으로 Secret을 사용할 수 있습니다.

7단계: 기존 Secret도 다시 써서 암호화한다

여기서 가장 많이 놓치는 부분이 있습니다.

 

암호화 설정을 활성화해도 이미 etcd에 저장된 데이터가 자동으로 다시 암호화되지는 않습니다. 설정 적용 후 새로 생성되거나 갱신된 리소스부터 암호화됩니다.

 

기존의 모든 Secret을 같은 내용으로 다시 저장하면 API 서버가 암호화 provider를 적용해 etcd에 기록합니다.

kubectl get secrets --all-namespaces -o json \
  | kubectl replace -f -

쓰기 충돌이 발생하면 다시 실행해도 안전합니다. 규모가 큰 클러스터에서는 한 번에 전체 Secret을 갱신하기보다 namespace 단위로 나눠 실행하는 편이 좋습니다.

 

ConfigMap도 암호화 대상으로 지정했다면 별도로 다시 저장해야 합니다.

kubectl get configmaps --all-namespaces -o json \
  | kubectl replace -f -

마이그레이션 후 처음 만들었던 secret-before도 암호화 접두사를 갖는지 확인합니다.

sudo ETCDCTL_API=3 etcdctl \
  --cacert=/etc/kubernetes/pki/etcd/ca.crt \
  --cert=/etc/kubernetes/pki/etcd/server.crt \
  --key=/etc/kubernetes/pki/etcd/server.key \
  get /registry/secrets/default/secret-before | hexdump -C

8단계: 평문 fallback을 제거할 것인가

모든 대상 데이터가 암호화되었음을 충분히 검증했다면 identity provider를 제거해 API 서버가 평문 저장 데이터를 읽지 못하게 할 수 있습니다.

providers:
  - aescbc:
      keys:
        - name: key1
          secret: <BASE64_ENCODED_32_BYTE_KEY>

그러나 이 단계는 신중해야 합니다. 암호화되지 않은 리소스가 하나라도 남아 있으면 API 서버가 그 객체를 읽지 못합니다.

 

따라서 다음 조건을 모두 충족한 뒤 진행해야 합니다.

  • 모든 대상 리소스를 다시 저장했는가
  • 모든 컨트롤 플레인이 동일한 설정을 사용하는가
  • etcd에서 암호화 접두사를 표본 또는 전체 검사했는가
  • 암호화 키를 안전하게 백업했는가
  • 복구 절차를 실제로 확인했는가

운영 초기에는 identity를 읽기용 fallback으로 유지하고, 마이그레이션 검증이 끝난 뒤 별도 변경 작업으로 제거하는 편이 안전합니다.

9단계: 암호화 키를 무중단으로 교체하는 원리

키 교체는 기존 키를 바로 삭제하는 작업이 아닙니다.

 

Kubernetes 공식 문서가 안내하는 순서는 다음과 같습니다.

  1. 새 키를 기존 provider의 두 번째 키로 모든 컨트롤 플레인에 추가한다.
  2. 모든 API 서버가 새 키를 읽을 수 있도록 설정을 반영한다.
  3. 새 키를 안전하게 백업한다.
  4. 새 키를 키 목록의 첫 번째로 올린다.
  5. 모든 API 서버가 새 쓰기에 새 키를 사용하도록 설정을 반영한다.
  6. 기존 Secret을 kubectl replace로 다시 저장한다.
  7. 모든 데이터가 새 키로 암호화된 것을 검증한 뒤 이전 키를 제거한다.

예를 들면 설정은 중간 단계에서 다음과 같은 형태가 됩니다.

providers:
  - aescbc:
      keys:
        - name: key2
          secret: <NEW_BASE64_KEY>
        - name: key1
          secret: <OLD_BASE64_KEY>
  - identity: {}

첫 번째 키는 새 데이터를 암호화하는 데 사용되고, 뒤에 있는 키는 과거 데이터를 복호화하는 데 사용됩니다. 모든 데이터를 새 키로 다시 저장하기 전에 이전 키를 삭제하면 기존 Secret을 복호화할 수 없습니다.

 

Kubernetes는 --encryption-provider-config-automatic-reload=true 옵션으로 설정 파일 자동 재로딩도 지원합니다. 활성화하면 API 서버가 파일 변경을 주기적으로 확인하므로 키 교체 때마다 API 서버를 재시작하지 않아도 됩니다. 다만 실제 적용 시점은 관련 메트릭으로 확인해야 합니다.

고가용성 컨트롤 플레인에서 꼭 지켜야 할 순서

컨트롤 플레인이 여러 대라면 모든 노드를 동시에 바꾸지 말고 한 대씩 순차 적용합니다.

단계 확인 사항
1 모든 노드에 동일한 enc.yaml을 안전하게 배포
2 첫 번째 API 서버 설정 변경 및 정상 복귀 확인
3 나머지 API 서버를 한 대씩 변경
4 로드밸런서를 통해 API 요청이 정상인지 확인
5 새 데이터의 암호화 접두사 확인
6 기존 데이터 재저장

롤링 변경 중에도 모든 API 서버가 현재 etcd 데이터를 읽을 수 있어야 합니다. 새 키를 모든 노드에 배포하기 전에 그 키를 첫 번째 쓰기 키로 승격해서는 안 됩니다.

운영 환경에서는 KMS를 권장하는 이유

지금까지 사용한 aescbc 방식은 공식 절차를 따라 저장 암호화의 동작을 실습하는 데는 유용합니다. 그러나 현재 공식 문서는 이 방식을 약한 암호화로 분류하며, 암호화 키도 /etc/kubernetes/enc/enc.yaml에 들어 있습니다.

 

공격자가 etcd 데이터와 컨트롤 플레인의 설정 파일을 함께 얻으면 암호문과 복호화 키를 동시에 확보할 수 있습니다.

 

KMS provider를 사용하면 실제 데이터 암호화 키를 클러스터 외부의 KMS나 HSM에서 보호할 수 있습니다. 또한 다음 운영 기능을 활용하기 쉬워집니다.

  • 키 접근 권한 분리
  • 중앙 집중식 키 회전
  • 키 사용 감사 로그
  • HSM 또는 클라우드 관리형 KMS 활용
  • Kubernetes 관리자와 키 관리자의 역할 분리

따라서 이 글의 로컬 aescbc 구성은 저장 암호화의 동작 원리를 익히는 출발점으로 보고, 높은 보안 수준이 필요한 운영 클러스터에서는 KMS provider를 이용한 데이터 암호화를 검토하는 것이 좋습니다.

자주 하는 실수

1. Secret의 Base64를 암호화라고 생각한다

Base64는 누구나 복원할 수 있는 표현 방식입니다. etcd 저장 암호화와는 별개입니다.

2. identity를 첫 번째 provider로 둔다

읽기는 가능하지만 새 데이터가 평문으로 저장됩니다. 쓰기 provider는 목록의 첫 번째 항목입니다.

3. 설정만 적용하고 기존 Secret을 다시 저장하지 않는다

기존 데이터는 자동 변환되지 않습니다. kubectl replace 같은 재쓰기 과정이 필요합니다.

4. 노드마다 다른 키를 배포한다

HA 환경에서 API 서버별로 복호화 가능 여부가 달라져 간헐적인 장애가 발생할 수 있습니다.

5. 키 백업 없이 이전 키를 삭제한다

해당 키로 암호화된 데이터는 복구할 수 없게 됩니다. etcd 백업만 있고 키 백업이 없다면 암호화된 Secret을 되살릴 수 없습니다.

6. 암호화 설정 파일 권한을 느슨하게 둔다

로컬 provider의 키는 사실상 복호화 권한입니다. root와 API 서버 실행 주체만 읽을 수 있도록 제한해야 합니다.

실습 정리

이번 실습에서 확인한 핵심은 다음과 같습니다.

  1. Kubernetes Secret의 Base64는 암호화가 아니다.
  2. 기본 설정에서는 API 리소스가 etcd에 저장 암호화 없이 기록될 수 있다.
  3. EncryptionConfiguration의 첫 번째 provider가 새 데이터를 암호화한다.
  4. identity는 마이그레이션 중 기존 평문 데이터를 읽기 위한 fallback이다.
  5. 설정 적용만으로 기존 데이터가 암호화되지는 않는다.
  6. etcd에서 k8s:enc:aescbc:v1:key1: 접두사를 확인해야 한다.
  7. 키와 etcd 백업은 함께 관리하되 접근 권한은 분리해야 한다.
  8. 운영 환경에서는 외부 KMS 연동이 더 강한 키 보호를 제공한다.

저장 암호화는 공격자가 Kubernetes API 권한을 얻는 상황을 막아주는 기능은 아닙니다. 대신 etcd 디스크, 스냅샷과 백업 파일이 유출됐을 때 민감 데이터가 곧바로 노출되는 위험을 낮춥니다.

 

Kubernetes 보안은 RBAC, 네트워크 보호, 감사 로그, 백업 보안과 저장 암호화가 함께 작동할 때 완성됩니다.


참고한 공식 문서

반응형