Published on

EKS IRSA AccessDenied 해결 - OIDC·신뢰정책

Authors

서버리스/컨테이너 환경에서 AWS 권한 문제는 대부분 AccessDenied 한 줄로 뭉뚱그려 보이지만, EKS의 IRSA(IAM Roles for Service Accounts)는 특히 OIDC ProviderIAM Role 신뢰정책(AssumeRoleWithWebIdentity) 조건이 조금만 어긋나도 즉시 실패합니다. 이 글은 “왜 거부되는지”를 증거(토큰 클레임, OIDC issuer, trust policy condition)로 좁혀가며 해결하는 실전 체크리스트입니다.

IRSA 문제를 디버깅하다 보면 파드가 정상 기동은 하는데 AWS API만 실패하는 경우가 많습니다. 이때는 애플리케이션 레벨 로그만 보지 말고, 쿠버네티스 상태/프로브 및 배포 파이프라인 이슈도 함께 의심해볼 만합니다. 예를 들어 배포 이후 상태가 꼬였다면 Argo CD Sync 실패 - OutOfSync·Degraded 해결, 파드가 재기동 루프라면 Kubernetes CrashLoopBackOff, 로그 없이 진단하는 법, 준비 상태가 미묘하게 실패한다면 EKS에서 Readiness 실패인데 로그는 정상일 때도 같이 참고하면 전체 원인 분리가 빨라집니다.

IRSA 동작 원리(어디서 틀어지는지)

IRSA는 대략 아래 흐름으로 동작합니다.

  1. EKS 클러스터에는 OIDC issuer URL이 있고, AWS IAM에는 그 issuer를 신뢰하는 OIDC Provider가 등록됩니다.
  2. 특정 Kubernetes ServiceAccount에 eks.amazonaws.com/role-arn 어노테이션으로 IAM Role을 연결합니다.
  3. 파드는 ServiceAccount 토큰(JWT)을 마운트하고, AWS SDK는 AssumeRoleWithWebIdentity로 STS에 토큰을 제출합니다.
  4. IAM Role의 신뢰정책(trust policy) 이 토큰의 sub, aud 등 클레임과 일치하면 임시 자격 증명을 발급합니다.

따라서 AccessDenied의 핵심 원인은 보통 다음 4가지로 수렴합니다.

  • OIDC Provider가 없거나 issuer가 불일치
  • Role trust policy의 Condition이 토큰 클레임과 불일치(sub, aud)
  • ServiceAccount 어노테이션/namespace/name 불일치
  • 파드가 올바른 토큰을 사용하지 못함(구버전 토큰, automountServiceAccountToken 비활성화, 잘못된 env/volume)

증상별 에러 메시지로 1차 분류

CloudWatch/애플리케이션 로그에서 흔히 보는 패턴입니다.

  • AccessDenied: Not authorized to perform sts:AssumeRoleWithWebIdentity
    • 거의 항상 trust policy 또는 OIDC Provider 설정 문제
  • InvalidIdentityToken: No OpenIDConnect provider found in your account for ...
    • OIDC Provider 미등록 또는 issuer URL 불일치
  • AccessDeniedException (S3, DynamoDB 등 개별 서비스 액션 거부)
    • IRSA는 성공했지만 권한 정책(permissions policy) 이 부족

이 글의 초점은 첫 번째 케이스(AssumeRoleWithWebIdentity 단계에서 거부)입니다.

1단계: 클러스터 OIDC issuer 확인

먼저 EKS 클러스터의 issuer를 확인합니다.

aws eks describe-cluster \
  --name my-eks \
  --region ap-northeast-2 \
  --query "cluster.identity.oidc.issuer" \
  --output text

출력은 보통 https://oidc.eks.ap-northeast-2.amazonaws.com/id/XXXXXXXXXXXX 형태입니다. 이 값이 이후 모든 설정의 기준이 됩니다.

2단계: IAM OIDC Provider 존재/일치 확인

AWS 계정에 OIDC Provider가 등록되어 있는지 확인합니다.

aws iam list-open-id-connect-providers

목록에서 ARN을 하나 골라 세부를 봅니다.

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

여기서 중요한 포인트는 아래 2가지입니다.

  • Provider URL 경로가 issuer의 호스트+패스와 정확히 일치해야 함
  • ClientIDList에 최소 sts.amazonaws.com가 포함되어야 함

만약 Provider가 없거나 불일치라면, eksctl로 가장 빠르게 생성할 수 있습니다.

