- Published on
EKS IRSA AccessDenied 해결 - OIDC와 Trust 정책
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
EKS에서 IRSA(IAM Roles for Service Accounts)를 붙였는데도 애플리케이션 로그에 AccessDenied가 뜨면, 대부분 원인은 두 가지입니다. 첫째, 클러스터의 OIDC Provider와 IAM Role Trust 정책(AssumeRoleWithWebIdentity 조건)이 서로 정확히 맞물리지 않은 경우. 둘째, 실제로 파드가 해당 ServiceAccount로 실행되지 않거나 토큰/환경변수 주입이 기대와 다르게 동작하는 경우입니다.
이 글은 “왜 AccessDenied가 나는지”를 추측하지 않고, 어디를 어떤 순서로 확인하면 반드시 원인을 좁힐 수 있는지를 OIDC·Trust 정책 중심으로 정리합니다.
관련해서 ExternalDNS 같은 컴포넌트에서 자주 터지는 케이스는 아래 글도 함께 보면 맥락이 이어집니다.
- EKS ExternalDNS가 Route53 생성 실패할 때 IRSA 점검
- IRSA 자체는 되는데 호출량 때문에 STS가 막히는 경우: EKS IRSA는 되는데 STS 429 Throttling 해결
IRSA AccessDenied의 전형적인 증상
다음 로그/에러 중 하나라도 보이면 IRSA 경로를 의심합니다.
- AWS SDK 에러:
AccessDenied: Not authorized to perform sts:AssumeRoleWithWebIdentity - 혹은
AccessDeniedException계열로 특정 AWS API 호출이 거절됨(예: Route53, S3, DynamoDB) InvalidIdentityToken또는No OpenIDConnect provider found in your account
여기서 중요한 분기점은 두 가지입니다.
- STS AssumeRoleWithWebIdentity 단계에서 막히는가 (Trust/OIDC/토큰 문제)
- AssumeRole은 되는데 특정 서비스 API에서 막히는가 (Permission policy 문제)
이 글은 1번(특히 AccessDenied)을 집중적으로 다룹니다.
전체 구조: 무엇이 무엇과 매칭되어야 하나
IRSA는 아래 4가지가 정확히 일치해야 합니다.
- EKS 클러스터의 OIDC Issuer URL
- AWS 계정에 등록된 IAM OIDC Provider
- IAM Role의 Trust policy에서
Principal과Condition(특히sub,aud) - Kubernetes ServiceAccount의 annotation
eks.amazonaws.com/role-arn
그리고 파드가 실제로 그 ServiceAccount로 실행되어야 하며, 토큰이 마운트되어야 합니다.
1단계: 파드가 “그 ServiceAccount”로 뜨는지 확인
의외로 가장 흔한 실수입니다. Deployment에 serviceAccountName을 안 넣었거나, Helm values가 덮어써서 default SA로 떠버리는 케이스가 많습니다.
kubectl -n kube-system get pod -l app=external-dns -o jsonpath='{.items[0].spec.serviceAccountName}'
원하는 ServiceAccount가 아니라면, IRSA는 아무리 잘 만들어도 동작하지 않습니다.
ServiceAccount annotation도 확인합니다.
kubectl -n kube-system get sa external-dns -o yaml
예상 형태(인라인 코드로 표기):
eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/external-dns-irsa-role
2단계: 클러스터 OIDC Issuer URL 확인
EKS 클러스터는 OIDC Issuer를 갖고 있고, 이 URL이 IAM OIDC Provider와 1:1로 연결됩니다.
aws eks describe-cluster \
--name my-eks \
--query 'cluster.identity.oidc.issuer' \
--output text
출력 예시는 보통 다음 형태입니다(부등호 금지이므로 인라인 코드로만 표기).
https://oidc.eks.ap-northeast-2.amazonaws.com/id/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
이 값에서 https:// 를 뺀 호스트+패스가 Trust policy의 Condition 키에 그대로 들어갑니다.
3단계: IAM OIDC Provider가 “정확히” 등록되어 있는지
OIDC Provider가 아예 없거나, 다른 클러스터의 OIDC를 등록해 둔 경우가 있습니다.
aws iam list-open-id-connect-providers
나오는 ARN 중에서 해당 클러스터의 issuer id가 포함된 항목을 찾아 describe 합니다.
aws iam get-open-id-connect-provider \
--open-id-connect-provider-arn arn:aws:iam::123456789012:oidc-provider/oidc.eks.ap-northeast-2.amazonaws.com/id/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
여기서 확인 포인트:
Url이 클러스터 issuer에서https://뺀 값과 같은지ClientIDList에sts.amazonaws.com가 있는지
ClientIDList가 다르면 Trust policy에서 aud 조건을 맞춰도 실패할 수 있습니다.
만약 OIDC Provider가 없다면, 보통 아래 중 하나로 생성합니다.
eksctl utils associate-iam-oidc-provider --cluster my-eks --approve
4단계: Trust policy에서 가장 많이 틀리는 5가지
AccessDenied의 핵심은 IAM Role의 Trust policy입니다. 특히 Condition의 sub 와 aud 가 정확히 일치해야 합니다.
(1) Principal.Federated가 잘못된 OIDC Provider를 가리킴
Trust policy의 Principal.Federated는 반드시 해당 클러스터의 OIDC Provider ARN이어야 합니다.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789012:oidc-provider/oidc.eks.ap-northeast-2.amazonaws.com/id/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
},
"Action": "sts:AssumeRoleWithWebIdentity"
}
]
}
여기가 다른 클러스터 OIDC를 가리키면 100% 실패합니다.
(2) Condition 키에서 issuer 문자열이 미세하게 다름
Condition 키는 보통 다음 두 개를 씁니다.
oidc.eks.region.amazonaws.com/id/ID:suboidc.eks.region.amazonaws.com/id/ID:aud
여기서 https:// 를 넣으면 안 됩니다. 또한 마지막 슬래시 유무, region, id가 조금이라도 다르면 매칭이 깨집니다.
(3) sub 값이 ServiceAccount와 불일치
sub는 Kubernetes의 SA 정체성을 나타내며 형식이 고정입니다.
system:serviceaccount:NAMESPACE:SERVICEACCOUNT_NAME
예시 Trust policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789012:oidc-provider/oidc.eks.ap-northeast-2.amazonaws.com/id/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"oidc.eks.ap-northeast-2.amazonaws.com/id/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA:aud": "sts.amazonaws.com",
"oidc.eks.ap-northeast-2.amazonaws.com/id/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA:sub": "system:serviceaccount:kube-system:external-dns"
}
}
}
]
}
자주 하는 실수:
- namespace를
default로 착각 - SA 이름이 Helm 릴리스명 접두사로 바뀌었는데 반영 안 함
sub를StringLike로 넓게 열어야 하는데StringEquals로 박아두고 SA가 여러 개인 상황(예: canary)과 충돌
(4) aud 조건 누락 또는 값 불일치
EKS IRSA 기본은 aud가 sts.amazonaws.com 입니다.
Trust policy에 aud를 넣지 않아도 동작하는 구성도 있지만, 보안상 넣는 것을 권장하고, 무엇보다 조직 내 표준 템플릿이 aud를 강제하는 경우 누락 시 실패로 이어집니다.
반대로 OIDC Provider의 ClientIDList가 sts.amazonaws.com를 포함하지 않으면, Trust policy가 맞아도 실패할 수 있습니다.
(5) Role은 맞는데 Pod에 토큰이 주입되지 않음
IRSA는 웹 아이덴티티 토큰 파일을 사용합니다. 파드 안에 아래 환경변수가 있어야 정상 경로입니다.
AWS_ROLE_ARNAWS_WEB_IDENTITY_TOKEN_FILE
확인:
kubectl -n kube-system exec -it deploy/external-dns -- env | grep -E 'AWS_ROLE_ARN|AWS_WEB_IDENTITY_TOKEN_FILE'
둘 중 하나라도 없으면, IRSA가 아니라 노드 IAM Role(EC2 instance profile)로 호출하다가 AccessDenied가 나는 형태가 됩니다.
5단계: STS 호출을 직접 재현해 Trust 문제인지 확정하기
애플리케이션이 뭘 하는지와 무관하게, STS AssumeRoleWithWebIdentity를 직접 때려보면 Trust/OIDC 문제를 빠르게 확정할 수 있습니다.
파드 내부에서 토큰 파일을 읽어 STS를 호출합니다.
kubectl -n kube-system exec -it deploy/external-dns -- sh -lc '
echo "ROLE=$AWS_ROLE_ARN";
echo "TOKEN_FILE=$AWS_WEB_IDENTITY_TOKEN_FILE";
aws sts assume-role-with-web-identity \
--role-arn "$AWS_ROLE_ARN" \
--role-session-name irsa-debug \
--web-identity-token file://$AWS_WEB_IDENTITY_TOKEN_FILE \
--duration-seconds 900
'
여기서 AccessDenied면 Permission policy가 아니라 Trust/OIDC/SA 매칭 문제입니다.
반대로 여기서 성공하면 IRSA 경로는 살아있고, 이후의 AccessDenied는 해당 Role에 붙은 IAM policy(예: route53:ChangeResourceRecordSets) 부족일 가능성이 큽니다.
6단계: Permission policy와 Trust policy를 혼동하지 않기
정리하면:
sts:AssumeRoleWithWebIdentity단계 AccessDenied- Trust policy, OIDC Provider,
sub/aud, 토큰 주입 문제
- Trust policy, OIDC Provider,
- AssumeRole은 성공했는데 서비스 API AccessDenied
- Role에 붙은 IAM permission policy 문제
현장에서는 두 에러가 로그상 비슷하게 보여서(둘 다 AccessDenied) 섞여 진단이 꼬입니다. 위의 STS 재현 커맨드로 먼저 경계를 확정하는 게 시간을 가장 아낍니다.
7단계: 자주 쓰는 Trust policy 패턴 3가지
패턴 A: ServiceAccount 1개만 허용(가장 안전)
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789012:oidc-provider/oidc.eks.ap-northeast-2.amazonaws.com/id/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"oidc.eks.ap-northeast-2.amazonaws.com/id/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA:aud": "sts.amazonaws.com",
"oidc.eks.ap-northeast-2.amazonaws.com/id/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA:sub": "system:serviceaccount:prod:myapp"
}
}
}
]
}
패턴 B: 같은 네임스페이스의 여러 SA 허용(운영 편의)
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789012:oidc-provider/oidc.eks.ap-northeast-2.amazonaws.com/id/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"oidc.eks.ap-northeast-2.amazonaws.com/id/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA:aud": "sts.amazonaws.com"
},
"StringLike": {
"oidc.eks.ap-northeast-2.amazonaws.com/id/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA:sub": "system:serviceaccount:prod:*"
}
}
}
]
}
패턴 C: 여러 클러스터를 하나의 Role로(권장도 낮음)
보안 경계가 흐려지기 쉬워서 신중해야 합니다. 가능하면 Role을 클러스터별로 분리하고, 어쩔 수 없으면 Statement를 분리해 issuer별로 조건을 명확히 둡니다.
8단계: 디버깅 체크리스트(현장용)
아래 순서대로 보면 대부분 10분 안에 원인이 좁혀집니다.
- 파드가 의도한
serviceAccountName으로 실행 중인가 - ServiceAccount에
eks.amazonaws.com/role-arnannotation이 정확한가 - 파드 env에
AWS_ROLE_ARN,AWS_WEB_IDENTITY_TOKEN_FILE가 존재하는가 - 클러스터 issuer를 조회해 OIDC URL을 확보했는가
- IAM OIDC Provider가 그 issuer로 등록되어 있고
sts.amazonaws.comclient id가 있는가 - Role Trust policy의
Principal.Federated가 올바른 OIDC Provider ARN인가 - Trust policy의 Condition 키(issuer 문자열)와
sub가 정확히 일치하는가 - 파드 내부에서 STS assume-role-with-web-identity를 직접 호출해 재현되는가
여기까지 통과하면, 남은 문제는 대개 permission policy(서비스 권한) 또는 호출량/레이트리밋(STS 429)로 수렴합니다.
마무리
EKS IRSA의 AccessDenied는 “권한이 없어서”라기보다 “신원 연동이 실패해서”인 경우가 많습니다. OIDC Provider와 Trust policy의 sub/aud 조건은 한 글자만 달라도 매칭이 깨지므로, 감으로 수정하기보다 파드 실행 SA 확인 → issuer/OIDC Provider 확인 → Trust policy 조건 확인 → STS 직접 재현 순서로 기계적으로 좁혀가면 안정적으로 해결됩니다.
만약 IRSA는 정상인데도 간헐적으로 장애가 난다면, STS 호출 폭주로 인한 제한도 함께 점검해 보세요.