Published on

EKS Pod에서 STS 403 InvalidIdentityToken 해결

Authors

서론

EKS에서 IRSA(IAM Roles for Service Accounts)를 붙인 Pod가 AWS SDK 호출을 시작하자마자 sts:AssumeRoleWithWebIdentity에서 403 InvalidIdentityToken을 뱉는 경우가 있습니다. 메시지는 단순하지만 원인은 꽤 다양합니다. 예를 들면 OIDC Provider가 잘못되었거나(thumbprint/issuer 불일치), ServiceAccount 토큰의 aud/sub 클레임이 Trust Policy 조건과 맞지 않거나, Pod가 아예 다른 SA로 떠 있거나, 심지어 노드 시간 드리프트로 토큰의 nbf/exp 검증이 깨지는 경우도 있습니다.

이 글은 “무엇부터 확인해야 하는지”를 체크리스트처럼 정리하고, 실제로 손으로 검증 가능한 커맨드와 예시 정책을 함께 제공합니다. IRSA 자체 점검이 더 필요하다면 EKS IRSA인데 AccessDenied? OIDC·TrustPolicy·SA 점검도 함께 보시면 원인 분류가 빨라집니다. 또한 OIDC thumbprint 변경 이슈가 의심된다면 EKS OIDC Thumbprint 변경 후 IRSA 403 복구가 바로 맞는 케이스일 수 있습니다.

1) 에러의 의미: InvalidIdentityToken은 “토큰이 신뢰되지 않음”

InvalidIdentityToken은 대체로 다음 중 하나입니다.

  • STS가 OIDC issuer(클러스터 OIDC URL) 자체를 신뢰하지 못함
    • IAM OIDC Provider 미생성/삭제/오류
    • thumbprint 불일치(갱신/프록시/중간 CA 변경)
  • STS는 issuer는 알지만, 토큰 클레임이 Role trust policy 조건과 불일치
    • sub(serviceaccount) 불일치
    • audsts.amazonaws.com이 아님
  • Pod 내부에서 사용하는 토큰이 웹 아이덴티티 토큰이 아닌 다른 토큰
    • 잘못된 AWS_WEB_IDENTITY_TOKEN_FILE
    • SA 토큰 projection이 꺼져 있거나(구버전/설정) 파일이 비어 있음
  • 시간 문제
    • 노드/컨테이너 시간 오차로 exp/nbf/iat 검증 실패

핵심은 “IRSA 토큰이 STS 관점에서 유효한 OIDC JWT로 보이는가?”입니다.

2) 가장 빠른 재현/검증: Pod 안에서 환경변수와 토큰부터 확인

먼저 문제 Pod에 들어가 아래를 확인합니다.

kubectl -n <ns> exec -it <pod> -- sh

# 1) IRSA 환경변수 존재 확인
env | egrep 'AWS_ROLE_ARN|AWS_WEB_IDENTITY_TOKEN_FILE|AWS_REGION|AWS_DEFAULT_REGION'

# 2) 토큰 파일 존재/크기 확인
ls -l ${AWS_WEB_IDENTITY_TOKEN_FILE}
wc -c ${AWS_WEB_IDENTITY_TOKEN_FILE}

# 3) (옵션) AWS SDK가 어떤 자격증명을 쓰는지 디버그
# Go SDK: AWS_SDK_LOAD_CONFIG=1, Java: -Daws.sdk.disableCertChecking 등은 케이스별

정상이라면 보통 다음이 보입니다.

  • AWS_ROLE_ARN=arn:aws:iam::<account-id>:role/<role-name>
  • AWS_WEB_IDENTITY_TOKEN_FILE=/var/run/secrets/eks.amazonaws.com/serviceaccount/token

여기서부터 갈립니다.

  • 환경변수 자체가 없다 → IRSA 주입이 안 된 것(대개 SA annotation 누락 또는 Pod가 다른 SA로 실행)
  • 토큰 파일이 없거나 0바이트 → SA 토큰 projection 문제 또는 볼륨 마운트 문제
  • 토큰은 있는데 STS가 거부 → OIDC Provider/Trust Policy/클레임 불일치 가능성이 큼

3) ServiceAccount가 맞게 붙었는지: “Pod spec”이 진실

IRSA는 “ServiceAccount에 role-arn annotation” + “Pod가 그 SA를 사용”해야 합니다.

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

ServiceAccount에 아래와 같은 annotation이 있어야 합니다.

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

