- Published on
S3 AccessDenied 403 급발생 - OAC·정책·KMS 30분 진단
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버나 애플리케이션 코드는 그대로인데, 어느 순간부터 S3 요청이 403 AccessDenied로 바뀌면 대부분은 권한 경로(Principal/Policy/KMS/조건/캐시) 중 하나가 바뀐 것입니다. 문제는 “어디가 바뀌었는지”를 감으로 찾기 시작하면 2~3시간이 순식간에 날아간다는 점입니다.
이 글은 CloudFront(OAC/OAI) + S3(버킷 정책/퍼블릭 액세스 차단) + KMS(SSE-KMS) 조합에서 흔히 터지는 403을 30분 안에 좁히는 체크리스트입니다. (특히 갑자기 터졌다는 전제: 배포/정책 변경/키 로테이션/캐시 만료 같은 이벤트가 있었을 확률이 높습니다.)
> 참고로 “403이 계속 난다”는 증상 자체는 WAF/봇 차단 같은 다른 레이어에서도 발생합니다. CloudFront 앞단에 WAF가 붙어 있다면 AWS WAF Bot Control 막힘으로 403 지속될 때도 함께 확인하세요.
0) 30분 타임박스 진단 플로우(결론부터)
아래 순서대로 보면, 대개 10~30분 안에 범인을 특정할 수 있습니다.
- 요청 경로 분리: 403이 S3 직접 호출인지, CloudFront 경유인지 분리
- CloudFront/OAC 확인: OAC 설정/원본 요청 정책/서명 전달 여부
- S3 버킷 정책 확인: Principal/Action/Resource/Condition(특히
AWS:SourceArn,AWS:SourceAccount) - S3 Public Access Block/ACL 확인: “갑자기”는 여기서도 많이 납니다
- SSE-KMS 여부 확인: KMS 키 정책 + CloudFront 서비스 프린시펄/역할 권한
- CloudTrail로 거짓말 잡기: 실제 Deny 주체(Policy vs KMS vs 조건 불일치)
- 캐시/배포 이슈: CloudFront 캐시된 403, 배포 미반영, 멀티 리전/멀티 계정 혼선
1) 먼저: 403이 “어디서” 나는지 분리하기
1-1. S3 직접 호출로 재현
S3 URL(가상 호스팅/경로 스타일)로 직접 접근해봅니다.
# 객체가 퍼블릭/프라이빗 여부와 무관하게, 현재 상태를 빠르게 확인
curl -I "https://YOUR_BUCKET.s3.amazonaws.com/path/to/object.jpg"
# 리전 엔드포인트를 명시(리다이렉트/서명 이슈를 줄임)
curl -I "https://YOUR_BUCKET.s3.ap-northeast-2.amazonaws.com/path/to/object.jpg"
- S3 직접도 403이면: 버킷 정책, 퍼블릭 액세스 차단, KMS, 객체 소유권/ACL 쪽이 유력
- S3 직접은 OK인데 CloudFront만 403이면: OAC/OAI, 원본 요청 정책, 서명 전달, CloudFront 설정이 유력
1-2. CloudFront 경유 요청으로 재현
curl -I "https://d123456abcdef8.cloudfront.net/path/to/object.jpg"
CloudFront 응답 헤더에 아래가 있으면 분기 힌트가 됩니다.
x-cache: Error from cloudfront→ 원본에서 에러 받음Via: 1.1 ... cloudfront/X-Amz-Cf-Id→ CloudFront 경유 확실
2) CloudFront OAC/OAI에서 갑자기 403 나는 대표 원인
OAC(Origin Access Control)는 OAI보다 “요즘 표준”이지만, 설정이 조금만 어긋나도 S3는 즉시 AccessDenied를 줍니다.
2-1. OAC이 붙었는데 버킷 정책이 OAI를 바라보는 경우
이전에는 OAI로 쓰다가 OAC로 바꿨는데, 버킷 정책이 그대로 OAI Canonical User ID 기반이면 CloudFront가 S3에 접근 못 합니다.
- OAI 기반 정책: Principal이
CanonicalUser형태 - OAC 기반 정책: Principal이
Service: cloudfront.amazonaws.com+Condition: AWS:SourceArn형태
2-2. OAC은 붙었는데 “서명(Signing)”이 꺼져 있거나 잘못된 경우
CloudFront 콘솔에서 OAC 설정에 보통 아래 옵션이 있습니다.
- Signing behavior:
Sign requests (recommended) - Signing protocol:
SigV4
여기서 서명이 안 붙으면 S3는 “익명 요청”으로 취급하고, 버킷이 프라이빗이면 403입니다.
2-3. CloudFront 배포/원본 요청 정책(Origin Request Policy) 문제
특정 헤더/쿼리스트링을 원본으로 전달해야 하는 구조(예: presigned URL, 커스텀 인증)라면 원본 요청 정책이 바뀌면서 403이 날 수 있습니다.
다만 OAC로 S3에 접근하는 정적 파일은 보통 추가 헤더 전달이 필요 없고, 대부분은 버킷 정책 조건 불일치가 원인입니다.
3) S3 버킷 정책: OAC 표준 템플릿부터 검증
OAC 사용 시, 가장 안전한 출발점은 아래 형태입니다.
3-1. OAC용 최소 버킷 정책 예시
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowCloudFrontServicePrincipalReadOnly",
"Effect": "Allow",
"Principal": { "Service": "cloudfront.amazonaws.com" },
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::YOUR_BUCKET/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::YOUR_ACCOUNT_ID:distribution/YOUR_DISTRIBUTION_ID"
}
}
}
]
}
여기서 “갑자기”를 만드는 포인트
- 배포를 새로 만들었는데
distribution ID가 바뀜 →AWS:SourceArn불일치로 즉시 403 - 멀티 계정/멀티 리전에서 Account ID 혼동 →
AWS:SourceArn또는SourceAccount불일치 Resource를arn:aws:s3:::bucket/path/*처럼 좁혀놨는데 실제 요청 경로가 바뀜
3-2. ListBucket가 필요한데 GetObject만 허용한 경우
웹에서 단일 객체 접근은 GetObject로 충분하지만, 어떤 SDK/라이브러리는 사전 확인으로 ListBucket을 호출하기도 합니다.
필요하다면 다음을 추가합니다.
{
"Sid": "AllowListBucket",
"Effect": "Allow",
"Principal": { "Service": "cloudfront.amazonaws.com" },
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::YOUR_BUCKET",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::YOUR_ACCOUNT_ID:distribution/YOUR_DISTRIBUTION_ID"
}
}
}
4) S3 Public Access Block / Object Ownership / ACL: “어제까지 됐는데 오늘 403”의 단골
4-1. Public Access Block가 켜지면서 ACL/퍼블릭 정책이 무력화
조직 단위(SCP)나 보안 가드레일로 다음이 바뀌는 경우가 많습니다.
BlockPublicAclsIgnorePublicAclsBlockPublicPolicyRestrictPublicBuckets
퍼블릭 객체를 ACL로 열어두던 레거시 구조면, 어느 날 갑자기 403이 될 수 있습니다.
확인:
aws s3api get-public-access-block --bucket YOUR_BUCKET
4-2. Object Ownership(Bucket owner enforced) 변경으로 ACL이 무시
Bucket owner enforced가 켜지면 ACL은 비활성화됩니다. 외부 계정이 업로드한 객체를 ACL로 공유하던 패턴이면 갑자기 접근이 막힙니다.
확인:
aws s3api get-bucket-ownership-controls --bucket YOUR_BUCKET
5) SSE-KMS가 걸린 S3: “버킷 정책은 맞는데도 403”의 핵심
S3 객체가 SSE-KMS로 암호화되어 있으면, s3:GetObject 권한만으로는 부족합니다. 복호화 과정에서 KMS 권한이 추가로 필요합니다.
5-1. 증상 패턴
- CloudFront → S3 원본 접근은 되는데 특정 객체만 403
- 동일 버킷 내에서도 SSE-S3는 OK, SSE-KMS만 403
- CloudTrail에서 S3는 AccessDenied, 혹은 KMS
Decrypt가 Denied
5-2. KMS 키 확인
aws s3api head-object --bucket YOUR_BUCKET --key path/to/object.jpg \
--query "ServerSideEncryption"
aws s3api head-object --bucket YOUR_BUCKET --key path/to/object.jpg \
--query "SSEKMSKeyId"
5-3. KMS 키 정책(또는 키에 연결된 권한)에서 빠지는 것들
CloudFront OAC로 S3에 접근하는 경우, 실제로 KMS를 호출하는 주체는 상황에 따라 다르게 보일 수 있습니다(서비스 연동/역할/요청 경로). 실무에서 가장 안전한 접근은:
- S3가 KMS를 사용할 수 있도록(S3 서비스가 해당 키를 통해 객체를 복호화할 수 있도록) 키 정책/권한을 구성
- 필요 시 조건으로 버킷/계정/암호화 컨텍스트를 제한해서 과도한 오픈을 피함
예시(개념 템플릿: 실제 환경에 맞게 최소화/조건 강화 필요):
{
"Sid": "AllowS3UseOfTheKeyForSpecificBucket",
"Effect": "Allow",
"Principal": { "Service": "s3.amazonaws.com" },
"Action": [
"kms:Decrypt",
"kms:Encrypt",
"kms:ReEncrypt*",
"kms:GenerateDataKey*",
"kms:DescribeKey"
],
"Resource": "*",
"Condition": {
"StringEquals": {
"aws:SourceAccount": "YOUR_ACCOUNT_ID"
},
"StringLike": {
"aws:SourceArn": "arn:aws:s3:::YOUR_BUCKET"
}
}
}
> 주의: KMS 조건 키/컨텍스트는 구성에 따라 달라질 수 있습니다. 가장 확실한 방법은 **CloudTrail에서 KMS 이벤트를 보고 “누가 어떤 Action을 Deny 당했는지”**를 확인한 뒤, 그 주체와 조건에 맞춰 최소 권한으로 조정하는 것입니다.
6) CloudTrail로 403의 “진짜 Deny 지점” 5분 만에 찾기
403을 눈으로만 보면 S3가 거절한 것처럼 보이지만, 실제로는:
- 버킷 정책의 조건 불일치
- 명시적 Deny(조직 SCP/Permission Boundary 포함)
- KMS Decrypt Deny
중 하나인 경우가 많습니다.
6-1. S3 데이터 이벤트를 켰다면 바로 필터
CloudTrail Lake/이벤트 히스토리에서:
- Event source:
s3.amazonaws.com - Event name:
GetObject - Error code:
AccessDenied
을 찾고, userIdentity와 requestParameters를 봅니다.
6-2. KMS 이벤트도 같이 확인
- Event source:
kms.amazonaws.com - Event name:
Decrypt - Error code:
AccessDenied
S3는 결국 “복호화 못 해서 실패”인데, 표면상은 S3 403으로 보이는 케이스가 있습니다.
7) CloudFront 캐시된 403 / 배포 미전파: 고쳤는데도 계속 403일 때
7-1. CloudFront가 403을 캐시할 수 있음
정책을 고쳤는데도 계속 403이면, CloudFront가 에러 응답을 캐시하고 있을 수 있습니다.
대응:
- 해당 경로 무효화(invalidation)
- 에러 캐싱 TTL 확인
aws cloudfront create-invalidation \
--distribution-id YOUR_DISTRIBUTION_ID \
--paths "/path/to/object.jpg" "/static/*"
7-2. 멀티 배포/멀티 오리진에서 “다른 배포”를 고친 경우
특히 스테이징/프로덕션 배포가 비슷한 도메인 구조를 가지면, 잘못된 Distribution을 수정하는 실수가 잦습니다.
- 실제 요청의
X-Amz-Cf-Id로 배포를 역추적 - 도메인의 CNAME이 어느 배포를 가리키는지 재확인
8) 실전: 30분 진단 체크리스트(복붙용)
아래를 위에서부터 체크하면 됩니다.
8-1. 1단계(5분): 재현/분리
- S3 직접
curl -I는 200/403? - CloudFront
curl -I는 403? - 특정 객체만 403인가, 전부 403인가?
8-2. 2단계(10분): OAC/버킷 정책
- CloudFront 원본이 S3이고 OAC가 연결되어 있는가?
- 버킷 정책 Principal이
cloudfront.amazonaws.com인가? -
AWS:SourceArn의 distribution ID가 정확한가? - Resource 경로가 실제 키 prefix와 일치하는가?
8-3. 3단계(10분): Public Access Block/Ownership/ACL
- Public Access Block 설정이 최근 바뀌지 않았나?
- Object Ownership가
Bucket owner enforced로 바뀌지 않았나? - ACL 기반 퍼블릭/공유 모델을 쓰고 있진 않나?
8-4. 4단계(5분~): KMS
- 객체가 SSE-KMS인가?
- KMS 키 정책에 필요한 주체가
Decrypt가능한가? - CloudTrail에서 KMS
AccessDenied가 보이는가?
9) 자주 하는 실수 6가지(원인별로 바로 잡기)
- OAI 정책을 OAC로 착각: OAC는 Service Principal + SourceArn 조건이 핵심
- 배포 새로 만들고 SourceArn 미갱신: “갑자기”의 1순위
- RestrictPublicBuckets 켜짐: 퍼블릭 버킷/ACL은 즉시 영향
- SSE-KMS를 켰는데 KMS 권한 미구성: 버킷 정책만으론 해결 안 됨
- CloudFront 403 캐시: 고쳤는데도 계속 403이면 무효화
- 다른 계정/다른 배포 수정: 멀티 환경에서 흔한 인적 오류
10) 마무리: “403은 권한 문제”가 아니라 “경로 문제”다
S3 AccessDenied 403은 결국 요청 주체(Principal)가 무엇이고, 어떤 정책/조건을 통과해야 하며, KMS 복호화까지 포함한 전체 경로가 열려 있는지의 문제입니다. 특히 OAC + SourceArn 조건은 안전하지만, 배포/계정/리소스가 조금만 바뀌어도 바로 403이 납니다.
장애 대응 관점에서는 “추측해서 정책을 더 열기”보다, CloudTrail로 Deny 지점을 먼저 특정하고 최소 변경으로 복구하는 게 가장 빠르고 안전합니다.
비슷한 방식으로 ‘에러를 원인별로 잘라서’ 체크리스트로 줄이는 접근이 필요하다면, 인프라 에러 대응 글로 AWS ALB 502·504 난사 - 원인별 해결 체크리스트도 함께 참고하면 문제 분리 감각을 잡는 데 도움이 됩니다.