RBAC / ServiceAccount 깊이
14장 RBAC의 기본 위에 운영 클러스터에서 마주치는 깊이를 한 층 더 얹습니다. ClusterRole을 라벨로 합치는 Aggregated ClusterRole, 다른 주체의 권한으로 호출하는 Impersonation, ServiceAccount 토큰이 영구 Secret에서 만료 · audience · rotation을 갖춘 projected token으로 바뀐 흐름, 그리고 EKS의 IRSA · GKE의 Workload Identity로 K8s ServiceAccount를 클라우드 IAM과 묶는 모델까지 한 사이클로 정리합니다.
14장 RBAC / NetworkPolicy / ResourceQuota에서 RBAC의 네 객체 (Role, ClusterRole, RoleBinding, ClusterRoleBinding)와 권한을 받는 세 주체 (User, Group, ServiceAccount)를 다뤘습니다. “최소 권한"이라는 원칙과 표준 ClusterRole을 RoleBinding으로 네임스페이스에 한정해 쓰는 패턴까지가 그 챕터의 마무리였습니다. 이번 챕터에서는 그 위에 한 층 더 들어가는 주제 넷을 정리합니다 — Aggregated ClusterRole (권한 묶음을 라벨로 확장), Impersonation (다른 주체의 권한으로 일시적으로 호출), ServiceAccount 토큰의 lifecycle 변화 (K8s 1.22의 projected token 기본화와 1.24의 legacy secret 자동 생성 중단), 외부 IAM과의 연결 (EKS의 IRSA, GKE의 Workload Identity).
이번 챕터의 끝에서는 K8s의 ServiceAccount가 클러스터 안의 RBAC 권한과 클러스터 바깥의 클라우드 IAM 권한을 동시에 들고 있는 운영의 표준 모양이 손에 들어옵니다. 29장 시크릿 운영의 “비밀번호 0” 패턴의 핵심 토대가 본 챕터의 IRSA / Workload Identity입니다.
Aggregated ClusterRole — 라벨로 합쳐지는 권한 묶음 #
운영 클러스터에서 표준 ClusterRole을 직접 손대지 않고 권한을 확장하고 싶을 때가 있습니다. 예를 들어 K8s가 미리 만들어 두는 view ClusterRole은 모든 표준 자원의 read 권한을 묶어 둔 객체입니다. 우리 팀이 CRD (CustomResourceDefinition)로 정의한 새 자원 종류를 추가했을 때, “view 권한을 가진 사람은 이 새 자원도 자동으로 읽을 수 있어야” 한다고 칠 때, view ClusterRole을 직접 수정하지 않고도 이 동작을 표현할 수 있어야 합니다.
이 빈 곳을 채우는 객체가 Aggregated ClusterRole입니다. 모델은 단순합니다 — 한 ClusterRole이 자기 권한 목록을 직접 적는 대신, aggregationRule로 라벨 셀렉터를 적어 두면 K8s가 그 셀렉터에 맞는 다른 ClusterRole 들의 rules를 모아 합쳐 줍니다.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: view
aggregationRule:
clusterRoleSelectors:
- matchLabels:
rbac.authorization.k8s.io/aggregate-to-view: "true"
rules: [] # 비어 있음 — 컨트롤러가 자동으로 채움rules가 비어 있는 이유가 핵심입니다. K8s의 RBAC 컨트롤러가 클러스터의 모든 ClusterRole을 훑어서, 라벨이 rbac.authorization.k8s.io/aggregate-to-view: "true" 인 것들의 rules를 모아 위 ClusterRole의 rules 필드에 채워 줍니다. 새 권한을 추가하고 싶을 때는 그 라벨이 붙은 새 ClusterRole을 하나 만들면 됩니다.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: my-crd-view
labels:
rbac.authorization.k8s.io/aggregate-to-view: "true"
rules:
- apiGroups: ["myteam.example.com"]
resources: ["widgets"]
verbs: ["get", "list", "watch"]이 매니페스트 한 장을 적용하면 그 순간부터 표준 view ClusterRole에 widgets 자원의 read 권한이 자동으로 합쳐집니다. view를 RoleBinding으로 받고 있던 모든 사용자가 코드 한 줄 안 바꾸고 새 자원의 read 권한을 갖게 됩니다.
표준 라벨은 셋입니다 — aggregate-to-view, aggregate-to-edit, aggregate-to-admin. K8s가 미리 만들어 두는 view / edit / admin ClusterRole이 각각의 라벨로 권한을 모읍니다. 18장 CRD와 Operator 패턴에서 다룰 CRD를 도입한 운영 팀이 사용자 정의 RBAC를 가장 깔끔하게 확장하는 길이 이 모델입니다. 권한의 source of truth가 흩어지지만, 표준 ClusterRole의 의미를 자연스럽게 확장한다는 이점이 큽니다.
Impersonation — 다른 주체의 권한으로 호출하기 #
운영 중에는 “이 사용자는 어떤 동작을 할 수 있을까"를 미리 확인하고 싶을 때가 있습니다. 사람 사용자에게 새 권한을 부여하기 전에 그 권한으로 어떤 동작이 가능한지 검증하거나, 외부에서 들어온 권한 이슈를 재현해 보거나, ServiceAccount의 권한 범위를 확인하는 경우입니다.
K8s의 Impersonation 기능은 이 요구를 채워 줍니다. 호출자가 자기 자격으로 API를 부르면서 “이 호출은 사용자 X로 행세해 달라” 고 헤더를 같이 보내면, API 서버가 권한 검사를 그 X의 권한으로 수행합니다. 호출자 자신의 권한이 아니라 X의 권한이 적용되므로, 호출자에게는 먼저 impersonate 권한이 있어야 합니다.
kubectl --as=alice@example.com get pods -n payments
kubectl --as=system:serviceaccount:default:my-sa get secrets--as 옵션이 호출자 측의 impersonation 표현입니다. 위 첫 줄은 “alice@example.com 사용자의 권한으로 payments 네임스페이스의 Pod를 조회” 하는 호출이고, 두 번째 줄은 “default 네임스페이스의 my-sa ServiceAccount의 권한으로 Secret을 조회” 하는 호출입니다. 14장 §“kubectl auth can-i"에서 한 번 짚었던 옵션의 깊이를 본 챕터에서 펼칩니다.
이 호출이 통하려면 호출자의 RBAC에 다음 권한이 있어야 합니다.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: user-impersonator
rules:
- apiGroups: [""]
resources: ["users", "groups", "serviceaccounts"]
verbs: ["impersonate"]impersonate verb이 핵심입니다. 이 권한이 있는 사용자는 자기 권한을 넘어서 다른 사용자의 권한으로 호출을 보낼 수 있으므로, 사실상 클러스터의 권한 모델 전체를 우회할 수 있는 권한입니다. impersonate 권한 자체가 매우 민감해서, 운영 환경에서는 SRE 또는 보안팀의 소수 인원에게만 부여합니다.
권한 점검의 표준 도구 — kubectl auth can-i #
impersonation의 일상적인 활용은 보통 kubectl auth can-i 명령과 묶입니다.
kubectl auth can-i create secrets --as=alice@example.com -n paymentsyes이 호출은 실제로 Secret을 만들지는 않습니다. API 서버의 권한 결정 로직만 돌려서 yes / no를 돌려줍니다. 새 RoleBinding을 적용하기 전에 그 의도가 맞게 풀리는지 확인할 때, 또는 권한 이슈가 들어왔을 때 어디가 막히는지 짚을 때 가장 먼저 부르는 도구입니다. 디버깅 트리의 완성된 흐름은 27장 kubectl 디버깅 패턴에서 정리합니다.
ServiceAccount 토큰 — legacy secret에서 projected token으로 #
14장에서 ServiceAccount의 토큰이 Pod 안의 /var/run/secrets/kubernetes.io/serviceaccount/token에 자동으로 마운트된다고 짚었습니다. 이 흐름은 두 번 나뉘어 바뀌었습니다. K8s 1.22에서는 projected token이 기본이 되었고, K8s 1.24에서는 legacy secret의 자동 생성이 중단되었습니다. 운영 클러스터에서 자주 부딪히는 변화이므로 순서대로 짚어 둡니다.
옛 모델 — 자동 생성되는 Secret 객체의 영구 토큰 #
K8s 1.21 까지의 기본 동작은 다음과 같았습니다. ServiceAccount를 만들면 K8s가 자동으로 같은 이름의 Secret 객체를 만들고, 그 Secret 안에 ServiceAccount의 JWT 토큰을 넣어 두었습니다. 이 토큰은 만료가 없는 영구 토큰이었습니다. Pod가 그 ServiceAccount를 쓰면 kubelet이 그 Secret을 마운트해서 토큰을 컨테이너 안에 넣어 주는 방식입니다. 6장 ConfigMap과 Secret의 Secret type 표에서 kubernetes.io/service-account-token으로 짧게 짚었던 그 모델입니다.
이 모델의 문제는 둘이었습니다.
- 만료가 없다 — 토큰이 한 번 외부로 새면 그 ServiceAccount를 지우거나 토큰을 회전하기 전까지 영구히 유효합니다.
- Pod와 토큰이 분리된다 — Secret 안의 토큰은 Pod의 생애주기와 무관합니다. 토큰의 audience (누구를 위한 토큰인가) 정보도 없습니다.
새 모델 — Projected Token (Bound ServiceAccount Token) #
K8s 1.22부터 projected token이 기본 동작이 되었습니다. ServiceAccount를 만들어도 legacy Secret 객체가 자동으로 생기지는 않습니다. 대신 Pod가 뜰 때 kubelet이 **그 Pod 전용의 단기 JWT 토큰을 발급해서 Pod의 파일시스템에 직접 마운트합니다.**이 토큰의 특징은 셋입니다.
- 만료가 있다 — 기본 1시간, 옵션으로 더 짧게 / 더 길게 조정 가능합니다. kubelet이 만료 전에 자동으로 새 토큰을 발급해 다시 마운트합니다 (rotate).
- audience가 명시된다 — 이 토큰이 어느 API 서버의 호출에만 유효한지가 토큰 안에 적혀 있습니다.
- Pod에 묶인다 — 토큰이 Pod의 UID와 묶여 있어서, Pod가 사라지면 그 토큰도 더 이상 유효하지 않습니다.
spec:
containers:
- name: app
volumeMounts:
- name: kube-api-access
mountPath: /var/run/secrets/kubernetes.io/serviceaccount
readOnly: true
volumes:
- name: kube-api-access
projected:
sources:
- serviceAccountToken:
path: token
expirationSeconds: 3607
audience: ""
- configMap:
name: kube-root-ca.crt
items:
- key: ca.crt
path: ca.crt
- downwardAPI:
items:
- path: namespace
fieldRef:
fieldPath: metadata.namespacekubelet이 Pod를 만들 때 이 projected 볼륨을 자동으로 추가합니다. 매니페스트에 직접 적지 않아도 모든 Pod에 들어가는 기본 동작입니다. 컨테이너 안의 경로 (/var/run/secrets/...)는 옛 모델과 같으므로, 컨테이너 안의 코드는 토큰 모델 변화를 신경 쓸 필요가 없습니다.
옛 방식이 필요할 때 — 명시적 Secret #
CI 파이프라인의 외부 도구가 K8s API를 호출할 때, 또는 IDE의 K8s 플러그인이 한 번 받아 두고 계속 쓸 토큰이 필요할 때처럼 만료 없는 토큰이 필요한 경우가 남아 있습니다. 이런 경우는 이제 명시적으로 Secret을 만들어야 합니다.
apiVersion: v1
kind: Secret
metadata:
name: my-sa-token
annotations:
kubernetes.io/service-account.name: my-sa
type: kubernetes.io/service-account-tokentype과 annotation이 핵심입니다. 이 Secret을 만들면 K8s가 그 안에 my-sa의 영구 토큰을 채워 줍니다. 만료가 없는 토큰이므로 외부 노출 시 영향이 큽니다 — 운영에서는 가능한 한 projected token을 쓰고, 정말 필요한 경우에만 명시적 Secret을 만들고 토큰 회전 정책을 같이 잡아 두는 편이 안전합니다. 회전 정책의 본격적인 운영 패턴은 29장 시크릿 운영에서 다룹니다.
외부 IAM과의 연결 — IRSA와 Workload Identity #
지금까지의 이야기는 K8s API 자체에 대한 권한이었습니다. 그러나 운영 클러스터의 워크로드는 K8s API 외에도 클라우드의 다른 서비스를 부릅니다 — S3 버킷, RDS 데이터베이스, KMS의 키, Secrets Manager의 비밀. 이 호출들은 K8s의 RBAC 바깥의 영역, 즉 클라우드의 IAM에서 권한이 평가됩니다.
전통적인 방법은 클라우드 자격 증명 (access key + secret key)을 K8s Secret에 넣어서 Pod에 마운트하는 것이었습니다. 이 방법의 문제는 명백합니다 — 자격 증명이 Secret으로 한 번 들어오면 만료가 없고, 회전이 어렵고, 한 번 새면 그 영향이 큽니다.
EKS의 **IRSA (IAM Roles for Service Accounts)**와 GKE의 Workload Identity는 이 문제를 같은 방식으로 풉니다 — K8s의 ServiceAccount를 클라우드의 IAM Role과 연결하고, projected token을 IAM의 임시 자격 증명으로 교환 합니다. 자격 증명을 정적으로 보관하지 않고, 호출 시점에 단기 토큰을 발급받아 쓰는 모델입니다.
IRSA의 흐름 — EKS의 ServiceAccount + IAM Role #
IRSA의 셋업은 세 단계입니다.
1. EKS 클러스터에 OIDC provider 활성화
→ EKS의 ServiceAccount JWT 토큰을 AWS IAM이 신뢰하도록 설정
2. AWS IAM Role을 만들고 trust policy에 위 OIDC provider + 특정 ServiceAccount 적기
→ "ns/my-app의 SA = app-sa가 발급한 토큰만 이 Role을 가져갈 수 있다"
3. K8s ServiceAccount에 Role ARN annotation 부착
→ eks.amazonaws.com/role-arn: arn:aws:iam::ACCOUNT:role/my-app-role이 셋업이 끝난 클러스터에서 Pod가 AWS API를 부르면 다음 흐름이 자동으로 돕니다.
1. Pod에 마운트된 projected token 읽음
2. AWS STS의 AssumeRoleWithWebIdentity API에 그 토큰 보냄
3. STS가 토큰을 EKS OIDC provider로 검증
4. trust policy에 적힌 ServiceAccount와 일치하는지 확인
5. 일치하면 IAM Role의 임시 자격 증명(15분~12시간) 반환
6. AWS SDK가 그 자격 증명으로 S3 호출이 모든 단계가 AWS SDK 안에서 자동으로 일어나므로, 애플리케이션 코드는 자격 증명을 직접 다루지 않습니다. 코드는 그냥 boto3.client('s3')를 호출하면 끝이고, SDK 내부의 자격 증명 체인이 위 흐름을 따라갑니다.
apiVersion: v1
kind: ServiceAccount
metadata:
name: app-sa
namespace: my-app
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/my-app-s3-role
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
namespace: my-app
spec:
replicas: 1
selector:
matchLabels:
app: app
template:
metadata:
labels:
app: app
spec:
serviceAccountName: app-sa # ← 위에서 만든 SA 사용
containers:
- name: app
image: my-app:latestServiceAccount 한 개와 IAM Role 한 개가 1:1로 묶이고, 그 ServiceAccount를 쓰는 모든 Pod가 그 IAM Role의 권한을 갖습니다. 정적 자격 증명을 K8s Secret에 보관할 필요가 사라집니다. EKS 위 IRSA의 실전 셋업 (Terraform으로 OIDC provider · IAM Role 만들기 등)은 21장 EKS 클러스터 셋업에서 본격적으로 다루고, RDS IAM auth와 결합한 “DB 비밀번호 0” 패턴은 23장 DB 연동 — RDS · External Secrets에서 정리합니다.
GKE Workload Identity — 같은 모델, 다른 이름 #
GKE의 Workload Identity도 본질적으로 같은 모델입니다. K8s ServiceAccount를 GCP IAM의 Service Account와 묶고, projected token을 GCP STS로 교환해 임시 자격 증명을 받는 흐름입니다.
apiVersion: v1
kind: ServiceAccount
metadata:
name: app-sa
namespace: my-app
annotations:
iam.gke.io/gcp-service-account: my-app-sa@PROJECT.iam.gserviceaccount.comannotation의 키와 값 형식만 다릅니다. AKS도 Azure Workload Identity로 같은 모델을 도입했고, OIDC + STS 기반 토큰 교환이라는 동일한 구조 위에 클라우드 사업자별 어댑터가 얹혀 있는 모양입니다.
도입 시 잡아 둘 운영 원칙 #
IRSA와 Workload Identity는 보안 측면에서는 거의 표준에 가깝지만, 셋업 단계의 운영 원칙 몇 가지를 짚어 둡니다.
- ServiceAccount 1개 = 클라우드 IAM Role 1개의 1:1 매핑 — 한 SA에 너무 많은 권한을 몰지 말고, 워크로드 단위로 ServiceAccount를 가르고 각 SA에 최소 권한 IAM Role을 붙입니다.
- trust policy에 namespace와 SA name 둘 다 명시 — namespace만 명시하면 그 namespace의 다른 SA가 같은 Role을 가져갈 수 있어 격리가 깨집니다.
- 토큰 audience 검증 — STS가 토큰을 검증할 때 audience (
sts.amazonaws.com등)를 확인합니다. ServiceAccount의 projected token의 audience를 STS가 기대하는 값과 맞춰야 합니다. - 자격 증명의 lifetime — IAM Role의 임시 자격 증명은 STS 호출 시점에 발급되고 보통 1시간 유효합니다. AWS SDK가 자동으로 갱신하므로 애플리케이션 측 처리는 필요 없습니다.
연습문제 #
- 본인의 클러스터에 표준
viewClusterRole의aggregationRule이 어떻게 적혀 있는지 확인합니다 (kubectl get clusterrole view -o yaml).rules가 비어 있고aggregationRule만 있는 모양을 §“Aggregated ClusterRole"의 모델과 맞춰 한 단락으로 정리하고, 새 CRD의 read 권한을 view에 합치는 매니페스트 한 장을 직접 적어 봅니다. kubectl auth can-i create pods --as=system:serviceaccount:default:default -n default로 default ServiceAccount가 Pod를 만들 수 있는지 확인합니다. 그 다음 14장의pod-readerRoleBinding을 떠올려, 그 SA의 입장에서kubectl auth can-i list pods --as=system:serviceaccount:dev:pod-reader -n dev가 yes로 응답하는 흐름을 §“Impersonation"의 모델로 재현합니다. impersonate 권한이 없는 사용자가 같은 명령을 시도했을 때 어떤 에러가 나는지도 기록합니다.- EKS 환경에서 ServiceAccount 한 개에 IRSA annotation을 붙이고, 그 SA를 쓰는 Pod 안의 AWS SDK가 자격 증명을 어떻게 얻는지 §“IRSA의 흐름"의 6단계로 직접 시뮬레이션해 봅니다. trust policy에 namespace만 적고 SA name을 안 적으면 어떤 격리 문제가 생기는지, 29장 시크릿 운영의 “비밀번호 0” 패턴이 본 챕터의 IRSA 위에 어떻게 얹히는지 한 단락으로 정리합니다.
한 줄 요약: Aggregated ClusterRole로 표준 권한 묶음을 라벨로 확장하고, Impersonation으로 다른 주체의 권한 의도를
kubectl auth can-i로 검증한다. ServiceAccount 토큰은 K8s 1.22부터 만료 · audience · rotation을 갖춘 projected token이 기본이고, 1.24부터 legacy Secret 자동 생성이 중단됐다. IRSA / Workload Identity는 K8s ServiceAccount를 클라우드 IAM Role과 1:1 매핑해 정적 자격 증명을 클러스터 밖으로 빼낸 모델로, OIDC + STS 토큰 교환이라는 공통 구조 위에 클라우드 사업자별 어댑터가 얹힌다.
다음 챕터 #
본 챕터까지 RBAC과 ServiceAccount의 권한 모델 깊이를 정리했습니다. 다음 챕터의 주제는 시점을 다시 한 단 옮깁니다 — 매니페스트가 etcd에 저장되기 직전에 검사 · 변형하는 단계입니다. RBAC이 “이 사용자가 이 동작을 할 수 있는가"를 묻는다면, 다음 챕터의 admission은 “이 매니페스트가 클러스터의 정책에 맞는가"를 묻습니다.
17장 Admission Controller에서는 ValidatingAdmissionWebhook · MutatingAdmissionWebhook의 K8s 표준 모델과, 그 위에 정책 엔진을 얹은 두 도구 — OPA Gatekeeper (Rego 언어 기반)와 Kyverno (K8s 네이티브 YAML 정책)의 비교, 그리고 “limits 없는 컨테이너 거부”, “특정 라벨 강제” 같은 운영 정책의 매니페스트 패턴까지 한 사이클로 따라갑니다. 14장 §“흔한 함정 — 너무 넓은 ClusterRole"에서 짚었던 “cluster-admin 묶음 금지” 같은 정책을 admission 단계에서 강제하는 길이 본격적으로 열립니다.