- Published on
CloudFront 403 AccessDenied 원인 8가지와 해결
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
CloudFront를 운영하다 보면 가장 자주 마주치는 에러 중 하나가 403 AccessDenied입니다. 문제는 같은 403이라도 원인이 매우 다양하다는 점입니다. 오리진이 S3인지, ALB인지, 혹은 Lambda@Edge/CloudFront Functions를 쓰는지에 따라 진단 루트가 완전히 달라집니다.
이 글에서는 실무에서 자주 터지는 CloudFront 403 AccessDenied 원인 8가지를 "증상"과 "확인 방법", "해결"로 나눠 정리합니다. 마지막에는 빠른 트러블슈팅 체크리스트와 로그 기반 확인 방법도 제공합니다.
참고로 인증/검증 실패 패턴은 OAuth에서도 유사하게 나타납니다. 원인 분류 방식이 도움이 된다면 OAuth PKCE invalid_grant 검증 실패 8가지 원인도 함께 보면 좋습니다.
먼저: 403이 CloudFront에서 난 건지 오리진에서 난 건지 구분
403은 크게 두 갈래로 나뉩니다.
- CloudFront가 자체적으로 거절: WAF, 서명 URL/쿠키, Geo restriction, 뷰어 프로토콜 정책 등
- 오리진이 거절하고 CloudFront가 전달: S3 권한, ALB/앱 인증, 오리진 커스텀 헤더 누락 등
가장 빠른 구분법은 CloudFront 표준 로그(standard log) 또는 실시간 로그(real-time log)에서 다음 필드를 보는 것입니다.
x-edge-result-type/x-edge-response-result-typeAccessDenied,Forbidden등
sc-status(HTTP status)cs-host,cs-uri-stemx-edge-detailed-result-type(상세 원인)
CloudFront 표준 로그를 S3에 쌓는 경우 Athena로 바로 조회할 수 있습니다.
-- Athena 예시 (CloudFront standard logs 테이블이 있다고 가정)
SELECT
date,
time,
x_edge_result_type,
x_edge_detailed_result_type,
sc_status,
cs_method,
cs_host,
cs_uri_stem,
cs_user_agent
FROM cloudfront_logs
WHERE sc_status = 403
ORDER BY date DESC, time DESC
LIMIT 50;
또 하나의 힌트는 응답 헤더의 via와 x-cache입니다.
x-cache: Error from cloudfront이면 CloudFront 레이어에서 막혔을 가능성이 큽니다.x-cache: Miss from cloudfront인데 403이면 오리진 403을 전달했을 가능성이 큽니다.
원인 1) S3 오리진 권한 문제 (OAI/OAC, 버킷 정책)
전형적인 증상
- S3를 오리진으로 쓰는 정적 사이트에서만 403
- 특정 경로만 403 또는 전체 403
- CloudFront에서
AccessDenied가 뜨고, S3 직접 접근도 막혀 있거나(퍼블릭 차단) 정책이 꼬여 있음
확인 방법
- S3 버킷의
Block Public Access설정 확인 - CloudFront 오리진이 OAI(Origin Access Identity)인지 OAC(Origin Access Control)인지 확인
- 버킷 정책에서 CloudFront 배포(distribution) 또는 OAI/OAC에 대한
s3:GetObject권한이 있는지 확인
해결
- OAC를 쓰는 경우, 버킷 정책에
cloudfront.amazonaws.com서비스 프린시펄과AWS:SourceArn조건을 정확히 추가 - OAI를 쓰는 경우, OAI 캐노니컬 유저에
s3:GetObject허용
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowCloudFrontServicePrincipalReadOnly",
"Effect": "Allow",
"Principal": { "Service": "cloudfront.amazonaws.com" },
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-bucket/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::123456789012:distribution/EDFDVBD6EXAMPLE"
}
}
}
]
}
운영 팁: OAI에서 OAC로 전환 중이라면, 정책이 "둘 다"를 커버해야 하는 과도기를 만들지 말고 배포/정책 변경 순서를 명확히 하세요. 중간 상태에서 403이 가장 많이 납니다.
원인 2) S3 객체 키/경로 문제 (SPA 라우팅, 기본 루트 오브젝트)
전형적인 증상
/는 되는데/app같은 경로에서 403 또는 404- SPA(React/Next.js 정적 export 등)에서 새로고침 시 403
- 디렉터리처럼 보이는 경로(
/docs/) 접근 시 403
확인 방법
- CloudFront
Default root object가index.html로 설정되어 있는지 - S3에 실제로 해당 키가 존재하는지(예:
app/index.html) - S3 정적 웹사이트 엔드포인트를 오리진으로 쓰는지, S3 REST 엔드포인트를 쓰는지 확인
해결
- SPA라면 CloudFront의 "커스텀 에러 응답"으로 403/404를
index.html로 매핑해 라우팅 처리 - 혹은 S3에 디렉터리 인덱스 파일을 배치
CloudFront Custom Error Response 예시
- HTTP Error Code: 403
- Response Page Path: /index.html
- HTTP Response Code: 200
- TTL: 0 (또는 짧게)
주의: 이 방식은 진짜 권한 문제로 인한 403까지 200으로 바꿔버릴 수 있어, API 경로와 정적 경로의 behavior를 분리하는 것이 안전합니다.
원인 3) 오리진(ALB/앱)에서 Host 헤더 기반으로 차단
전형적인 증상
- CloudFront 도메인으로는 403, 커스텀 도메인으로는 정상(또는 그 반대)
- ALB 리스너 규칙이
Host헤더로 라우팅/차단 - 앱 서버가 허용 도메인(allowed hosts) 체크를 함
확인 방법
- CloudFront 오리진 설정의
Origin domain이 ALB DNS인지 확인 - ALB의 리스너 룰이
Host조건을 요구하는지 확인 - 애플리케이션이
Host또는X-Forwarded-Host를 검증하는지 확인
해결
- CloudFront가 오리진으로 전달하는
Host가 ALB 라우팅과 맞도록 설정 - 필요 시 CloudFront에서 오리진 요청 정책(Origin Request Policy)으로
Host전달 여부를 조정 - 앱의 allowed hosts에 CloudFront 도메인/커스텀 도메인을 모두 포함
Nginx를 오리진으로 쓰는 경우, 서버 블록이 server_name 미매칭이면 403/444 등으로 떨어질 수 있습니다.
원인 4) Signed URL/Signed Cookie 설정 오류
전형적인 증상
- 브라우저로 직접 열면 403, 서명 붙이면 될 것 같은데 계속 403
- 특정 파일만 보호하려고 했는데 전부 막힘
- 만료 시간 이후/이전 시간대에서만 재현(서버 시간 오차)
확인 방법
- 해당 behavior에서
Restrict Viewer Access(서명 필요)가 켜져 있는지 - 키 그룹(Key group) 또는 신뢰할 수 있는 서명자(Trusted signer) 설정이 맞는지
- URL의 정책(policy), 만료(expires), 서명(signature) 파라미터가 올바른지
- 서명 생성 서버의 시간이 정확한지(NTP)
해결
- 키 그룹을 사용하는 경우, 퍼블릭 키가 CloudFront에 등록되어 있고 behavior에 연결되었는지 확인
- 만료 시간을 너무 짧게 잡지 말고, 서버 시간 동기화 필수
- 캐시 키에 쿼리스트링이 포함되지 않으면, 서명 파라미터가 무시되어 403이 날 수 있으니 캐시 정책을 점검
서명 URL 예시는 언어/SDK마다 다르지만, 공통적으로 "키 선택"과 "정책/만료"와 "캐시 키"가 한 세트로 맞아야 합니다.
원인 5) CloudFront WAF 또는 AWS WAF 규칙에 의해 차단
전형적인 증상
- 특정 국가/ASN/UA/경로/쿼리에서만 403
- 봇/크롤러, 혹은 특정 IP 대역만 막힘
- 응답 바디에 WAF 차단 메시지 또는
Request blocked류 문구
확인 방법
- Web ACL이 CloudFront 배포에 연결되어 있는지
- WAF 로그(Kinesis Firehose, S3, CloudWatch Logs)에서
action = BLOCK인지 확인 - 관리형 룰(Managed Rules)의 false positive 여부 확인
해결
- 문제 규칙을
COUNT로 전환해 영향 범위 확인 후 예외(Allow) 룰 추가 - 특정 경로(API)만 보호하려면 WAF scope-down statement로 범위를 좁히기
WAF는 "정상 요청도 공격처럼 보이게" 만드는 경우가 많습니다. 특히 GraphQL, 긴 쿼리스트링, Base64 페이로드가 자주 걸립니다.
원인 6) Geo restriction(국가 제한) 또는 컴플라이언스 설정
전형적인 증상
- 특정 국가에서만 403
- VPN을 켜면 재현되거나 반대로 VPN에서만 됨
확인 방법
- CloudFront 배포의
Restrictions에서 Geo restriction이Whitelist또는Blacklist인지 확인 - WAF의 Geo match rule도 함께 확인(둘 중 하나만 걸려도 403)
해결
- 허용 국가 목록을 재검토하고, 운영/테스트 IP의 국가 판정 이슈를 고려
- B2B 서비스라면 국가 제한을 WAF로 옮겨 로그 기반으로 운영하는 편이 진단이 쉽습니다.
원인 7) Behavior 경로 패턴/우선순위 문제로 "의도치 않은" 정책 적용
전형적인 증상
/api/*는 인증을 붙이고/static/*은 오픈하려 했는데, 정적도 403- 특정 확장자만 403
- 배포 수정 후 일부 경로만 갑자기 막힘
확인 방법
- CloudFront behavior의 path pattern 우선순위(가장 구체적인 패턴이 먼저 매칭되는지) 확인
- 해당 behavior에 연결된 캐시 정책, 오리진 요청 정책, 뷰어 요청 정책(서명 필요 여부 등) 확인
해결
/api/*같은 구체 패턴을 기본 behavior보다 우선 적용되도록 재정렬- 정적/동적 오리진을 분리하고, 인증/헤더 전달 정책도 분리
운영 팁: "정적은 S3, API는 ALB" 구성이라면 behavior를 최소 2개로 명확히 분리하고, 기본 behavior를 정적으로 두는 편이 사고가 적습니다.
원인 8) Lambda@Edge / CloudFront Functions에서 요청을 거절
전형적인 증상
- 배포/코드 업데이트 이후부터 403
- 특정 헤더/쿠키가 없으면 403
- 로컬에서는 되는데 엣지에서만 실패
확인 방법
- 연결된 함수가 viewer request / origin request 단계 중 어디에 붙었는지 확인
- CloudWatch Logs(리전 주의)에서 함수 로그 확인
- 함수가 응답을 직접 생성하면서
statusCode: 403을 반환하는지 확인
해결
- 인증 로직이 있다면 실패 시 401/403을 명확히 구분하고, 디버깅용 헤더를 임시로 추가
- 캐시 때문에 "한 번 403"이 엣지에 남아 계속 재현될 수 있으니, 에러 캐싱 TTL과 invalidation 전략을 함께 점검
CloudFront Functions 예시(헤더 없으면 403). 본문에 부등호가 들어가지 않도록 화살표 대신 단순 조건문 형태로만 예시를 듭니다.
function handler(event) {
var request = event.request;
var headers = request.headers;
if (!headers["x-my-auth"] || !headers["x-my-auth"].value) {
return {
statusCode: 403,
statusDescription: "AccessDenied",
headers: {
"content-type": { "value": "text/plain" }
},
body: "AccessDenied"
};
}
return request;
}
빠른 트러블슈팅 체크리스트
아래 순서대로 보면 보통 10분 안에 범위를 좁힐 수 있습니다.
curl -I로 응답 헤더 확인 (x-cache,via,server)- CloudFront 로그에서
x-edge-detailed-result-type확인 - 오리진이 S3면 버킷 정책(OAI/OAC)부터 확인
- 오리진이 ALB면
Host기반 라우팅/앱 allowed hosts 확인 - WAF 연결 여부 및 WAF 로그에서
BLOCK확인 - Geo restriction 확인
- behavior 우선순위와 정책 연결(서명 필요, 헤더/쿼리 전달)을 재점검
- Lambda@Edge/Functions 코드 변경 이력과 로그 확인
curl로 최소 재현 요청을 만들면 원인 분리가 빨라집니다.
# 헤더 없이 호출
curl -I https://d111111abcdef8.cloudfront.net/protected/file.jpg
# Host를 바꿔서 호출(오리진/앱이 Host 검증할 때 유용)
curl -I https://d111111abcdef8.cloudfront.net/ \
-H 'Host: example.com'
운영에서 자주 놓치는 포인트 3가지
- 403도 캐시된다: 에러 TTL이나 캐시 정책에 따라 403이 엣지에 남아 "고쳤는데도 계속 403"처럼 보일 수 있습니다. 이때는 invalidation 또는 TTL 조정이 필요합니다.
- 정적과 API를 같은 behavior로 묶지 말기: 커스텀 에러 응답으로 SPA 라우팅을 처리할 때, API까지 200으로 바꿔버리는 사고가 납니다.
- 변경 순서 관리: OAC 전환, WAF 룰 추가, behavior 재정렬은 배포 단위로 영향이 큽니다. GitHub Actions 등으로 변경을 코드화하면 회귀를 줄일 수 있습니다. 관련해서는 GitHub Actions 재사용 워크플로우로 모노레포 CI 통합처럼 파이프라인을 표준화하는 접근이 도움이 됩니다.
마무리
CloudFront 403 AccessDenied는 단일 문제라기보다 "정책/권한/라우팅/보안"이 겹쳐서 나타나는 현상입니다. 따라서 먼저 CloudFront 레이어 차단인지 오리진 차단인지 구분하고, 그 다음에 S3 권한(OAI/OAC), WAF, 서명 설정, behavior 우선순위, 엣지 함수 순으로 좁혀가면 가장 빠르게 해결됩니다.
원인을 특정하기 위해 로그를 먼저 잡아두는 것이 장기적으로 가장 큰 비용 절감 포인트입니다. CloudFront 로그와 WAF 로그를 항상 켜두고, 403 급증 알람을 걸어두면 장애를 "원인 불명"으로 남기지 않을 수 있습니다.