Kubernetes Secret은 기본 암호화가 아니다…etcd부터 다시 잠그는 법

|작성자: QUASA 편집팀|6 분 소요
Kubernetes Secret은 기본 암호화가 아니다…etcd부터 다시 잠그는 법

Kubernetes Secret이 etcd에 암호화된 상태로 남게 하려면 API 서버의 저장 암호화를 설정하고 이미 저장된 Secret도 다시 기록해야 한다. Kubernetes의 Secret 보안 지침은 base64 인코딩이 암호화가 아니며, 별도 설정이 없는 Secret은 etcd에 암호화되지 않은 상태로 저장된다고 설명한다. Secret 매니페스트의 base64 값을 읽을 수 있는 사람은 원래 값도 복원할 수 있다.

제어 영역을 직접 관리한다면 EncryptionConfiguration을 모든 API 서버에 적용한 뒤 기존 객체를 재기록한다. GKE에서는 Google이 관리하는 기본 저장 암호화가 이미 적용되며, 운영자가 Cloud KMS 키를 지정하는 애플리케이션 계층 암호화를 추가할 수 있다. 두 환경은 설정 권한과 기존 데이터의 처리 방식이 다르므로, 먼저 어느 쪽의 제어 영역을 운영하는지 구분해야 한다.

자체 관리 클러스터: API 서버의 쓰기 경로 암호화

저장 암호화는 API에서 Secret을 조회할 때 보이는 base64 표현을 바꾸지 않는다. API 서버가 객체를 etcd에 쓸 때 암호화 제공자를 적용하고, 읽을 때는 복호화해 권한 있는 요청자에게 전달한다. Kubernetes의 저장 암호화 절차는 무작위 32바이트 키를 만드는 명령으로 head -c 32 /dev/urandom | base64를 제시하며, 암호화 설정의 첫 번째 제공자가 새 쓰기에 사용된다고 명시한다. 이 명령의 base64 출력은 암호화된 Secret 값이 아니라 실제 암호화 키를 설정 파일에 넣기 위한 표현이다.

  1. 키를 안전한 환경에서 만들고 복구에 필요한 사본을 보호한다. 로컬 키 방식의 설정 파일에는 etcd 데이터를 복호화할 수 있는 키 자료가 들어가므로 공유 저장소나 일반 백업에 무심코 포함하지 않는다. 키를 클러스터 밖에서 관리할 필요가 있다면 KMS 제공자 구성을 검토한다.
  2. EncryptionConfiguration에 apiVersion: apiserver.config.k8s.io/v1과 kind: EncryptionConfiguration을 지정한다. resources에는 secrets를 넣고 providers의 첫 항목에 aescbc와 키 이름·키 값을 둔다. 초기 전환 중 기존 평문 객체를 읽으려면 identity를 후순위에 둔다. identity를 맨 앞으로 옮기면 새 쓰기가 암호화되지 않는다.
  3. 각 제어 영역 노드에 같은 설정 파일을 안전하게 배포하고 kube-apiserver의 --encryption-provider-config가 그 파일을 가리키도록 한다. 정적 Pod로 API 서버를 실행한다면 설정 파일을 Pod에 마운트하는 작업도 필요하다. 파일 권한은 API 서버 실행 주체로 제한하고, 여러 API 서버를 운영한다면 각 서버가 저장 데이터를 계속 읽을 수 있도록 순차적으로 적용한다.

적용 뒤 새 Secret을 생성하고 etcd에서 저장 값을 조회한다. aescbc 예시에서는 값 앞의 k8s:enc:aescbc:v1: 접두어와 설정한 키 이름을 확인할 수 있다. 이어 Kubernetes API로 같은 Secret을 읽어 정상 복호화도 확인한다. API 조회만 성공해서는 etcd의 저장 형태를 알 수 없고, etcd의 접두어만 확인해서는 클러스터의 모든 API 서버가 값을 읽는다고 단정할 수 없다.

기존 Secret: 설정 변경 뒤에도 재기록이 필요하다

기존 Kubernetes Secret을 다시 기록하자 etcd 레코드에 암호화 접두어가 적용된 상태

