Published on

AWS STS 토큰 만료로 403? IRSA·AssumeRole 점검

Authors

서버/파드에서 잘 되던 AWS 호출이 어느 순간부터 403으로 깨지면, 많은 경우 권한 자체가 아니라 STS 임시 자격증명(토큰) 만료/갱신 실패가 원인입니다. 특히 EKS의 IRSA(IAM Roles for Service Accounts), 애플리케이션 내부의 AssumeRole 체인, 그리고 웹 아이덴티티 토큰 파일/환경 변수가 꼬이면 증상은 전형적으로 ExpiredToken, AccessDenied, InvalidIdentityToken 등으로 나타납니다.

이 글에서는 “403이지만 정책은 맞는 것 같은데?”라는 상황에서 STS 토큰 만료 관점으로 원인을 좁히고, IRSA 및 AssumeRole을 단계적으로 점검하는 방법을 정리합니다.

> 참고: S3 자체 정책/KMS/VPCE 문제로 403이 나는 케이스는 별도 체크가 필요합니다. 해당 축은 AWS S3 AccessDenied 403 - 정책·KMS·VPCE 점검도 함께 확인하세요.

1) 403의 정체부터 분류하기: 에러 문자열이 힌트다

같은 403이라도 AWS SDK/CLI가 뱉는 에러 코드는 원인 분리에 결정적입니다.

자주 보는 STS/토큰 관련 에러

  • ExpiredToken: 말 그대로 세션 토큰 만료. 갱신이 안 되는 경로(IRSA/AssumeRole/credential provider chain)가 의심됩니다.
  • InvalidClientTokenId: 액세스 키/세션 토큰 조합이 유효하지 않음(잘못된 키, 잘못된 프로파일, 환경 변수 오염).
  • InvalidIdentityToken: IRSA의 웹 아이덴티티 토큰(JWT) 검증 실패(대개 OIDC 설정/신뢰 정책/SA annotation/토큰 파일 문제).
  • AccessDenied (하지만 “정책은 맞다”): 실제로는 다른 Role로 Assume되어 호출 중이거나, 세션 정책/Permission Boundary로 잘리고 있을 수 있습니다.

CloudTrail에서 먼저 확인할 것

CloudTrail 이벤트에서 다음 필드를 보면 방향이 잡힙니다.

  • userIdentity.type (AssumedRole, WebIdentityUser 등)
  • userIdentity.arn (실제로 어떤 Role로 호출했는지)
  • errorCode, errorMessage
  • requestParameters.roleArn (AssumeRole 호출 실패 시)

2) STS 임시 자격증명 기본: “만료”는 정상, “갱신 실패”가 문제

STS 기반 자격증명은 원래 만료됩니다.

  • AssumeRole: 세션 지속 시간(기본 1시간, Role의 MaxSessionDuration 범위 내에서 조정)
  • IRSA(AssumeRoleWithWebIdentity): SDK가 토큰 파일을 읽어 STS로 교환하고, 만료 전 자동 갱신

따라서 장애는 보통 아래 중 하나입니다.

  1. 갱신을 해야 하는데 갱신 경로가 막힘 (토큰 파일 접근/환경 변수/네트워크/STS 엔드포인트)
  2. 애플리케이션이 임시 자격증명을 캐시해 놓고 갱신을 안 함 (커스텀 Credential Provider, 잘못된 싱글톤)
  3. AssumeRole 체인이 길어져 중간 Role 만료/정책 변경/ExternalId 조건 미충족

3) EKS IRSA에서 403/ExpiredToken이 날 때 체크리스트

IRSA는 “서비스어카운트(SA) → OIDC JWT → STS AssumeRoleWithWebIdentity → 임시 자격증명” 흐름입니다. 어디가 끊겼는지 순서대로 확인합니다.

3.1 파드 내부에서 현재 자격증명 소스를 확인

먼저 파드에 들어가 환경 변수를 확인합니다.

kubectl exec -it deploy/my-app -- sh

env | egrep 'AWS_(ROLE_ARN|WEB_IDENTITY_TOKEN_FILE|REGION|DEFAULT_REGION)' || true
ls -l $AWS_WEB_IDENTITY_TOKEN_FILE
head -c 40 $AWS_WEB_IDENTITY_TOKEN_FILE && echo

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

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

여기서 토큰 파일이 없거나 권한이 없으면 갱신이 즉시 깨집니다.

3.2 서비스어카운트 annotation과 Role ARN 매칭

kubectl get sa my-sa -n my-ns -o yaml | sed -n '1,120p'

