- Published on
AWS STS 토큰 만료로 403? IRSA·AssumeRole 점검
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버/파드에서 잘 되던 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,errorMessagerequestParameters.roleArn(AssumeRole 호출 실패 시)
2) STS 임시 자격증명 기본: “만료”는 정상, “갱신 실패”가 문제
STS 기반 자격증명은 원래 만료됩니다.
- AssumeRole: 세션 지속 시간(기본 1시간, Role의
MaxSessionDuration범위 내에서 조정) - IRSA(AssumeRoleWithWebIdentity): SDK가 토큰 파일을 읽어 STS로 교환하고, 만료 전 자동 갱신
따라서 장애는 보통 아래 중 하나입니다.
- 갱신을 해야 하는데 갱신 경로가 막힘 (토큰 파일 접근/환경 변수/네트워크/STS 엔드포인트)
- 애플리케이션이 임시 자격증명을 캐시해 놓고 갱신을 안 함 (커스텀 Credential Provider, 잘못된 싱글톤)
- 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이 다른 클러스터 것Condition의sub가 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이 나는 지점은 크게 두 가지입니다.
- A 세션이 만료되었는데 앱이 B 자격증명을 갱신하지 않음
- 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”처럼 보이기도 합니다.
우선순위는 단순합니다.
- CloudTrail/
get-caller-identity로 실제 호출 Role을 확정하고 - IRSA의 OIDC/SA/trust policy를 맞춘 뒤
- 앱 코드에서 임시 자격증명 갱신 가능 구조인지 확인하세요.
S3에서만 403이 터지는 상황이라면 STS 외에도 버킷 정책/KMS/엔드포인트 정책이 얽힐 수 있으니, 앞서 언급한 AWS S3 AccessDenied 403 - 정책·KMS·VPCE 점검 체크리스트를 함께 보면 원인 분리가 더 빨라집니다.