Published on

EKS Pod에서 STS 403 ExpiredToken 해결법

Authors

서버리스/컨테이너 환경에서 AWS API를 호출하다가 403 ExpiredToken을 만나면 대부분 “토큰이 만료됐나 보다”로 끝나기 쉽습니다. 하지만 EKS Pod에서의 ExpiredToken어떤 자격 증명 소스(credential provider chain)가 선택됐는지, 만료 갱신 로직이 정상 동작하는지, 클러스터/노드의 시간 동기화가 맞는지에 따라 원인이 크게 갈립니다.

이 글은 EKS Pod에서 STS 호출(또는 STS 기반으로 발급되는 임시 자격 증명 사용) 중 ExpiredToken이 발생할 때, 재현 → 원인 분기 → 해결 순서로 정리합니다. IRSA를 쓰는 경우와 노드 IAM Role(EC2 Instance Profile)에 기대는 경우를 모두 다룹니다.

관련해서 IRSA 자체의 네트워크/타임아웃 이슈가 의심된다면 EKS IRSA에서 AssumeRoleWithWebIdentity 0s 타임아웃 해결도 함께 확인해보면 좋습니다.

증상 정리: 어떤 로그가 보이나?

대표적인 에러는 다음 형태로 나타납니다.

  • AWS SDK 공통
    • ExpiredToken: The security token included in the request is expired
    • InvalidClientTokenId가 함께 섞여 나오기도 함
  • STS 직접 호출 시
    • AssumeRole / GetCallerIdentity / AssumeRoleWithWebIdentity 등에서 403

여기서 중요한 포인트는 STS 자체가 만료된 토큰을 받았다는 사실만 알릴 뿐, “왜 갱신이 안 됐는지”는 알려주지 않는다는 점입니다. 따라서 진단은 (1) 어떤 자격 증명을 쓰는지부터 시작해야 합니다.

가장 흔한 원인 5가지

1) IRSA를 의도했는데 노드 자격 증명을 사용(또는 그 반대)

EKS에서 Pod가 AWS 자격 증명을 얻는 경로는 크게 두 가지입니다.

  • IRSA (AssumeRoleWithWebIdentity): ServiceAccount에 Role ARN을 붙이고, Pod 내부에 projected token(AWS_WEB_IDENTITY_TOKEN_FILE)을 마운트
  • 노드 IAM Role (IMDS): Pod가 노드의 Instance Profile을 사용(과거 방식/기본 fallback)

문제는 애플리케이션/SDK가 예상과 다른 provider를 선택할 수 있다는 겁니다. 예를 들어 IRSA 설정이 누락되었거나, 환경변수/설정이 꼬여서 IMDS로 떨어지면, 노드 자격 증명 회전과 애플리케이션 캐시가 맞지 않아 ExpiredToken이 발생할 수 있습니다.

2) SDK/애플리케이션이 임시 자격 증명을 “고정”해서 재사용

STS로 발급받은 임시 키를 애플리케이션이 어딘가에 저장해두고(환경변수/파일/메모리 캐시) 만료 후에도 계속 쓰면 100% 재현됩니다.

특히 다음 패턴이 위험합니다.

  • 컨테이너 시작 시 aws sts assume-role로 키를 받아 .env에 써놓고 프로세스가 그걸 계속 사용
  • Java/Python/Node에서 커스텀 credential provider를 만들었는데 refresh 로직이 없음
  • Sidecar가 토큰을 갱신해줄 거라 가정했지만 실제로는 갱신 안 됨

3) 노드/컨테이너 시간 드리프트(NTP 불일치)

STS 토큰은 시간에 민감합니다. 노드 시간이 몇 분만 틀어져도 다음이 발생할 수 있습니다.

  • “이미 만료된” 것으로 판단되어 ExpiredToken
  • 아직 유효한데도 “NotYetValid” 류의 문제(서비스에 따라 다름)

EKS 노드는 일반적으로 chrony/NTP가 맞지만, 커스텀 AMI/보안 설정/네트워크 제한으로 NTP가 막히면 드리프트가 생길 수 있습니다.

