서종호(가시다)님의 AWS EKS Workshop Study(AEWS) 4주차 학습 내용을 기반으로 합니다.
기초 개념 및 실습
IAM Role
Role이라는 개념은 이미 익숙할 것이다. assumeRole은 한 IAM 엔티티가 다른 Role의 임시 자격 증명(Temporary Credentials)을 발급받아 해당 Role의 권한으로 작업을 수행하는 메커니즘이다. 예를 들어 EC2 노드에는 IAM Role을 설정할 수 있는데 그 과정을 CLI 명령어로 뜯어보면 아래와 같다.
# CLI에서는 Instance Profile을 명시적으로 따로 만들어야 함
# 1. Role 생성
aws iam create-role --role-name MyRole ...
# 2. Instance Profile 별도 생성
aws iam create-instance-profile --instance-profile-name MyProfile
# 3. Profile에 Role 연결
aws iam add-role-to-instance-profile \
--instance-profile-name MyProfile \
--role-name MyRole
# 4. EC2에 Profile 연결
aws ec2 associate-iam-instance-profile \
--instance-id i-1234567890 \
--iam-instance-profile Name=MyProfile위와 같은 과정을 거쳐 해당 노드가 특정한 Role을 사용하도록 설정을 해주면 그 뒤에 EC2에서는 STS에 assumeRole 요청을 보내 실제 해당 역할을 수행할 수 있는 임시자격을 부여받는다. 실제로 콘솔에서 특정 IAM Role을 연결하고 나서 CloudTrail 이벤트를 살펴보면 아래와 같은 assumeRole 이벤트가 발생한것을 확인할 수 있다.

Service Account

Service Account는 Kubernetes에서 사람이 아닌(non-human) 계정의 한 유형으로, 클러스터 내에서 고유한 ID를 제공한다. Pod, 시스템 컴포넌트, 그리고 클러스터 내외부의 엔티티들이 특정 Service Account의 자격 증명을 사용하여 해당 Service Account로 식별될 수 있다.
쉽게 말해, 사람(User Account)이 kubectl 등을 통해 API Server에 인증하는 것과 달리, Service Account는 Pod 내부에서 실행되는 애플리케이션이 API Server와 통신할 때 사용하는 신원(identity)이다.
Service Account의 주요 특성은 다음과 같다.
- Namespaced: 각 Service Account는 특정 네임스페이스에 바인딩된다. 모든 네임스페이스는 생성 시 자동으로 default Service Account를 갖는다.
- Lightweight: 클러스터 내에 존재하며 Kubernetes API에서 정의된다. 빠르게 생성하여 특정 작업을 활성화할 수 있다.
- Portable: 복잡한 컨테이너화된 워크로드의 구성 번들에 시스템 컴포넌트에 대한 Service Account 정의를 포함할 수 있어 구성의 이식성이 높다.
Service Account와 User Account의 핵심적인 차이를 정리하면, User Account는 사람(Human)이 사용하는 외부 인증 기반의 계정이고 Kubernetes API에 객체로 존재하지 않는다. 반면 Service Account는 워크로드와 자동화를 위한 계정으로 Kubernetes API에 ServiceAccount 객체로 존재하며, Kubernetes RBAC을 통해 접근 제어가 이루어진다.
아래 그림은 Pod가 Service Account를 통해 API Server에 인증하는 전체 흐름을 보여준다. 즉, Pod라는 리소스가 SA를 통해 인증을 하고 Role과 연결된 RoleBinnding을 통해서 인가까지도 관계를 맺고 있는 리소스라고 이해할 수 있다.