암호화 설정은 앞으로 etcd에 쓰는 데이터에 적용된다. 이전에 저장한 Secret은 설정 파일을 배포했다는 이유만으로 바뀌지 않는다. 모든 API 서버가 새 설정으로 동작하는 것을 확인한 뒤, 전체 Secret을 읽고 수정할 권한이 있는 관리자는 kubectl get secrets --all-namespaces -o json | kubectl replace -f - 명령으로 기존 객체를 다시 쓸 수 있다. 같은 값을 재기록하더라도 저장 시점에는 현재 첫 번째 암호화 제공자가 적용된다.

규모가 큰 클러스터라면 네임스페이스별로 나눠 처리할 수 있다. 다른 작업이 같은 Secret을 갱신해 충돌이 나면 실패한 객체를 다시 처리해야 하며, 명령이 끝났다는 사실만으로 전체 재암호화를 완료했다고 판단해서는 안 된다. 대상 목록과 실패 항목을 대조하고, 재기록한 객체의 etcd 저장 접두어와 API 조회 결과를 확인한다. 재기록 작업은 Secret 값을 읽으므로 명령 출력과 작업 로그에 민감한 값이 남지 않도록 다룬다.

초기 전환 때 넣은 identity 제공자를 제거하려면 평문으로 남은 Secret이 없는지 먼저 확인한다. identity가 빠진 API 서버는 암호화되지 않은 기존 객체를 읽지 못한다. 반대로 확인을 마치고 identity를 제거하면 이후 평문 저장 값을 실수로 읽어들이는 경로도 닫힌다. 이 변경 역시 모든 API 서버에 순차적으로 적용해야 한다.

키 순환: 새 키를 읽게 한 뒤 쓰기에 사용한다

자체 관리 클러스터에서 키를 바꿀 때는 먼저 새 키를 기존 제공자의 후순위 키로 모든 제어 영역 노드에 추가한다. API 서버를 순차적으로 다시 시작해 모두가 두 키를 읽을 수 있게 한 다음, 새 키의 안전한 백업을 확보한다. 그 뒤 새 키를 키 목록의 맨 앞으로 옮기고 모든 API 서버를 다시 적용한다. 새로 기록되는 Secret은 이때부터 새 키로 암호화된다.

기존 Secret은 여전히 이전 키로 암호화된 채 남아 있을 수 있으므로 재기록을 반복한다. etcd에서 확인한 객체의 접두어 뒤 키 이름이 새 키로 바뀌었는지 살피고, 재기록에 실패한 객체를 처리한 뒤에만 이전 키를 설정에서 제거한다. 재기록 전에 이전 키를 없애면 해당 키로 암호화된 객체를 API 서버가 읽을 수 없다. 새 키의 모든 사본을 잃어도 복호화할 수 없으므로 etcd 백업의 복구 가능성은 데이터와 그 데이터를 읽을 키 자료를 함께 보존했는지에 달려 있다.

로컬 키를 설정 파일에 두는 방식에서는 제어 영역 호스트의 파일 접근 권한이 곧 키 보호 경계가 된다. 외부 KMS를 선택하면 키의 보관 위치가 달라지지만, API 서버가 KMS를 호출하는 데 필요한 인증 정보와 KMS 접근 권한은 계속 보호해야 한다. 어느 방식을 택하든 키를 교체하기 전에 이전 데이터의 읽기 경로와 백업 복구 경로를 함께 점검하는 순서가 중요하다.

GKE: 기본 보호 위에 Cloud KMS 계층을 더한다

GKE에서 Secret 암호화 키를 Cloud KMS 키로 감싸 상태 데이터베이스에 저장하는 관계

GKE는 Secret을 포함한 고객 데이터를 기본적으로 저장 암호화한다. 운영자가 Cloud KMS 키를 지정하는 애플리케이션 계층 암호화는 그 위에 추가되는 보호다. 이 구성에서 GKE는 데이터 암호화 키로 대상 객체를 암호화하고, 해당 데이터 키를 Cloud KMS의 키로 감싸 상태 데이터베이스에 저장한다. 따라서 자체 관리 클러스터의 기본 상태를 GKE에 그대로 적용하거나, GKE의 기본 저장 암호화를 운영자 지정 Cloud KMS 키 사용과 같은 뜻으로 해석하면 설정 범위를 잘못 판단하게 된다.