4) IRSA projected token 파일이 갱신되지 않거나 마운트가 깨짐

IRSA는 Pod에 projected service account token을 주입하고, AWS SDK는 이를 읽어 STS로 AssumeRoleWithWebIdentity를 호출합니다.

  • 토큰 파일 경로가 잘못되었거나
  • Pod spec에서 automountServiceAccountToken: false로 꺼져 있거나
  • ServiceAccount annotation이 잘못되어 다른 Role을 보거나
  • 오래된 SDK가 IRSA를 제대로 지원하지 않는 경우

이런 상황에서 SDK가 fallback으로 다른 provider를 쓰거나, 토큰 갱신이 되지 않아 만료로 이어집니다.

5) STS 엔드포인트/리전 설정 불일치로 인한 우회적 실패

일부 환경에서 STS를 글로벌 엔드포인트로 호출하거나, VPC 엔드포인트/프록시를 통해 우회하면서 실패가 ExpiredToken처럼 보이는 경우가 있습니다(특히 로그가 축약되는 라이브러리에서).

이때는 AWS_REGION, AWS_DEFAULT_REGION, AWS_STS_REGIONAL_ENDPOINTS=regional 등을 점검해야 합니다.

10분 진단 체크리스트

1) Pod가 어떤 자격 증명을 쓰는지 확인

가장 먼저 Pod 환경변수/마운트를 확인합니다.

kubectl -n <ns> exec -it <pod> -- env | egrep 'AWS_|KUBERNETES'

IRSA라면 보통 아래가 보입니다.

  • AWS_ROLE_ARN=arn:aws:iam::...:role/...
  • AWS_WEB_IDENTITY_TOKEN_FILE=/var/run/secrets/eks.amazonaws.com/serviceaccount/token
  • AWS_REGION 또는 AWS_DEFAULT_REGION

또한 토큰 파일이 실제로 존재하는지 확인합니다.

kubectl -n <ns> exec -it <pod> -- ls -al /var/run/secrets/eks.amazonaws.com/serviceaccount/
kubectl -n <ns> exec -it <pod> -- head -c 50 /var/run/secrets/eks.amazonaws.com/serviceaccount/token && echo

만약 위 파일/변수가 없으면 IRSA가 아닌 경로(IMDS/정적키 등)를 쓰고 있을 확률이 큽니다.

2) AWS CLI로 현재 호출자 확인(GetCallerIdentity)

Pod 안에서 가능하면 아래를 실행합니다.

kubectl -n <ns> exec -it <pod> -- aws sts get-caller-identity
  • IRSA라면 Arnassumed-role/<role-name>/<session-name> 형태로 나옵니다.
  • 노드 Role을 쓰고 있다면 노드 인스턴스 프로파일 Role로 보일 수 있습니다.

ExpiredToken이 여기서도 재현되면 “애플리케이션 코드” 문제가 아니라 “Pod의 현재 자격 증명 상태” 문제일 가능성이 큽니다.

3) 시간 확인(노드/컨테이너)

Pod에서 UTC 시간을 확인합니다.

kubectl -n <ns> exec -it <pod> -- date -u

그리고 로컬/신뢰 가능한 시간과 비교합니다. 수 분 이상 차이 나면 NTP/chrony 점검이 우선입니다.

4) ServiceAccount/Annotation 확인(IRSA)

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

다음을 확인합니다.

  • eks.amazonaws.com/role-arn: arn:aws:iam::...:role/...
  • automountServiceAccountToken 설정

또한 Pod가 그 ServiceAccount를 실제로 사용 중인지도 확인합니다.

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

해결법: 원인별 처방

1) IRSA를 올바르게 고정하고 IMDS 접근을 차단

의도는 IRSA인데 가끔 IMDS로 새는 경우(또는 반대로 노드 Role을 쓰는데 IRSA 변수만 남아있는 경우)를 막으려면, IMDS 차단이 효과적입니다.