실습으로 SA를 만들어보고 기본적인 인증 토큰(jwt)이 잘 생성되는지 확인해보자. 우선 각 네임스페이스와 SA를 생성한다.
# 네임스페이스(Namespace, NS) 생성 및 확인
kubectl create namespace dev-team
kubectl create ns infra-team
# 네임스페이스 확인
kubectl get ns
# 네임스페이스에 각각 서비스 어카운트 생성 : serviceaccounts 약자(=sa)
kubectl create sa dev-k8s -n dev-team
kubectl create sa infra-k8s -n infra-team
# 서비스 어카운트 정보 확인
kubectl get sa -n dev-team
kubectl get sa dev-k8s -n dev-team -o yaml
kubectl get sa -n infra-team
kubectl get sa infra-k8s -n infra-team -o yaml이후 파드를 생성하고 나서 실제 생성된 토큰을 확인하자. 파드의 경우 앞서 생성한 SA를 사용하도록 한다.
# 각각 네임스피이스에 kubectl 파드 생성 - [컨테이너이미지](https://hub.docker.com/r/bitnami/kubectl/)
# docker run --rm --name kubectl -v /path/to/your/kube/config:/.kube/config bitnami/kubectl:latest
cat <<EOF | kubectl create -f -
apiVersion: v1
kind: Pod
metadata:
name: dev-kubectl
namespace: dev-team
spec:
serviceAccountName: dev-k8s
containers:
- name: kubectl-pod
image: bitnami/kubectl
command: ["tail"]
args: ["-f", "/dev/null"]
terminationGracePeriodSeconds: 0
EOF
cat <<EOF | kubectl create -f -
apiVersion: v1
kind: Pod
metadata:
name: infra-kubectl
namespace: infra-team
spec:
serviceAccountName: infra-k8s
containers:
- name: kubectl-pod
image: bitnami/kubectl
command: ["tail"]
args: ["-f", "/dev/null"]
terminationGracePeriodSeconds: 0
EOF
# 파드에 기본 적용되는 서비스 어카운트(토큰) 정보 확인
kubectl exec -it dev-kubectl -n dev-team -- ls /run/secrets/kubernetes.io/serviceaccount
kubectl exec -it dev-kubectl -n dev-team -- cat /run/secrets/kubernetes.io/serviceaccount/token
kubectl exec -it dev-kubectl -n dev-team -- cat /run/secrets/kubernetes.io/serviceaccount/namespace
kubectl exec -it dev-kubectl -n dev-team -- cat /run/secrets/kubernetes.io/serviceaccount/ca.crt총 3가지 파일이 마운팅 되어서 보인다. 여기서 token의 경우 jwt 토큰이고 아래와 같은 흐름으로 생성된 기본 토큰이다. 해당 토큰은 SA를 생성한다고 만들어지지 않고 아래 흐름을 거치며 생성된다.
ServiceAccount 생성
│
▼
(토큰 없음, SA만 존재)
│
▼
Pod 생성 (spec.serviceAccountName: dev-k8s)
│
▼
kubelet → TokenRequest API 호출
│
▼
API Server가 JWT 발급 (exp, pod 정보, SA 정보 포함)
│
▼
Projected Volume으로 Pod에 마운트
│
▼
kubelet이 만료 전 자동 갱신 (80% 시점)실제 토큰을 디코드 해보면 아래와 같이 페이로드 값들을 확인 가능하다. 각 두 파드에 대해서 이름, 설정한 SA 객체, 생성 시간들은 다르다. 이러한 다른 값들을 아래와 같이 디코드 할 경우 확인 할 수 있다.


RBAC

RBAC(Role-Based Access Control)는 Kubernetes에서 인가(Authorization)를 처리하는 핵심 메커니즘이다. 앞서 Service Account를 통해 Pod가 API Server에 인증(Authentication)하는 과정을 살펴보았는데, 인증만으로는 실제 리소스에 대한 접근을 제어할 수 없다. RBAC는 "누가(Subject), 어떤 리소스(Resource)에, 어떤 작업(Verb)을 할 수 있는가"를 정의하는 권한 체계로, 클러스터의 보안을 담당하는 가장 중요한 구성 요소 중 하나이다.
Kubernetes의 RBAC API는 rbac.authorization.k8s.io API 그룹에 속하며, 크게 네 가지 리소스로 구성된다. Role은 특정 네임스페이스 내에서 허용할 작업을 정의하고, ClusterRole은 클러스터 전체 범위에서 허용할 작업을 정의한다. RoleBinding은 Role을 특정 Subject(User, Group, ServiceAccount)에 바인딩하여 해당 네임스페이스 내에서 권한을 부여하며, ClusterRoleBinding은 ClusterRole을 Subject에 바인딩하여 클러스터 전체에 걸쳐 권한을 부여한다.
아래 다이어그램은 네임스페이스 범위에서의 RBAC 동작 구조를 보여준다. Subject(사용자 또는 ServiceAccount)가 RoleBinding을 통해 Role에 연결되고, Role에 정의된 리소스와 행위 조합에 따라 API 접근이 허용된다. Subject와 Role 사이는 M:N 관계로, 하나의 Subject가 여러 Role에 바인딩될 수 있고, 하나의 Role이 여러 Subject에게 부여될 수도 있다.

이제 앞서 생성한 Service Account에 실제 권한을 부여하기 위해 Role과 RoleBinding을 생성해보자. Role에는 해당 네임스페이스 내에서 허용할 apiGroups, resources, verbs를 지정한다. 이번 실습에서는 dev-team과 infra-team 네임스페이스 각각에 모든 리소스에 대한 모든 권한(["*"])을 갖는 Role을 생성한다. 실무에서는 최소 권한 원칙에 따라 필요한 권한만 부여하는 것이 권장된다.
# 각각 네임스페이스내의 모든 권한에 대한 롤 생성
cat <<EOF | kubectl create -f -
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: role-dev-team
namespace: dev-team
rules:
- apiGroups: ["*"]
resources: ["*"]
verbs: ["*"]
EOF
cat <<EOF | kubectl create -f -
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: role-infra-team
namespace: infra-team
rules:
- apiGroups: ["*"]
resources: ["*"]
verbs: ["*"]
EOF
# 롤 확인
kubectl get roles -n dev-team
kubectl get roles -n infra-team
kubectl get roles -n dev-team -o yaml
kubectl describe roles role-dev-team -n dev-team
...
PolicyRule:
Resources Non-Resource URLs Resource Names Verbs
--------- ----------------- -------------- -----
*.* [] [] [*]
# 롤바인딩 생성 : '서비스어카운트 <-> 롤' 간 서로 연동
cat <<EOF | kubectl create -f -
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: roleB-dev-team
namespace: dev-team
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: role-dev-team
subjects:
- kind: ServiceAccount
name: dev-k8s
namespace: dev-team
EOF
cat <<EOF | kubectl create -f -
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: roleB-infra-team
namespace: infra-team
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: role-infra-team
subjects:
- kind: ServiceAccount
name: infra-k8s
namespace: infra-team
EOF
# 롤바인딩 확인
kubectl get rolebindings -n dev-team
kubectl get rolebindings -n infra-team
kubectl get rolebindings -n dev-team -o yaml
kubectl describe rolebindings roleB-dev-team -n dev-team위 코드 블록에서 수행한 작업을 정리하면, 먼저 dev-team과 infra-team 네임스페이스에 각각 role-dev-team, role-infra-team이라는 Role을 생성하였다. 이 Role들은 apiGroups, resources, verbs 모두 와일드카드(*)로 설정되어 해당 네임스페이스 내의 모든 리소스에 대한 전체 권한을 가진다. 이후 RoleBinding을 생성하여 각각의 ServiceAccount(dev-k8s, infra-k8s)를 해당 Role에 바인딩하였다. 이로써 각 Pod에서 동작하는 ServiceAccount가 자신의 네임스페이스 내에서 리소스를 자유롭게 조회, 생성, 수정, 삭제할 수 있게 된다.
아래 스크린샷은 실제로 Role과 RoleBinding이 정상적으로 생성되었는지 확인한 결과이다. kubectl describe roles 명령어로 Role의 PolicyRule을 확인하면 모든 리소스(.)에 대해 모든 동작([*])이 허용된 것을 볼 수 있고, kubectl get rolebindings 명령어로 RoleBinding의 상세 정보를 확인하면 ServiceAccount가 올바르게 연결되어 있음을 확인할 수 있다.


