Published on

EKS IRSA 설정했는데 STS AccessDenied 뜰 때

Authors

서론

EKS에서 IRSA(IAM Roles for Service Accounts)를 설정해두면 파드가 노드 IAM 역할 대신 ServiceAccount에 매핑된 IAM Role을 통해 AWS API를 호출할 수 있습니다. 그런데 설정을 “다 했는데도” 애플리케이션 로그에 아래와 같은 오류가 뜨는 경우가 흔합니다.

  • AccessDenied: Not authorized to perform sts:AssumeRoleWithWebIdentity
  • InvalidIdentityToken: No OpenIDConnect provider found in your account
  • AccessDenied: ... is not authorized to perform: sts:AssumeRoleWithWebIdentity on resource: ...

이 글은 IRSA의 동작 원리를 짧게 정리한 뒤, STS AccessDenied를 재현 가능한 형태로 쪼개서 어디가 잘못됐는지 빠르게 찾는 실전 체크리스트를 제공합니다. (ECR Pull 권한 문제와 섞여 보이는 경우도 많아 관련 글로 Kubernetes ImagePullBackOff 401 - ECR·IRSA·imagePullSecrets도 함께 참고하면 좋습니다.)

IRSA에서 STS가 실패하는 지점(문제 분해)

IRSA는 크게 다음 흐름으로 동작합니다.

  1. EKS 클러스터에 OIDC Provider가 등록되어 있다.
  2. Pod는 ServiceAccount로부터 Projected ServiceAccount Token(JWT) 을 받는다.
  3. AWS SDK는 AWS_ROLE_ARN, AWS_WEB_IDENTITY_TOKEN_FILE을 보고 STS의 AssumeRoleWithWebIdentity를 호출한다.
  4. STS는
    • 토큰의 iss(issuer)가 계정에 등록된 OIDC provider와 일치하는지
    • Role의 Trust Policy가 해당 토큰의 sub(serviceaccount)와 aud를 허용하는지
    • Role ARN이 맞는지 를 검증한 뒤 임시 자격증명을 발급한다.

따라서 STS AccessDenied는 보통 아래 4종류 중 하나로 귀결됩니다.

  • (A) OIDC Provider 미등록/불일치
  • (B) Trust Policy 조건 불일치(sub, aud, provider ARN)
  • (C) ServiceAccount annotation/네임스페이스 불일치(다른 SA가 붙음)
  • (D) IRSA는 성공했지만, Role policy 권한 부족을 STS AccessDenied로 오해(혹은 다른 AccessDenied)

이제 (A)~(D)를 순서대로 확인합니다.

1) 에러 메시지부터 분류하기

가장 먼저 애플리케이션 로그/CloudTrail에서 정확한 에러 문자열을 확인하세요.

1-1. No OpenIDConnect provider found

  • OIDC provider가 아예 없거나
  • 클러스터 issuer URL과 등록된 provider가 불일치합니다.

1-2. Not authorized to perform sts:AssumeRoleWithWebIdentity

  • (대부분) Role Trust Policy가 토큰 조건과 맞지 않습니다.
  • 또는 ServiceAccount가 원하는 Role ARN을 가리키지 않습니다.

1-3. AccessDenied가 STS가 아니라 S3/ECR/Secrets Manager 등에서 뜬다

  • IRSA AssumeRole은 성공했지만 해당 Role에 API 권한이 없음입니다.
  • CloudTrail에서 eventName이 AssumeRoleWithWebIdentity인지, GetObject/GetSecretValue인지 구분하세요.

2) OIDC Provider가 “정확히” 등록되어 있는지

EKS OIDC issuer URL을 확인합니다.

aws eks describe-cluster \
  --name <cluster-name> \
  --query "cluster.identity.oidc.issuer" \
  --output text

출력 예:

  • https://oidc.eks.ap-northeast-2.amazonaws.com/id/EXAMPLED539D4633E53DE1B716D3041E

이제 IAM에 등록된 OIDC provider 목록에서 위 URL의 host/path가 일치하는지 확인합니다.

aws iam list-open-id-connect-providers --query "OpenIDConnectProviderList[].Arn" --output text

# 특정 provider 상세
aws iam get-open-id-connect-provider --open-id-connect-provider-arn <provider-arn>

확인 포인트:

  • Url이 issuer에서 https://를 뗀 값과 동일해야 합니다.
  • Thumbprint/ClientIDList도 정상이어야 합니다(일반적으로 sts.amazonaws.com).

OIDC provider가 없다면(혹은 잘못됐다면) 보통 다음 중 하나로 생성합니다.

# eksctl 사용
eksctl utils associate-iam-oidc-provider \
  --cluster <cluster-name> \
  --approve

Terraform을 쓴다면 aws_iam_openid_connect_provider 리소스가 클러스터 issuer를 참조하는지 다시 점검하세요.

3) ServiceAccount가 올바른 Role ARN을 가리키는지

