- Published on
EKS IRSA로 S3 403? STS 토큰 만료 진단법
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버가 잘 돌다가 갑자기 S3 요청이 403 AccessDenied 또는 ExpiredToken 비슷한 에러로 터지면, 많은 경우 “권한 정책이 틀렸다”보다 “IRSA로 발급받는 STS 세션 토큰이 만료됐는데 갱신이 안 됐다” 쪽이 더 빠른 정답입니다. 특히 Pod가 오래 떠 있거나(장수 Pod), 네트워크/프록시/SDK 설정이 꼬여서 STS 호출이 막히면, 처음엔 되다가 일정 시간이 지난 뒤 403이 발생하는 패턴이 나옵니다.
이 글은 EKS IRSA로 S3를 쓰는 상황에서 403이 났을 때, 정책 문제 vs STS 토큰 만료/갱신 문제를 구분하고, 로그와 명령으로 원인을 좁히는 순서로 정리합니다.
관련해서 OIDC 기반 AssumeRole 실패 패턴은 GitHub Actions에서도 유사하게 나타납니다. OIDC 트러블슈팅 감각을 확장하려면 이 글도 같이 보면 좋습니다: GitHub Actions OIDC로 AWS AssumeRole 실패 해결
문제 패턴: “처음엔 되는데 나중에 403”이면 토큰 갱신을 의심
IRSA는 대략 다음 흐름으로 동작합니다.
- Pod에 ServiceAccount를 붙임
- EKS가 Pod에 웹 아이덴티티 토큰 파일을 마운트
- AWS SDK가
AssumeRoleWithWebIdentity로 STS 임시 자격증명을 발급 - SDK가 만료 전에 자동 갱신(Refresh)
여기서 갱신 단계가 실패하면, 애플리케이션은 만료된 자격증명으로 S3를 호출하게 되고 결과가 403으로 보입니다.
흔한 증상
- 애플리케이션 로그에
ExpiredToken/The security token included in the request is expired/InvalidToken류 메시지 - CloudTrail에서 동일 Role로 STS 호출이 어느 시점부터 끊김
- Pod 재시작하면 잠깐 정상(새 토큰 발급)인데 다시 몇 시간 후 실패
반대로 처음부터 계속 403이면 정책/신뢰 정책(Trust) 또는 버킷 정책/암호화(KMS) 쪽일 확률이 큽니다.
1단계: “정말 IRSA를 타고 있나”를 Pod 안에서 확인
가장 먼저, Pod가 IRSA에 필요한 환경을 제대로 가지고 있는지 확인합니다.
Pod 환경 변수와 토큰 파일 확인
Pod 안에서:
# IRSA에서 흔히 세팅되는 환경 변수
env | grep -E 'AWS_ROLE_ARN|AWS_WEB_IDENTITY_TOKEN_FILE|AWS_REGION|AWS_DEFAULT_REGION'
# 토큰 파일 존재 확인
ls -al "$AWS_WEB_IDENTITY_TOKEN_FILE"
# 토큰 일부(너무 길면 앞부분만)
head -c 50 "$AWS_WEB_IDENTITY_TOKEN_FILE"; echo
정상이라면 보통 다음이 보입니다.
AWS_ROLE_ARN이 존재AWS_WEB_IDENTITY_TOKEN_FILE이/var/run/secrets/eks.amazonaws.com/serviceaccount/token비슷한 경로
만약 위가 비어 있다면:
- ServiceAccount에
eks.amazonaws.com/role-arn어노테이션이 없거나 - Pod가 다른 ServiceAccount를 쓰고 있거나
- IRSA 대신 node IAM role(EC2 instance profile)을 타고 있을 수 있습니다.
실제 호출 주체 확인: STS GetCallerIdentity
가능하면 aws-cli를 임시로 넣어 확인하는 게 가장 빠릅니다.
aws sts get-caller-identity
여기서 Arn이 기대한 Role(예: assumed-role/your-irsa-role/...)로 나오면 IRSA는 일단 정상 경로입니다.
만약 node role로 나온다면, IRSA가 아니라 노드 권한으로 접근 중이었고, 이후 노드 롤 변경/만료/정책 변경이 원인일 수 있습니다.
2단계: 에러가 “AccessDenied”인지 “ExpiredToken”인지 분리
S3 403은 다 같은 403이 아닙니다. 다음처럼 구분합니다.
AccessDenied: 정책/버킷 정책/KMS/조건식 문제 가능성이 큼ExpiredToken: STS 세션 만료 및 갱신 실패 가능성이 큼
애플리케이션이 AWS SDK를 쓰고 있다면, SDK가 남기는 에러 코드(예: RequestFailure의 Code)를 반드시 로깅하도록 바꿔두는 게 좋습니다.
예를 들어 Go AWS SDK v2라면:
package main
import (
"context"
"fmt"
"log"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/smithy-go"
)
func main() {
ctx := context.Background()
cfg, err := config.LoadDefaultConfig(ctx)
if err != nil {
log.Fatal(err)
}
client := s3.NewFromConfig(cfg)
_, err = client.ListBuckets(ctx, &s3.ListBucketsInput{})
if err != nil {
var apiErr smithy.APIError
if ok := (err != nil) && (fmt.Sprintf("%T", err) != "") && (func() bool {
// errors.As를 쓰는 게 정석이지만, MDX에서 꺾쇠를 피하려고 간단히 작성
return false
}()); ok {
_ = apiErr
}
// 실무에서는 errors.As(err, &apiErr)로 Code/Message를 뽑아 로깅하세요.
log.Printf("s3 call failed: %v", err)
return
}
log.Println("ok")
}
위 코드는 개념 예시이고, 실전에서는 errors.As로 smithy.APIError를 캐스팅해 ErrorCode()를 로깅하세요. 핵심은 403만 찍지 말고 AWS 에러 코드와 메시지를 남기는 것입니다.
3단계: STS 토큰 갱신 실패의 대표 원인 6가지
원인 1) STS 엔드포인트 네트워크 차단(가장 흔함)
갱신은 결국 STS API 호출입니다. 다음 상황이면 “처음 발급은 됐는데, 이후 갱신 시점에 실패”가 나올 수 있습니다.
- Pod에서 인터넷 나가는 경로가 바뀜(NAT 장애, 라우팅 변경)
- 사내 프록시 도입 후 STS 도메인 차단
- VPC 엔드포인트 정책이 STS를 막음(Interface Endpoint)
- DNS 이슈로
sts.amazonaws.com해석 실패
Pod에서 STS 연결을 확인합니다.
# DNS
nslookup sts.amazonaws.com
# HTTPS 연결
curl -sS -o /dev/null -w '%{http_code}\n' https://sts.amazonaws.com/
사설망에서 STS를 VPC 엔드포인트로 강제하는 구조라면, STS용 Interface Endpoint 존재 여부와 정책을 점검하세요.
VPC 엔드포인트 정책 때문에 403이 나는 패턴은 ECR에서도 자주 보입니다. 접근 제어 감각을 잡는 데는 이 글이 도움이 됩니다: EKS ImagePullBackOff 403? ECR VPC 엔드포인트 정책 점검
원인 2) 시간 동기화(NTP) 문제로 만료 판정이 꼬임
STS 토큰은 시간에 민감합니다. 노드 시간이 틀어지면 다음이 발생할 수 있습니다.
- 아직 유효한데 만료로 판단
- 이미 만료인데 유효로 판단하고 요청하다가 403
노드에서 chronyd/systemd-timesyncd 상태를 확인하고, 컨테이너에서도 현재 시간을 확인하세요.
date -u
원인 3) AWS SDK 자격증명 체인 오염(환경 변수로 고정 자격증명 주입)
IRSA를 쓰는데도 다음 환경 변수가 들어가 있으면, SDK가 IRSA 대신 고정 키를 우선할 수 있습니다.
AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEYAWS_SESSION_TOKEN
이 키들이 만료되면(특히 AWS_SESSION_TOKEN) S3는 403을 줍니다.
Pod에서 확인:
env | grep -E 'AWS_ACCESS_KEY_ID|AWS_SECRET_ACCESS_KEY|AWS_SESSION_TOKEN'
있다면 배포 파이프라인(Helm values, Secret, CI 환경 변수)에서 제거하세요.
원인 4) ServiceAccount 토큰/Projected token 설정 문제
IRSA는 서비스어카운트 토큰을 웹 아이덴티티 토큰으로 사용합니다. 클러스터/파드 설정에 따라 토큰이 갱신되지 않거나 예상과 다르게 마운트되는 경우가 있습니다.
확인할 것:
- Pod 스펙에
automountServiceAccountToken: false가 걸려 있지 않은지 - ServiceAccount가 맞는지
- 토큰 파일 경로가 실제로 존재하는지
kubectl get pod -n your-ns your-pod -o jsonpath='{.spec.serviceAccountName}'; echo
kubectl get sa -n your-ns your-sa -o yaml
원인 5) IAM Role Trust Policy 조건 불일치
Trust Policy가 아래처럼 sub(서비스어카운트)나 aud 조건을 엄격히 걸어둔 경우, 토큰이 갱신되며 클레임이 기대와 달라져 STS AssumeRole이 실패할 수 있습니다.
예: system:serviceaccount:ns:name 오타, 네임스페이스 변경, ServiceAccount 교체 등.
Trust Policy의 핵심은 다음입니다.
- Principal이 EKS OIDC provider
- Action이
sts:AssumeRoleWithWebIdentity - Condition에
sub와aud가 정확
검증 팁: CloudTrail에서 AssumeRoleWithWebIdentity 이벤트가 AccessDenied로 떨어지는지 확인합니다.
원인 6) S3는 되는데 KMS에서 403(암호화)로 보이는 케이스
버킷이 SSE-KMS를 쓰면 S3 Put/Get 자체 권한 외에 KMS 권한이 필요합니다.
kms:Decrypt,kms:Encrypt,kms:GenerateDataKey등- KMS Key policy에도 Role이 허용되어야 함
이 경우 에러는 S3 403처럼 보이지만, 실제 원인은 KMS입니다. CloudTrail에서 KMS 이벤트를 같이 보세요.
4단계: CloudTrail로 “STS 갱신이 끊겼는지” 확인
토큰 만료 문제를 확정하는 가장 빠른 방법은 CloudTrail에서 다음을 보는 겁니다.
AssumeRoleWithWebIdentity호출이 주기적으로 발생하는지- 어느 시점부터 호출이 사라지거나 실패하는지
패턴 예시:
- 정상: Pod가 살아있는 동안 STS 호출이 간헐적으로 계속 보임(SDK가 만료 전 갱신)
- 비정상: 특정 시점 이후 STS 호출이 0건, 그 뒤 S3 403 발생
만약 STS 호출이 실패한다면, 실패 이유(예: AccessDenied, InvalidIdentityToken)가 Trust Policy/토큰 클레임 문제인지, 네트워크 문제인지 가르는 단서가 됩니다.
5단계: 재현 테스트로 “정책 문제”와 “만료 문제”를 분리
테스트 1) Pod 재시작 후 즉시 S3 성공 여부
- 재시작 직후 성공하고 몇 시간 뒤 실패: 만료/갱신 문제 가능성 큼
- 재시작 직후부터 실패: 정책/버킷/KMS/엔드포인트 정책 문제 가능성 큼
테스트 2) 같은 Pod에서 STS 호출이 되는지
S3가 403일 때 STS가 살아있는지 확인합니다.
aws sts get-caller-identity
- 여기서도 실패하면 STS 경로(네트워크/Trust/토큰) 문제
- STS는 되는데 S3만 403이면 S3 정책/버킷 정책/KMS 가능성
6단계: 실무 해결책 체크리스트
해결 1) STS 네트워크 경로 안정화
- 사설망이면 STS Interface Endpoint를 구성하고 보안그룹/정책 허용
- NAT 의존이면 NAT 장애/라우팅 변경 감시
- DNS 장애 대비(코어DNS/노드 DNS 캐시 점검)
해결 2) 애플리케이션에 “만료 시 재시도”가 있는지 확인
SDK는 보통 자동 갱신하지만, 다음 상황이면 갱신 실패가 치명적일 수 있습니다.
- 자격증명을 앱이 직접 캐싱해버림
- 커스텀 S3 클라이언트를 싱글턴으로 만들고, 내부 credential provider가 갱신되지 않게 구성
가능하면 “기본 credential chain”을 그대로 쓰고, 자격증명 객체를 직접 들고 다니는 코드를 피하세요.
해결 3) 환경 변수로 주입된 임시 토큰 제거
CI나 Helm values에 남아 있는 AWS_SESSION_TOKEN은 장수 Pod에서 시한폭탄입니다. IRSA를 쓴다면 고정 키 주입을 원칙적으로 금지하는 편이 안전합니다.
해결 4) Trust Policy와 ServiceAccount 매핑을 코드로 고정
네임스페이스/서비스어카운트 이름이 바뀌면 바로 장애로 이어집니다. Terraform 등으로 다음을 명시적으로 관리하세요.
- OIDC provider ARN
- Role trust condition의
sub - ServiceAccount annotation
7단계: 예시 구성(정상 IRSA) 빠른 점검
ServiceAccount 예시
apiVersion: v1
kind: ServiceAccount
metadata:
name: app-sa
namespace: app
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/app-irsa-role
Pod에서 ServiceAccount 사용
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
namespace: app
spec:
replicas: 2
selector:
matchLabels:
app: app
template:
metadata:
labels:
app: app
spec:
serviceAccountName: app-sa
containers:
- name: app
image: your-image
env:
- name: AWS_REGION
value: ap-northeast-2
IAM Policy 예시(S3 최소 권한)
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:ListBucket"
],
"Resource": "arn:aws:s3:::your-bucket"
},
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject"
],
"Resource": "arn:aws:s3:::your-bucket/*"
}
]
}
마무리: 403을 “권한”으로만 보지 말고 “갱신”으로도 보자
EKS IRSA에서 S3 403은 크게 두 갈래입니다.
- 처음부터 403: 정책/버킷 정책/KMS/Trust Policy를 의심
- 한참 잘 되다 403: STS 토큰 만료 및 갱신 실패(네트워크, 시간, SDK 체인 오염)를 의심
진단의 핵심은 “Pod 내부에서 IRSA 환경 확인”과 “CloudTrail에서 AssumeRoleWithWebIdentity 흐름 확인”입니다. 이 두 가지만 잡아도, 감으로 디버깅하던 시간이 로그 기반으로 줄어듭니다.