Published on

EKS IRSA 설정했는데 AccessDenied 뜨는 9가지

Authors

서버리스 워크로드든 배치 잡이든, EKS에서 AWS 권한을 가장 안전하게 주는 방식은 IRSA입니다. 그런데 IRSA를 “설정했다”고 생각했는데도 애플리케이션 로그에는 AccessDenied 혹은 AccessDeniedException 이 계속 찍히는 경우가 많습니다.

이 글은 “IRSA 자체가 안 붙는 문제”와 “IRSA는 붙었는데 특정 AWS 서비스에서만 거부되는 문제”를 분리해, 현장에서 자주 만나는 9가지 원인을 재현 가능한 체크리스트로 정리합니다.

관련해서 S3만 403이 나는 케이스는 아래 글도 함께 보면 진단 시간이 확 줄어듭니다.

진단 시작 전: “누가 거부당했는지”부터 확정

먼저 에러 메시지에서 User: 또는 arn:aws:sts::...:assumed-role/... 를 확인해야 합니다. IRSA 문제인지, 노드 IAM Role 문제인지가 여기서 갈립니다.

애플리케이션이 AWS SDK를 쓰는 경우, 보통 아래 중 하나로 나타납니다.

  • User: arn:aws:sts::123456789012:assumed-role/my-irsa-role/xxxxxxxx is not authorized to perform: ...
  • User: arn:aws:iam::123456789012:role/eks-node-role is not authorized ...

두 번째 형태로 노드 Role이 보이면, IRSA가 적용되지 않았거나 SDK가 IRSA 자격증명 체인을 못 타고 있는 겁니다.

빠른 확인 커맨드

Pod 내부에서 다음을 확인합니다.

# IRSA에서는 이 파일이 있어야 합니다.
ls -al /var/run/secrets/eks.amazonaws.com/serviceaccount/

# SDK가 이 환경변수를 읽어야 합니다.
env | grep -E 'AWS_ROLE_ARN|AWS_WEB_IDENTITY_TOKEN_FILE|AWS_REGION|AWS_DEFAULT_REGION'

그리고 가능하면 aws sts get-caller-identity 로 “현재 누구로 호출되는지”를 못 박습니다.

aws sts get-caller-identity

1) ServiceAccount에 role annotation이 실제로 안 붙음

가장 흔합니다. Helm values나 Kustomize overlay에서 ServiceAccount가 따로 생성되거나, serviceAccountName 이 다른 이름을 가리키는 경우입니다.

확인:

kubectl -n myns get sa mysa -o yaml | sed -n '1,120p'

아래 annotation이 있어야 합니다.

metadata:
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/my-irsa-role

해결 포인트:

  • Deployment의 spec.template.spec.serviceAccountName 이 실제 SA와 일치하는지 확인
  • Helm 차트가 serviceAccount.createtrue 로 만들면서 다른 SA를 생성하지 않는지 확인

2) Pod가 기본 ServiceAccount로 떠버림

ServiceAccount를 만들어도 Deployment/Job에서 지정하지 않으면 기본 SA를 씁니다. 특히 CronJob에서 자주 발생합니다.

확인:

kubectl -n myns get pod mypod -o jsonpath='{.spec.serviceAccountName}'; echo

default 로 나오면 IRSA는 거의 100% 의도대로 적용되지 않습니다.

해결:

  • Deployment/Job/CronJob 모두에 serviceAccountName 명시
  • 템플릿이 여러 개인 경우(예: Argo Rollouts, Spark Operator) 실제 Pod 템플릿 위치에 들어갔는지 재확인

3) IAM Role 신뢰 정책(trust policy)의 조건 키가 틀림

IRSA의 핵심은 IAM Role의 trust policy에서 OIDC provider와 sub 조건을 정확히 매칭하는 것입니다.

