- Published on
EKS IRSA에서 Pod AWS API 403? STS 진단법
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
EKS에서 IRSA(IAM Roles for Service Accounts)를 붙였는데 Pod가 AWS API를 호출하면 403 AccessDenied가 떨어지는 경우가 있습니다. 겉으로는 “권한이 없나?”처럼 보이지만, 실제로는 STS에서 역할을 제대로 Assume하지 못했거나, Assume은 됐는데 권한 정책/경계/리소스 정책에서 막힌 케이스가 대부분입니다.
이 글은 AssumeRoleWithWebIdentity 관점에서 IRSA 문제를 재현 가능하게 진단하는 순서를 제공합니다. 특히 403을 “AWS API 403”으로만 보지 말고, STS 단계의 실패인지, 서비스 API 단계의 실패인지를 분리하는 것이 핵심입니다.
관련해서 운영 중 자주 같이 터지는 장애 패턴은 Kubernetes CrashLoopBackOff 원인 7가지·즉시복구에서도 함께 참고하면 좋습니다. IRSA 실패로 애플리케이션이 부팅 단계에서 크래시 루프에 빠지는 경우가 많습니다.
1) 먼저 403의 “주체”를 분리하기
403은 크게 두 층에서 나옵니다.
- STS에서 403:
AssumeRoleWithWebIdentity자체가 거부됨 - 대상 서비스에서 403: STS는 성공했지만, 예를 들어 S3
GetObject가 거부됨
가장 먼저 해야 할 일은 실패한 API가 STS인지 확인하는 것입니다.
CloudTrail에서 STS 이벤트 확인
CloudTrail에서 이벤트 소스를 다음으로 필터링합니다.
eventSource:sts.amazonaws.comeventName:AssumeRoleWithWebIdentity
여기서 실패가 보이면 IRSA 연결(토큰, OIDC, Trust Policy) 문제일 확률이 높습니다. 반대로 STS 이벤트가 성공으로 찍히면, 이제는 권한 정책/리소스 정책/권한 경계/세션 정책을 봐야 합니다.
2) Pod 내부에서 “정말 IRSA 환경”인지 확인
IRSA는 결국 Pod 안에 다음 2가지가 있어야 합니다.
- 웹 아이덴티티 토큰 파일
- AWS SDK가 그 토큰을 읽어 STS로 Assume하도록 만드는 환경 변수
Pod에 들어가서 확인합니다.
kubectl -n <namespace> exec -it <pod> -- sh
# 필수 환경 변수
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 20 $AWS_WEB_IDENTITY_TOKEN_FILE; echo
AWS_WEB_IDENTITY_TOKEN_FILE가 비어있거나 파일이 없으면 IRSA가 제대로 주입되지 않은 것입니다.
서비스어카운트 어노테이션 확인
IRSA는 서비스어카운트에 eks.amazonaws.com/role-arn이 있어야 합니다.
kubectl -n <namespace> get sa <serviceaccount> -o yaml
다음이 있어야 합니다.
metadata.annotations.eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/YourRole
그리고 Pod가 정말 그 SA를 쓰는지도 확인합니다.
kubectl -n <namespace> get pod <pod> -o jsonpath='{.spec.serviceAccountName}'
echo
의외로 배포 YAML에서 serviceAccountName 누락으로 default SA를 쓰고 있는 경우가 매우 흔합니다.
3) OIDC Provider 연결 상태 점검
IRSA는 EKS 클러스터의 OIDC Issuer URL과 IAM OIDC Provider가 정확히 매칭되어야 합니다.
클러스터 OIDC Issuer 확인
aws eks describe-cluster \
--name <cluster-name> \
--query 'cluster.identity.oidc.issuer' \
--output text
출력 예시는 대략 다음 형태입니다.
https://oidc.eks.ap-northeast-2.amazonaws.com/id/XXXXXXXXXXXX
IAM OIDC Provider 존재 확인
aws iam list-open-id-connect-providers
# 특정 Provider 상세
aws iam get-open-id-connect-provider \
--open-id-connect-provider-arn <provider-arn>
여기서 Url이 위 Issuer에서 https://를 뺀 값과 동일해야 합니다.
- Issuer:
https://oidc.eks.../id/ABC - Provider Url:
oidc.eks.../id/ABC
불일치하면 STS에서 바로 거부됩니다.
4) IAM Role Trust Policy(신뢰 정책)에서 가장 많이 틀리는 지점
STS AssumeRoleWithWebIdentity는 Role의 Trust Policy 조건이 정확해야 통과합니다.
정상적인 Trust Policy 예시
아래는 가장 흔한 형태입니다. 특히 sub와 aud 조건이 핵심입니다.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789012:oidc-provider/oidc.eks.ap-northeast-2.amazonaws.com/id/ABCDEF"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"oidc.eks.ap-northeast-2.amazonaws.com/id/ABCDEF:sub": "system:serviceaccount:<namespace>:<serviceaccount>",
"oidc.eks.ap-northeast-2.amazonaws.com/id/ABCDEF:aud": "sts.amazonaws.com"
}
}
}
]
}
자주 발생하는 실수
sub의 네임스페이스 또는 SA 이름 오타- 클러스터를 재생성했는데 OIDC
id가 바뀌었고, Trust Policy는 예전 값을 참조 StringLike와StringEquals혼용으로 예상치 못한 매칭 실패aud누락 또는sts.amazonaws.com이 아닌 값
sub를 와일드카드로 넓히는 방식(system:serviceaccount:ns:*)은 편하지만 보안적으로 강하게 권장되진 않습니다. 최소 범위로 고정하는 게 안전합니다.
5) 토큰의 aud, sub, iss를 실제 값으로 검증하기
IRSA 토큰은 JWT 형태입니다. 토큰을 그대로 출력/로그로 남기면 보안 사고가 날 수 있으니, 로컬에서 최소한의 클레임만 확인하는 방식으로 접근하세요.
Pod 내부에서 다음처럼 디코딩할 수 있습니다(서명 검증이 아니라 클레임 확인 목적).
TOKEN_FILE="$AWS_WEB_IDENTITY_TOKEN_FILE"
JWT=$(cat "$TOKEN_FILE")
# payload만 base64url decode
python - <<'PY'
import os, json, base64
jwt = os.environ['JWT']
parts = jwt.split('.')
if len(parts) != 3:
raise SystemExit('not a jwt')
payload = parts[1] + '=='
payload = payload.replace('-', '+').replace('_', '/')
print(json.dumps(json.loads(base64.b64decode(payload)), indent=2))
PY
실행 시 환경 변수 주입이 필요합니다.
JWT="$JWT" python - <<'PY'
import os, json, base64
jwt = os.environ['JWT']
parts = jwt.split('.')
payload = parts[1] + '=='
payload = payload.replace('-', '+').replace('_', '/')
print(json.dumps(json.loads(base64.b64decode(payload)), indent=2))
PY
여기서 확인할 것:
iss가 클러스터 OIDC Issuer와 일치하는지aud가sts.amazonaws.com인지sub가system:serviceaccount:네임스페이스:서비스어카운트인지
이 값이 Trust Policy 조건과 1글자라도 다르면 STS가 거절합니다.
6) STS는 성공하는데 서비스 API에서 403이면: “권한” 레이어로 이동
CloudTrail에서 STS Assume이 성공인데도 403이면 이제는 IAM 정책/리소스 정책을 봐야 합니다.
6-1) 현재 호출 주체가 누구인지 확인
AWS SDK는 최종적으로 “내가 어떤 ARN으로 호출하고 있는지”를 GetCallerIdentity로 확인할 수 있습니다.
aws sts get-caller-identity
결과의 Arn이 다음과 유사해야 합니다.
arn:aws:sts::123456789012:assumed-role/YourRole/<session-name>
여기서 예상과 다른 Role이 보이면, IRSA가 아니라 다른 자격 증명(예: 노드 IAM Role, 이미지에 baked-in 키, 다른 환경 변수)을 사용 중일 수 있습니다.
6-2) 리소스 정책(S3/KMS/Secrets Manager 등) 확인
S3, KMS, Secrets Manager, SQS 등은 리소스 정책이 추가로 존재할 수 있습니다.
- S3 버킷 정책이 특정 Principal만 허용
- KMS 키 정책에서 해당 Role을 허용하지 않음
이 경우 IAM Role에 Allow가 있어도 리소스 정책에서 거부될 수 있습니다.
6-3) Permission Boundary / SCP / Session Policy
다음이 걸려 있으면 “정책은 있는데 왜 403?”이 발생합니다.
- IAM Permission Boundary
- AWS Organizations SCP
- STS 세션 정책(assume 시 추가 제한)
특히 조직 단위 SCP는 애플리케이션 팀이 인지 못한 상태로 적용되어 있는 경우가 많습니다.
7) EKS Pod Identity Webhook / 주입 실패 케이스
IRSA는 일반적으로 Pod에 토큰 볼륨과 환경 변수를 주입해야 합니다. 이 주입이 실패하면 AWS_WEB_IDENTITY_TOKEN_FILE 자체가 없을 수 있습니다.
확인 포인트:
kubectl describe pod <pod>에서 볼륨/환경 변수 주입 흔적- Mutating webhook 동작 여부
클러스터 애드온/웹훅 장애는 노드 네트워크(CNI) 문제와 함께 나타나기도 합니다. 네트워크/애드온 장애가 의심되면 K8s NodeNotReady - CNI 플러그인 장애 복구 가이드도 같이 점검하세요.
8) 실전 체크리스트: 10분 안에 원인 좁히기
아래 순서대로 보면 대부분 10분 내로 범위가 줄어듭니다.
- CloudTrail에서
AssumeRoleWithWebIdentity실패/성공 확인 - Pod 내부에서
AWS_ROLE_ARN,AWS_WEB_IDENTITY_TOKEN_FILE존재 확인 - Pod가 올바른 SA를 쓰는지 확인(
serviceAccountName) - SA 어노테이션에 Role ARN이 정확한지 확인
- EKS OIDC Issuer와 IAM OIDC Provider URL 매칭 확인
- Role Trust Policy의
sub,aud조건이 정확한지 확인 - STS 성공이면
aws sts get-caller-identity로 실제 주체 확인 - 서비스별 리소스 정책(S3/KMS 등)과 Boundary/SCP 확인
9) 재현 가능한 최소 예제: S3 ListBucket로 검증하기
애플리케이션 코드에서 복잡한 SDK 호출로 확인하면 변수가 많습니다. IRSA 검증용으로는 단순한 aws-cli 컨테이너를 띄워 sts와 s3를 순서대로 확인하는 게 좋습니다.
테스트용 Pod 예시
아래 YAML에서 serviceAccountName만 바꿔서 테스트합니다.
apiVersion: v1
kind: Pod
metadata:
name: irsa-debug
namespace: default
spec:
serviceAccountName: <serviceaccount>
containers:
- name: awscli
image: public.ecr.aws/aws-cli/aws-cli:2.15.0
command: ["sh", "-c", "sleep 36000"]
실행 후:
kubectl -n default exec -it irsa-debug -- sh
aws sts get-caller-identity
aws s3 ls s3://<bucket-name>
get-caller-identity가 실패하면 IRSA/STS 문제get-caller-identity는 성공인데 S3가403이면 S3 권한/버킷 정책/KMS 문제
10) 마무리: 403을 “STS 단계”로 되돌려 생각하기
IRSA에서 403이 나오면 많은 경우 애플리케이션 로그에는 단순히 “AccessDenied”만 남습니다. 하지만 실제 분기점은 거의 항상 다음 둘 중 하나입니다.
- STS
AssumeRoleWithWebIdentity가 Trust Policy/OIDC/토큰 클레임 불일치로 실패 - STS는 성공했지만 서비스 API 권한 또는 리소스 정책(KMS/S3 등)에서 거부
운영 관점에서는 CloudTrail로 STS 성공 여부를 먼저 판정하고, Pod 내부에서 토큰 주입과 SA 매핑을 확인한 뒤, 마지막으로 정책 레이어로 이동하는 것이 가장 빠른 루트입니다.
추가로 IRSA 문제로 애플리케이션이 시작 단계에서 종료되며 배포가 불안정해졌다면, 크래시 루프 관점의 즉시 복구 루틴은 Kubernetes CrashLoopBackOff 원인 7가지·즉시복구도 함께 참고하세요.