두 번째 스크린샷의 YAML 출력을 보면 RoleBinding 리소스의 구조를 상세히 확인할 수 있다. roleRef 필드에서 바인딩된 Role의 이름(role-dev-team)과 API 그룹(rbac.authorization.k8s.io)을, subjects 필드에서 연결된 ServiceAccount(dev-k8s)와 네임스페이스(dev-team)를 확인할 수 있다. 이처럼 RoleBinding은 Role과 Subject를 연결하는 접착제 역할을 하며, 이 세 가지 요소가 모두 올바르게 구성되어야 RBAC 인가가 정상적으로 동작한다.
정리하면, Kubernetes의 RBAC는 인증(Authentication) 이후 단계인 인가(Authorization)를 담당하며, ServiceAccount → RoleBinding → Role의 관계를 통해 Pod 내 워크로드가 API Server의 어떤 리소스에 어떤 작업을 수행할 수 있는지를 세밀하게 제어한다. 이전 섹션에서 SA를 생성하고 토큰을 확인한 것이 인증 단계라면, 이번 RBAC 실습은 그 인증된 주체에게 실제 권한을 부여하는 인가 단계에 해당한다. 클러스터 운영 시에는 네임스페이스별로 적절한 Role을 설계하고, 필요한 ServiceAccount에만 최소한의 권한을 부여하는 것이 보안의 핵심이다.
Service Account Token Volume Projection

앞서 살펴본 것처럼, Kubernetes v1.22 이후의 현재 버전에서는 Pod가 생성될 때 kubelet이 TokenRequest API를 통해 시간 제한이 있는 JWT 토큰을 발급받고, 이를 Projected Volume으로 Pod에 마운트한다. 이것이 현재 기본 동작이다. 그렇다면 Service Account Token Volume Projection이 별도로 존재하는 이유는 무엇일까?
핵심 차이를 이해하기 위해 먼저 기본 토큰의 역사를 짚어보자. Kubernetes v1.22 이전에는 ServiceAccount를 생성하면 자동으로 Secret 객체가 만들어지고, 그 안에 만료 기간이 없는 영구 토큰이 저장되었다. 이 Secret 기반 토큰은 Pod에 볼륨으로 마운트되어 사용되었는데, 토큰이 한번 발급되면 영원히 유효하기 때문에 유출 시 심각한 보안 위협이 될 수 있었다. v1.22 이후 이 방식은 Bound Service Account Token Volume 메커니즘으로 대체되었다. 이제는 kubelet이 TokenRequest API를 호출하여 기본 1시간 만료의 토큰을 받고, 이를 Projected Volume으로 Pod에 마운트하며, 만료 80% 시점에 자동으로 갱신한다.

여기서 중요한 개념 정리가 필요하다. "Projected Volume"은 PersistentVolume(PV)과는 완전히 다른 개념이다. PV는 실제 디스크 스토리지를 추상화한 것이지만, Projected Volume은 여러 볼륨 소스(serviceAccountToken, configMap, downwardAPI, secret 등)를 하나의 디렉토리에 합쳐서 마운트하는 인메모리(tmpfs) 기반의 가상 볼륨이다. 즉, Projected Volume에 마운트된 토큰은 디스크에 영구적으로 쓰이는 것이 아니라 메모리 위에 존재하며, kubelet이 API Server로부터 받은 토큰을 주기적으로 갱신하여 파일 내용을 업데이트한다. Pod가 삭제되면 이 볼륨과 토큰도 함께 사라진다. 따라서 "PV에 저장하면 디스크에 영구적으로 쓰는 것 아닌가?"라는 의문의 답은, Projected Volume은 PV가 아니고 메모리 기반이며 토큰 자체에 만료 시간(exp)이 JWT 클레임으로 포함되어 있어 API Server가 만료된 토큰을 거부한다는 것이다. 시크릿 기반 토큰이 경우 만료 시간이 JWT 클레임에 포함되어 있지 않다.