eksctl utils associate-iam-oidc-provider \
  --cluster my-eks \
  --region ap-northeast-2 \
  --approve

3단계: ServiceAccount 어노테이션 확인

IRSA는 ServiceAccount에 Role ARN을 정확히 달아야 합니다.

kubectl -n my-namespace get sa my-sa -o yaml

다음과 같은 형태를 확인합니다.

apiVersion: v1
kind: ServiceAccount
metadata:
  name: my-sa
  namespace: my-namespace
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/my-irsa-role

실무에서 자주 틀리는 부분:

  • 파드가 실제로는 다른 ServiceAccount를 사용 중(Deployment의 spec.template.spec.serviceAccountName 누락)
  • namespace가 달라서 sub가 불일치
  • Helm/Argo CD 템플릿 변수로 SA 이름이 바뀌었는데 trust policy는 고정

파드가 어떤 SA를 쓰는지 바로 확인합니다.

kubectl -n my-namespace get pod my-pod -o jsonpath='{.spec.serviceAccountName}'

4단계: 토큰 클레임을 직접 확인(가장 확실한 증거)

신뢰정책 조건 불일치는 “감”으로 고치기 어렵습니다. 토큰을 꺼내서 sub, aud, iss를 확인하면 바로 끝납니다.

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

kubectl -n my-namespace exec -it my-pod -- sh -lc 'ls -al /var/run/secrets/eks.amazonaws.com/serviceaccount'

대개 token 파일이 있습니다. JWT의 payload를 디코드합니다(서명 검증이 목적이 아니라 클레임 확인이 목적이므로 base64url decode만 합니다).

kubectl -n my-namespace exec -it my-pod -- sh -lc '
TOKEN=$(cat /var/run/secrets/eks.amazonaws.com/serviceaccount/token);
PAYLOAD=$(echo "$TOKEN" | cut -d . -f 2 | tr "_-" "/+" | base64 -d 2>/dev/null);
echo "$PAYLOAD";
'

여기서 확인해야 할 핵심 클레임:

  • sub: 보통 system:serviceaccount:my-namespace:my-sa
  • aud: 보통 sts.amazonaws.com
  • iss: 클러스터 OIDC issuer

이 값이 IAM Role trust policy의 조건과 1:1로 맞아야 합니다.

5단계: IAM Role 신뢰정책(trust policy) 점검

문제의 80%는 여기서 발생합니다. 역할(Role)의 trust policy를 확인합니다.

aws iam get-role \
  --role-name my-irsa-role \
  --query "Role.AssumeRolePolicyDocument" \
  --output json

정상적인 예시는 아래와 유사합니다(가독성을 위해 줄바꿈/들여쓰기를 유지했습니다).

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

체크 포인트:

  • Principal.Federated정확한 OIDC Provider ARN인지
  • Actionsts:AssumeRoleWithWebIdentity인지
  • Condition.StringEquals의 키가 issuer에서 https:// 를 뺀 값 + :aud / :sub 형태인지
  • sub 값의 namespace/sa가 실제와 같은지

흔한 실수 1: issuer 키에 https://를 포함

Condition 키는 https:// 없이 들어가야 합니다. 예를 들어 issuer가 https://oidc.eks.../id/ABC라면 Condition 키는 oidc.eks.../id/ABC:sub처럼 시작해야 합니다.

흔한 실수 2: StringLike 와일드카드 남용

여러 SA를 허용하려고 StringLikesystem:serviceaccount:ns:*를 쓰는 경우가 있는데, 보안상 권장되지 않습니다. 운영 환경에서는 가능한 한 SA를 명시적으로 고정하세요.

그래도 멀티 SA가 필요하면 최소 범위로 제한합니다.

"Condition": {
  "StringEquals": {
    "oidc.eks.ap-northeast-2.amazonaws.com/id/XXXXXXXXXXXX:aud": "sts.amazonaws.com"
  },
  "StringLike": {
    "oidc.eks.ap-northeast-2.amazonaws.com/id/XXXXXXXXXXXX:sub": "system:serviceaccount:my-namespace:app-*"
  }
}

6단계: 권한 정책(permissions policy)과 혼동하지 않기

IRSA가 성공했는데도 S3 호출에서 AccessDenied가 나면, 그건 trust policy가 아니라 Role에 붙은 permissions policy 문제입니다.

IRSA 성공 여부는 파드 내부에서 STS 호출로 빠르게 확인할 수 있습니다.

