- Published on
EKS Pod에서 S3 403 AccessDenied 원인 10가지
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버리스나 EC2에서 잘 되던 S3 접근이 EKS Pod 안으로 들어오면 갑자기 403 AccessDenied가 터지는 경우가 많습니다. 이유는 단순히 “권한이 없다”가 아니라, Pod → (IRSA/SDK) → STS → S3로 이어지는 경로에서 정책·조건·네트워크·암호화 설정이 조금만 어긋나도 동일한 403으로 수렴하기 때문입니다.
이 글은 “원인 후보를 빠르게 좁히는 것”을 목표로, 현장에서 가장 자주 만나는 원인 10가지와 각 항목별 확인 방법/수정 포인트/예시 코드를 제공합니다. IRSA 자체가 의심된다면 함께 읽을 만한 글로 EKS OIDC Thumbprint 변경 후 IRSA 403 복구도 참고하세요.
먼저: Pod 안에서 진단에 필요한 최소 정보 수집
1) 실제로 어떤 자격 증명을 쓰는지 확인
IRSA(웹 아이덴티티)인지, 노드 IAM Role(Instance Profile)인지부터 갈라야 합니다.
# Pod 쉘 진입
kubectl exec -it deploy/myapp -- sh
# IRSA를 쓰면 보통 이 환경변수가 존재
env | egrep 'AWS_(ROLE_ARN|WEB_IDENTITY_TOKEN_FILE|REGION|DEFAULT_REGION)'
# AWS SDK/CLI가 있으면 현재 호출 주체 확인
aws sts get-caller-identity
get-caller-identity가 원하던 Role이 아니면, 이후 모든 S3 403은 “정책이 틀렸다”가 아니라 “다른 주체가 호출한다”가 됩니다.
2) S3 에러가 진짜로 AccessDenied인지, KMS/조건부 거부인지 확인
가능하면 CloudTrail에서 eventName과 errorCode, additionalEventData를 봅니다.
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=EventName,AttributeValue=GetObject \
--max-results 20
GetObject는 403인데 CloudTrail에kms:Decrypt실패가 같이 보이면, 실질 원인은 KMS일 확률이 큽니다(아래 6번).
원인 1) IRSA 미적용(서비스어카운트가 다름/누락)
가장 흔합니다. Deployment는 IRSA가 붙은 ServiceAccount를 쓴다고 생각했는데, 실제 Pod는 default ServiceAccount를 쓰거나, Helm/ArgoCD 템플릿에서 SA가 덮이는 케이스입니다.
확인
kubectl get pod -l app=myapp -o jsonpath='{.items[0].spec.serviceAccountName}{"\n"}'
kubectl get sa myapp-sa -o yaml | sed -n '1,120p'
- ServiceAccount에 아래 어노테이션이 있어야 합니다.
apiVersion: v1
kind: ServiceAccount
metadata:
name: myapp-sa
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/myapp-irsa-role
해결
- Deployment/StatefulSet에
serviceAccountName: myapp-sa명시 - Helm values에서
serviceAccount.create,serviceAccount.name가 의도대로 적용되는지 확인
원인 2) IRSA Trust Policy(AssumeRoleWithWebIdentity) 조건 불일치
IRSA Role의 Trust Policy에서 sub(ServiceAccount) 또는 aud 조건이 실제 토큰과 다르면 STS AssumeRole이 실패하거나, SDK가 다른 자격 증명으로 폴백하여 결과적으로 S3 403이 납니다.
확인
aws iam get-role --role-name myapp-irsa-role \
--query 'Role.AssumeRolePolicyDocument' --output json
정상 예시(핵심은 sub):
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {"Federated": "arn:aws:iam::123456789012:oidc-provider/oidc.eks.ap-northeast-2.amazonaws.com/id/XXXX"},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"oidc.eks.ap-northeast-2.amazonaws.com/id/XXXX:aud": "sts.amazonaws.com",
"oidc.eks.ap-northeast-2.amazonaws.com/id/XXXX:sub": "system:serviceaccount:prod:myapp-sa"
}
}
}
]
}
해결
- 네임스페이스/서비스어카운트 이름 변경 시 Trust Policy도 함께 수정
- OIDC Provider가 바뀌거나 thumbprint 이슈가 있으면 EKS OIDC Thumbprint 변경 후 IRSA 403 복구 참고
원인 3) S3 버킷 정책에서 명시적 Deny(조건부 거부)
S3는 명시적 Deny가 Allow를 이깁니다. 조직 보안 정책으로 자주 들어가는 조건들(예: TLS 강제, 특정 VPC 엔드포인트 강제, 특정 Principal만 허용 등)이 Pod 트래픽과 충돌하면 403이 납니다.
흔한 Deny 패턴
aws:SecureTransport가false면 Denyaws:SourceVpce가 특정 VPCE가 아니면 Denyaws:PrincipalArn/aws:PrincipalOrgID조건 불일치
확인
aws s3api get-bucket-policy --bucket my-bucket --query Policy --output text | jq .
해결 예시: VPCE 강제 정책에서 EKS 경로가 빠진 경우
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyUnlessFromVPCE",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": ["arn:aws:s3:::my-bucket", "arn:aws:s3:::my-bucket/*"],
"Condition": {
"StringNotEquals": {"aws:SourceVpce": "vpce-0abc1234def567890"}
}
}
]
}
- EKS Pod가 NAT를 통해 공용 S3로 나가면
SourceVpce가 만족되지 않습니다. 이 경우 7번(VPC 엔드포인트)과 함께 봐야 합니다.
원인 4) IAM 정책 리소스 ARN 범위 실수(버킷 vs 오브젝트)
ListBucket은 버킷 ARN, GetObject/PutObject는 오브젝트 ARN이 필요합니다. 둘 중 하나만 넣어두면 상황에 따라 403이 납니다.
잘못된 예
s3:GetObject에arn:aws:s3:::my-bucket만 지정
올바른 최소 예시
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:ListBucket"],
"Resource": "arn:aws:s3:::my-bucket"
},
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": "arn:aws:s3:::my-bucket/*"
}
]
}
확인
aws iam simulate-principal-policy \
--policy-source-arn arn:aws:iam::123456789012:role/myapp-irsa-role \
--action-names s3:GetObject \
--resource-arns arn:aws:s3:::my-bucket/path/to/file
원인 5) 요청이 다른 계정/다른 Role로 나감(자격 증명 체인/ENV 오염)
컨테이너 이미지에 남아있는 ~/.aws/credentials, CI에서 주입한 AWS_ACCESS_KEY_ID, 또는 SDK의 credential provider chain 때문에 IRSA가 무시되는 케이스가 있습니다.
확인
env | egrep 'AWS_ACCESS_KEY_ID|AWS_SECRET_ACCESS_KEY|AWS_PROFILE'
ls -al ~/.aws || true
aws sts get-caller-identity
해결
- Pod에 정적 키를 주입하지 않기(가능하면 금지)
AWS_SDK_LOAD_CONFIG=1등 설정이 의도치 않게 profile을 읽지 않는지 점검- 애플리케이션이 명시적으로 특정 credential provider를 고정했는지 확인
원인 6) SSE-KMS 객체인데 kms:Decrypt 권한/키 정책 누락
S3 GetObject는 허용인데, 객체가 SSE-KMS로 암호화되어 있으면 복호화를 위해 KMS 권한이 추가로 필요합니다. 이때도 흔히 403으로 보입니다.
확인 포인트
- 객체/버킷 기본 암호화가 SSE-KMS인지
- KMS Key policy에 해당 Role이 들어있는지
- IAM policy에
kms:Decrypt가 있는지
aws s3api get-bucket-encryption --bucket my-bucket
# KMS 키 정책 확인
aws kms get-key-policy --key-id alias/my-key --policy-name default
최소 권한 예시
{
"Effect": "Allow",
"Action": ["kms:Decrypt", "kms:Encrypt", "kms:GenerateDataKey"],
"Resource": "arn:aws:kms:ap-northeast-2:123456789012:key/...."
}
IRSA는 정상인데 KMS에서만 403이 난다면 EKS IRSA는 되는데 KMS Decrypt 403 해결법이 가장 직접적인 체크리스트입니다.
원인 7) S3 Gateway/Interface VPC Endpoint 강제 조건과 EKS egress 경로 불일치
보안상 “반드시 VPC Endpoint로만 S3 접근”을 강제하는 경우가 많습니다. 하지만 EKS Pod egress가 NAT GW로 빠지면 aws:SourceVpce 조건을 만족하지 못해 403이 납니다.
확인
- 버킷 정책에
aws:SourceVpce조건이 있는지(3번) - VPC에 S3 Gateway Endpoint가 라우팅 테이블에 연결돼 있는지
- Pod가 붙은 서브넷 라우팅이 Endpoint를 타는지
# VPC 엔드포인트 확인
aws ec2 describe-vpc-endpoints --filters Name=service-name,Values=com.amazonaws.ap-northeast-2.s3
# 라우팅 테이블에 prefix list로 S3가 잡혀있는지 확인(콘솔/CLI)
네트워크가 간헐적으로 바뀌거나 SNAT/NAT 경로가 의심되면 EKS Pod egress 간헐 끊김 - SNAT·NAT GW 추적법처럼 egress 경로를 먼저 고정/가시화하는 것이 진단 시간을 줄입니다.
원인 8) 잘못된 리전/엔드포인트로 요청(리다이렉트/서명 불일치 후 403)
버킷 리전과 다른 리전으로 서명해 요청하면, SDK는 보통 리다이렉트를 따라가지만 일부 구성(프록시, 커스텀 엔드포인트, 서명 버전)에서는 AccessDenied 또는 유사한 403으로 보일 수 있습니다.
확인
aws s3api get-bucket-location --bucket my-bucket
# 앱/Pod의 리전 환경변수 확인
env | egrep 'AWS_REGION|AWS_DEFAULT_REGION'
해결
AWS_REGION을 버킷 리전과 일치- S3 클라이언트 생성 시 region 명시
(Python boto3) 예시
import boto3
s3 = boto3.client("s3", region_name="ap-northeast-2")
print(s3.get_object(Bucket="my-bucket", Key="path/to/file")['ContentType'])
원인 9) S3 Object Ownership/ACL 충돌(다른 계정 업로드 객체)
다른 AWS 계정이 업로드한 객체를 ACL로 제어하던 레거시 환경에서는, 버킷 정책이 있어도 객체 소유권/ACL 때문에 접근이 막히는 케이스가 있습니다. 최근에는 Object Ownership = Bucket owner enforced로 정리하는 편이 안전합니다.
확인
- 객체 소유 계정이 다른지
- 버킷의 Object Ownership 설정
- ACL 기반 접근이 남아있는지
aws s3api get-bucket-ownership-controls --bucket my-bucket
aws s3api get-object-acl --bucket my-bucket --key path/to/file
해결 방향
- 가능하면 Bucket owner enforced로 전환하고 ACL 사용 중단
- 교차 계정 업로드라면 업로드 계정에서 bucket-owner-full-control ACL 부여(전환 전 과도기)
원인 10) S3 Access Point / MRAP / 조건 키 사용 실수
S3 Access Point(또는 Multi-Region Access Point)를 쓰면 ARN과 조건 키가 달라집니다. 버킷 ARN 기준으로만 정책을 작성해두면 Access Point 경유 요청이 403이 날 수 있습니다.
확인
- 앱이
myaccesspoint-123456789012.s3-accesspoint...같은 엔드포인트를 쓰는지 - 정책 리소스가
arn:aws:s3:region:account:accesspoint/...형태인지
aws s3control list-access-points --account-id 123456789012
해결
- Access Point를 사용한다면 해당 Access Point 정책과 IAM 정책 리소스를 Access Point ARN 기준으로 재작성
빠른 체크리스트(현장용)
아래 순서로 보면 보통 10~20분 내로 원인 범위를 크게 줄일 수 있습니다.
- Pod에서
aws sts get-caller-identity로 호출 주체(Role) 확인 - ServiceAccount 어노테이션과 Deployment의
serviceAccountName일치 확인(1번) - IRSA Role Trust Policy의
sub/aud확인(2번) - CloudTrail에서 S3 이벤트와 함께 KMS 실패가 있는지 확인(6번)
- IAM policy에서
ListBucket/GetObject리소스 ARN 범위 확인(4번) - 버킷 정책의 Deny 조건(
SourceVpce,SecureTransport,PrincipalArn) 확인(3번) - VPC Endpoint 강제 시 EKS egress 경로가 VPCE를 타는지 확인(7번)
- 리전/엔드포인트 설정 확인(8번)
- 교차 계정/ACL/Ownership 확인(9번)
- Access Point/MRAP 사용 여부 확인(10번)
마무리: “403”은 하나지만, 경로는 여러 갈래
EKS Pod에서 S3 403은 크게 (A) 누가 호출하나(IRSA/자격증명), (B) 무엇이 거부하나(IAM/버킷정책/명시적 Deny), **(C) 뒤에서 또 막히나(KMS/VPCE/리전/소유권)**로 나눠 보면 빠르게 풀립니다.
특히 IRSA 관련 403은 원인이 비슷해 보여도 OIDC/Thumbprint/Trust Policy 등 레이어가 다르니, IRSA 자체가 불안정하거나 최근 클러스터/OIDC 설정 변경이 있었다면 EKS OIDC Thumbprint 변경 후 IRSA 403 복구와 KMS 연동 이슈라면 EKS IRSA는 되는데 KMS Decrypt 403 해결법을 함께 점검하는 것을 권장합니다.