Published on

EKS IRSA AccessDenied 해결 - OIDC와 Trust 정책

Authors

EKS에서 IRSA(IAM Roles for Service Accounts)를 붙였는데도 애플리케이션 로그에 AccessDenied가 뜨면, 대부분 원인은 두 가지입니다. 첫째, 클러스터의 OIDC Provider와 IAM Role Trust 정책(AssumeRoleWithWebIdentity 조건)이 서로 정확히 맞물리지 않은 경우. 둘째, 실제로 파드가 해당 ServiceAccount로 실행되지 않거나 토큰/환경변수 주입이 기대와 다르게 동작하는 경우입니다.

이 글은 “왜 AccessDenied가 나는지”를 추측하지 않고, 어디를 어떤 순서로 확인하면 반드시 원인을 좁힐 수 있는지를 OIDC·Trust 정책 중심으로 정리합니다.

관련해서 ExternalDNS 같은 컴포넌트에서 자주 터지는 케이스는 아래 글도 함께 보면 맥락이 이어집니다.


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

여기서 중요한 분기점은 두 가지입니다.

  1. STS AssumeRoleWithWebIdentity 단계에서 막히는가 (Trust/OIDC/토큰 문제)
  2. AssumeRole은 되는데 특정 서비스 API에서 막히는가 (Permission policy 문제)

이 글은 1번(특히 AccessDenied)을 집중적으로 다룹니다.


전체 구조: 무엇이 무엇과 매칭되어야 하나

IRSA는 아래 4가지가 정확히 일치해야 합니다.

  1. EKS 클러스터의 OIDC Issuer URL
  2. AWS 계정에 등록된 IAM OIDC Provider
  3. IAM Role의 Trust policy에서 PrincipalCondition(특히 sub, aud)
  4. 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:// 뺀 값과 같은지
  • ClientIDListsts.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입니다. 특히 Conditionsubaud 가 정확히 일치해야 합니다.

(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:sub
  • oidc.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 릴리스명 접두사로 바뀌었는데 반영 안 함
  • subStringLike로 넓게 열어야 하는데 StringEquals로 박아두고 SA가 여러 개인 상황(예: canary)과 충돌

(4) aud 조건 누락 또는 값 불일치

EKS IRSA 기본은 audsts.amazonaws.com 입니다.

Trust policy에 aud를 넣지 않아도 동작하는 구성도 있지만, 보안상 넣는 것을 권장하고, 무엇보다 조직 내 표준 템플릿이 aud를 강제하는 경우 누락 시 실패로 이어집니다.

반대로 OIDC Provider의 ClientIDListsts.amazonaws.com를 포함하지 않으면, Trust policy가 맞아도 실패할 수 있습니다.

(5) Role은 맞는데 Pod에 토큰이 주입되지 않음

IRSA는 웹 아이덴티티 토큰 파일을 사용합니다. 파드 안에 아래 환경변수가 있어야 정상 경로입니다.

  • AWS_ROLE_ARN
  • AWS_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, 토큰 주입 문제
  • 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분 안에 원인이 좁혀집니다.

  1. 파드가 의도한 serviceAccountName으로 실행 중인가
  2. ServiceAccount에 eks.amazonaws.com/role-arn annotation이 정확한가
  3. 파드 env에 AWS_ROLE_ARN, AWS_WEB_IDENTITY_TOKEN_FILE가 존재하는가
  4. 클러스터 issuer를 조회해 OIDC URL을 확보했는가
  5. IAM OIDC Provider가 그 issuer로 등록되어 있고 sts.amazonaws.com client id가 있는가
  6. Role Trust policy의 Principal.Federated가 올바른 OIDC Provider ARN인가
  7. Trust policy의 Condition 키(issuer 문자열)와 sub가 정확히 일치하는가
  8. 파드 내부에서 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 호출 폭주로 인한 제한도 함께 점검해 보세요.