포스팅은 CloudNet@팀 서종호(Gasida)님이 진행하시는 AWS EKS Workshop Study 내용을 참고하여 작성합니다.
안녕하세요!
오늘은 EKS Pod가 IAM Role을 사용할 때
주요 동작 방식에 대해 설명드려보고자 합니다.
이 챕터에서는 IRSA에 대해서 다루고,
다음 챕터에서는 Pod Identity를 다룰 예정입니다~!
먼저 EKS Pods가 IAM Role을 가지고 AWS 리소스를 사용하는 방식이 2가지가 있는데요,
기존에는 IRSA 방식으로 사용하였는데, 최근에 EKS Pod Identity 방식이 생겼습니다 !
항목
IRSA (기존)
EKS Pod Identity (신규)
인증 메커니즘
OIDC 기반 (ID 공급자 필요)
EKS 인증 웹훅 by Pod Identity Agent (직접 연동)
설정 복잡도
높음 (OIDC 생성, Trust Policy 복잡)
낮음 (단순 매핑)
신뢰 관계 (Trust)
oidc-eks.amazonaws.com 참조
pods.eks.amazonaws.com 서비스 참조
확장성
클러스터별로 OIDC 설정 필요
여러 클러스터에서 동일 역할 재사용 용이
SDK 지원
대부분 지원
최신 버전 SDK 필요
기존 방식에서 신규 방식을 소개하는 흐름으로 진행해보도록 하겠습니다.
01. Introduce to IRSA
IRSA (IAM Roles for Service Accounts) 는 Pod가 AWS 리소스에 접근할 때, 컨테이너 안에
AWS 키를 박아두지 않아도 되도록 Kubernetes Service Account와 IAM Role을 연동하는 메커니즘입니다.
핵심 아이디어는 "K8S가 발급한 OIDC 토큰을 AWS가 신뢰하도록 사전에 등록해두고,
Pod가 그 토큰으로 STS에 Role Assume을 요청한다"는 것입니다.
K8S가 발급한 OIDC 토큰을 AWS가 신뢰하도록 사전에 설정한다는 의미는 아래와 같습니다.
사전 설정 (Prerequisite)
① EKS OIDC Provider 등록 → AWS IAM에 클러스터를 신뢰 IdP로 등록
② IAM Role Trust Policy → pods.eks.amazonaws.com / oidc-eks.amazonaws.com 참조
③ ServiceAccount에 eks.amazonaws.com/role-arn 어노테이션 추가
그렇다면, 왜 OIDC로 신뢰하도록 할까요?
AWS가 K8S ServiceAccount 토큰을 직접 신뢰할 방법이 없습니다. 그래서 K8S(EKS)를 OIDC IdP로 AWS IAM에 등록해두면, AWS는 "이 클러스터가 서명한 토큰은 믿겠다"는 신뢰 관계를 갖게 됩니다.
📌 IRSA 특징
1) 최소 권한 원칙
서비스 계정에 IAM 역할을 사용하는 기능을 활용하면 더 이상 노드의 Pod가 AWS API를 호출할 수 있도록 노드 IAM 역할에 확장 권한을 부여할 필요가 없습니다.
IAM 권한을 서비스 계정으로 제한하면 해당 서비스 계정을 사용하는 Pod만 해당 권한에 접근할 수 있습니다.
2) 자격 증명 격리
컨테이너는 자신이 속한 서비스 계정과 연결된 IAM 역할에 대한 자격 증명만 가져올 수 있습니다.
컨테이너는 다른 파드에 속한 다른 컨테이너를 위한 자격 증명에는 절대 접근할 수 없습니다.
3) 감사 가능성
AWS CloudTrail을 통해 액세스 및 이벤트 로깅이 가능하므로 사후 감사를 보장할 수 있습니다.
How does IRSA work?
파드 생성 시 MutatingWebhook(pod-identity-webhook)이 ServiceAccount의
어노테이션(eks.amazonaws.com/role-arn)을 읽어 Pod Spec에 자동 주입합니다.
자, 생성 후 'kubectl get pod' 명령으로 확인하였더니 Error가 발생하였습니다. describe를 찍어볼게요
automountServiceAccountToken: false로 설정 시 아래와 같은 특징이 있겠습니다.
Pod 안에 K8S API 토큰이 없기 때문에 kubectl exec으로 파드 안에 들어가서 aws s3 ls 같은 걸 실행하는 건 가능하지만, kubectl 자체가 K8S API 서버와 통신할 때 쓰는 토큰과는 별개입니다.
즉, 파드 상태가 Error인 이유는 방금 생성한 eks-iam-test1 Pod가 restartPolicy: Never에 aws s3 ls 명령을 실행하고 바로 종료되기 때문입니다.
위와 같은 동작을 했던 결정적인 증거는, Exit Code가 254인 것과, Mounts가 <none>으로 되어 있습니다.
Status: Failed / Exit Code: 254 aws s3 ls 실행 시 자격증명이 없어서 AccessDenied로 종료된 겁니다. Mounts: none / Environment: none - automountServiceAccountToken: false 설정이 정상 적용된 증거입니다. - 토큰도 없고 환경변수도 없습니다. IRSA가 적용됐다면 여기에 AWS_ROLE_ARN, AWS_WEB_IDENTITY_TOKEN_FILE 이 보여야 합니다.
즉, IRSA가 없던 시절에는 이 문제를 해결하려면 노드 Role에 S3 권한을 직접 부여해야 했고,
그러면 해당 노드 위의 모든 Pod가 S3 접근 권한을 갖게 되는 과도한 권한 부여 문제가 생깁니다.
03. First token for Service Account : kubernetes.default.svc
📌해당 실습의 목적
Kubernetes가 Pod에 자동으로 주입하는 토큰이 무엇인지 직접 눈으로 확인한다
그 토큰이 AWS API에는 왜 사용될 수 없는지 한계를 명확히 이해한다
이 두 가지를 이해해야 IRSA가 왜 필요한지, 어떻게 동작하는지를 자연스럽게 납득할 수 있습니다.
기존 ServiceAccount 토큰의 문제점
Kubernetes 1.12 이전에는 ServiceAccount를 생성하면 kube-controller-manager가 Secret
오브젝트를 자동 생성하고, 그 안에 JWT 토큰을 저장했습니다.
kubectl create sa my-sa
└─ Secret/my-sa-token-xxxxx 자동 생성
└─ token: eyJhbGciOi... (영구 토큰, 만료 없음)
이 방식에는 치명적인 보안 문제가 있다고 지난 포스팅에도 말씀드렸는데요, 다시 말씀 드려보자면
토큰 만료가 없음 : 한번 발급된 토큰은 SA를 삭제하기 전까지 영원히 유효 audience 없음 : 어떤 서비스에서든 이 토큰을 제시하면 사용 가능 Secret에 평문 저장 : Secret을 읽을 권한이 있으면 토큰 탈취가 가능합니다. 자동 마운트 : 모든 Pod에 자동으로 마운트되어 불필요한 노출 발생
이러한 문제를 해결하기 위해 Kubernetes 1.12에서 ProjectedServiceAccountToken 기능이 추가되었는데요,
📌 ProjectedServiceAccountToken 이란?
핵심은 토큰을 Secret에 저장하는 것이 아니라, TokenRequest API를 통해 동적으로 발급하고
Pod에 Projected Volume 형태로 마운트한다는 점입니다.
Pod 생성 요청
└─ kubelet이 TokenRequest API 호출
└─ 만료 시간 있는 OIDC JWT 토큰 발급
└─ Projected Volume으로 Pod에 마운트
└─ kubelet이 만료 전 자동 갱신
여기서 Projected Volume이란
여러 소스(ServiceAccountToken, ConfigMap, Secret, DownwardAPI)를 하나의 볼륨으로 합쳐서 마운트하는 Kubernetes의 볼륨 타입입니다. 기존 Secret 볼륨과 달리 동적으로 갱신이 가능한 것이 특징입니다.
기존 방식과 비교하면 다음과 같습니다.
항목
기존 (Secret 기반)
ProjectedServiceAccountToken
발급 주체
kube-controller-manager
TokenRequest API (kubelet 경유)
저장 위치
etcd (Secret 오브젝트)
메모리 (etcd 미저장)
만료 시간
X
있음 (기본 1시간)
audience
X
명시적으로 지정
갱신
수동 (SA 재생성)
kubelet 자동 갱신
파드 정보 포함
X
있음 (pod, node, namespace)
EKS에서는 이 기능이 기본 활성화되어 있으며, 모든 Pod에 OIDC 규격의 JWT 토큰이 자동 마운트 됩니다.
테스트 파드를 생성해볼까요?
# 파드 생성
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
name: eks-iam-test2
spec:
containers:
- name: my-aws-cli
image: amazon/aws-cli:latest
command: ['sleep', '36000']
restartPolicy: Never
terminationGracePeriodSeconds: 0
EOF
# 확인
kubectl get pod
kubectl describe pod
kubectl get pod eks-iam-test2 -o yaml
amazon/aws-cli 이미지를 사용하는 이유가 있습니다. 단순히 토큰을 확인하는 것에서 그치지 않고, aws s3 ls 명령으로 AWS API 호출을 직접 시도해서 이 토큰이 AWS 인증에 사용될 수 없음을 몸소 확인하기 위해서입니다.
Step1. Pod YAML에서 자동 마운트된 볼륨 확인
'kubectl get pod eks-iam-test2 -o yaml' 출력 중, volumes 섹션을 주목합니다.
여기서 중요한 점은 serviceAccountToken의 expirationSeconds: 3607입니다.
기존 방식과 달리 토큰에 만료 시간이 명시되어 있습니다.
kubelet은 만료 80% 시점에 도달하면 자동으로 TokenRequest API를 재호출하여 새 토큰으로 교체합니다.
또한 SA 정보도 확인해봅시다.
별도로 SA를 지정하지 않았기 때문에 default 네임스페이스의 default ServiceAccount가 자동 할당된 것을 확인할 수 있습니다.
Step2. 마운트 경로 직접 탐색
Pod 내부에서 토큰이 실제로 어디에 위치하는지 확인합니다.
파일들이 모두 ..data 디렉토리를 가리키는 심볼릭 링크인 것을 확인할 수 있습니다.
kubelet이 토큰을 갱신할 때 ..data 심볼릭 링크가 가리키는 디렉토리를 원자적(atomic)으로 교체합니다.
이 덕분에 애플리케이션이 토큰을 읽는 도중 불완전한 토큰을 읽는 상황이 발생하지 않습니다
Step3. 토큰 추출 & 디코딩 분석
위와 같은 형태의 JWT 토큰이 출력됩니다.
JWT는 .으로 구분된 세 부분(헤더.페이로드.서명)으로 이루어진 Base64 인코딩 문자열입니다.
위 JWT 토큰을 (https://jwt.io/) 웹사이트를 통해 디코딩 해보면 아래와 같습니다.
서명에 사용된 키의 ID. 검증 시 OIDC Provider의 공개키 중 해당 kid를 가진 키로 서명 검증
typ
JWT
JSON Web Token 규격
📌 페이로드 분석
1) iss (Issuer — 발급자)
"iss": "https://oidc.eks.ap-northeast-2.amazonaws.com/id/F6A7523462E8E6CDADEE5D41DF2E71F6" 이 토큰을 발급한 주체입니다. EKS 클러스터마다 고유한 OIDC Provider URL이 부여됩니다. 토큰을 검증하는 측(예: Kubernetes API 서버, AWS STS)은 이 URL로 접근하여 공개키를 가져오고 서명을 검증합니다.
2) aud (Audience — 대상)
"aud": ["https://kubernetes.default.svc"] 이 토큰이 사용될 수 있는 서비스를 명시합니다. https://kubernetes.default.svc는 클러스터 내부에서 Kubernetes API 서버에 접근하는 ClusterIP 서비스 도메인입니다.
토큰을 수신한 서비스는 자신의 식별자가 aud에 포함되어 있는지 반드시 확인합니다. AWS STS의 경우 aud가 sts.amazonaws.com이어야 합니다. 이 토큰의 aud는 https://kubernetes.default.svc이므로 AWS STS는 이 토큰을 거부합니다.
3) sub (Subject — 주체)
"sub": "system:serviceaccount:default:default" 이 토큰의 소유자입니다. default 네임스페이스의 default ServiceAccount임을 나타냅니다. Kubernetes RBAC는 이 값을 기반으로 권한을 판단합니다.
4) exp / iat / nbf (시간 관련)
"iat": 1685083848 # 발급 시간 (Issued At)
"nbf": 1685083848 # 유효 시작 시간 (Not Before)
"exp": 1716619848 # 만료 시간 (Expiration)
이 토큰이 어떤 Pod에서, 어떤 노드에서, 어떤 SA로 발급되었는지를 담고 있습니다. warnafter는 Kubernetes 내부 로직에서 사용되며, 이 시간 이후부터는 토큰 갱신을 권장한다는 의미입니다.
Step4. 다음 실습 예고 및 정리
이번 실습에서 확인한 것은 아래와 같습니다.
ProjectedServiceAccountToken
├─ 위치: /var/run/secrets/kubernetes.io/serviceaccount/token
├─ 발급: TokenRequest API → kubelet이 자동 관리
├─ 특징: 만료 시간 있음, audience 명시, Pod 정보 포함
├─ aud: https://kubernetes.default.svc
│ └─ k8s API 서버 전용 토큰
└─ 한계: AWS API 인증 불가
이 토큰만으로 부족한 이유는 말씀드렸다 싶이 AWS STS가 이 토큰을 거부합니다.
밑에서 다룰 다음 실습은 'aud:sts.amazonaws.com'의 토큰을 토큰을 Kubernetes Pod에 주입하는 추가 구성!!
04. Second token for Service Account : sts.amazonaws.com
SA 두번째 토큰을 확인하기 이전에 IRSA를 사용하여 LBC Pod에만 딱 필요한 권한을 부여해보겠습니다.
목표는 아래와 같습니다.
aws-load-balancer-controller Pod → LBC 전용 IAM Role → ALB/NLB 제어 권한만
다른 모든 Pod → AWS 접근 불가 (또는 별도 Role)
Install AWS LBC and grant permissions necessary only for LBC pods using IRSA
1) IRSA 설정
IRSA의 핵심은 AWS가 EKS 클러스터를 신뢰하는 것입니다.
그 신뢰의 근거가 바로 IAM OIDC Provider 등록입니다. 기존 IAM OIDC 공급자를 확인해볼까요?
네, 클러스터의 OIDC ID를 확인하였고, IAM에 등록되어 있는것 까지 확인하였습니다.
실제로 AWS Console의 IAM에서 확인이 가능하니 참고 바랍니다.
OIDC Discovery 엔드포인트 확인
OIDC 표준에는 /.well-known/openid-configuration 경로를 통해 공개 메타데이터를 제공하는 규약이 있습니다.
AWS STS는 토큰 검증 시 이 경로를 조회해요.
이 공개키의 kid 값이 앞선 실습에서 디코딩한 JWT 토큰 헤더와 JWKS에서 일치해야 한다는 점입니다.
kid가 연결고리인 이유 JWT 헤더의 kid ──────────────────────→ JWKS에서 동일 kid의 키 선택 "890268660..." "890268660..."의 n, e로 서명 검증
JWKS에 키가 여러 개일 수 있기 때문에 kid로 "어떤 키로 서명했는지" 명시하는 것 (키 교체 시 구버전 토큰도 검증 가능하도록)
📌 IRSA 토큰 검증 원리
Pod가 보낸 JWT 토큰
↓
STS: 토큰의 iss 필드 확인
→ https://oidc.eks.ap-northeast-2.amazonaws.com/id/7AB27B...
↓
STS: iss/.well-known/openid-configuration 조회
→ jwks_uri 획득
↓
STS: jwks_uri/keys 에서 공개키 목록 가져옴
→ kid로 해당 키 선택
↓
STS: 공개키로 토큰 서명 검증
→ 변조 여부 확인
↓
STS: sub, aud 조건 검증
→ IAM Role Trust Policy 조건과 일치하는지 확인
↓
검증 성공 → 임시 자격증명 발급
2) LBC용 IAM Policy 생성
IAM Role에 붙일 권한 정책을 만듭니다.
LBC가 ALB/NLB를 제어하기 위해 필요한 EC2, ELB, WAF 등의 권한이 정의되어 있습니다.
# IAM Policy json 파일 다운로드 : Download an IAM policy for the AWS Load Balancer Controller that allows it to make calls to AWS APIs on your behalf.
curl -o aws_lb_controller_policy.json https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/refs/heads/main/docs/install/iam_policy.json
cat aws_lb_controller_policy.json | jq
# AWSLoadBalancerControllerIAMPolicy 생성 : Create an IAM policy using the policy downloaded in the previous step.
aws iam create-policy \
--policy-name AWSLoadBalancerControllerIAMPolicy \
--policy-document file://aws_lb_controller_policy.json
# 확인
ACCOUNT_ID=$(aws sts get-caller-identity --query "Account" --output text)
aws iam get-policy --policy-arn arn:aws:iam::$ACCOUNT_ID:policy/AWSLoadBalancerControllerIAMPolicy | jq
위에서 생성한 정책을 AWS Console 에서도 아래와 같이 확인이 가능합니다.
이 권한들이 노드 IAM Role에 붙어있다면 모든 Pod가 ALB를 생성·삭제할 수 있는 위험한 상황이 됩니다.
아래와 같이 배포가 성공하였고, ServiceAccounts 까지 생성된 것을 확인할 수 있습니다.
IAM Role 생성된 것을 확인하고, 관리 콘솔에서도 확인해볼까요?
(저는 ARN이 두개인데 하나는 저번 실습때 ClusterAutoScaler을 진행하고 안지운 것 같네요)
먼저 Turst Policy 먼저 살펴보도록 하겠습니다.
각 필드의 의미를 상세하게 분석 해보면 아래와 같습니다.
Principal.Federated : "이 EKS 클러스터의 OIDC Provider가 발급한 토큰만 신뢰"
→ 다른 클러스터의 토큰은 거부
Action: sts:AssumeRoleWithWebIdentity : "일반적인 AssumeRole과 다릅니다"
AssumeRole → IAM User/Role이 Role 전환
AssumeRoleWithWebIdentity → OIDC 토큰으로 Role 전환 (IRSA 방식)
Condition.sub (가장 중요한 조건)
"system:serviceaccount:kube-system:aws-load-balancer-controller"
─────────── ────────────────────────────
namespace SA 이름
→ kube-system 네임스페이스의 aws-load-balancer-controller SA만 허용
→ default 네임스페이스의 SA는 거부
→ 다른 이름의 SA는 거부
Condition.aud : "sts.amazonaws.com"
→ 앞선 실습의 첫 번째 토큰 (aud: kubernetes.default.svc) 은 거부!
→ 웹훅이 주입한 두 번째 토큰 (aud: sts.amazonaws.com) 만 허용
이번에는 LBC에 할당할 K8S SA 어노테이션을 확인해볼까요?
이 어노테이션이 트리거 역할을 합니다.
Pod 생성 시 amazon-eks-pod-identity-webhook이 이 어노테이션을 감지하고 두 번째 토큰을 주입합니다.
Webhook이 Pod에 주입하는 것을 아래와 같이 상세히 분석해보았습니다. (LBC Pod가 생성되면 웹훅이 자동으로 다음을 추가합니다.)
# LBC 파드 정상 동작 확인
kubectl get pods -n kube-system | grep aws-load-balancer
# ServiceAccount에 IRSA 어노테이션 확인
kubectl get sa aws-load-balancer-controller -n kube-system -o yaml | grep role-arn
# IRSA Role이 실제로 생성됐는지 확인
aws iam get-role --role-name myeks-aws-lb-controller-irsa --query "Role.RoleName"
# Helm release 상태 확인
helm list -n kube-system
# LBC 로그 확인 (옵션)
kubectl logs -n kube-system -l app.kubernetes.io/name=aws-load-balancer-controller --tail=20