주의할 점:

  • Deployment에서 serviceAccountName을 빼먹으면 기본 SA(default)로 뜹니다.
  • Helm/Kustomize로 배포 시 namespace가 섞여 SA가 다른 곳에 생성되는 실수도 흔합니다.

4) 토큰(JWT) 클레임을 직접 확인: sub/aud/iss를 눈으로 맞추기

웹 아이덴티티 토큰은 JWT입니다. payload를 디코딩해서 iss, sub, aud를 확인하면 원인 분류가 매우 빨라집니다.

4-1) Pod 내부에서 JWT payload 디코딩

TOKEN_FILE=${AWS_WEB_IDENTITY_TOKEN_FILE:-/var/run/secrets/eks.amazonaws.com/serviceaccount/token}

# JWT의 2번째 파트(payload)를 base64url 디코딩
PAYLOAD=$(cut -d. -f2 < "$TOKEN_FILE" | tr '_-' '/+' | awk '{print $0"=="}' )
echo "$PAYLOAD" | base64 -d 2>/dev/null | sed 's/,/,&\n/g'

확인 포인트:

  • iss: https://oidc.eks.<region>.amazonaws.com/id/<OIDC_ID> 형태
  • sub: system:serviceaccount:<namespace>:<serviceaccount>
  • aud: 보통 sts.amazonaws.com

4-2) Trust Policy 조건과 매칭

Role의 trust policy를 확인합니다.

aws iam get-role --role-name <role-name> --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/EXAMPLED539D4633E53DE1B716D3041E"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "oidc.eks.ap-northeast-2.amazonaws.com/id/EXAMPLED539D4633E53DE1B716D3041E:aud": "sts.amazonaws.com",
          "oidc.eks.ap-northeast-2.amazonaws.com/id/EXAMPLED539D4633E53DE1B716D3041E:sub": "system:serviceaccount:myns:myapp"
        }
      }
    }
  ]
}

여기서 한 글자라도 다르면(특히 namespace/SA 이름) STS는 토큰을 거부합니다.

  • 토큰의 sub와 trust policy의 ...:sub가 정확히 일치해야 함
  • 토큰의 audsts.amazonaws.com인데 trust policy가 다른 값을 요구하면 실패
  • Principal.Federated의 OIDC provider ARN이 클러스터의 issuer와 다르면 실패

5) OIDC Provider가 “클러스터 issuer”와 일치하는지 확인

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

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

출력 예:

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

IAM에 등록된 OIDC provider 목록과 매칭합니다.

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

# 해당 ARN의 설정 확인
aws iam get-open-id-connect-provider --open-id-connect-provider-arn <arn>

다음이 일치해야 합니다.

  • Provider URL(issuer에서 https:// 제거한 호스트/패스)
  • Client ID 목록에 sts.amazonaws.com 포함
  • Thumbprint가 현재 인증서 체인과 맞음(여기서 어긋나면 403 계열이 터지기 쉬움)

thumbprint 변경/불일치가 의심되면 상세 복구 절차는 EKS OIDC Thumbprint 변경 후 IRSA 403 복구를 참고하세요.

6) 자주 나오는 실전 원인 6가지와 해결

6-1) (가장 흔함) Trust Policy의 sub가 틀림

  • 증상: 특정 namespace/SA만 실패, 다른 SA는 정상
  • 해결: 토큰 payload의 sub를 그대로 trust policy에 반영
# 예: namespace가 default로 되어 있거나, SA 이름이 바뀐 경우
# trust policy의 StringEquals ...:sub 값을 수정
aws iam update-assume-role-policy --role-name <role-name> --policy-document file://trust.json

6-2) aud 조건 누락/불일치

  • 일부 구성은 StringLike로 넓게 잡기도 하지만, 보안상 StringEquals 권장
  • 토큰의 aud가 배열로 들어가는 경우도 있으니 payload를 실제로 확인

해결: ...:audsts.amazonaws.com으로 맞춥니다.

6-3) OIDC Provider가 다른 클러스터 것(issuer/id 불일치)

  • 멀티 클러스터/테라폼 모듈 재사용 시 흔함
  • trust policy의 key prefix(oidc.eks.../id/...:)가 다른 클러스터 ID면 무조건 실패

해결:

  • 올바른 issuer로 OIDC provider를 생성/수정
  • role trust policy의 Federated ARN 및 조건 key를 올바른 issuer로 교체

6-4) ServiceAccount 토큰 projection 문제(토큰 파일 없음)