그렇다면 기본 Bound SA Token과 Service Account Token Volume Projection의 차이는 무엇인가? 기본 토큰은 audience가 kube-apiserver로 고정되어 있고, 만료 시간도 기본값(1시간)이 적용된다. 하지만 Pod 내 애플리케이션이 Vault, 외부 서비스(STS 등) kube-apiserver가 아닌 다른 시스템에 인증해야 하는 경우, 대상(audience)과 유효 기간(expiration)을 직접 지정할 수 있어야 한다. 바로 이런 경우에 Service Account Token Volume Projection을 명시적으로 설정하여 사용한다. 아래 YAML 예시에서 audience를 vault로, expirationSeconds를 7200(2시간)으로 지정하여 Vault 인증에 특화된 토큰을 Pod에 마운트하는 것을 볼 수 있다.
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
containers:
- image: nginx
name: nginx
volumeMounts:
- mountPath: /var/run/secrets/tokens
name: vault-token
serviceAccountName: build-robot
volumes:
- name: vault-token
projected:
sources:
- serviceAccountToken:
path: vault-token
expirationSeconds: 7200
audience: vault코드에서 핵심은 volumes 섹션의 projected.sources.serviceAccountToken 설정이다. path는 토큰 파일이 마운트될 경로명, expirationSeconds는 토큰의 유효 기간(최소 600초, 기본 3600초), audience는 토큰의 수신 대상을 지정한다. 이렇게 생성된 토큰은 audience 클레임에 vault가 포함되어 있으므로, Vault 서버는 이 토큰을 검증할 때 자신이 의도된 수신자인지 확인할 수 있다. 공식 문서에 따르면 kubelet은 토큰의 TTL 80% 시점 또는 24시간 중 더 빠른 시점에 자동으로 토큰을 갱신하며, 애플리케이션은 토큰 파일을 주기적으로(예: 5분마다) 다시 읽어들이는 것이 권장된다. 아래 공식문서에서 더 자세한 내용을 확인할 수 있다.

생소한 개념인 Bound Service Account Token Volume, 서비스 어카운트 어드미션 컨트롤러를 다시 정리해보자.
첫째, Bound Service Account Token Volume은 Kubernetes v1.22부터 기본으로 적용되는 메커니즘으로, Pod 생성 시 서비스 어카운트 어드미션 컨트롤러가 자동으로 kube-api-access-<random> 이라는 이름의 Projected Volume을 Pod에 추가한다. 이 볼륨에는 serviceAccountToken(1시간 만료, kube-apiserver 대상), configMap(kube-root-ca.crt, CA 인증서), downwardAPI(네임스페이스 정보) 세 가지 소스가 포함된다.
둘째, 서비스 어카운트 어드미션 컨트롤러는 API Server의 일부로 동작하는 플러그인으로, Pod가 생성될 때 동기적으로 개입하여 serviceAccountName이 없으면 default로 설정하고, 해당 SA가 존재하는지 확인하며, automountServiceAccountToken이 false가 아니면 위에서 설명한 Projected Volume과 volumeMount를 자동 추가한다.
인증
eks 클러스터를 생성하면 해당 클러스터를 생성한 IAM 계정에서만 클러스터의 정보들에 접근할 수 있다. 만약 IAM 권한으로 adminFullAccess를 가지고 있다하더라도 다른 IAM 계정에서 접근을 할 경우 콘솔 및 CLI 상에서 해당 클러스터에 접근하지 못한다.
설치 후 auth 정보를 확인해보면 아래와 같이 나온다. Cli를 통해서 eks를 설치했고 설치 시 사용한 IAM user는 terraform이라는 user를 사용했다. 해당 정보가 아래처럼 보인다.

그러면 brido-macos라는 iam user는 앞서 말한것처럼 admin 권한이 있더라도 클러스터에 대한 정보를 얻어올 수 없다. 아래처럼 해당 iam 유저에 권한을 넣어주는 2가지 방법을 알아보자.

우선 아래 명령어로 현재 클러스터가 어떤 방식을 지원하는지 파악할 수 있다.
aws eks describe-cluster --name myeks --query "cluster.accessConfig" --output json
API_AND_CONFIG_MAP→ 두 방식 병행 (마이그레이션 단계)API→ Access Entries만 사용 (완전 전환)CONFIG_MAP-> 컨피그 맵 방식만 사용 (Deprecated 예정)
참고로 모드 전환은 단방향이다. CONFIG_MAP → API_AND_CONFIG_MAP → API 방향으로만 가능하고 되돌릴 수 없다.
방식 1. aws-auth ConfigMap (레거시)

여기서 configmap을 조회해서 IAM ARN을 K8s username/groups로 변환한다. 이 후 얻어낸 사용자가 무엇을 할 수 있는지 확인해서 요청에 대한 허용/거부를 정한다.
해당 방식의 가장 큰 단점은 ConfigMap을 수동으로 편집하다 잘못된 형식이 들어가면 클러스터 접근이 완전히 차단될 수 있다. 또한 클러스터 생성자 (실습에서는 terraform IAM 유저) 의 cluster-admin 권한을 취소시킬 수 없다.
방식 2. EKS Access Entries API