확인 포인트:

  • eks.amazonaws.com/role-arn이 정확한지
  • 배포가 실제로 그 SA를 쓰는지 (spec.serviceAccountName)

3.3 IAM Role 신뢰 정책(Trust policy) 점검

IRSA Role의 trust policy는 대략 이런 형태여야 합니다.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::<acct>:oidc-provider/oidc.eks.<region>.amazonaws.com/id/<OIDC_ID>"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "oidc.eks.<region>.amazonaws.com/id/<OIDC_ID>:sub": "system:serviceaccount:my-ns:my-sa"
        }
      }
    }
  ]
}

자주 틀리는 지점:

  • Principal.Federated의 OIDC provider ARN이 다른 클러스터 것
  • Conditionsub가 namespace/sa 이름과 불일치
  • StringLike/StringEquals 조건이 과도하게 제한되어 새로운 SA/네임스페이스를 막음

3.4 OIDC Provider가 클러스터에 연결되어 있는지

클러스터 OIDC issuer URL을 확인:

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

IAM OIDC provider 목록에서 issuer가 존재하는지 확인:

aws iam list-open-id-connect-providers
aws iam get-open-id-connect-provider --open-id-connect-provider-arn <arn>

3.5 STS 엔드포인트 네트워크/정책 이슈

프라이빗 클러스터/격리된 VPC에서 STS 호출이 막히면 “만료 후 갱신”이 실패합니다.

  • NAT/인터넷 egress가 없는 경우: STS VPC 엔드포인트(Interface Endpoint) 고려
  • 프록시 환경: HTTPS_PROXY 설정이 SDK에 영향을 주고, 인증서/리졸브 문제로 STS 호출 실패 가능

증상은 앱 로그에서 대개 Unable to execute HTTP request / connect timeout 등으로 나타납니다.

4) AssumeRole 체인에서 토큰 만료/403이 터지는 전형 패턴

4.1 “Role A(IRSA) → Role B(AssumeRole) → 서비스 호출” 구조

EKS에서는 보통 IRSA로 1차 자격증명을 얻고, 앱이 다시 다른 Role로 AssumeRole 하기도 합니다.

  • IRSA Role(A)의 권한: sts:AssumeRole로 B를 Assume할 수 있어야 함
  • Role B의 trust policy: A를 신뢰해야 함

여기서 만료/403이 나는 지점은 크게 두 가지입니다.

  1. A 세션이 만료되었는데 앱이 B 자격증명을 갱신하지 않음
  2. B의 MaxSessionDuration/조건(ExternalId/MFA/SourceIdentity 등) 때문에 갱신이 실패

4.2 Role 세션 시간(MaxSessionDuration)과 SDK 설정

Role B의 MaxSessionDuration이 1시간인데 애플리케이션이 12시간짜리 세션을 요청하면 AssumeRole이 실패합니다.

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

AssumeRole 요청에서 duration을 명시했다면(예: 43200) 이를 줄이거나 Role 설정을 늘려야 합니다.

4.3 세션 정책(Session Policy)로 권한이 잘리는 경우

AssumeRole 호출 시 Policy(inline session policy)나 PolicyArns를 붙이면 최종 권한은 Role 정책 ∩ 세션 정책이 됩니다. “Role에 권한이 있는데 AccessDenied”라면 이 케이스가 의외로 많습니다.

CloudTrail에서 requestParameters.policy가 있었는지 확인하세요.

5) 애플리케이션 레벨: “임시 자격증명 캐시”가 갱신을 막는다

AWS SDK는 기본적으로 만료를 감지하고 갱신하지만, 다음과 같은 구현은 위험합니다.

  • STS로 발급받은 AccessKeyId/SecretAccessKey/SessionToken환경 변수로 고정
  • 자격증명을 애플리케이션 시작 시 한 번만 가져와 싱글톤으로 영구 사용
  • 멀티스레드 환경에서 커스텀 provider를 만들어 race condition으로 만료 토큰을 재사용

Java(AWS SDK v2)에서 안전한 패턴

IRSA라면 WebIdentityTokenFileCredentialsProvider를 사용하면 자동 갱신을 기대할 수 있습니다.

import software.amazon.awssdk.auth.credentials.WebIdentityTokenFileCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.sts.StsClient;

public class AwsClients {
  static StsClient sts() {
    return StsClient.builder()
        .region(Region.AWS_GLOBAL) // 또는 실제 리전
        .credentialsProvider(WebIdentityTokenFileCredentialsProvider.create())
        .build();
  }
}