EKS는 보통 자동으로 projected token을 마운트하지만, 다음에서 문제가 납니다.

  • 매우 오래된 클러스터/설정
  • automountServiceAccountToken: false가 SA 또는 Pod에 설정
kubectl -n <ns> get sa <sa> -o jsonpath='{.automountServiceAccountToken}'; echo
kubectl -n <ns> get pod <pod> -o jsonpath='{.spec.automountServiceAccountToken}'; echo

해결: 필요 시 automountServiceAccountToken: true로 명시하고 재배포합니다.

6-5) 노드 시간 드리프트로 토큰 유효기간 검증 실패

JWT는 시간에 민감합니다. 노드의 시간이 틀어지면 InvalidIdentityToken로 보일 수 있습니다.

  • 증상: 특정 노드에 스케줄된 Pod만 실패했다가 재스케줄하면 해결

확인(노드에서):

# 노드에 SSM/SSH 접근 가능할 때
sudo timedatectl
chronyc tracking 2>/dev/null || true

해결: NTP/chrony 정상화, AMI/부팅 스크립트 점검, 노드 교체(Managed Node Group면 롤링) 등.

6-6) SDK가 IRSA 대신 다른 자격증명을 우선 사용

예: 컨테이너 이미지에 ~/.aws/credentials가 들어있거나, 환경변수 AWS_ACCESS_KEY_ID가 주입되면 웹 아이덴티티 플로우를 우회할 수 있습니다.

env | egrep 'AWS_ACCESS_KEY_ID|AWS_SECRET_ACCESS_KEY|AWS_SESSION_TOKEN'

해결: 정적 키 제거, 애플리케이션 런타임에서 credential provider chain 확인.

7) 최소 재현 코드: Pod에서 STS 호출로 IRSA 검증하기

애플리케이션이 복잡하면 “내 코드 문제인지/IRSA 문제인지” 분리하기 어렵습니다. 아래처럼 STS GetCallerIdentity만 호출하는 최소 코드를 Pod에 넣어 검증하면 빠릅니다.

7-1) Python(boto3) 예제

import boto3
import os

print("ROLE_ARN:", os.getenv("AWS_ROLE_ARN"))
print("TOKEN_FILE:", os.getenv("AWS_WEB_IDENTITY_TOKEN_FILE"))

sts = boto3.client("sts")
print(sts.get_caller_identity())

실행:

python app.py
  • 여기서도 InvalidIdentityToken이면 애플리케이션 로직이 아니라 IRSA/OIDC 계층 문제입니다.

7-2) Node.js(AWS SDK v3) 예제

import { STSClient, GetCallerIdentityCommand } from "@aws-sdk/client-sts";

const client = new STSClient({});
const out = await client.send(new GetCallerIdentityCommand({}));
console.log(out);

8) 문제 해결 후 재발 방지 체크리스트

  • 클러스터 생성/업그레이드 파이프라인에서 OIDC issuer와 IAM OIDC provider 존재/일치를 자동 검증
  • IRSA role trust policy는 가능하면 템플릿화하되, sub/issuer는 클러스터/네임스페이스별로 정확히 렌더링
  • 운영에서 403이 났을 때를 대비해 Pod에 AWS_ROLE_ARN, AWS_WEB_IDENTITY_TOKEN_FILE 로그(민감정보 제외)와 GetCallerIdentity 헬스체크를 준비

추가로, EKS에서 “Pod 자체가 정상 생성되지 않아” IRSA 이전 단계에서 막히는 경우도 많습니다. Pod가 아예 ContainerCreating에서 멈춘다면 EKS Pod가 ContainerCreating에 멈출 때 10분 진단 체크리스트로 인프라 계층부터 정리한 뒤 IRSA를 보시는 게 효율적입니다.

9) 요약

  • STS 403 InvalidIdentityToken은 대부분 OIDC issuer/provider 불일치 또는 trust policy의 aud/sub 조건 불일치입니다.
  • Pod 안에서 AWS_ROLE_ARN, AWS_WEB_IDENTITY_TOKEN_FILE 확인 → JWT payload의 iss/sub/aud 확인 → IAM Role trust policy와 1:1 매칭이 가장 빠른 해결 루트입니다.
  • 특정 노드에서만 재현되면 시간 동기화도 반드시 의심하세요.

이 흐름대로 점검하면 “감으로 설정을 바꾸는” 시간을 줄이고, 어떤 값이 실제로 틀렸는지 증거 기반으로 고칠 수 있습니다.