설정하려면 클러스터 위치에 맞는 Cloud KMS 키를 준비하고 Kubernetes Engine Service Agent에 그 키의 암호화·복호화 권한을 부여한다. 새 클러스터를 만들거나 기존 클러스터를 업데이트할 때 --database-encryption-key에 키의 전체 리소스 이름을 지정한다. 기존 클러스터에 기능을 켜면 GKE가 기존 Secret도 재암호화하며 제어 영역을 다시 시작한다. 영역 클러스터에서는 업데이트 중 제어 영역을 사용할 수 없는 구간을 계획에 반영해야 한다.

적용 범위는 gcloud container clusters describe의 databaseEncryption.state로 확인한다. ENCRYPTED는 Secret 대상의 애플리케이션 계층 암호화를, ALL_OBJECTS_ENCRYPTION_ENABLED는 상태 데이터베이스의 모든 Kubernetes API 객체 대상 암호화를 뜻한다. GKE가 사용하는 상태 저장소에 따라 대상 범위가 달라질 수 있다. GKE의 암호화 안내는 Cloud KMS 키 버전을 순환한 뒤 최소 3시간을 기다려 새 버전이 반영되도록 한 다음, Secret 대상 구성에서 기존 Secret을 다시 기록하도록 안내한다.

Secret 대상 구성에서는 네임스페이스별로 Secret을 읽어 주석을 갱신하는 방식으로 재암호화를 유도할 수 있다. 예를 들어 kubectl get secrets --namespace=NAMESPACE -o json | kubectl annotate --overwrite -f - encryption-key-rotation-time=TIME 명령에서 TIME에 해당 순환을 식별할 값을 넣는다. Standard 클러스터에서는 권한에 따라 전체 네임스페이스를 대상으로 할 수 있지만, Autopilot에서는 운영자가 소유한 네임스페이스만 직접 갱신할 수 있다. 모든 API 객체가 암호화되는 구성에는 이 수동 Secret 재기록 절차가 필요하지 않다.

Autopilot의 관리형 네임스페이스에는 이전 키 버전을 쓰는 Secret이 남을 수 있다. 그러므로 운영자가 직접 갱신한 Secret만 보고 이전 버전을 비활성화하거나 파기해서는 안 된다. 아직 사용 중인 Cloud KMS 키 버전이 파기되면 클러스터가 성능 저하 상태에 들어가 결국 동작하지 않을 수 있으며, 파기 완료 뒤에는 해당 버전으로 보호한 데이터를 복호화할 수 없다. 키 순환과 키 파기는 별개의 결정으로 다뤄야 한다.

RBAC: 저장 암호화가 지키지 않는 읽기 경로

저장 암호화는 etcd에 기록된 값을 보호하지만, Secret 읽기 권한이 있는 주체에게는 API 서버가 복호화한 값을 반환한다. 사람과 워크로드에는 업무에 필요한 Secret의 get 권한만 부여하고, 여러 Secret의 내용을 가져올 수 있는 list와 변경 내용을 계속 받는 watch는 더 엄격히 제한한다. etcd에 대한 읽기 접근도 클러스터 관리자에게 한정해야 한다.

Pod 생성 권한도 Secret 접근 경계에 포함된다. 사용자가 Secret을 직접 조회할 권한이 없어도 해당 Secret을 참조하는 Pod를 만들 수 있다면 값을 노출할 수 있다. 여러 컨테이너가 있는 Pod에서는 필요한 컨테이너에만 Secret을 전달하고, 애플리케이션이 읽은 값을 로그에 남기거나 신뢰할 수 없는 곳으로 전송하지 않도록 해야 한다. 이렇게 해야 저장소의 암호문과 실행 중 사용되는 평문을 각각 해당 경로에서 보호할 수 있다.

함께 읽기:

공유:

뉴스레터 구독

최신 Web3, AI, 암호화폐 뉴스를 이메일로 받아보세요.

0