컨피그 맵 방식과 가장 큰 차이는 Kubernetes API가 아닌 EKS API 레이어가 매핑을 처리한다. 컨피그 맵 방식의 단점들을 보완가능하다는 장점이 있다.
아래처럼 우선 access-entry를 생성한다.
# IAM 사용자에게 클러스터 관리자 권한 부여
aws eks create-access-entry \
--cluster-name my-cluster \
--principal-arn arn:aws:iam::123456789012:user/brido-macos \
--type STANDARD
# AWS 관리형 정책 연결 (가장 간단)
aws eks associate-access-policy \
--cluster-name my-cluster \
--principal-arn arn:aws:iam::123456789012:user/brido-macos \
--policy-arn arn:aws:eks::aws:cluster-access-policy/AmazonEKSClusterAdminPolicy \
--access-scope type=cluster이후 aws eks list-access-entries --cluster-name my-cluster 로 생성한 iam user가 적절하게 추가되어있는지 확인하고 콘솔, cli 등에서 실제 클러스터에 접근 가능한지 확인하면 된다. 아래처럼 brido-macos 라는 iam 사용자가 적절하게 추가된것을 확인할 수 있다.

인가 (IRSA)
1. IRSA 개념
IRSA(IAM Roles for Service Accounts)는 Amazon EKS에서 Kubernetes Service Account에 IAM Role을 매핑하여, Pod 내 애플리케이션이 AWS 서비스에 접근할 때 필요한 자격 증명을 안전하게 제공하는 메커니즘이다. EC2 Instance Profile이 EC2 인스턴스에 자격 증명을 제공하는 것과 유사한 방식으로, Pod 단위의 세분화된 권한 관리를 가능하게 한다.
IRSA는 OIDC(OpenID Connect) 기반 페더레이션 ID 지원을 기반으로 동작한다. AWS STS의 AssumeRoleWithWebIdentity API를 통해 OIDC JWT 토큰을 IAM 임시 자격 증명으로 교환하는 구조이다.
2. 사전 구성 요소
IRSA가 동작하기 위해서는 다음 구성 요소들이 사전에 설정되어야 한다.
IAM OIDC Identity Provider
EKS 클러스터마다 하나의 OIDC Provider를 IAM에 등록해야 한다. EKS 클러스터는 고유한 OIDC Issuer URL을 가지며, 이 URL을 IAM Identity Provider로 등록하면 AWS STS가 해당 클러스터에서 발급된 JWT 토큰을 신뢰할 수 있게 된다.
# 클러스터의 OIDC Provider ID 추출
oidc_id=$(aws eks describe-cluster --name myeks \
--query "cluster.identity.oidc.issuer" --output text | cut -d '/' -f 5)
echo $oidc_id
032357E88E266F4AE7C2E8CF6F5EFEB0
# 해당 OIDC Provider가 IAM에 이미 등록되어 있는지 확인
aws iam list-open-id-connect-providers | grep $oidc_id | cut -d "/" -f4
032357E88E266F4AE7C2E8CF6F5EFEB0
출력이 반환되면 해당 클러스터의 OIDC Provider가 IAM에 이미 등록된 상태이다. 출력이 없으면 eksctl utils associate-iam-oidc-provider --cluster myeks --approve 명령으로 등록해야 한다.
OIDC Discovery 엔드포인트 확인
등록된 OIDC Provider는 공개 엔드포인트를 통해 자신의 메타데이터를 노출한다. 이 엔드포인트가 IRSA의 JWT 토큰 검증에 핵심적인 역할을 한다.
① Discovery 문서 (.well-known/openid-configuration)
IDP=$(aws eks describe-cluster --name myeks --query cluster.identity.oidc.issuer --output text)
curl -s $IDP/.well-known/openid-configuration | jq .
issuer: 이 OIDC Provider의 고유 식별자. JWT 토큰의issclaim과 일치해야 한다.jwks_uri: JWT 서명을 검증할 공개키를 가져올 수 있는 엔드포인트.claims_supported:sub(Service Account 정보)와iss(Issuer) — Trust Policy의 Condition에서 이 claim들을 검증한다.id_token_signing_alg_values_supported:RS256— RSA SHA-256 서명 알고리즘을 사용한다.
② JWKS(JSON Web Key Set) 엔드포인트
Discovery 문서의 jwks_uri로 요청하면, JWT 서명 검증에 사용하는 공개키 세트를 받는다.
curl -s $IDP/keys | jq .
각 필드의 의미:
kty: 키 타입 (RSA)kid: Key ID — JWT 헤더의kid와 매칭하여 어떤 키로 서명했는지 식별use:sig— 서명 검증 용도n,e: RSA 공개키의 Modulus와 Exponent 값
이 공개키는 EKS가 관리하며 7일마다 자동 로테이션된다. AWS STS는 JWT 토큰 수신 시 이 엔드포인트에서 공개키를 가져와 서명을 검증한다.
IAM Role + Trust Policy
Service Account가 Assume할 IAM Role을 생성하고, Trust Policy에 OIDC Provider를 Federated Principal로 지정한다. Condition 절에서 특정 네임스페이스와 Service Account만 이 Role을 Assume할 수 있도록 제한한다.
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::<ACCOUNT_ID>:oidc-provider/oidc.eks.<region>.amazonaws.com/id/<CLUSTER_ID>"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"oidc.eks.<region>.amazonaws.com/id/<CLUSTER_ID>:sub": "system:serviceaccount:<namespace>:<sa-name>",
"oidc.eks.<region>.amazonaws.com/id/<CLUSTER_ID>:aud": "sts.amazonaws.com"
}
}
}]
}IAM Policy + IRSA 생성 (eksctl 활용)
실무에서는 eksctl create iamserviceaccount 명령을 사용하여 IAM Role 생성, Trust Policy 설정, Service Account 어노테이션까지 한 번에 처리한다. 내부적으로 CloudFormation 스택을 통해 IAM Role이 생성된다.
# IAM Policy 생성 (예: AWS Load Balancer Controller용)
ACCOUNT_ID=$(aws sts get-caller-identity --query "Account" --output text)
aws iam create-policy \
--policy-name AWSLoadBalancerControllerIAMPolicy \
--policy-document file://aws_lb_controller_policy.json
# IRSA 생성: IAM Role + Service Account를 한 번에 구성
eksctl create iamserviceaccount \
--cluster=myeks \
--namespace=kube-system \
--name=aws-load-balancer-controller \
--attach-policy-arn=arn:aws:iam::$ACCOUNT_ID:policy/AWSLoadBalancerControllerIAMPolicy \
--override-existing-serviceaccounts \
--approveeksctl이 수행하는 작업:
- CloudFormation 스택 생성 → IAM Role 생성 (OIDC Provider 기반 Trust Policy 자동 구성)
- 생성된 Role ARN을 Kubernetes Service Account의
eks.amazonaws.com/role-arn어노테이션에 설정 - Service Account가 존재하지 않으면 자동 생성
생성되었는지 확인한다.