정상 예시(중요 부분만):

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::123456789012:oidc-provider/oidc.eks.ap-northeast-2.amazonaws.com/id/EXAMPLED539D4633E53DE1B716D3041E"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "oidc.eks.ap-northeast-2.amazonaws.com/id/EXAMPLED539D4633E53DE1B716D3041E:sub": "system:serviceaccount:myns:mysa",
          "oidc.eks.ap-northeast-2.amazonaws.com/id/EXAMPLED539D4633E53DE1B716D3041E:aud": "sts.amazonaws.com"
        }
      }
    }
  ]
}

실수 패턴:

  • OIDC provider URL에서 id/... 값이 클러스터 것과 다름
  • sub 에 namespace 또는 SA 이름 오타
  • StringLikeStringEquals 혼용하면서 의도치 않게 매칭 실패

검증 팁:

aws eks describe-cluster --name mycluster --query 'cluster.identity.oidc.issuer' --output text

이 issuer와 trust policy의 키 prefix가 정확히 동일해야 합니다.

4) OIDC Provider 자체가 계정에 등록되지 않았거나 다른 클러스터 것임

클러스터를 새로 만들고 IRSA Role은 예전 설정을 복붙한 경우, “provider ARN이 존재하지 않거나 다른 issuer” 일 수 있습니다.

확인:

aws iam list-open-id-connect-providers

그리고 provider 상세에서 URL 확인:

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/EXAMPLE

해결:

  • eksctl utils associate-iam-oidc-provider 를 사용하거나 Terraform로 현재 클러스터 issuer를 등록
  • 멀티 클러스터라면 “클러스터별 issuer” 가 다르다는 점을 전제로 Role을 분리

5) 애플리케이션이 IRSA 자격증명 체인을 안 탐

IRSA는 보통 AWS_WEB_IDENTITY_TOKEN_FILEAWS_ROLE_ARN 을 통해 SDK가 AssumeRoleWithWebIdentity 를 호출하도록 유도합니다.

하지만 다음 상황이면 SDK가 이를 무시하고 다른 자격증명 소스를 씁니다.

  • 컨테이너에 AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY 가 하드코딩되어 우선순위를 선점
  • AWS SDK 버전이 너무 낮아 web identity를 제대로 지원하지 않음
  • 커스텀 credential provider를 강제로 사용

확인:

env | grep -E 'AWS_ACCESS_KEY_ID|AWS_SECRET_ACCESS_KEY|AWS_PROFILE|AWS_SDK_LOAD_CONFIG'

해결:

  • 정적 키 환경변수 제거
  • 언어별 SDK 버전 업(특히 Java, Go, Node에서 구버전이면 IRSA 인식이 어긋나는 사례가 있음)

6) STS 호출은 되는데 대상 서비스에서 AccessDenied: 정책 리소스/조건 불일치

IRSA가 성공하면 sts get-caller-identity 는 통과합니다. 그런데 S3, DynamoDB, SQS 같은 서비스 호출에서만 거부되면 대개 IAM policy가 “역할에 붙어 있지만 조건/리소스가 안 맞는” 문제입니다.