kubectl -n my-namespace exec -it my-pod -- sh -lc 'aws sts get-caller-identity'
  • 여기서도 거부되면 trust/OIDC 문제
  • 여기서 성공하고, 특정 서비스 액션만 거부되면 permissions policy 문제

7단계: automountServiceAccountToken 및 토큰 경로 이슈

보안 강화를 위해 ServiceAccount 또는 Pod에 automountServiceAccountToken: false를 설정해둔 경우, IRSA 토큰이 마운트되지 않아 실패할 수 있습니다.

ServiceAccount와 Pod 양쪽을 확인합니다.

kubectl -n my-namespace get sa my-sa -o jsonpath='{.automountServiceAccountToken}'
kubectl -n my-namespace get pod my-pod -o jsonpath='{.spec.automountServiceAccountToken}'

또한 EKS IRSA는 기본 서비스어카운트 토큰 경로가 아니라 eks.amazonaws.com 경로로 projected token을 사용합니다. 커스텀 이미지/엔트리포인트에서 AWS SDK 관련 env를 덮어쓰거나, AWS_WEB_IDENTITY_TOKEN_FILE를 잘못 지정하면 실패합니다.

파드에서 관련 환경 변수를 확인합니다.

kubectl -n my-namespace exec -it my-pod -- sh -lc 'env | grep -E "AWS_ROLE_ARN|AWS_WEB_IDENTITY_TOKEN_FILE|AWS_REGION|AWS_DEFAULT_REGION"'

8단계: 재현 가능한 “정상 IRSA” 매니페스트 예시

문제 원인을 분리하려면, 최소 구성으로 IRSA가 되는지부터 확인하는 게 좋습니다.

ServiceAccount

apiVersion: v1
kind: ServiceAccount
metadata:
  name: irsa-debug-sa
  namespace: irsa-debug
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/my-irsa-role

Pod(awscli로 STS 확인)

apiVersion: v1
kind: Pod
metadata:
  name: irsa-debug
  namespace: irsa-debug
spec:
  serviceAccountName: irsa-debug-sa
  containers:
    - name: awscli
      image: public.ecr.aws/aws-cli/aws-cli:2.15.0
      command: ["sh", "-lc", "aws sts get-caller-identity && sleep 3600"]

적용 후 확인:

kubectl create ns irsa-debug
kubectl apply -f irsa-debug.yaml
kubectl -n irsa-debug logs -f pod/irsa-debug

이 최소 구성에서 성공하면, 원래 워크로드 쪽의 SA/토큰/env 오버라이드 문제일 확률이 큽니다.

9단계: EKS 업그레이드/재생성 후 issuer 변경 이슈

클러스터를 새로 만들거나(또는 특정 방식으로 재프로비저닝) OIDC issuer의 id/XXXXXXXX 부분이 바뀌면, 기존 IAM OIDC Provider ARN과 trust policy 키가 모두 어긋납니다.

이 경우의 전형적인 증상은:

  • 갑자기 모든 IRSA 워크로드가 동시에 AssumeRoleWithWebIdentity에서 실패
  • get-open-id-connect-provider는 존재하지만 issuer가 다름

해결은 새 issuer로 OIDC Provider를 다시 연결하고, 관련 Role들의 trust policy condition 키를 일괄 수정하는 것입니다. 운영 환경에서는 Terraform 등 IaC로 OIDC provider와 role trust policy를 함께 관리해 “드리프트”를 줄이는 것을 권장합니다.

최종 체크리스트(가장 빨리 잡히는 순서)

  • aws eks describe-cluster로 issuer 확인
  • IAM에 해당 issuer의 OIDC Provider가 존재하고 sts.amazonaws.com가 client ID에 포함되는지 확인
  • ServiceAccount 어노테이션의 role ARN, namespace/name 일치 확인
  • 파드가 실제로 그 ServiceAccount를 사용하는지 확인
  • 파드 내부에서 토큰 JWT payload를 디코드해 sub, aud, iss를 확인
  • IAM Role trust policy의 Principal.Federated, Condition 키/값이 토큰과 정확히 일치하는지 확인
  • aws sts get-caller-identity로 IRSA 성공 여부를 분리

IRSA의 AccessDenied는 “권한이 없다”가 아니라, 대부분 “신원 증명이 신뢰정책과 매칭되지 않는다”는 뜻입니다. 토큰 클레임을 근거로 trust policy를 맞추면, 재발도 함께 줄일 수 있습니다.