본문으로 건너뛰기

7.2 Secret

Secret은 암호, API 토큰, TLS 개인키처럼 민감한 데이터를 담는 오브젝트다. API 구조는 ConfigMap과 거의 같아서 만드는 방법도 주입하는 방법도 그대로 통한다. 다른 점은 다루는 데이터의 성격이고, 여기서 오해가 시작된다. Secret이라는 이름과 base64로 감싼 겉모습 때문에 저장만 하면 보호된다고 믿기 쉽지만, 기본 설정의 Secret에는 암호화가 없다. 무엇이 보호되고 무엇이 보호되지 않는지 경계선을 정확히 아는 것이 이 챕터의 목표다.

타입

type 필드는 Secret의 용도를 선언하고, 타입마다 요구하는 key 형식이 있다.

타입용도요구 key
Opaque임의의 사용자 데이터(기본값)제약 없음
kubernetes.io/tlsTLS 인증서와 개인키tls.crt, tls.key
kubernetes.io/dockerconfigjson프라이빗 레지스트리 인증.dockerconfigjson
kubernetes.io/service-account-tokenServiceAccount 토큰(레거시)자동 채움
kubernetes.io/basic-authID·비밀번호 쌍username, password

kubectl create secret generic으로 만들면 Opaque가 된다. kubernetes.io/tls는 Ingress의 TLS 종료 등에서 참조하는 형식이고 kubectl create secret tls로 만든다. kubernetes.io/dockerconfigjson은 Pod의 imagePullSecrets가 참조해 프라이빗 이미지를 받을 때 쓴다.

kubernetes.io/service-account-token은 이제 레거시에 가깝다. Kubernetes 1.24부터는 ServiceAccount를 만들어도 토큰 Secret이 자동 생성되지 않으며, Pod는 TokenRequest API로 발급된 만료 시간 있는 토큰을 projected volume으로 받는다. 이 타입을 새로 만드는 경우는 장기 유효 토큰이 꼭 필요한 예외 상황 정도다.

base64는 인코딩이지 암호화가 아니다

Secret 매니페스트의 data 필드가 base64로 적히는 이유는 바이너리 값을 YAML에 담기 위한 직렬화 형식이기 때문이다. 보호 장치가 아니다. 키 없이 즉시 되돌리는 변환이라, get 권한이 있는 사람은 한 줄로 원문을 복원한다.

kubectl create secret generic db-cred --from-literal=password='S3cr3t!'
kubectl get secret db-cred -o jsonpath='{.data.password}' | base64 -d
# S3cr3t!

매니페스트를 직접 쓸 때는 stringData 필드에 평문으로 적어도 된다. 저장될 때 서버가 base64로 변환해 data에 넣는다. 입력이 편해질 뿐 보안 수준은 같다.

같은 이유로 Secret 매니페스트를 Git 저장소에 커밋하면 비밀번호를 평문으로 커밋한 것과 같다. base64가 눈으로 바로 읽히지 않는다는 사실에 의미를 두면 안 된다.

저장 암호화와 접근 제한

기본 구성에서 kube-apiserver는 Secret을 암호화하지 않은 채 etcd에 기록한다. etcd의 데이터 파일이나 그 백업에 접근하는 사람은 클러스터의 모든 Secret을 읽게 된다. 이 경로를 막는 장치가 encryption at rest, 즉 EncryptionConfiguration이다.

apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
  - resources:
      - secrets
    providers:
      - aescbc:
          keys:
            - name: key1
              secret: <base64로 인코딩한 32바이트 키>
      - identity: {}

이 파일을 kube-apiserver의 --encryption-provider-config 플래그로 지정하면 이후 저장되는 Secret이 암호화된다. provider에는 aescbc, aesgcm 같은 로컬 키 방식과, 외부 키 관리 서비스에 위임하는 kms가 있다. KMS v2는 1.29에서 GA다. 관리형 Kubernetes 서비스라면 제공자가 이 구성을 대신 처리하는 경우가 많으므로 해당 서비스 문서에서 확인한다.

EncryptionConfiguration은 적용 이후에 저장되는 데이터에만 작동한다. 기존 Secret은 평문 그대로 남아 있으므로, 전체를 다시 저장해 암호화를 거치게 해야 한다. kubectl get secrets -A -o json | kubectl replace -f -를 실행한다.

encryption at rest가 etcd 유출에 대한 방어라면, API를 통한 접근은 RBAC로 제한한다. 여기서 자주 놓치는 것이 list와 watch다. Secret의 list는 이름 목록이 아니라 모든 Secret의 본문 전체를 반환하므로, list 권한은 사실상 전부 읽기 권한이다. watch도 같다. get, list, watch 모두 꼭 필요한 주체에게 namespace 단위로 최소한만 부여하고, 와일드카드 리소스 권한에 Secret이 포함되지 않았는지 검사한다. RBAC 설계 전반은 Part IX. 보안에서 다룬다. 참고로 kubelet은 자기 노드에 스케줄된 Pod가 참조하는 Secret만 API 서버에서 받도록 제한된다.

주입은 volume이 기본, 정본은 외부에

컨테이너로 전달할 때는 env 변수보다 volume 마운트가 안전한 쪽이다. env로 들어간 값은 프로세스 환경에 상주하므로 노출 경로가 많다. 애플리케이션이 크래시하면서 환경 전체를 덤프에 남기거나, 진단 엔드포인트가 환경을 출력하거나, 자식 프로세스에 그대로 상속된다. volume으로 마운트한 Secret은 노드에서 tmpfs에 저장되어 디스크에 기록되지 않고, 값이 바뀌면 파일 갱신도 전파된다. env 주입은 갱신도 되지 않는다.

    flowchart TD
  S["Secret"] --> E["etcd 저장"]
  S --> A["API 접근"]
  S --> P["Pod 주입"]
  E --> E2["기본 평문<br/>encryption at rest"]
  A --> A2["RBAC로<br/>get·list 최소화"]
  P --> P2["env보다<br/>volume 마운트"]
  

한 단계 더 나가면 정본(source of truth)을 클러스터 밖에 두는 구성이 있다. Git에 평문을 두지 않으면서 선언적 배포를 유지하려는 요구에서 나온 방식이다. External Secrets Operator는 AWS Secrets Manager, HashiCorp Vault 같은 외부 저장소의 값을 읽어 Kubernetes Secret으로 동기화하는 Operator다. 매니페스트에는 외부 저장소의 참조만 남고 실제 값은 클러스터가 실행 시점에 받아온다. Vault를 직접 쓰는 경우에는 sidecar 주입이나 CSI 드라이버로 Pod에 값을 직접 전달해 Kubernetes Secret 오브젝트를 아예 만들지 않는 구성도 가능하다. 어느 쪽이든 발급, 회전, 감사 기록을 외부 시스템이 담당하고 Kubernetes는 전달 통로가 된다.

정리

Secret의 보호는 계층별로 쌓아야 완성된다. base64는 보호가 아니므로 계산에 넣지 않고, etcd 계층은 EncryptionConfiguration으로, API 계층은 get, list, watch를 최소화한 RBAC로 막는다. Pod에는 env 대신 volume으로 전달하고, 규모가 커지면 External Secrets Operator나 Vault처럼 정본을 외부에 두는 방식을 검토한다.