Published on

EKS IRSA 403 권한오류 - STS·OIDC 디버깅

Authors

EKS에서 IRSA(IAM Roles for Service Accounts)를 붙였는데 애플리케이션이 AWS API 호출에서 403(대개 AccessDenied 또는 InvalidIdentityToken)을 뱉는 상황은 흔합니다. 문제는 “권한이 없다” 한 줄로 끝나지 않고, STS 웹 아이덴티티 플로우(서비스어카운트 JWT sub/aud/iss)와 OIDC Provider, Role Trust Policy, Pod 환경변수/토큰 마운트, 그리고 실제 IAM Policy까지 여러 층을 동시에 맞춰야 한다는 점입니다.

이 글은 STS·OIDC 관점에서 IRSA 403을 빠르게 좁혀가는 체크리스트를 제공합니다. 네트워크 레벨 403(예: WAF/NAT)과 혼동되는 경우도 있으니, 외부 API 403 케이스는 별도로 EKS Pod만 외부 API 403 - NAT IP·WAF로 해결도 함께 참고하면 좋습니다.


1) 먼저 “어떤 403인지” 에러 타입부터 분리

IRSA에서 말하는 403은 보통 아래 두 갈래입니다.

A. STS에서 거절됨 (웹 아이덴티티/트러스트/OIDC 문제)

  • InvalidIdentityToken
  • AccessDenied: Not authorized to perform sts:AssumeRoleWithWebIdentity
  • No OpenIDConnect provider found in your account for ...

이 경우는 OIDC Provider 등록, Role trust policy 조건, 서비스어카운트 토큰의 클레임(sub, aud, iss) 중 하나가 불일치합니다.

B. STS AssumeRole은 성공했지만, 서비스 API에서 거절됨 (IAM Policy 문제)

  • AccessDeniedException (서비스별)
  • User: arn:aws:sts::...:assumed-role/... is not authorized to perform ...

이 경우는 IRSA 연결은 됐고, Role에 붙은 Permission Policy가 부족하거나 리소스 ARN/조건이 틀린 것입니다.

가장 먼저 해야 할 일은 애플리케이션 로그에서 “어느 API가 403을 냈는지” 확인하는 것입니다. STS면 IRSA 골격 문제, 서비스 API면 정책 문제로 분기합니다.


2) Pod 내부에서 IRSA가 “적용”됐는지 10초 확인

IRSA가 제대로 주입되면 Pod 안에 아래가 존재합니다.

  • 환경변수 AWS_ROLE_ARN
  • 환경변수 AWS_WEB_IDENTITY_TOKEN_FILE
  • 토큰 파일 경로에 JWT 존재

다음처럼 확인합니다.

kubectl -n <namespace> exec -it <pod> -- sh -lc '
  echo "AWS_ROLE_ARN=$AWS_ROLE_ARN";
  echo "AWS_WEB_IDENTITY_TOKEN_FILE=$AWS_WEB_IDENTITY_TOKEN_FILE";
  ls -l $AWS_WEB_IDENTITY_TOKEN_FILE;
  head -c 30 $AWS_WEB_IDENTITY_TOKEN_FILE; echo;
'

여기서 흔한 실패 패턴:

  • AWS_ROLE_ARN 이 비어 있음: 서비스어카운트 annotation이 없거나 Pod가 그 SA를 사용하지 않음
  • AWS_WEB_IDENTITY_TOKEN_FILE 이 비어 있음: IRSA가 아니라 노드 IAM(Instance Profile)로 돌고 있을 가능성
  • 토큰 파일이 없음: automountServiceAccountToken: false 이거나, SA 토큰 마운트 정책이 바뀐 경우

또한 AWS SDK가 엉뚱한 자격증명을 먼저 집어드는 경우도 있습니다. 예를 들어 컨테이너에 AWS_ACCESS_KEY_ID 같은 정적 키가 들어있으면 IRSA보다 우선될 수 있습니다. Pod 환경변수에 정적 키가 있는지 같이 확인하세요.


3) 서비스어카운트 annotation과 실제 Pod의 SA 일치 확인

IRSA에서 가장 많이 틀리는 지점이 “SA를 만들었는데 Pod가 그 SA를 안 씀”입니다.

서비스어카운트 확인

kubectl -n <namespace> get sa <serviceaccount> -o yaml

아래 annotation이 있어야 합니다.

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

Pod가 어떤 SA를 쓰는지 확인

kubectl -n <namespace> get pod <pod> -o jsonpath='{.spec.serviceAccountName}'; echo

Deployment/Helm values에서 serviceAccountName이 다른 이름으로 박혀 있거나, serviceAccount.create 옵션이 켜져서 차트가 별도 SA를 생성하는 경우가 많습니다.


4) OIDC Provider가 “클러스터 것”으로 등록됐는지 확인

IRSA는 EKS 클러스터의 OIDC issuer URL을 IAM에 Provider로 등록해야 합니다.

클러스터의 issuer 확인

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