AssumeRole이 필요하면 StsAssumeRoleCredentialsProvider를 붙여 자동 갱신되는 provider를 구성합니다.

import software.amazon.awssdk.auth.credentials.StsAssumeRoleCredentialsProvider;
import software.amazon.awssdk.services.sts.model.AssumeRoleRequest;

StsAssumeRoleCredentialsProvider roleBCreds = StsAssumeRoleCredentialsProvider.builder()
    .stsClient(sts())
    .refreshRequest(AssumeRoleRequest.builder()
        .roleArn("arn:aws:iam::<acct>:role/role-b")
        .roleSessionName("my-app")
        .build())
    .build();

핵심은 정적(Static) 자격증명 객체에 STS 결과를 박아두지 말고, “갱신 가능한 provider”를 사용해야 한다는 점입니다.

Python(boto3)에서 만료 토큰을 고정하면 생기는 문제

다음은 흔한 안티패턴입니다.

# 안티패턴: STS 결과를 받아 env로 고정 -> 만료되면 영구 403
import os
import boto3

sts = boto3.client("sts")
creds = sts.assume_role(RoleArn=os.environ["ROLE_ARN"], RoleSessionName="app")["Credentials"]

os.environ["AWS_ACCESS_KEY_ID"] = creds["AccessKeyId"]
os.environ["AWS_SECRET_ACCESS_KEY"] = creds["SecretAccessKey"]
os.environ["AWS_SESSION_TOKEN"] = creds["SessionToken"]

대신 boto3의 기본 credential chain(IRSA 포함)에 맡기거나, assume role이 필요하면 botocore의 refreshable credentials 패턴을 사용하세요(구현이 길어 생략). 최소한 “만료 시간”을 추적하여 재발급하는 로직이 있어야 합니다.

6) 빠른 재현/진단: CLI로 현재 호출 주체를 확인

문제가 나는 런타임(파드/인스턴스)에서 다음을 실행해보면 “내가 누구로 호출 중인지”가 바로 드러납니다.

aws sts get-caller-identity
  • 기대한 Role ARN이 아니면: credential chain이 다른 소스를 잡고 있는 것(예: 노드 IAM Role, 다른 프로파일, 오래된 env)
  • 호출 자체가 ExpiredToken이면: 현재 잡힌 자격증명이 이미 만료, 그리고 갱신이 실패 중

추가로 STS 토큰 만료 시간을 확인하려면(AssumeRole 결과를 직접 받는 구조라면) 응답의 Expiration을 로그로 남기세요.

7) 운영 체크리스트(요약)

IRSA 쪽

  • 파드에 AWS_ROLE_ARN, AWS_WEB_IDENTITY_TOKEN_FILE 존재
  • SA annotation의 role-arn 정확, deployment가 해당 SA 사용
  • Role trust policy의 OIDC provider/sub 조건 일치
  • 클러스터 OIDC issuer와 IAM OIDC provider 연결 확인
  • STS 호출 네트워크 경로(NAT/VPCE/프록시) 확보

AssumeRole 쪽

  • Role A에 sts:AssumeRole 권한 존재
  • Role B trust policy가 Role A를 신뢰
  • MaxSessionDuration과 요청 duration 불일치 없음
  • ExternalId/SourceIdentity/세션 정책으로 권한이 잘리지 않음

애플리케이션 쪽

  • STS 결과를 정적으로 캐시/환경 변수로 고정하지 않음
  • 갱신 가능한 credentials provider 사용
  • get-caller-identity로 실제 호출 주체를 항상 확인

8) 마무리: “정책은 맞는데 403”이면 토큰 갱신부터 의심하자

권한(Policy)만 들여다보면 STS 만료/갱신 실패를 놓치기 쉽습니다. 특히 IRSA는 정상 동작 시 자동 갱신이 투명하게 일어나기 때문에, 한 번 꼬이면 만료 시점에만 장애가 터져 “간헐적 403”처럼 보이기도 합니다.

우선순위는 단순합니다.

  1. CloudTrail/get-caller-identity실제 호출 Role을 확정하고
  2. IRSA의 OIDC/SA/trust policy를 맞춘 뒤
  3. 앱 코드에서 임시 자격증명 갱신 가능 구조인지 확인하세요.

S3에서만 403이 터지는 상황이라면 STS 외에도 버킷 정책/KMS/엔드포인트 정책이 얽힐 수 있으니, 앞서 언급한 AWS S3 AccessDenied 403 - 정책·KMS·VPCE 점검 체크리스트를 함께 보면 원인 분리가 더 빨라집니다.