EKS에서는 Pod 단에서 아래 환경변수를 넣어 SDK가 IMDS를 보지 않게 할 수 있습니다.

apiVersion: apps/v1
kind: Deployment
spec:
  template:
    spec:
      containers:
        - name: app
          image: your-image
          env:
            - name: AWS_EC2_METADATA_DISABLED
              value: "true"

이렇게 하면 IRSA가 설정되지 않은 Pod는 즉시 “자격 증명 없음”으로 실패하므로, 애매한 ExpiredToken 대신 명확한 구성 오류로 드러납니다.

추가로, 조직 정책상 더 강하게 막고 싶다면 네트워크 정책/iptables로 169.254.169.254(IMDS) 접근을 차단하는 방식도 사용합니다.

2) IRSA 설정 재점검(서비스어카운트/토큰 마운트)

IRSA의 핵심은 ServiceAccount annotation + projected token + role trust policy 3종 세트입니다.

ServiceAccount 예시

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

Deployment에서 ServiceAccount 지정

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

만약 automountServiceAccountToken: false가 걸려 있으면 IRSA 토큰이 주입되지 않습니다. 보안 목적으로 껐다면, IRSA가 필요한 워크로드에만 켜거나 projected token을 명시적으로 구성해야 합니다.

3) 애플리케이션이 임시 키를 고정하지 않도록 수정

가장 안전한 패턴은 “STS로 직접 키를 받아서 들고 있기”가 아니라, SDK의 기본 credential provider chain(IRSA/IMDS/프로파일)을 그대로 쓰는 것입니다.

Python(boto3) 권장 패턴

import boto3

# 자격 증명은 환경(IRSA)에서 자동으로 로딩/갱신됨
session = boto3.Session()
sts = session.client("sts")
print(sts.get_caller_identity())

s3 = session.client("s3")
print(s3.list_buckets())
  • AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY/AWS_SESSION_TOKEN를 코드에서 직접 주입하지 마세요.
  • 커스텀 assume-role 로직을 넣어야 한다면, 만료 시간 기반 refresh를 구현하거나(권장 X) 가능하면 IRSA로 역할을 직접 부여하세요.