출력 예시는 대략 https://oidc.eks.<region>.amazonaws.com/id/<oidc-id> 형태입니다.

IAM OIDC provider 목록에서 존재 확인

aws iam list-open-id-connect-providers

그리고 특정 provider의 상세를 봅니다.

aws iam get-open-id-connect-provider \
  --open-id-connect-provider-arn arn:aws:iam::<account-id>:oidc-provider/oidc.eks.<region>.amazonaws.com/id/<oidc-id>

여기서 체크 포인트:

  • Provider ARN의 host/path가 클러스터 issuer와 정확히 일치하는가
  • ClientIDListsts.amazonaws.com 가 포함돼 있는가

No OpenIDConnect provider found 류는 대개 여기서 걸립니다.


5) Trust Policy의 sub/aud 조건이 토큰과 일치하는지 검증

STS AssumeRoleWithWebIdentity가 403이면, 거의 항상 Role trust policy 조건 불일치입니다.

5-1) Pod의 SA 토큰(JWT) 클레임 확인

JWT는 header.payload.signature 구조이고, payload는 base64url로 인코딩되어 있습니다. 아래는 payload를 디코딩해 iss, sub, aud를 확인하는 예시입니다.

kubectl -n <namespace> exec -it <pod> -- sh -lc '
  TOKEN=$(cat $AWS_WEB_IDENTITY_TOKEN_FILE);
  PAYLOAD=$(echo "$TOKEN" | cut -d. -f2);
  python - <<"PY"
import os,sys,base64,json
p=os.environ.get("PAYLOAD")
if not p:
  sys.exit("no payload")
# base64url padding
p += "=" * (-len(p) % 4)
print(json.dumps(json.loads(base64.urlsafe_b64decode(p)), indent=2))
PY
' 2>/dev/null

위 스크립트가 환경변수 전달을 못 받으면, 아래처럼 한 번에 처리해도 됩니다.

kubectl -n <namespace> exec -it <pod> -- sh -lc '
  TOKEN=$(cat $AWS_WEB_IDENTITY_TOKEN_FILE);
  python - <<"PY"
import base64,json
import sys
token=open("/var/run/secrets/eks.amazonaws.com/serviceaccount/token").read().strip()
payload=token.split(".")[1]
payload += "=" * (-len(payload) % 4)
print(json.dumps(json.loads(base64.urlsafe_b64decode(payload)), indent=2))
PY
'

여기서 핵심은:

  • iss: OIDC issuer URL
  • sub: 보통 system:serviceaccount:<namespace>:<serviceaccount>
  • aud: 보통 sts.amazonaws.com (환경/버전에 따라 배열일 수도 있음)

5-2) IAM Role trust policy 점검

Role의 trust policy를 확인합니다.

aws iam get-role --role-name <role-name> --query 'Role.AssumeRolePolicyDocument'

정상적인 예시는 아래와 유사합니다(핵심만 발췌).

{
  "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 가 다른 클러스터의 OIDC provider를 가리킴
  • Condition 키의 prefix(oidc.eks.../id/...:)가 issuer와 불일치
  • sub에 namespace 또는 SA 이름 오타
  • StringLike/StringEquals를 잘못 선택해 매칭이 안 됨

운영에서 여러 SA를 허용하려고 와일드카드를 쓰는 경우는 아래처럼 StringLike를 사용합니다.

"StringLike": {
  "oidc.eks.<region>.amazonaws.com/id/<oidc-id>:sub": "system:serviceaccount:<namespace>:*"
}

다만 범위를 넓히면 보안도 약해지므로, 가능하면 SA 단위로 좁히는 편이 좋습니다.


6) STS 호출 자체를 Pod 안에서 재현해 “IRSA vs Policy”를 분리

애플리케이션이 복잡하면, Pod 안에서 AWS CLI로 최소 재현을 만드는 게 가장 빠릅니다.

6-1) 현재 자격증명 주체 확인

kubectl -n <namespace> exec -it <pod> -- sh -lc 'aws sts get-caller-identity'
  • 여기서 에러가 나면: IRSA 주입/STS assume 단계 문제
  • 여기서 성공하면: IRSA는 대체로 정상, 이후 서비스 권한 정책을 점검

출력의 Arnassumed-role/<role-name>/... 형태면 IRSA AssumeRole이 된 것입니다.

6-2) 특정 서비스 권한 확인(예: S3)

kubectl -n <namespace> exec -it <pod> -- sh -lc 'aws s3 ls s3://<bucket-name> --region <region>'

여기서 AccessDenied면 permission policy(또는 bucket policy/KMS) 문제로 넘어가면 됩니다.


7) CloudTrail로 AssumeRoleWithWebIdentity 이벤트를 추적

STS 단계에서 403이 나면, CloudTrail은 거의 정답지를 줍니다.

  • 이벤트: AssumeRoleWithWebIdentity
  • 에러 코드: AccessDenied, InvalidIdentityToken
  • requestParameters에 들어있는 roleArn, webIdentityToken 관련 메타

