- Published on
AWS S3 AccessDenied 403 - 정책·KMS·VPCE 점검
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버/배치/애플리케이션에서 S3 호출이 갑자기 403 AccessDenied로 떨어지면 대부분은 “IAM 권한이 없나?”로 시작하지만, 실제 현장에서는 버킷 정책의 명시적 Deny, SSE-KMS의 키 정책/그랜트, VPC 엔드포인트(S3 Gateway/Interface)의 정책·라우팅 같은 네트워크/암호화 계층에서 막히는 경우가 더 까다롭습니다. 더 골치 아픈 점은, S3는 동일한 403이라도 원인이 다르면 로그/CloudTrail/에러 메시지의 단서가 달라서 진단 순서가 중요합니다.
이 글은 “지금 당장 403을 풀어야 하는” 상황에서, 원인을 빠르게 좁히는 체크리스트를 버킷 정책 → SSE-KMS → VPC 엔드포인트 중심으로 정리합니다. (EKS/사설망 환경에서 특히 자주 터집니다. 네트워크 계층 진단 패턴은 EKS Pod는 뜨는데 트래픽 0 - NetPol·SG·CNI 10분 진단도 함께 참고하면 좋습니다.)
1) 403 AccessDenied를 “어떤 403인지” 먼저 분류하기
1-1. 클라이언트 에러 메시지에서 단서 뽑기
S3는 SDK/CLI에서 보통 이런 식으로 보입니다.
An error occurred (AccessDenied) when calling the GetObject operation: Access DeniedAccessDenied: Access Denied(boto3)Forbidden(프록시/게이트웨이 경유)
여기서 중요한 건 **어떤 API가 막혔는지(GetObject/PutObject/ListBucket/HeadObject 등)**입니다. 예를 들어:
ListBucket403 →s3:ListBucket(버킷 ARN) 권한/조건 문제GetObject403 →s3:GetObject(오브젝트 ARN) + (SSE-KMS면)kms:Decrypt문제 가능PutObject403 →s3:PutObject+ (SSE-KMS면)kms:Encrypt/kms:GenerateDataKey문제 가능
1-2. CloudTrail에서 “거부 주체” 확인
가장 빠른 방법은 CloudTrail Event history에서 해당 시간대 S3 이벤트를 보고, errorCode=AccessDenied와 함께:
userIdentity(누가 호출했는지: IAM Role/AssumedRole/IRSA)eventName(GetObject/PutObject 등)resources(버킷/키)additionalEventData/requestParameters
를 확인하는 것입니다.
CLI로도 좁힐 수 있습니다(계정에 따라 조회 범위 제한 가능).
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=EventName,AttributeValue=GetObject \
--max-results 20
> 팁: EKS라면 IRSA 관련 AssumeRole 단계부터 막히는 케이스도 있어, S3만 보지 말고 STS 이벤트도 같이 확인하세요. IRSA 타임아웃/인증 이슈는 EKS IRSA에서 AssumeRoleWithWebIdentity 0s 타임아웃 해결 패턴과도 연결됩니다.
2) 버킷 정책(Bucket Policy): “Allow가 있어도 Deny가 이긴다”
S3 권한 문제의 절반은 명시적 Deny입니다. IAM 정책에 Allow가 있어도, 아래 중 하나가 있으면 403이 납니다.
- 버킷 정책에
Deny가 존재 - SCP(Organizations)에서 거부
- Permission Boundary로 제한
- Object ACL/Ownership 설정과 충돌
여기서는 버킷 정책을 중심으로 봅니다.
2-1. 가장 흔한 Deny 패턴 3가지
(1) TLS 강제 Deny
{
"Sid": "DenyInsecureTransport",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::my-bucket",
"arn:aws:s3:::my-bucket/*"
],
"Condition": {
"Bool": {"aws:SecureTransport": "false"}
}
}
- 프라이빗 네트워크/프록시에서 의도치 않게 HTTP로 나가면 403
- ALB/프록시/SDK 설정 점검 필요
(2) 특정 VPC 엔드포인트만 허용 (aws:sourceVpce)
{
"Sid": "DenyIfNotFromVPCE",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::my-bucket",
"arn:aws:s3:::my-bucket/*"
],
"Condition": {
"StringNotEquals": {"aws:sourceVpce": "vpce-0123456789abcdef0"}
}
}
- VPC 밖(로컬/다른 VPC/다른 VPCE)에서 접근하면 무조건 403
- 같은 VPC라도 라우팅이 인터넷/NAT로 빠지면
sourceVpce가 매칭되지 않습니다.
(3) 조직/계정 제한 (aws:PrincipalOrgID / aws:PrincipalAccount)
{
"Sid": "DenyOutsideOrg",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::my-bucket",
"arn:aws:s3:::my-bucket/*"
],
"Condition": {
"StringNotEquals": {"aws:PrincipalOrgID": "o-xxxxxxx"}
}
}
- 크로스어카운트 구성에서 특히 자주 발생
2-2. ListBucket vs GetObject: 리소스 ARN이 다르다
ListBucket은 버킷 ARN에 권한이 있어야 하고, GetObject/PutObject는 오브젝트 ARN에 권한이 있어야 합니다.
{
"Effect": "Allow",
"Action": ["s3:ListBucket"],
"Resource": "arn:aws:s3:::my-bucket"
}
{
"Effect": "Allow",
"Action": ["s3:GetObject"],
"Resource": "arn:aws:s3:::my-bucket/*"
}
aws s3 ls s3://my-bucket/는 되는데 특정 키만 403이면 오브젝트 레벨 정책/암호화(KMS)/소유권(ACL) 쪽을 의심해야 합니다.
2-3. S3 Object Ownership(ACL 비활성화)와 교차 계정 업로드
버킷이 Bucket owner enforced(ACL 비활성화)인데, 업로드 주체가 다른 계정/역할이면 소유권/권한 모델이 단순해지는 대신, 정책으로 명확히 열어줘야 합니다. 반대로 ACL 기반으로 억지로 풀려던 구성이 깨질 수 있습니다.
점검:
aws s3api get-bucket-ownership-controls --bucket my-bucket
3) SSE-KMS: S3 권한이 있어도 KMS에서 403이 난다
SSE-KMS(aws:kms)를 쓰면, S3 API는 통과해도 KMS 키 권한이 없어서 최종적으로 403/AccessDenied가 발생합니다. 특히 GetObject 시 Decrypt, PutObject 시 Encrypt/GenerateDataKey가 필요합니다.
3-1. 증상 패턴
- 같은 버킷의 SSE-S3(AES256) 객체는 잘 읽히는데, SSE-KMS 객체만 403
- CloudTrail에서 S3 이벤트는 AccessDenied인데, 동시에 KMS 이벤트에서도 Deny 흔적
3-2. 필요한 KMS 권한(최소 집합)
읽기(GetObject) 기준으로는 보통 아래가 필요합니다.
kms:Decrypt- (상황에 따라)
kms:DescribeKey
쓰기(PutObject) 기준:
kms:Encryptkms:GenerateDataKeykms:DescribeKey
중요: IAM 정책에 KMS 권한을 줘도, KMS Key policy가 허용하지 않으면 실패합니다. KMS는 “키 정책이 최종 관문”인 경우가 많습니다.
3-3. Key policy에서 S3 서비스/역할 허용 확인
대표적으로 같은 계정 내 역할이 접근해야 한다면, 키 정책에 해당 Role ARN이 들어가거나, 계정 루트에 위임(그리고 IAM에서 제어)하는 형태여야 합니다.
예시(개념용, 운영에서는 최소 권한/조건 추가 권장):
{
"Sid": "AllowUseOfKeyForAppRole",
"Effect": "Allow",
"Principal": {"AWS": "arn:aws:iam::123456789012:role/my-app-role"},
"Action": [
"kms:Decrypt",
"kms:Encrypt",
"kms:GenerateDataKey",
"kms:DescribeKey"
],
"Resource": "*"
}
3-4. KMS 조건으로 S3 경유만 허용하는 경우(서비스 제약)
보안을 위해 kms:ViaService 또는 encryption context 조건을 걸어두면, “직접 KMS 호출은 금지, S3를 통해서만 허용” 같은 정책이 됩니다. 이때 리전 불일치나 서비스 경유 조건이 맞지 않으면 403이 납니다.
점검 포인트:
- 버킷 리전과 KMS 키 리전이 동일한지
kms:ViaService가s3.<region>.amazonaws.com로 맞는지
3-5. 빠른 확인 커맨드
객체의 KMS 사용 여부 확인:
aws s3api head-object --bucket my-bucket --key path/to/object.json
출력에 아래가 보이면 SSE-KMS입니다.
ServerSideEncryption: "aws:kms"SSEKMSKeyId: "..."
4) VPC 엔드포인트(S3 VPCE): 정책, 라우팅, DNS가 403을 만든다
사설망에서 S3 접근을 강제하면 흔히 **Gateway Endpoint(com.amazonaws.<region>.s3)**를 씁니다. 여기서 403은 두 갈래로 나뉩니다.
- 버킷 정책이 VPCE를 강제하는데, 트래픽이 VPCE로 안 나감 → 403
- VPCE Endpoint policy가 더 좁게 막음 → 403
4-1. 라우팅 테이블에서 S3 PrefixList가 VPCE로 가는지
Gateway Endpoint는 라우팅 테이블에 S3 PrefixList 경로가 있어야 합니다.
점검 순서:
- 해당 서브넷이 연결된 Route Table 확인
pl-xxxx(S3 PrefixList) 대상이vpce-xxxx인지 확인
CLI 예(개념):
aws ec2 describe-route-tables --route-table-ids rtb-xxxxxxxx
4-2. Endpoint policy가 S3 액션/리소스를 제한하는지
VPCE 정책이 s3:* 전체를 허용하지 않고 특정 버킷/프리픽스만 허용하면, 그 외 요청은 403이 납니다.
예시(특정 버킷만 허용):
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": "*",
"Action": ["s3:GetObject"],
"Resource": "arn:aws:s3:::my-bucket/*"
}
]
}
여기서 ListBucket이 필요하면 버킷 ARN도 추가해야 합니다.
4-3. 버킷 정책의 sourceVpce와 “실제 VPCE” 불일치
멀티 VPC/멀티 엔드포인트(예: dev/prod) 환경에서 버킷 정책에 aws:sourceVpce를 하나만 넣어두고, 다른 엔드포인트에서 접근하면 403이 납니다.
해결은 보통 둘 중 하나입니다.
- 버킷 정책에 허용할 VPCE를 복수로 등록
- 환경별로 버킷을 분리
4-4. Private DNS/Interface Endpoint 혼동
S3는 일반적으로 Gateway Endpoint를 쓰지만, 특정 요구(예: 온프레에서 PrivateLink, 혹은 특정 서비스 조합)로 Interface Endpoint를 붙이는 경우도 있습니다. 이때 DNS 해석/프록시 경유로 경로가 달라져 aws:sourceVpce 조건이 깨질 수 있습니다.
5) “빠르게” 원인을 좁히는 실전 체크리스트(10분 루틴)
아래 순서로 보면 불필요한 삽질을 줄일 수 있습니다.
5-1. 호출 주체 확정
- 애플리케이션이 실제로 사용하는 Role ARN 확인(AssumedRole 세션명 포함)
- EKS라면 ServiceAccount ↔ Role 매핑(IRSA) 확인
5-2. 어떤 API가 403인지 확인
ListBucket인지GetObject인지에 따라 정책 리소스가 다름
5-3. CloudTrail로 “Deny 지점” 찾기
- S3 이벤트: 어떤 버킷/키/조건에서 거부?
- KMS 이벤트: Decrypt/Encrypt 거부 흔적?
5-4. 버킷 정책에서 Deny 조건부터 검색
aws:SecureTransportaws:sourceVpceaws:PrincipalOrgID/aws:PrincipalAccount- IP 제한(
aws:SourceIp)이 NAT/프록시로 바뀌며 불일치하는지
5-5. SSE-KMS면 KMS 키 정책/권한 확인
- 객체가 SSE-KMS인지(
head-object) - 해당 Role이 키 정책에서 허용되는지
kms:ViaService/리전 불일치 없는지
5-6. VPCE 강제 환경이면 라우팅/엔드포인트 정책 확인
- Route Table에 S3 PrefixList → VPCE 라우트 존재?
- VPCE policy가 필요한 액션/리소스를 포함?
6) 재현/검증용 커맨드 모음
6-1. 동일 자격증명으로 S3 API별 테스트
# 버킷 목록(버킷 레벨)
aws s3api list-objects-v2 --bucket my-bucket --max-keys 1
# 객체 헤더(권한/암호화 단서)
aws s3api head-object --bucket my-bucket --key path/to/object
# 다운로드(GetObject)
aws s3 cp s3://my-bucket/path/to/object /tmp/object
6-2. 요청이 VPCE를 타는지 간접 확인(조건 기반)
버킷 정책에 aws:sourceVpce 강제가 걸려 있다면,
- VPCE 경유: 성공
- NAT/인터넷 경유: 403
즉, 동일 자격증명으로 VPC 내부/외부에서 테스트하면 네트워크 경로 문제인지 빠르게 분리됩니다.
6-3. KMS 키 메타 확인
aws kms describe-key --key-id arn:aws:kms:ap-northeast-2:123456789012:key/....
> Disabled 상태거나, 다른 리전 키를 참조하면 의외로 단순한데 찾기 어렵습니다.
7) 운영에서 자주 하는 실수와 예방책
7-1. “S3는 열었는데 KMS를 안 열었다”
SSE-KMS는 S3 권한만으로 끝나지 않습니다. S3 정책 변경 시 KMS 키 정책도 같이 코드로 관리(Terraform/CloudFormation)하는 게 안전합니다.
7-2. 버킷 정책에 Deny를 먼저 넣고 예외를 나중에 추가
Deny 우선순위 때문에 예외 Allow가 먹지 않는 형태가 자주 나옵니다. Deny 조건을 쓸 때는:
- 조건 키가 실제 트래픽에서 항상 기대값을 갖는지
- 멀티 VPCE/멀티 계정/멀티 리전 확장 시 깨지지 않는지
를 먼저 검증하세요.
7-3. VPCE 정책을 최소화하다가 필요한 API를 누락
예를 들어 앱은 GetObject만 한다고 생각했는데, SDK가 내부적으로 HeadObject를 먼저 치거나, 특정 라이브러리가 ListBucket을 호출하는 경우가 있습니다. 403이 특정 라이브러리/버전에서만 나면 이 케이스를 의심하세요.
네트워크 경유/타임아웃과 섞여 보이면, 장애가 403이 아니라 5xx/timeout로도 확장될 수 있습니다. 경계면 장애 대응 패턴은 AWS ALB 502·504 난사 - 원인별 해결 체크리스트도 함께 보면 좋습니다.
마무리
S3 403 AccessDenied는 “권한 하나 추가”로 끝나는 문제처럼 보여도, 실제로는 (1) 버킷 정책의 Deny/조건, (2) SSE-KMS의 키 정책/권한, (3) VPC 엔드포인트 정책·라우팅의 3축에서 가장 많이 발생합니다.
가장 효율적인 접근은:
- CloudTrail로 호출 주체/막힌 API를 확정하고,
- 버킷 정책의 Deny 조건을 먼저 제거/수정 후보로 좁힌 뒤,
- SSE-KMS라면 KMS 키 정책까지 함께 검증하고,
- VPCE 강제 환경이면 라우팅과 VPCE policy까지 확인
하는 순서입니다. 이 루틴만 잡아도 “왜 403인지 감으로 때려맞추는” 시간을 크게 줄일 수 있습니다.