IRSA에서 가장 흔한 실수는 “Role은 만들었는데, 파드가 그 SA를 안 쓰는” 상황입니다.

3-1. ServiceAccount annotation 확인

kubectl get sa -n <ns> <sa-name> -o yaml

필수로 확인할 값:

  • metadata.annotations.eks.amazonaws.com/role-arn: arn:aws:iam::<account-id>:role/<role-name>

3-2. Pod가 그 ServiceAccount를 실제로 쓰는지

Deployment/Job에 serviceAccountName이 누락되면 default SA로 뜁니다.

kubectl get pod -n <ns> <pod-name> -o jsonpath='{.spec.serviceAccountName}'

원하는 SA가 아니라면 매니페스트를 수정하세요.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: app
  namespace: my-ns
spec:
  template:
    spec:
      serviceAccountName: my-sa
      containers:
        - name: app
          image: <image>

3-3. 동일 이름 SA를 다른 네임스페이스에 만들어둔 경우

IRSA의 sub네임스페이스까지 포함합니다.

  • system:serviceaccount:<namespace>:<serviceaccount>

네임스페이스가 다르면 Trust Policy에서 매칭 실패합니다.

4) IAM Role Trust Policy가 토큰과 매칭되는지(핵심)

STS AccessDenied의 대부분은 여기서 납니다. Trust Policy는 “누가 이 Role을 Assume 할 수 있는가”를 정의합니다.

4-1. Trust Policy 기본 템플릿

아래는 가장 흔한 형태입니다.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::<ACCOUNT_ID>:oidc-provider/oidc.eks.<REGION>.amazonaws.com/id/<OIDC_ID>"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "oidc.eks.<REGION>.amazonaws.com/id/<OIDC_ID>:aud": "sts.amazonaws.com",
          "oidc.eks.<REGION>.amazonaws.com/id/<OIDC_ID>:sub": "system:serviceaccount:<NAMESPACE>:<SERVICEACCOUNT>"
        }
      }
    }
  ]
}

여기서 자주 틀리는 지점:

  • Principal.Federated의 provider ARN이 다른 클러스터의 OIDC를 가리킴
  • Condition key에 들어가는 issuer host/path가 issuer와 불일치
  • sub의 네임스페이스/SA명이 다름
  • audsts.amazonaws.com이 아닌 값으로 들어감

4-2. StringLike로 와일드카드 허용(운영 편의 vs 보안)

여러 SA를 허용하려고 StringLike를 사용하기도 합니다.

"Condition": {
  "StringEquals": {
    "oidc.eks.<REGION>.amazonaws.com/id/<OIDC_ID>:aud": "sts.amazonaws.com"
  },
  "StringLike": {
    "oidc.eks.<REGION>.amazonaws.com/id/<OIDC_ID>:sub": "system:serviceaccount:my-ns:*"
  }
}

운영상 편하지만, namespace 단위 권한 범위가 커질 수 있으니 최소 권한 원칙을 지키는 게 좋습니다.

4-3. 실제 토큰 클레임을 확인해서 Trust Policy를 역으로 맞추기

“내 토큰의 sub/aud가 뭔지”를 보면 디버깅이 빨라집니다.

Pod 안에서 토큰 파일 경로를 확인합니다.

kubectl exec -n <ns> -it <pod> -- sh -lc 'env | egrep "AWS_ROLE_ARN|AWS_WEB_IDENTITY_TOKEN_FILE"'

토큰을 디코딩(서명 검증이 아니라 payload 확인 목적)합니다.

kubectl exec -n <ns> -it <pod> -- sh -lc '
TOKEN=$(cat $AWS_WEB_IDENTITY_TOKEN_FILE); 
PAYLOAD=$(echo $TOKEN | cut -d. -f2 | tr "-_" "/+" | base64 -d 2>/dev/null); 
echo "$PAYLOAD" | sed "s/{/{\n/; s/}/\n}/; s/,/\n/g"'

여기서 확인할 핵심:

  • iss: OIDC issuer (클러스터 값과 일치해야 함)
  • sub: system:serviceaccount:ns:sa
  • aud: 보통 sts.amazonaws.com

이 값을 Trust Policy의 Condition과 1:1로 맞추면 됩니다.

5) IRSA는 성공했는데도 “AccessDenied”가 나는 경우(권한 정책)

sts:AssumeRoleWithWebIdentity가 아니라, 예를 들어 S3에서 AccessDenied가 날 수 있습니다.

  • AssumeRole은 성공
  • 하지만 Role에 s3:GetObject가 없음

CloudTrail에서 다음처럼 이벤트를 구분하세요.

  • 성공: AssumeRoleWithWebIdentity 이벤트가 200으로 찍힘
  • 실패: 이후 GetObject/GetSecretValue 등에서 AccessDenied

Role에 붙은 permission policy를 점검합니다.