CloudTrail에서 확인할 것:

  • 실제로 어떤 roleArn을 시도했는지 (애플리케이션 설정이 다른 Role을 가리키는 경우)
  • 에러 메시지에 sub/aud 불일치 힌트가 있는지

조직에서 SCP(Service Control Policy)로 STS가 제한되는 경우도 있어, IAM Role만 봐서는 안 풀리는 403이 발생할 수 있습니다. CloudTrail의 errorMessage가 이를 암시하는 경우가 있습니다.


8) aud 이슈: 토큰 audience가 sts.amazonaws.com 이 아닌 경우

EKS의 projected service account token은 audience를 지정할 수 있습니다. IRSA는 기본적으로 aud=sts.amazonaws.com 매칭을 기대합니다.

  • 토큰의 aud가 다르면 trust policy의 ...:aud 조건에서 탈락
  • 또는 trust policy에서 aud 조건을 아예 안 넣었는데, 보안팀 정책으로 반드시 넣도록 요구하는 경우도 있음

해결 방향:

  • 토큰의 audsts.amazonaws.com으로 발급하도록 설정
  • 또는 trust policy를 실제 aud에 맞게 수정

애플리케이션/사이드카가 별도 audience로 토큰을 요청하는 구조(예: Vault, SPIFFE 연동)라면 특히 주의가 필요합니다.


9) 권한 정책은 맞는데도 403이면: 리소스 정책·KMS·조건 키 확인

IRSA가 성공했고 get-caller-identity도 되는데 서비스 API에서 403이라면, 다음을 추가로 봅니다.

  • S3: bucket policy에서 role principal을 허용하는지, aws:PrincipalArn 조건이 있는지
  • KMS: 암호화된 리소스 접근 시 KMS key policy에 role이 포함되는지
  • ECR: ecr:GetAuthorizationToken 같은 필수 액션 누락 여부
  • 조건부 정책: aws:RequestedRegion, aws:SourceVpce, aws:PrincipalTag 등 조건이 Pod 환경과 충돌하는지

이 단계는 IRSA 자체 디버깅을 넘어 IAM 설계로 들어가지만, 실무에서 “IRSA 403”으로 뭉뚱그려 보고되는 케이스의 상당수가 여기입니다.


10) 실전 체크리스트(요약)

아래 순서대로 보면 대개 10~20분 안에 원인을 고립할 수 있습니다.

  1. 애플리케이션 로그에서 403을 낸 주체가 STS인지 서비스 API인지 확인
  2. Pod 내부에서 AWS_ROLE_ARN, AWS_WEB_IDENTITY_TOKEN_FILE 존재 확인
  3. Pod의 serviceAccountName과 SA annotation의 role ARN 일치 확인
  4. 클러스터 issuer와 IAM OIDC provider 등록 일치 확인
  5. 토큰 JWT의 iss/sub/aud를 디코딩해 trust policy 조건과 비교
  6. Pod에서 aws sts get-caller-identity로 IRSA AssumeRole 성공 여부 확인
  7. CloudTrail의 AssumeRoleWithWebIdentity 이벤트로 에러 메시지 확인
  8. STS 성공 후에도 403이면 permission policy 및 리소스 정책(S3/KMS 등) 점검

부록) 최소 구성 예시: SA + Role trust + Deployment

아래 예시는 “정상 동작 기준선”을 만들 때 유용합니다.

ServiceAccount

apiVersion: v1
kind: ServiceAccount
metadata:
  name: app-sa
  namespace: default
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::<account-id>:role/app-irsa-role

Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: app
  namespace: default
spec:
  replicas: 1
  selector:
    matchLabels:
      app: app
  template:
    metadata:
      labels:
        app: app
    spec:
      serviceAccountName: app-sa
      containers:
        - name: app
          image: amazon/aws-cli:2.15.0
          command: ["sh", "-lc"]
          args:
            - |
              env | grep AWS_ || true
              aws sts get-caller-identity
              sleep 3600

이 Pod에서 aws sts get-caller-identity가 실패하면, 애플리케이션 문제가 아니라 IRSA/OIDC/Trust 문제로 범위를 확 줄일 수 있습니다.


마무리

IRSA의 403은 “권한 부족”이 아니라 STS 웹 아이덴티티 체인의 불일치(OIDC issuer, provider ARN, trust policy의 sub/aud) 또는 AssumeRole 이후의 실제 권한 정책 문제로 나뉩니다. Pod 내부에서 토큰 클레임을 확인하고, trust policy와 1:1로 대조한 뒤, CloudTrail로 STS 이벤트를 확인하면 대부분의 케이스가 깔끔하게 정리됩니다.

EKS에서 원인 분리가 잘 안 되는 문제를 더 다루고 있다면, 프로토콜/환경 차이로 특정 조합만 실패하는 사례는 EKS에서 TLS 1.3만 실패할 때 - OpenSSL·ALPN도 함께 보면 디버깅 접근법을 확장하는 데 도움이 됩니다.