대표 예:

  • S3에서 arn:aws:s3:::my-bucket/* 만 열어놓고 ListBucketarn:aws:s3:::my-bucket 에 안 줌
  • KMS를 쓰는데 kms:Decrypt 권한이 없거나 key policy가 role을 허용하지 않음
  • 조건에 aws:RequestedRegion 등을 걸어놓고 실제 리전이 다름

S3 예시(자주 틀리는 포인트 포함):

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ListBucket",
      "Effect": "Allow",
      "Action": ["s3:ListBucket"],
      "Resource": "arn:aws:s3:::my-bucket"
    },
    {
      "Sid": "RWObjects",
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:PutObject"],
      "Resource": "arn:aws:s3:::my-bucket/*"
    }
  ]
}

진단은 CloudTrail에서 errorCode=AccessDenied 이벤트를 보고 “어떤 action이 어떤 resource로 평가됐는지” 확인하는 게 가장 빠릅니다.

7) Permission Boundary 또는 SCP 때문에 최종적으로 거부됨

Role에 정책을 붙여도, 다음 상위 제약이 있으면 결과는 AccessDenied입니다.

  • IAM Permission Boundary
  • AWS Organizations의 SCP
  • 세션 정책(session policy)을 추가로 씌우는 구조

특징:

  • 정책 문서만 보면 허용인데, 실제 호출은 계속 거부
  • CloudTrail에 explicitDeny 또는 조직 정책 관련 흔적이 보이기도 함

확인 포인트:

aws iam get-role --role-name my-irsa-role

출력에서 PermissionsBoundary 가 있는지 봅니다. SCP는 계정/OU 단에서 확인해야 합니다.

8) 토큰/시간/네트워크 이슈가 AccessDenied로 보이는 경우

정확히는 AccessDenied 가 아니라 InvalidIdentityToken RequestExpired SignatureDoesNotMatch 등으로 나타나기도 하지만, 운영 로그에서 뭉뚱그려 “권한 문제”로 분류되어 놓치기 쉽습니다.

체크:

  • 노드 시간 동기화 문제로 STS 요청이 만료 처리
  • 프록시/네트워크 경로에서 STS 엔드포인트 접근이 비정상
  • IPv6 경로에서만 STS가 실패하는 환경

특히 IPv6-only 또는 듀얼스택에서 STS 접근이 꼬이는 케이스는 아래 글의 체크리스트가 바로 도움이 됩니다.

9) 동일 Pod에 여러 컨테이너가 있고, 일부만 IRSA를 못 받는 구조

Pod 단위로 ServiceAccount는 하나지만, 실제로는 다음 이유로 컨테이너별 동작이 달라질 수 있습니다.

  • initContainer에서 AWS 호출을 하는데, 그 이미지의 SDK/CLI가 web identity를 지원하지 않음
  • 사이드카(예: 로그/메트릭 에이전트)가 자체적으로 정적 키 또는 다른 프로파일을 사용
  • AWS_REGION 이나 엔드포인트 설정이 컨테이너별로 다름

해결:

  • AWS 호출이 있는 모든 컨테이너에서 aws sts get-caller-identity 를 각각 실행
  • initContainer 이미지에 들어있는 AWS CLI 버전 확인
# 컨테이너를 지정해서 실행
kubectl -n myns exec -it mypod -c app -- aws sts get-caller-identity
kubectl -n myns exec -it mypod -c sidecar -- aws sts get-caller-identity

재현 가능한 10분 체크리스트

아래 순서대로 보면 대부분 10분 안에 “IRSA가 안 붙는 문제”와 “권한 정책 문제”를 분리할 수 있습니다.

  1. Pod에서 aws sts get-caller-identity 실행
  2. Pod의 serviceAccountName 확인
  3. ServiceAccount annotation에 eks.amazonaws.com/role-arn 확인
  4. Role trust policy의 issuer, sub, aud 확인
  5. 계정에 OIDC provider가 현재 클러스터 issuer로 등록되어 있는지 확인
  6. 정적 키 환경변수(AWS_ACCESS_KEY_ID 등) 유무 확인
  7. CloudTrail에서 거부된 action/resource 확인
  8. Permission boundary/SCP 존재 여부 확인
  9. KMS 사용 시 key policy와 role policy를 함께 점검

마무리: “IRSA는 됐는데 AccessDenied”는 결국 정책 평가 문제다

IRSA는 인증(누구냐)을 해결해주고, AccessDenied는 인가(무엇을 할 수 있냐)에서 터지는 경우가 많습니다. 그래서 진단도 sts get-caller-identity 로 인증을 먼저 확정한 뒤, CloudTrail로 인가 평가를 따라가는 방식이 가장 빠릅니다.

특정 서비스(S3)에서만 403이 반복된다면, 엔드포인트/VPC 경로/KMS까지 같이 보는 종합 진단이 필요합니다.