aws iam list-attached-role-policies --role-name <role-name>
aws iam list-role-policies --role-name <role-name>

그리고 리소스 ARN/조건(특히 KMS, S3 bucket policy, Secrets Manager resource policy)이 추가로 막고 있는지도 확인해야 합니다.

6) SDK/런타임 이슈: 환경변수, 토큰 경로, IMDS 우선순위

컨테이너에 따라 AWS SDK가 자격증명 공급자 체인을 다르게 타기도 합니다.

6-1. AWS_ROLE_ARN, AWS_WEB_IDENTITY_TOKEN_FILE이 없을 때

IRSA가 제대로 주입되지 않은 것입니다.

  • Pod가 해당 SA를 안 씀
  • EKS 버전/설정 문제로 projected token이 마운트되지 않음
  • (드물게) mutating webhook/sidecar가 env를 덮어씀

6-2. 노드 IAM(Role)로 호출이 새어 나가는 경우

일부 환경에서 SDK가 IRSA보다 IMDS(노드 메타데이터)를 먼저 쓰는 듯 보이는 혼란이 생깁니다. 원칙적으로는 web identity가 있으면 그걸 사용하지만, 애플리케이션이 별도 credential provider를 강제하거나, 오래된 SDK/설정이 꼬이면 문제가 됩니다.

  • AWS SDK 버전 업
  • 명시적으로 web identity provider 사용
  • 불필요한 AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY 환경변수 제거

7) CloudTrail로 “왜 거부됐는지” 한 방에 보기

가장 빠른 방법은 CloudTrail에서 AssumeRoleWithWebIdentity 이벤트를 찾아 errorMessagerequestParameters를 보는 것입니다.

  • 어떤 roleArn을 시도했는지
  • 어떤 provider를 통해 왔는지
  • 조건 불일치인지(대개 AccessDenied로 뭉뚱그려짐)

운영 자동화/배포(OIDC)에서 AccessDenied를 다루는 감각은 유사하니, CI/CD 쪽 사고 패턴은 GitHub Actions OIDC로 AWS 배포 AccessDenied 해결도 참고하면 원인 분해에 도움이 됩니다.

8) 실전 체크리스트(10분 컷)

아래를 위에서 아래로 수행하면 대부분의 STS AccessDenied를 해결할 수 있습니다.

  1. 에러 문자열 분류: OIDC provider 없음 vs AssumeRole 거부 vs API 권한 부족
  2. aws eks describe-cluster로 issuer 확인
  3. IAM OIDC provider 존재/URL 일치 확인
  4. ServiceAccount에 eks.amazonaws.com/role-arn annotation 확인
  5. Pod가 해당 ServiceAccount를 실제로 사용하는지 확인
  6. Pod 내부에서 AWS_ROLE_ARN, AWS_WEB_IDENTITY_TOKEN_FILE 확인
  7. 토큰 payload에서 iss/sub/aud 확인
  8. Role Trust Policy의 provider ARN/Condition key/sub/aud가 토큰과 일치하는지 확인
  9. CloudTrail에서 AssumeRoleWithWebIdentity 이벤트로 최종 확인
  10. AssumeRole 성공인데도 AccessDenied면 permission policy 및 리소스 정책(S3/KMS/Secrets) 점검

9) 자주 나오는 “틀린 Trust Policy” 예시 3가지

9-1. sub 오타(네임스페이스 누락)

  • 잘못: system:serviceaccount:my-sa
  • 정답: system:serviceaccount:my-ns:my-sa

9-2. issuer key가 다른 OIDC ID를 가리킴

Trust Policy Condition key의 .../id/<OIDC_ID>: 부분이 실제 클러스터 OIDC ID와 다르면 무조건 실패합니다.

9-3. aud 누락

aud 조건이 없으면 되는 경우도 있지만(정책에 따라), 보안적으로 권장되지 않고 환경에 따라 실패/성공이 갈릴 수 있습니다. 기본은 sts.amazonaws.com을 명시하세요.

결론

EKS IRSA에서 STS AccessDenied는 “권한이 없어서”라기보다 OIDC/Trust/SA 매칭이 어긋나서 생기는 경우가 훨씬 많습니다. 해결의 핵심은 감으로 고치는 게 아니라,

  • 클러스터 issuer ↔ IAM OIDC provider
  • 토큰의 iss/sub/aud ↔ Role trust policy condition
  • Pod의 serviceAccountName ↔ SA annotation

이 3개의 매칭을 증거(명령 출력/토큰 payload/CloudTrail) 로 맞춰가는 것입니다.

같은 IRSA 맥락에서 ECR 인증 문제까지 겹쳐 보인다면 Kubernetes ImagePullBackOff 401 - ECR·IRSA·imagePullSecrets도 함께 점검해보세요. IRSA는 “붙였는데도 안 된다”가 아니라, “어떤 매칭이 깨졌는지”를 찾는 게임에 가깝습니다.