쿠버네티스 운영 실무 · Secret 보관 방법 · 이론
Secret — 어디에 저장되고 누가 읽을 수 있는가
한 줄 요약
쿠버네티스 Secret 의 base64 는 인코딩이지 암호화가 아니고, 기본 설정에서는 etcd 에 평문으로 저장된다. etcd 디스크를 손에 넣은 사람은 모든 시크릿을 손에 넣는다.
왜 이게 필요했나
Secret 매니페스트를 처음 보면 값이 알아볼 수 없는 문자열로 되어 있어 암호화된 것처럼 보인다. 그러나 되돌리는 데 명령 하나면 된다.
kubectl get secret app-db -o jsonpath='{.data.password}' | base64 -d# pr0d-Db-Pass키도 없고 비밀도 없다. base64 는 바이너리를 텍스트로 안전하게 옮기기 위한 표현 방식일 뿐이다. 더 중요한 사실은 그다음이다. 기본 설정의 클러스터에서 apiserver 는 Secret 을 평문 그대로 etcd 에 쓴다. etcd 스냅샷 파일, 디스크 이미지, 백업 아카이브를 얻은 사람은 그 안에서 비밀번호 문자열을 그냥 읽을 수 있다. 앞 모듈에서 스냅샷을 뜨고 옮기는 법을 배웠으니, 그 파일이 얼마나 민감한 물건인지도 같이 알아야 한다.
어떻게 동작하나
대응은 층으로 쌓는다.
1층 — 저장 시 암호화(EncryptionConfiguration). apiserver 에 설정 파일을 물리면 Secret 을 etcd 에 쓰기 전에 암호화한다.
apiVersion: apiserver.config.k8s.io/v1kind: EncryptionConfigurationresources: - resources: [secrets] providers: - aescbc: keys: - name: key1 secret: <32바이트 base64 키> - identity: {}여기서 순서가 전부다. 목록의 첫 번째 프로바이더가 쓰기에 쓰이고, 읽을 때는 위에서부터 차례로 시도한다. identity 는 "암호화하지 않음"을 뜻하므로 마지막에 있어야 한다. 앞에 두면 그 순간부터 평문 저장으로 되돌아간다. 그리고 설정을 바꿔도 기존 오브젝트는 다시 쓰기 전까지 평문으로 남는다. 전체를 한 번 읽어서 다시 쓰는 작업이 필요하다.
2층 — 봉투 암호화(KMS v2). 키를 클러스터 설정 파일에 적어 두는 것은 결국 또 하나의 시크릿을 만드는 일이다. KMS 프로바이더는 이 문제를 두 층으로 나눈다. 데이터는 데이터 키(DEK)로 암호화하고, 그 데이터 키를 다시 KMS 의 마스터 키(KEK)로 암호화한다. 마스터 키는 클러스터 밖 하드웨어 보안 모듈이나 클라우드 KMS 에 있고, apiserver 는 유닉스 소켓으로 붙은 플러그인을 통해서만 접근한다. 키를 회전할 때 데이터를 전부 다시 암호화할 필요가 없다는 것도 큰 이점이다.
3층 — 접근 통제(RBAC). 저장을 암호화해도 API 로 읽을 수 있는 사람에게는 아무 소용이 없다. 어떤 네임스페이스에 get secrets 권한이 있는 사람은 그 네임스페이스의 모든 시크릿을 읽는다. 편의로 준 편집 권한이 사실상 프로덕션 자격증명 열람 권한인 경우가 매우 흔하다. 범위를 좁히려면 resourceNames 로 이름을 못 박는다.
rules: - apiGroups: [""] resources: ["secrets"] resourceNames: ["db-password"] verbs: ["get"]다만 resourceNames 는 list 와 watch 에는 통하지 않는다. 목록 조회는 대상 이름을 미리 알 수 없는 요청이기 때문이다. 그래서 이름 단위 제한은 get 을 좁히는 도구이고, list 는 애초에 주지 않는 것이 답이다.
4층 — 값을 클러스터 밖에 두기. External Secrets Operator 같은 도구는 실제 값을 시크릿 매니저에 두고 클러스터에는 참조만 커밋한다. SecretStore 가 "어디서 가져올지"를, ExternalSecret 이 "무엇을 어떤 이름으로 만들지"를 정의한다. 매니페스트에 값이 없으므로 깃에 그대로 커밋해도 된다. 다만 오퍼레이터가 결국 클러스터 Secret 을 만들어 내므로 앞의 RBAC 와 저장 시 암호화 문제는 그대로 남는다.
마지막으로 immutable: true 가 있다. 값을 못 바꾸게 잠그면 실수로 인한 변경이 막히고, kubelet 이 변경 감시를 하지 않아도 되어 부하도 준다. 대신 바꾸려면 새 이름으로 만들어 참조를 옮겨야 한다.
현장에서 만나는 모습
첫째, 대응 시간이 성숙도다. 시크릿 관리의 수준을 재는 질문은 "잘 숨겼는가"가 아니라 "지금 이 값이 공개됐다고 가정할 때 폐기하고 교체하는 데 몇 분이 걸리는가" 다. 그 답이 시간 단위라면 도구를 더 사는 것보다 로테이션 경로를 먼저 만들어야 한다.
둘째, 유출 대응의 순서. 깃 히스토리에서 키를 발견했을 때 첫 조치는 히스토리 재작성이 아니라 폐기다. 공개된 값은 수 초 만에 자동 스캐너에 수집되므로, 히스토리를 아무리 지워도 이미 복사된 값은 되돌릴 수 없다. 폐기 → 영향 조사 → 히스토리 정리가 올바른 순서다.
셋째, 환경변수는 저장 위치가 아니라 전달 방식이다. 컨테이너에 주입된 환경변수는 같은 호스트에서 /proc/<PID>/environ 으로 읽히고, 자식 프로세스에 상속되고, 크래시 리포트와 로그로 새어 나간다. 파일 마운트가 더 안전한 이유가 여기에 있다.
다음 실습에서 할 것
Secret 을 만들고 base64 를 직접 되돌려 본 뒤, 저장 시 암호화 설정과 KMS v2 봉투 암호화 설정을 작성한다. 외부 시크릿 저장소 연동 매니페스트를 값 없이 만들고, resourceNames 로 시크릿 하나만 여는 역할을 만들어 그 한계를 확인한다. 불변 Secret 을 만들어 수정이 거부되는 것을 보고, 마지막에는 etcd 를 직접 조회해 평문이 보이는 것을 증거로 남기고 대응 방안을 정리한다.