9.3 ServiceAccount
9.1에서 사람 사용자는 클러스터 안에 대응하는 오브젝트가 없다고 했다. Pod는 반대다. Pod 안의 프로세스가 apiserver를 호출할 때 제시하는 신원인 ServiceAccount는 네임스페이스에 속하는 실제 API 오브젝트이며, 생성하고 조회하고 삭제하는 대상이다. Prometheus가 Pod 목록을 수집하고 컨트롤러가 리소스 변경을 감시하는 클러스터 내부 API 호출의 신원이 전부 여기서 나온다.
default ServiceAccount와 신원 형식
모든 네임스페이스에는 default라는 이름의 ServiceAccount가 자동으로 만들어진다. Pod spec에 serviceAccountName을 지정하지 않으면 이 default가 연결되므로, 클러스터의 모든 Pod는 예외 없이 어떤 ServiceAccount의 신원을 가진다.
kubectl create serviceaccount app-sa -n dev
kubectl get serviceaccounts -n devRBAC 관점에서 ServiceAccount의 사용자 이름은 system:serviceaccount:<네임스페이스>:<이름> 형식이다. dev 네임스페이스의 app-sa라면 system:serviceaccount:dev:app-sa가 되고, 그룹으로는 system:serviceaccounts(클러스터 전체)와 system:serviceaccounts:dev(네임스페이스 단위)에 속한다. 9.2에서 본 RoleBinding의 subject에 이 이름을 적으면 Pod에 권한이 연결된다.
projected 토큰: 만료와 audience가 생겼다
Pod가 실제로 제시하는 자격 증명은 JWT 형식의 bearer 토큰이다. 이 토큰의 발급 방식이 크게 바뀌었다. 과거에는 ServiceAccount를 만들면 만료가 없는 토큰을 담은 Secret이 자동으로 생성됐고, 그 Secret이 Pod에 mount됐다. 만료가 없으니 유출되면 영구히 유효했고, 무효화하려면 Secret을 지우고 토큰을 재발급하는 수밖에 없었다.
지금의 기본 방식은 TokenRequest API 기반의 projected 토큰이다(1.22에서 GA). kubelet이 Pod를 시작할 때 apiserver의 TokenRequest API를 호출해 토큰을 발급받고, projected 볼륨으로 컨테이너에 넣는다.
flowchart TD
K["kubelet"] -->|"TokenRequest 호출"| A["apiserver"]
A -->|"단기 토큰 발급"| K
K -->|"projected 볼륨"| P["Pod 컨테이너"]
P -->|"Bearer 토큰 제시"| A2["apiserver API 호출"]
이 토큰에는 무기한 Secret 토큰에 없던 세 가지 속성이 있다.
- 만료(expiration): 기본 1시간짜리 단기 토큰이고, kubelet이 만료 전에 새 토큰을 받아 볼륨 내용을 교체한다. 유출돼도 유효 시간이 짧다.
- audience: 토큰의 aud claim에 사용처가 기록된다. apiserver용으로 발급된 토큰을 다른 서비스에 제시하면 검증에 실패한다.
- Pod 바인딩: 토큰이 특정 Pod에 묶여 있어, Pod가 삭제되면 남은 유효 시간과 무관하게 토큰도 무효가 된다.
1.24부터는 ServiceAccount를 만들어도 무기한 Secret 토큰이 자동 생성되지 않는다. 클러스터 밖에서 쓸 토큰이 필요하면 명령으로 단기 토큰을 발급받는다.
kubectl create token app-sa -n dev --duration=10m만료 없는 토큰이 반드시 필요한 예외 상황이라면 kubernetes.io/service-account-token 타입의 Secret을 직접 만들어 연결하는 방법이 남아 있지만, 공식 문서는 TokenRequest 방식이 불가능할 때의 차선책으로만 안내한다.
토큰은 컨테이너 안 /var/run/secrets/kubernetes.io/serviceaccount/ 경로에 mount된다. token 파일과 함께 apiserver 인증서를 검증할 ca.crt, 현재 네임스페이스가 적힌 namespace 파일이 같이 들어 있다.
automountServiceAccountToken: false
대부분의 애플리케이션은 apiserver를 호출하지 않는다. 웹 서버, 배치 작업, 데이터 처리 워크로드가 그렇다. 그런 Pod에도 토큰이 기본으로 mount되는데, 컨테이너가 침해당하면 이 토큰이 곧바로 탈취 대상이 된다. 토큰에 연결된 권한이 없더라도 인증된 신원 자체가 정찰의 발판이 된다. system:serviceaccounts 그룹에 부여된 권한이 있다면 그대로 물려받는 것도 문제다.
API 호출이 필요 없는 워크로드는 mount 자체를 끈다. ServiceAccount 수준과 Pod 수준 양쪽에서 설정하며, 둘이 다르면 Pod 쪽 값이 우선한다.
apiVersion: v1
kind: ServiceAccount
metadata:
name: no-api-sa
namespace: dev
automountServiceAccountToken: falseapiVersion: v1
kind: Pod
metadata:
name: web
namespace: dev
spec:
serviceAccountName: no-api-sa
automountServiceAccountToken: false
containers:
- name: app
image: nginx:1.27imagePullSecrets 연결
ServiceAccount에는 토큰 외의 부가 기능이 하나 더 있다. private registry 자격 증명(docker-registry 타입 Secret)을 ServiceAccount에 연결해 두면, 그 계정을 쓰는 Pod의 spec에 imagePullSecrets가 자동으로 추가된다. Pod마다 registry Secret을 반복해서 적을 필요가 없어진다.
kubectl create secret docker-registry regcred -n dev \
--docker-server=registry.example.com \
--docker-username=deploy --docker-password='<password>'
kubectl patch serviceaccount default -n dev \
-p '{"imagePullSecrets": [{"name": "regcred"}]}'이 주입을 수행하는 것이 apiserver의 ServiceAccount 어드미션 컨트롤러다. Pod 생성 요청이 저장되기 전에 spec을 고쳐 넣는 mutating 동작으로, 어드미션 제어의 대표적인 내장 사례이기도 하다.
정리
- 모든 Pod는 ServiceAccount 신원을 가지며, RBAC에서는
system:serviceaccount:<네임스페이스>:<이름>으로 참조한다. default에는 권한을 부여하지 않는다. - 토큰은 TokenRequest API 기반 projected 방식이 기본이다. 만료와 audience가 있고 Pod에 바인딩되며, 1.24부터 무기한 Secret 토큰의 자동 생성이 중단됐다.
- apiserver를 호출하지 않는 워크로드는
automountServiceAccountToken: false로 토큰 mount를 끈다.