생성한 SA에 어노테이션이 잘 설정되었는지 확인한다.

3. Pod Identity Webhook 내부 동작
IRSA의 핵심 메커니즘 중 하나는 EKS 컨트롤 플레인에서 동작하는 Amazon EKS Pod Identity Webhook이다. 이 Webhook은 Kubernetes의 Mutating Admission Controller로서, Pod 생성 요청을 가로채어 AWS 자격 증명 획득에 필요한 요소들을 자동으로 주입한다.
Webhook 등록 확인
EKS 클러스터에는 pod-identity-webhook이라는 이름의 MutatingWebhookConfiguration이 기본 등록되어 있다.

Webhook의 상세 설정을 확인하면, 웹훅 이름이 iam-for-pods.amazonaws.com이며 Pod CREATE 이벤트에 반응하도록 구성되어 있다.
kubectl describe MutatingWebhookConfiguration pod-identity-webhook
# name: iam-for-pods.amazonaws.com
# Pod CREATE 시 호출동작 트리거 조건
Pod Identity Webhook은 Pod 생성 시 해당 Pod의 serviceAccountName에 지정된 Service Account를 조회한다. 해당 Service Account에 eks.amazonaws.com/role-arn 어노테이션이 존재하면 Webhook이 Pod Spec을 변형(mutate)한다. 어노테이션이 없으면 아무 작업도 수행하지 않는다.
Mutation 증명: Deployment 원본 vs 실제 Pod Spec
Pod Identity Webhook의 동작을 가장 명확하게 확인하는 방법은, Deployment 원본에 정의되지 않은 env/volume이 실제 Pod에 존재하는지 비교하는 것이다. AWS Load Balancer Controller를 예시로 살펴본다.
Deployment 원본 확인:
kubectl get deploy -n kube-system aws-load-balancer-controller -o yamlDeployment의 Pod template에는 AWS_ROLE_ARN, AWS_WEB_IDENTITY_TOKEN_FILE 환경 변수나 aws-iam-token 볼륨이 정의되어 있지 않다.
실제 Pod 확인:
kubectl describe pod -n kube-system -l app.kubernetes.io/name=aws-load-balancer-controller
Deployment 원본에는 없던 5개의 환경 변수와 aws-iam-token Volume Mount가 Pod에 존재한다. 이것이 바로 Pod Identity Webhook이 Mutating Admission 단계에서 주입한 결과이다.
주입되는 환경 변수 상세
env:
- name: AWS_STS_REGIONAL_ENDPOINTS
value: regional
- name: AWS_DEFAULT_REGION
value: ap-northeast-2
- name: AWS_REGION
value: ap-northeast-2
- name: AWS_ROLE_ARN
value: arn:aws:iam::911283464785:role/eksctl-myeks-addon-...
- name: AWS_WEB_IDENTITY_TOKEN_FILE
value: /var/run/secrets/eks.amazonaws.com/serviceaccount/tokenAWS_ROLE_ARN: Service Account 어노테이션에서 읽어온 IAM Role ARN. SDK가AssumeRoleWithWebIdentity호출 시 이 Role을 Assume한다.AWS_WEB_IDENTITY_TOKEN_FILE: JWT 토큰이 마운트된 파일 경로. SDK가 이 파일에서 토큰을 읽어 STS에 전달한다.AWS_DEFAULT_REGION/AWS_REGION: 클러스터 리전. Webhook의--aws-default-region플래그 설정에 의해 주입된다.AWS_STS_REGIONAL_ENDPOINTS:regional로 설정되면 STS 호출이 글로벌 엔드포인트(us-east-1) 대신 리전별 엔드포인트로 라우팅된다. Service Account 어노테이션eks.amazonaws.com/sts-regional-endpoints: "true"또는 Webhook의--sts-regional-endpoint플래그에 의해 주입된다.
Webhook은 이미 Pod Spec에 해당 환경 변수가 존재하는 경우 덮어쓰지 않는다.
주입되는 Volume 상세: 3개 Volume의 역할 구분
실제 LBC Pod에는 3개의 Volume이 마운트되어 있다. 각각의 역할이 다르므로 정확히 구분해야 한다.