Node.js(@aws-sdk/*) 권장 패턴

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

const client = new STSClient({}); // IRSA/환경 기반 자동 로딩
const out = await client.send(new GetCallerIdentityCommand({}));
console.log(out);

Node SDK v3는 기본 provider chain이 잘 구성되어 있으나, 컨테이너 시작 시점에 토큰을 읽고 고정하는 커스텀 코드를 넣으면 만료 이슈가 발생합니다.

4) 시간 동기화(NTP/chrony) 문제 해결

시간 드리프트가 의심되면, 노드 레벨에서 확인해야 합니다(컨테이너는 노드 커널 시간을 공유).

  • 관리형 노드 그룹: 보통 자동이지만, 보안 그룹/네트워크 ACL로 NTP가 막히지 않았는지 확인
  • 커스텀 AMI: chrony 설정 확인

SSM 접속이 가능하다면 노드에서 다음을 확인합니다.

timedatectl
chronyc tracking || true

드리프트가 크면 NTP 서버 접근 허용, chrony 재시작/설정 수정이 필요합니다. 이 이슈는 STS뿐 아니라 TLS/인증 전반에 영향을 주므로 우선순위가 높습니다.

5) STS 리전/엔드포인트를 명시하고, VPC 엔드포인트 사용 시 정책 점검

EKS에서 프라이빗 서브넷만 사용하고 STS를 VPC 엔드포인트로 붙이는 경우, 다음을 권장합니다.

  • AWS_REGION/AWS_DEFAULT_REGION을 명확히 설정
  • AWS_STS_REGIONAL_ENDPOINTS=regional 설정
env:
  - name: AWS_REGION
    value: ap-northeast-2
  - name: AWS_STS_REGIONAL_ENDPOINTS
    value: regional

VPC 엔드포인트 정책이 너무 타이트하면 간헐적으로 실패가 나고, 애플리케이션 레벨에서는 토큰 문제처럼 보일 수 있습니다. CloudTrail/STS 응답을 함께 보며 분기하세요.

실전 디버깅: “만료된 토큰”이 어디서 왔는지 추적하기

1) 컨테이너 내부에서 현재 세션 토큰 확인(노출 주의)

보안상 권장하진 않지만, 디버깅 환경에서만 다음을 확인하면 방향이 잡힙니다.

kubectl -n <ns> exec -it <pod> -- sh -lc 'env | egrep "AWS_(ACCESS_KEY_ID|SESSION_TOKEN|SECRET_ACCESS_KEY)"'
  • 여기 값이 박혀 있다면(특히 AWS_SESSION_TOKEN) 누군가가 정적 주입하고 있는 겁니다.
  • IRSA를 제대로 쓰면 보통 위 값은 환경변수로 고정 주입되지 않습니다(프로세스가 필요 시 토큰 파일을 읽어 STS로 교환).

2) 애플리케이션 재시작으로만 해결된다면 “캐시 고정” 의심

Pod 재시작 시 잠깐 정상 → 일정 시간 후 다시 ExpiredToken이면 거의 항상 다음 중 하나입니다.

  • 앱이 STS 결과를 메모리/파일에 캐시하고 갱신하지 않음
  • 오래된 SDK/라이브러리 버그로 refresh가 동작하지 않음

이 경우 해결은 “재시작 자동화”가 아니라 자격 증명 획득 방식을 SDK 기본으로 되돌리는 것입니다.

운영 관점의 재발 방지 체크

1) IRSA를 표준으로 만들고, 노드 Role 권한 최소화

노드 IAM Role에 광범위한 권한이 있으면, IRSA가 깨졌을 때도 서비스가 “어느 정도 동작”하다가 만료/회전 타이밍에 이상하게 깨질 수 있습니다. 노드 Role은 CNI/ECR/로그 등 최소 권한으로 줄이고, 워크로드 권한은 IRSA로 분리하세요.

2) CloudTrail로 ExpiredToken 이벤트를 역추적

CloudTrail에서 eventSource=sts.amazonaws.com + errorCode=ExpiredToken으로 필터링하면, 어떤 Role/어떤 UserAgent(SDK)가 어떤 엔드포인트로 호출했는지 단서가 나옵니다.

3) “Pod는 되는데 특정 AWS 서비스만 403”과 구분

STS ExpiredToken은 모든 서비스 호출에 영향을 주는 경우가 많지만, 권한/정책 문제로 특정 서비스만 403이 나는 케이스도 흔합니다. 그 경우는 토큰 만료가 아니라 권한 경로가 의심됩니다. 비슷한 결로 헷갈린다면 EKS에서 Pod는 되는데 SQS만 403 뜰 때처럼 서비스별 403 분기 글도 참고하세요.

결론: 가장 빠른 해결 순서

  1. Pod에서 aws sts get-caller-identity현재 자격 증명 소스를 확정한다.
  2. IRSA를 의도했다면 AWS_WEB_IDENTITY_TOKEN_FILE, ServiceAccount annotation, token 마운트를 확인하고, 가능하면 AWS_EC2_METADATA_DISABLED=true로 IMDS fallback을 차단한다.
  3. 재시작으로만 잠깐 해결되면 앱/SDK가 토큰을 고정 캐시하고 있는지 점검하고, SDK 기본 provider chain으로 단순화한다.
  4. 시간이 조금이라도 의심되면 노드 NTP/chrony를 확인한다.
  5. STS 리전/엔드포인트/VPC 엔드포인트 정책을 점검해 “토큰 만료처럼 보이는 네트워크/정책 문제”를 배제한다.

위 순서대로 보면, 대부분의 EKS Pod에서 STS 403 ExpiredToken은 30분 내에 원인 범위를 좁히고, 구성/코드 중 어디를 고쳐야 하는지 결론을 낼 수 있습니다.