Certified Kubernetes Application Developer (CKAD) #14 ServiceAccount와 RBAC (앱 관점)
#13 ConfigMap과 Secret 깊이에서 앱에 설정과 비밀 값을 주입하는 법을 익혔습니다. 그런데 앱이 단순히 값을 받기만 하는 것이 아니라, Pod 안에서 직접 쿠버네티스 API를 호출해야 할 때가 있습니다. 컨트롤러 패턴으로 다른 리소스를 읽거나, 자기 네임스페이스의 Pod 목록을 조회하거나, ConfigMap을 갱신하는 앱이 그렇습니다.
이때 “그 호출이 누구의 권한으로 일어나는가"를 정하는 것이 **ServiceAccount(SA)**와 RBAC입니다. 사람이 kubectl로 클러스터를 다룰 때 쓰는 신원이 사용자 계정이라면, Pod 안 앱이 API를 호출할 때 쓰는 신원이 ServiceAccount입니다. CKAD는 운영자 시험인 CKA만큼 RBAC을 깊게 묻지는 않지만, 앱에 적절한 SA를 붙이고 최소 권한을 주는 수준은 시험 범위입니다. 이번 글은 그 앱 관점에 집중하겠습니다.
Pod 안 앱이 API를 호출하는 그림 #
먼저 큰 그림을 잡겠습니다. 앱이 쿠버네티스 API를 호출하면 API 서버는 두 단계를 거칩니다.
- 인증(Authentication): 이 요청을 보낸 신원이 누구인가. Pod 안 앱은 ServiceAccount 토큰으로 자기 신원을 증명합니다.
- 인가(Authorization): 그 신원이 이 동작을 할 권한이 있는가. RBAC(Role-Based Access Control)이 “어떤 신원이 어떤 리소스에 어떤 동작을 할 수 있는지"를 규칙으로 판정합니다.
ServiceAccount는 1단계의 신원을 제공하고, RBAC은 2단계의 권한을 정의합니다. 둘은 RoleBinding으로 연결됩니다. 즉 SA를 만들고, Role로 권한을 정의하고, RoleBinding으로 그 둘을 묶는 흐름입니다. 이 흐름을 그대로 손에 익히면 시험 문제 대부분이 풀립니다. 쿠버네티스 인증,인가의 더 넓은 배경은 K8s 중급 #7에서 함께 정리했습니다.
ServiceAccount #
네임스페이스마다 있는 default SA #
ServiceAccount는 네임스페이스에 속하는 리소스입니다. 네임스페이스를 만들면 default라는 이름의 ServiceAccount가 자동으로 하나 생깁니다. Pod spec에 serviceAccountName을 명시하지 않으면, Pod는 자기 네임스페이스의 default SA로 실행됩니다.
# 현재 네임스페이스의 SA 목록
k get serviceaccount
k get sa # 축약형
# default SA 상세
k get sa default -o yamldefault SA에는 기본적으로 아무 권한도 부여되어 있지 않습니다. 그래서 default SA로 도는 앱이 API를 호출하면 대부분 Forbidden으로 거부됩니다. 권한이 필요한 앱에는 전용 SA를 따로 만들어 최소 권한만 부여하는 것이 정석입니다.
SA 생성 #
전용 SA는 명령형으로 한 줄에 만듭니다.
# app-sa라는 SA 생성
k create serviceaccount app-sa
# dry-run으로 매니페스트 뼈대만 뽑기
k create sa app-sa $do > sa.yaml생성된 매니페스트는 kind: ServiceAccount에 이름과 네임스페이스만 담긴 매우 단순한 형태입니다.
Pod에 SA 붙이기 #
Pod가 어떤 SA로 실행될지는 spec의 serviceAccountName으로 지정합니다.
apiVersion: v1
kind: Pod
metadata:
name: api-client
spec:
serviceAccountName: app-sa
containers:
- name: app
image: bitnami/kubectl
command: ["sleep", "3600"]실행 중인 Deployment의 SA를 바꿔야 할 때는 spec을 직접 편집하거나 set 명령을 씁니다.
# Deployment의 SA 교체
k set serviceaccount deployment web app-saserviceAccountName은 Pod 생성 시점에만 적용되므로, 기존 Pod의 SA를 바꾸려면 Pod를 다시 생성해야 합니다. Deployment라면 위 명령이 새 ReplicaSet으로 롤아웃을 일으켜 자동으로 교체됩니다.
토큰 마운트 #
자동 마운트되는 SA 토큰 #
Pod가 뜨면 쿠버네티스는 그 SA의 토큰을 컨테이너 안 정해진 경로에 자동으로 마운트합니다.
/var/run/secrets/kubernetes.io/serviceaccount/
├── token # SA의 JWT 토큰
├── ca.crt # API 서버 검증용 CA 인증서
└── namespace # Pod가 속한 네임스페이스 이름앱이나 클라이언트 라이브러리(in-cluster config)는 이 경로의 토큰을 읽어 API 서버에 인증합니다. 컨테이너 안에서 직접 확인해 보면 토큰이 마운트되어 있는 것을 볼 수 있습니다.
k exec api-client -- cat /var/run/secrets/kubernetes.io/serviceaccount/token
k exec api-client -- ls /var/run/secrets/kubernetes.io/serviceaccount/자동 마운트 끄기 #
API를 호출하지 않는 앱에까지 토큰을 마운트하는 것은 불필요한 노출입니다. 최소 권한 원칙에 따라, 토큰이 필요 없는 Pod는 자동 마운트를 끄는 것이 안전합니다. automountServiceAccountToken: false로 끕니다.
apiVersion: v1
kind: Pod
metadata:
name: no-token
spec:
automountServiceAccountToken: false
containers:
- name: app
image: nginx이 설정은 ServiceAccount 쪽에도 둘 수 있습니다. SA에 걸면 그 SA를 쓰는 모든 Pod에 기본 적용되고, Pod spec의 설정이 SA 설정보다 우선합니다.
apiVersion: v1
kind: ServiceAccount
metadata:
name: app-sa
automountServiceAccountToken: falseprojected token: audience와 expiration #
요즘 쿠버네티스는 자동 마운트되는 토큰을 projected volume으로 주입합니다. 이 방식은 토큰에 **수명(expiration)**과 **대상(audience)**을 지정할 수 있어, 무기한 유효한 토큰보다 안전합니다. 자동 마운트되는 기본 토큰은 클러스터가 알아서 이 형태로 넣어 주지만, 특정 audience나 만료 시간이 필요하면 직접 projected volume으로 명시합니다.
apiVersion: v1
kind: Pod
metadata:
name: projected-token
spec:
serviceAccountName: app-sa
containers:
- name: app
image: nginx
volumeMounts:
- name: token
mountPath: /var/run/secrets/tokens
readOnly: true
volumes:
- name: token
projected:
sources:
- serviceAccountToken:
path: token
audience: my-api # 토큰을 받을 대상 식별자
expirationSeconds: 3600 # 1시간 후 만료expirationSeconds로 토큰 수명을 제한하고, audience로 그 토큰이 어느 서비스를 향한 것인지를 못 박습니다. kubelet은 만료가 가까워지면 토큰을 자동으로 갱신해 다시 마운트합니다.
RBAC: 앱에 권한 주기 #
SA를 붙이는 것만으로는 권한이 생기지 않습니다. default SA가 그렇듯, 새로 만든 SA도 처음에는 아무것도 못 합니다. 권한을 주려면 Role로 권한을 정의하고 RoleBinding으로 그 권한을 SA에 연결해야 합니다.
Role: 네임스페이스 범위의 권한 묶음 #
Role은 “어떤 리소스(resources)에 어떤 동작(verbs)을 허용하는가"를 한 네임스페이스 안에서 정의합니다. 동작은 get, list, watch, create, update, patch, delete 같은 API 동사입니다.
# pod에 대한 get/list/watch 권한을 가진 Role 생성
k create role pod-reader \
--verb=get,list,watch \
--resource=pods
# dry-run으로 매니페스트 확인
k create role pod-reader --verb=get,list,watch --resource=pods $do생성된 Role 매니페스트는 rules 아래에 apiGroups,resources,verbs 세 가지가 들어갑니다. core API 그룹(pod, service, configmap 등)의 apiGroups는 빈 문자열 ""라는 점이 자주 틀리는 부분입니다. 전체 형태는 아래 합본 예제에서 확인하겠습니다.
RoleBinding: Role을 SA에 연결 #
RoleBinding은 Role이 정의한 권한을 특정 주체(subject)에게 부여합니다. 주체는 사용자일 수도, 그룹일 수도, ServiceAccount일 수도 있습니다. 앱 관점에서는 SA가 주체입니다.
# pod-reader Role을 dev 네임스페이스의 app-sa SA에 부여
k create rolebinding app-sa-pod-reader \
--role=pod-reader \
--serviceaccount=dev:app-sa--serviceaccount 값은 네임스페이스:SA 이름 형식입니다. 생성된 RoleBinding은 roleRef(어떤 권한을 줄지)와 subjects(누구에게 줄지) 두 부분으로 이루어집니다. 이제 app-sa로 도는 Pod는 dev 네임스페이스에서 Pod를 조회할 수 있습니다. 완성된 형태는 아래 합본 예제에서 함께 보겠습니다.
권한 검증: auth can-i #
권한이 제대로 붙었는지는 kubectl auth can-i로 확인합니다. --as 플래그로 특정 신원을 가장해서 검사하는 것이 핵심입니다. SA의 신원 문자열은 system:serviceaccount:네임스페이스:SA 이름 형식입니다.
# app-sa가 dev에서 pod를 get할 수 있는가
k auth can-i get pods \
--as=system:serviceaccount:dev:app-sa \
-n dev
# -> yes
# app-sa가 pod를 delete할 수 있는가 (Role에 없음)
k auth can-i delete pods \
--as=system:serviceaccount:dev:app-sa \
-n dev
# -> noyes 또는 no로 즉시 답이 나오므로, RBAC 문제를 풀고 나서 검산하는 가장 빠른 방법입니다. 시험에서 권한을 부여했다면 반드시 이 명령으로 한 번 확인하는 습관을 들이는 것이 안전합니다.
ClusterRole과 ClusterRoleBinding #
Role과 RoleBinding이 한 네임스페이스 안에서만 유효한 반면, ClusterRole과 ClusterRoleBinding은 클러스터 전체 범위입니다. 노드처럼 네임스페이스에 속하지 않는 리소스나, 여러 네임스페이스에 걸친 권한이 필요할 때 씁니다. 앱 관점에서는 대부분 네임스페이스 범위로 충분하므로 Role,RoleBinding을 우선 쓰고, ClusterRole의 상세한 활용은 운영자 시험인 CKA 영역에서 더 깊이 다룹니다.
전체 예제: SA + Role + RoleBinding + Pod #
지금까지의 조각을 하나로 합치면, 전용 SA로 도는 앱에 Pod 조회 권한만 최소로 부여하는 완성된 매니페스트가 됩니다.
# ServiceAccount: 앱 전용 신원
apiVersion: v1
kind: ServiceAccount
metadata:
name: app-sa
namespace: dev
---
# Role: dev 네임스페이스에서 pod를 읽는 권한
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: pod-reader
namespace: dev
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]
---
# RoleBinding: pod-reader 권한을 app-sa에 부여
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: app-sa-pod-reader
namespace: dev
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: pod-reader
subjects:
- kind: ServiceAccount
name: app-sa
namespace: dev
---
# Pod: app-sa로 실행되는 앱
apiVersion: v1
kind: Pod
metadata:
name: api-client
namespace: dev
spec:
serviceAccountName: app-sa
containers:
- name: app
image: bitnami/kubectl
command: ["sleep", "3600"]적용한 뒤 Pod 안에서 직접 권한을 확인해 보겠습니다. SA 토큰이 자동 마운트되어 있으므로, 컨테이너 안의 kubectl은 그 토큰으로 인증합니다.
k apply -f app-rbac.yaml
# 권한이 붙었는지 가장 신뢰도 높은 검증: 실제 Pod 안에서 호출
k exec -n dev api-client -- kubectl get pods
# -> dev 네임스페이스의 Pod 목록이 출력되면 성공
k exec -n dev api-client -- kubectl get secrets
# -> Forbidden. Role에 secret 권한이 없으므로 거부됨마지막 두 명령이 보여 주듯, 부여한 권한 안에서만 동작하고 그 밖은 거부됩니다. 이것이 최소 권한 원칙이 실제로 작동하는 모습입니다.
시험 포인트 #
- SA는 네임스페이스 리소스입니다. 명시하지 않으면 Pod는
defaultSA로 실행되고, default SA에는 권한이 없습니다. - Pod에 SA를 붙이는 필드는
spec.serviceAccountName입니다. 실행 중 Deployment는k set serviceaccount deploy <이름> <sa>로 교체합니다. - SA 토큰은
/var/run/secrets/kubernetes.io/serviceaccount/에 자동 마운트됩니다. 필요 없으면automountServiceAccountToken: false로 끕니다(Pod spec이 SA 설정보다 우선). - 권한 부여는 Role(권한 정의) + RoleBinding(SA에 연결) 두 단계입니다.
k create role과k create rolebinding --serviceaccount=ns:sa를 한 줄로 만드는 손에 익히는 것이 시간을 아낍니다. - 검산은
k auth can-i <verb> <resource> --as=system:serviceaccount:<ns>:<sa> -n <ns>.yes/no로 즉시 확인됩니다. - ClusterRole,ClusterRoleBinding은 클러스터 범위입니다. 앱 관점에서는 Role,RoleBinding을 우선 사용합니다.
- Role의
rules에서 core API 그룹(pod, service, configmap 등)의apiGroups는 빈 문자열""입니다. 이 점을 자주 틀립니다.
정리 #
이번 글에서 잡은 것:
- ServiceAccount는 Pod 안 앱이 API를 호출할 때 쓰는 신원입니다. 전용 SA를 만들어 최소 권한만 부여하는 것이 정석입니다.
- 토큰 자동 마운트는
automountServiceAccountToken으로 제어하고, projected token으로 audience와 만료 시간을 지정할 수 있습니다. - RBAC은 Role로 권한을 정의하고 RoleBinding으로 SA에 연결하는 두 단계입니다.
- 검증은
k auth can-i ... --as=system:serviceaccount:...로 즉시 확인합니다. - 권한은 항상 필요한 만큼만 부여합니다. 이것이 다음 글의 컨테이너 권한 제한과 함께 앱 보안의 두 축을 이룹니다.
다음: SecurityContext와 Capabilities #
이번 글은 앱이 API 서버에 대해 무엇을 할 수 있는지를 RBAC으로 제한했습니다. 다음 글은 시선을 한 단계 안쪽으로 옮겨, 컨테이너가 자기가 도는 노드와 커널에 대해 무엇을 할 수 있는지를 제한하겠습니다.
#15 SecurityContext와 Capabilities에서는 runAsUser와 runAsNonRoot로 컨테이너를 비루트로 실행하는 법, fsGroup으로 볼륨 소유권을 맞추는 법, readOnlyRootFilesystem으로 루트 파일시스템을 읽기 전용으로 잠그는 법, 그리고 리눅스 capabilities를 add/drop으로 정밀하게 조정하는 법을 직접 만들어 보며 정리하겠습니다.