| Volume | 주입 주체 | Audience | 용도 |
|---|---|---|---|
aws-iam-token | Pod Identity Webhook | sts.amazonaws.com | AWS STS에 제출하여 IAM 임시 자격 증명 획득 |
kube-api-access-* | kubelet (자동) | K8s API 서버 | Kubernetes API 서버 인증 |
cert | Deployment 정의 | - | LBC의 Validating/Mutating Webhook TLS 인증서 |
핵심적인 차이는 audience이다. aws-iam-token의 audience는 sts.amazonaws.com으로, 이 토큰은 Kubernetes API 서버가 아닌 AWS STS를 대상으로 발급된 것이다. 반면 kube-api-access의 토큰은 Kubernetes API 서버 인증 전용이다. 두 토큰 모두 Projected Service Account Token이지만, audience가 다르기 때문에 서로의 용도로 사용할 수 없다.
Projected Service Account Token과 Legacy Token의 차이
Kubernetes에는 두 종류의 Service Account Token이 존재하며, IRSA는 Projected Token을 사용한다. (앞서 언급된 내용 정리)
Legacy Service Account Token: Secret 리소스에 저장되는 정적 토큰으로, 만료 기간이 없고 Audience 바인딩이 없다. Kubernetes API 서버 인증 전용이다.
Projected Service Account Token (IRSA용): Kubernetes 1.12부터 지원되는 동적 토큰으로, 다음 특징을 갖는다:
- 유효 기간 설정 가능 (
expirationSeconds) — 만료 후 kubelet이 자동 갱신 - Audience 바인딩 — 특정 수신자(예:
sts.amazonaws.com)를 대상으로 발급 - OIDC JWT 형식 —
iss(Issuer),sub(Subject),aud(Audience) claim을 포함 - 서명 키 자동 로테이션 — EKS에서 관리하는 서명 키 쌍이 7일마다 자동 로테이션
Service Account 어노테이션 옵션
Pod Identity Webhook은 다음 어노테이션들을 인식한다:
eks.amazonaws.com/role-arn: (필수) Assume할 IAM Role의 ARNeks.amazonaws.com/audience: 토큰의 Audience. 기본값은sts.amazonaws.comeks.amazonaws.com/token-expiration: 토큰 만료 시간(초). 기본값은86400(24시간)eks.amazonaws.com/sts-regional-endpoints:"true"설정 시AWS_STS_REGIONAL_ENDPOINTS=regional환경 변수 주입eks.amazonaws.com/skip-containers: 특정 컨테이너에 대해 Volume/환경 변수 주입을 건너뛸 수 있는 쉼표 구분 목록
IRSA 동작 흐름 (6단계)

