- Published on
EKS IRSA 권한 누락 AccessDenied 해결 가이드
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
EKS에서 IRSA(IAM Roles for Service Accounts)를 구성했는데 애플리케이션이 AWS API 호출 시 AccessDenied로 실패하는 경우가 생각보다 자주 발생합니다. 특히 Pod는 정상 기동되고, 노드 자체 권한(Instance Profile)로는 되던 작업이 IRSA 적용 후 갑자기 막히면 원인 파악이 더 헷갈립니다.
이 글은 “IRSA를 붙였는데도 왜 AccessDenied가 뜨는가”를 증상 기반으로 빠르게 좁혀가는 디버깅 흐름으로 정리합니다. 마지막에는 정상 동작하는 최소 구성 예제(OIDC, Trust Policy, ServiceAccount, 권한 정책)도 제공합니다.
문제 상황이 네트워크인지 헷갈릴 때는 EKS Pod는 뜨는데 트래픽 0 - NetPol·SG·CNI 10분 진단도 함께 참고하면 진단 시간을 줄일 수 있습니다.
IRSA AccessDenied가 의미하는 것
IRSA에서 AccessDenied는 크게 두 부류입니다.
역할을 아예 못 가져옴
- STS의
AssumeRoleWithWebIdentity단계에서 막힘 - 보통 Trust Policy, OIDC Provider, ServiceAccount 토큰/어노테이션 문제
- STS의
역할은 가져왔는데 권한이 부족함
- STS는 성공했지만 실제 API(예: S3, SQS, DynamoDB, Secrets Manager) 호출에서 거부
- IAM Permission Policy(권한 정책) 리소스/액션/조건 문제
로그에서 어느 단계에서 터지는지 먼저 분리해야 합니다.
대표 에러 패턴
AccessDenied: Not authorized to perform sts:AssumeRoleWithWebIdentityInvalidIdentityToken: No OpenIDConnect provider found in your accountAccessDeniedException(SQS),AccessDenied(S3),AccessDeniedException(DynamoDB)
0단계: Pod가 실제로 어떤 자격증명을 쓰는지 확인
IRSA 디버깅의 핵심은 “앱이 정말로 IRSA 역할을 쓰고 있는가”를 먼저 확인하는 것입니다.
환경 변수 확인
IRSA가 정상 주입되면 Pod에는 보통 아래 환경 변수가 존재합니다.
AWS_ROLE_ARNAWS_WEB_IDENTITY_TOKEN_FILE
확인 명령:
kubectl -n <namespace> exec -it <pod-name> -- env | grep -E 'AWS_ROLE_ARN|AWS_WEB_IDENTITY_TOKEN_FILE|AWS_REGION|AWS_DEFAULT_REGION'
위 명령에서 AWS_ROLE_ARN이 비어 있거나, 값이 기대한 Role ARN이 아니면 ServiceAccount 어노테이션부터 다시 봐야 합니다.
STS로 현재 호출자 확인
애플리케이션 대신 AWS CLI로 GetCallerIdentity를 찍어보면 가장 빠릅니다.
kubectl -n <namespace> exec -it <pod-name> -- aws sts get-caller-identity
결과의 Arn이 아래 형태면 IRSA로 Role을 잘 Assume한 상태입니다.
arn:aws:sts::<account-id>:assumed-role/<role-name>/<session-name>
만약 Node Instance Profile(노드 역할)로 찍힌다면, IRSA가 아니라 노드 권한을 사용 중입니다. 이 경우는 앱 SDK 설정이나 ServiceAccount 연결이 잘못된 경우가 많습니다.
1단계: ServiceAccount 어노테이션과 Pod 연결 확인
IRSA는 “Pod가 어떤 ServiceAccount로 뜨는지”와 “그 ServiceAccount에 Role ARN이 어노테이션 되었는지”가 전부입니다.
Pod가 사용하는 ServiceAccount 확인
kubectl -n <namespace> get pod <pod-name> -o jsonpath='{.spec.serviceAccountName}'; echo
예상한 ServiceAccount가 아니면 Deployment/Job/CronJob 템플릿의 serviceAccountName부터 수정해야 합니다.
ServiceAccount에 Role ARN 어노테이션 확인
kubectl -n <namespace> get sa <service-account-name> -o yaml
필수 어노테이션:
eks.amazonaws.com/role-arn: arn:aws:iam::<account-id>:role/<role-name>
예시:
apiVersion: v1
kind: ServiceAccount
metadata:
name: app-sa
namespace: app
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/app-irsa-role
여기서 흔한 실수:
- 다른 namespace의 ServiceAccount를 만들어놓고 Pod는 다른 namespace에서 실행
- Role ARN 오타(계정 ID, role name)
- Helm values에서
serviceAccount.create는 true인데serviceAccount.annotations가 누락
2단계: OIDC Provider가 클러스터에 연결되어 있는지 확인
InvalidIdentityToken류는 대부분 OIDC Provider 미연결/오타입니다.
클러스터 OIDC Issuer 확인
aws eks describe-cluster --name <cluster-name> --query 'cluster.identity.oidc.issuer' --output text
출력 예:
https://oidc.eks.ap-northeast-2.amazonaws.com/id/ABCDEFG1234567890
IAM OIDC Provider 존재 확인
aws iam list-open-id-connect-providers
OIDC Provider ARN 목록이 나오는데, 해당 클러스터의 id/ABCDEFG...가 포함된 Provider가 있어야 합니다.
없다면 eksctl로 생성하는 것이 가장 빠릅니다.
eksctl utils associate-iam-oidc-provider --cluster <cluster-name> --approve
3단계: IAM Role Trust Policy 조건 불일치 점검
OIDC Provider가 있어도 sts:AssumeRoleWithWebIdentity가 AccessDenied라면, 대개 Trust Policy의 sub 조건이 ServiceAccount와 불일치합니다.
IRSA Trust Policy의 핵심은 아래 두 조건입니다.
aud가sts.amazonaws.comsub가system:serviceaccount:<namespace>:<serviceaccount>
정상적인 Trust Policy 예시
아래 예시는 가장 흔한 형태입니다. StringEquals의 키에 들어가는 OIDC URL은 https://를 제거한 값이 사용됩니다.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789012:oidc-provider/oidc.eks.ap-northeast-2.amazonaws.com/id/ABCDEFG1234567890"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"oidc.eks.ap-northeast-2.amazonaws.com/id/ABCDEFG1234567890:aud": "sts.amazonaws.com",
"oidc.eks.ap-northeast-2.amazonaws.com/id/ABCDEFG1234567890:sub": "system:serviceaccount:app:app-sa"
}
}
}
]
}
흔한 실수 체크
sub에 namespace가 다름:system:serviceaccount:default:app-sa로 되어 있는데 실제는appnamespace- ServiceAccount 이름이 다름:
appvsapp-sa - OIDC ID가 다른 클러스터 것(클러스터 재생성 후 OIDC 변경)
StringLike로 넓게 열어놓고도 키를 잘못 적어 조건 불일치
여러 ServiceAccount를 허용해야 한다면
운영에서 팀별/워크로드별로 ServiceAccount가 여러 개면 StringLike로 namespace 단위로 묶는 경우가 있습니다.
{
"Condition": {
"StringEquals": {
"oidc.eks.ap-northeast-2.amazonaws.com/id/ABCDEFG1234567890:aud": "sts.amazonaws.com"
},
"StringLike": {
"oidc.eks.ap-northeast-2.amazonaws.com/id/ABCDEFG1234567890:sub": "system:serviceaccount:app:*"
}
}
}
단, 과도하게 넓히면 권한 경계가 약해지므로 최소 범위로 유지하세요.
4단계: 권한 정책(Policy) 자체가 부족한 경우
STS Assume은 성공하는데 S3/SQS 등에서 AccessDenied가 나면 이제는 순수 IAM 권한 문제입니다.
에러 메시지에서 액션과 리소스 추출
예를 들어 S3라면 보통 아래처럼 나옵니다.
User: arn:aws:sts::<account-id>:assumed-role/app-irsa-role/... is not authorized to perform: s3:GetObject on resource: arn:aws:s3:::my-bucket/path/file
여기서 필요한 것은:
- Action:
s3:GetObject - Resource:
arn:aws:s3:::my-bucket/path/*
S3 예시 정책
버킷 리스트와 오브젝트 읽기를 분리해야 하는 패턴이 흔합니다.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ListBucket",
"Effect": "Allow",
"Action": ["s3:ListBucket"],
"Resource": ["arn:aws:s3:::my-bucket"]
},
{
"Sid": "ReadObjects",
"Effect": "Allow",
"Action": ["s3:GetObject"],
"Resource": ["arn:aws:s3:::my-bucket/path/*"]
}
]
}
SQS 예시 정책
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"sqs:ReceiveMessage",
"sqs:DeleteMessage",
"sqs:GetQueueAttributes",
"sqs:ChangeMessageVisibility"
],
"Resource": "arn:aws:sqs:ap-northeast-2:123456789012:my-queue"
}
]
}
KMS가 끼어 있는지 확인
S3 SSE-KMS, Secrets Manager, SQS 암호화 등에서 kms:Decrypt 누락으로 AccessDenied가 나는 경우가 많습니다.
- 증상: S3
GetObject권한은 있는데 다운로드가 실패 - 해결: 해당 KMS Key에
kms:Decrypt및 필요 시kms:Encrypt,kms:GenerateDataKey추가
정책 예:
{
"Effect": "Allow",
"Action": ["kms:Decrypt"],
"Resource": "arn:aws:kms:ap-northeast-2:123456789012:key/11111111-2222-3333-4444-555555555555"
}
5단계: 잘못된 주체가 권한 평가에 걸리는 경우(노드 Role로 호출)
GetCallerIdentity가 노드 Role로 보이면, 앱이 IRSA를 무시하고 다른 credential chain을 타고 있을 수 있습니다.
대표 원인:
- 컨테이너에
AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY가 고정 주입됨(Secret, ConfigMap, CI 이미지) - SDK가 WebIdentity를 지원하지 않는 매우 구버전
AWS_PROFILE같은 로컬 개발 설정이 이미지에 남아 있음
확인:
kubectl -n <namespace> exec -it <pod-name> -- env | grep -E 'AWS_ACCESS_KEY_ID|AWS_SECRET_ACCESS_KEY|AWS_PROFILE'
발견되면 제거하고, SDK 버전을 최신으로 올리세요.
6단계: EKS Pod Identity와 혼동하지 않기
AWS는 IRSA 외에도 EKS Pod Identity라는 기능을 제공합니다. 둘을 섞어 구성하면 “어떤 메커니즘으로 Role이 주입되는지”가 꼬여 디버깅이 어려워집니다.
- IRSA: ServiceAccount 토큰 + OIDC + STS WebIdentity
- Pod Identity: 별도 에이전트/연동을 통해 Pod에 Role 제공
조직 표준이 IRSA라면 IRSA 한 가지로 통일하고, 점진적 전환 중이라면 워크로드 단위로 명확히 분리하세요.
7단계: 재현 가능한 최소 구성 예제(정상 템플릿)
아래는 “S3 특정 prefix 읽기”를 목표로 하는 최소 예시입니다.
1) ServiceAccount
apiVersion: v1
kind: ServiceAccount
metadata:
name: app-sa
namespace: app
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/app-irsa-role
2) Deployment에서 ServiceAccount 지정
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
namespace: app
spec:
replicas: 1
selector:
matchLabels:
app: app
template:
metadata:
labels:
app: app
spec:
serviceAccountName: app-sa
containers:
- name: app
image: public.ecr.aws/aws-cli/aws-cli:2.15.0
command: ["sh", "-c"]
args:
- |
aws sts get-caller-identity
aws s3 ls s3://my-bucket/path/
sleep 3600
3) IAM Role Trust Policy
앞 절의 Trust Policy 예시처럼 sub를 system:serviceaccount:app:app-sa로 정확히 맞춥니다.
4) IAM Permission Policy
S3 List/Get 정책을 Role에 Attach합니다.
빠른 체크리스트(10분 컷)
아래 순서대로 보면 대부분의 IRSA AccessDenied는 10분 내로 원인이 드러납니다.
- Pod 내부에서
aws sts get-caller-identity실행 AWS_ROLE_ARN,AWS_WEB_IDENTITY_TOKEN_FILE존재 확인- Pod의
serviceAccountName이 기대값인지 확인 - ServiceAccount 어노테이션
eks.amazonaws.com/role-arn확인 - 클러스터 OIDC Issuer와 IAM OIDC Provider 매칭 확인
- Role Trust Policy의
aud,sub조건 정확성 확인 - STS는 되는데 서비스 API만 실패하면 Permission Policy에서 Action/Resource/KMS 조건 점검
- 컨테이너에 고정 AWS 키가 주입되어 IRSA를 덮어쓰는지 확인
마무리
IRSA는 한 번 제대로 잡아두면 노드 권한을 최소화하고 워크로드 단위로 권한을 분리할 수 있는 강력한 패턴이지만, 실제 장애는 대부분 “사소한 문자열 불일치(Trust Policy의 sub), OIDC 연결 누락, 또는 권한 정책의 리소스 범위 실수”에서 발생합니다.
위의 흐름대로 STS Assume 단계와 실제 서비스 권한 단계를 분리해 확인하면, AccessDenied를 감으로 때려 맞추는 대신 재현 가능한 방식으로 빠르게 해결할 수 있습니다.