아래 흐름은 첨부된 아키텍처 다이어그램의 ①~⑥ 번호에 대응한다.
Step 0 (사전 단계): Pod 생성 시 Pod Identity Webhook에 의한 Mutation
kubectl apply로 Pod 생성 요청이 Kubernetes API 서버에 도달하면, Mutating Admission 단계에서 pod-identity-webhook(iam-for-pods.amazonaws.com)이 호출된다. Webhook은 Service Account의 eks.amazonaws.com/role-arn 어노테이션을 감지하고, 3절에서 설명한 환경 변수, Projected Volume, Volume Mount를 Pod Spec에 자동 주입한다. 이 모든 과정은 사용자가 인지하지 못하는 사이에 투명하게 처리된다.
Step ① Pod → AWS STS: 자격 증명 요청
Pod 내 AWS SDK가 Default Credential Chain을 따라 AWS_WEB_IDENTITY_TOKEN_FILE과 AWS_ROLE_ARN 환경 변수를 감지한다. SDK는 /var/run/secrets/eks.amazonaws.com/serviceaccount/token 파일에서 JWT 토큰을 읽고, 자동으로 AWS STS에 AssumeRoleWithWebIdentity API를 호출한다. 이때 JWT 토큰과 IAM Role ARN을 함께 전송한다.
다이어그램: "I need credentials to list S3 buckets. Here is my JWT & IAM Role ARN"
AssumeRoleWithWebIdentity 호출 시 AWS 보안 자격 증명이 필요하지 않다. 호출자의 ID는 웹 ID 공급자의 토큰으로 검증된다.
Step ② AWS STS → IAM: 토큰 검증 및 권한 확인 요청
STS는 수신한 JWT 토큰과 IAM Role ARN을 기반으로 IAM에 검증을 요청한다. IAM은 다음 항목들을 확인한다:
- 해당 IAM Role의 Trust Policy에 지정된 OIDC Provider와 토큰의
issclaim이 일치하는지 - Trust Policy의 Condition에 명시된
subclaim(예:system:serviceaccount:kube-system:aws-load-balancer-controller)이 토큰과 일치하는지 - Trust Policy의 Condition에 명시된
audclaim(sts.amazonaws.com)이 토큰과 일치하는지
다이어그램: "Hey, can you validate and let me know if I can send this POD temporary credentials to list S3 buckets? Here is the token and IAM Role ARN"
Step ③ IAM → OIDC Provider: JWT 서명 검증
IAM은 2.1절에서 확인한 OIDC Discovery 엔드포인트의 jwks_uri를 호출하여 공개키를 가져온다. 이 키의 kid와 JWT 헤더의 kid를 매칭한 뒤, 해당 RSA 공개키(n, e)로 JWT 서명을 암호학적으로 검증한다.
검증 과정:
- 토큰 수신: STS가 Pod에서 받은 JWT 토큰을 IAM에 전달
- Issuer 확인: 토큰의
iss필드에서 OIDC Provider URL 추출 - 공개키 획득:
{issuer}/.well-known/openid-configuration→jwks_uri→/keys경로에서 현재 유효한 공개키(JWKS) 수신 - 서명 검증: 공개키와 토큰의 서명(Signature)을 대조하여 위변조 여부 확인
다이어그램: "Trust — Get Public Keys from JWKS URL using /.well-known/OpenID-configuration and verify"
Step ④ IAM → STS: 검증 결과 응답
모든 검증이 통과하면, IAM은 STS에 해당 Role에 연결된 IAM Policy의 권한으로 임시 자격 증명을 발급해도 된다는 승인 응답을 보낸다.
다이어그램: "Everything looks good. Go ahead!"
Step ⑤ STS → Pod: 임시 자격 증명 발급
STS는 Pod에 임시 보안 자격 증명을 반환한다. 이 자격 증명은 다음 세 가지로 구성된다:
- Access Key ID
- Secret Access Key
- Session Token
이 자격 증명은 유효 기간이 한정된 임시 자격 증명이며, 만료되면 SDK가 자동으로 새 토큰을 사용해 재발급을 요청한다.
다이어그램: "Here are your temporary credentials"
Step ⑥ Pod → AWS Service: API 호출
Pod는 발급받은 임시 자격 증명을 사용하여 실제 AWS 서비스(예: S3)에 API 요청을 수행한다. 이때 요청은 IAM Role에 Attach된 Policy에서 허용하는 범위 내에서만 가능하다.
다이어그램: "Give me list of buckets. Here are my Temporary credentials"

OAuth2 역할 매핑 정리
Pod (AWS SDK) = Client OAuth2에서 Client가 Auth Server에 토큰을 요청하듯, Pod가 K8s OIDC Provider로부터 받은 PSAT을 들고 STS에 크레덴셜을 요청
K8s OIDC Provider = Auth Server JWT(PSAT)를 발급하고 서명해. JWKS 엔드포인트로 공개키를 외부에 제공하는 것도 Auth Server의 역할
AWS STS = Token Exchange (RFC 8693) 표준 OAuth2에는 없는 개념. PSAT(OIDC 토큰)를 받아서 AWS 전용 임시 크레덴셜로 교환해주는 중간 교환소
AWS IAM = Authorization 정책 레이어 OAuth2의 scope/권한 정의에 해당하고. Trust Policy로 "어떤 sub, aud를 가진 토큰만 허용"을 정의하고, IAM Policy로 "허용된 액션"을 정의
AWS S3 / RDS 등 = Resource Server 임시 크레덴셜을 받은 Client(Pod)가 최종적으로 접근하는 보호된 리소스
AWS S3 읽기 전용 권한이 필요한 파드에 IRSA 실습
우선 서비스 어카운트 리소스를 새롭게 하나 생성한다.
# Create an iamserviceaccount - AWS IAM role bound to a Kubernetes service account
eksctl create iamserviceaccount \
--name my-sa \
--namespace default \
--cluster $CLUSTER_NAME \
--approve \
--role-name eksctl-myeks-pod-irsa-s3-readonly-role \
--attach-policy-arn $(aws iam list-policies --query 'Policies[?PolicyName==`AmazonS3ReadOnlyAccess`].Arn' --output text)아래처럼 생성된것을 확인한다.

# Pod Identity Webhook은 mutating webhook을 통해 아래 Env 내용과 1개의 볼륨을 추가함
kubectl get pod eks-iam-test3
kubectl get pod eks-iam-test3 -o yaml또한 웹훅에 의해서 아래 토큰 및 볼륨이 추가된것도 확인할 수 있다.

다음으로 파드 내에서 해당 Role이 잘 사용되어 s3 리소스에 접근되는지 보자.
kubectl exec -it eks-iam-test3 -- aws sts get-caller-identity --query Arn
인가 (EKS Pod Identity)

eksctl create podidentityassociation \
--cluster $CLUSTER_NAME \
--namespace default \
--create-service-account \
--service-account-name s3-sa \
--role-name s3-eks-pod-identity-role \
--permission-policy-arns arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess \
--region ap-northeast-2





