HTTP 403
HTTP 403 은 서버가 요청을 거절할 때 돌려주는 상태 코드입니다. 요청을 못 알아들었다는 뜻이 아닙니다. 알아듣고 나서 들어주지 않겠다고 답한 것입니다. 왜 거절했는지는 안 알려줘도 됩니다.
상세
403 은 HTTP(HyperText Transfer Protocol) 상태 코드 가운데 4xx 부류에 속합니다. RFC 9110 15.5 는 4xx 부류를 클라이언트가 잘못한 것으로 보이는 경우로 두고, HEAD 요청에 답할 때를 빼면 오류 상황을 설명하는 표현을 함께 보내라고 권합니다(SHOULD). 이것은 부류 전체에 걸리는 규정이고, 403 하나에만 걸리는 것은 아래 절들이 받습니다.
이 부류에 들어 있다고 해서 서버가 요청을 못 읽었다는 뜻은 아닙니다. RFC 9110 15.5.4 의 첫 문장은 서버가 요청을 이해했으나 이행하기를 거부한다고 적습니다. 거부가 일어난 자리는 요청의 문법이 아니라 그 요청을 들어줄지 말지에 대한 서버의 판단입니다.
사유를 알려줄지는 서버가 정합니다. 같은 절은 요청이 왜 금지됐는지 공개하고자 하는 서버는 그 이유를 응답 본문에 적을 수 있다고 적습니다. 본문이 있다면 그렇습니다. 공개하겠다는 의사가 전제로 붙어 있으므로, 사유가 비어 있는 403 도 명세를 어긴 것이 아닙니다. 이 성질이 뒤의 운영을 통째로 바꿉니다. 원인은 응답이 아니라 거절한 쪽의 로그에 있습니다.
값 자체는 IANA(Internet Assigned Numbers Authority) 의 HTTP 상태 코드 레지스트리에 올라 있습니다. 403 행의 설명은 Forbidden 이고 근거 문서는 RFC 9110 15.5.4 입니다.
명세상 뜻
403 을 정한 문서는 RFC 9110 이고 절은 15.5.4 입니다. IANA 의 HTTP 상태 코드 레지스트리도 403 Forbidden 의 근거로 같은 절을 가리킵니다.
정의 문장은 이것입니다. 403 상태 코드는 서버가 요청을 이해했으나 이행하기를 거부함을 가리킵니다.
같은 절이 함께 정해 둔 것은 아래와 같습니다. 한정어를 지우면 안 되는 것이 되게 만드는 문장들이라 원문의 한정어를 그대로 붙여 둡니다.
| 명세가 정한 것 | 한정어 |
|---|---|
| 요청이 왜 금지됐는지 공개하고자 하는 서버는 그 이유를 응답 본문에 적을 수 있습니다. 본문이 있다면 그렇습니다 | can |
| 요청에 인증 크리덴셜이 실려 있었다면, 서버는 그것이 접근을 허용하기에 불충분하다고 본 것입니다 | 없음 |
| 클라이언트는 같은 크리덴셜로 요청을 자동으로 되풀이해서는 안 됩니다 | SHOULD NOT |
| 클라이언트는 새로운 크리덴셜이나 다른 크리덴셜로 요청을 되풀이할 수 있습니다 | MAY |
| 다만 요청은 크리덴셜과 무관한 이유로 금지될 수도 있습니다 | might |
| 금지된 대상 리소스의 현재 존재를 숨기고자 하는 오리진 서버는 403 대신 [[HTTP 404 | 404]] 로 응답할 수도 있습니다 |
상태 줄에 붙는 낱말은 고정이 아닙니다. RFC 9110 15.1 은 이 명세가 열거한 이유 구절이 권고일 뿐이라고 적습니다. 로컬 등가물로 바꿔도 되고 통째로 빠져도 프로토콜에는 영향이 없습니다. Forbidden 이라는 낱말이 안 보여도 값이 403 이면 403 입니다.
캐시 취급도 같은 절이 정합니다. RFC 9110 15.1 이 휴리스틱하게 캐시 가능한 것으로 예시한 값은 200, 203, 204, 206, 300, 301, 308, 404, 405, 410, 414, 501 입니다. 403 은 그 안에 없습니다. 같은 절은 메서드 정의나 명시적 캐시 제어가 달리 지시하지 않는 한 그 밖의 모든 상태 코드는 휴리스틱하게 캐시할 수 없다고 적습니다.
실제 원인
명세는 거부했다는 사실까지만 말합니다. 무엇이 거부를 결정했는지는 값을 낸 쪽마다 다릅니다. 같은 403 이라도 웹 서버의 인가 모듈에서 나온 것, 오브젝트 스토리지의 정책에서 나온 것, 원 서버에 닿기도 전에 앞단에서 나온 것이 섞여 있습니다.
인증은 통과했는데 인가에서 막힐 때
Apache httpd 의 mod_authz_core 는 Require 지시어로 인증된 사용자가 인가 제공자에 의해
허가되는지를 시험합니다. Require all granted 는 무조건 허용이고 Require all denied 는
무조건 거부입니다. Require env 는 주어진 환경 변수 가운데 하나가 설정된 경우에만,
Require method 는 주어진 HTTP 메서드에 대해서만, Require expr 은 표현식이 참으로
평가되는 경우에만 접근을 허용합니다. 컨텍스트는 directory 와 .htaccess 입니다.
인증이 성공하고 인가가 실패했을 때 Apache httpd 가 기본으로 돌려주는 값은 401 입니다.
문서는 이 때문에 브라우저가 사용자에게 비밀번호 대화상자를 다시 띄우는 일이 잦고, 그것이
모든 상황에서 원하는 동작은 아니라고 적습니다. AuthzSendForbiddenOnFailure On 이 그 응답
코드를 403 으로 바꿉니다. 기본값은 Off 이고, Apache httpd 2.3.11 이후에 쓸 수 있습니다.
가리는 법은 이 지시어입니다. 크리덴셜은 그대로인데 401 대신 403 이 온다면 여기가 켜져 있는지 봅니다. 같은 문서는 보안 경고를 붙여 둡니다. 인가가 없을 때의 응답을 바꾸면 비밀번호의 보안이 약해집니다. 추측한 비밀번호가 맞았다는 것을 공격자에게 드러내기 때문입니다.
정책이 허용을 안 적어 뒀을 때
AWS(Amazon Web Services) S3(Simple Storage Service) 문서는 Access Denied 오류를 403 Forbidden 과 같은 것으로 두고, AWS 가 인가 요청을 명시적으로 또는 묵시적으로 거부할 때 나타난다고 적습니다. 명시적 거부는 정책에 그 AWS 작업에 대한 Deny 문이 있을 때입니다. 묵시적 거부는 적용되는 Deny 문도 없고 적용되는 Allow 문도 없을 때입니다. IAM(Identity and Access Management) 정책은 기본적으로 IAM 프린시펄을 묵시적으로 거부하므로, 정책이 그 프린시펄에게 작업을 수행하도록 명시적으로 허용해야 합니다. 그렇지 않으면 정책이 묵시적으로 접근을 거부합니다.
거부를 결정할 수 있는 자리가 여럿이라 원인이 한 곳으로 모이지 않습니다. 같은 문서가 세는 자리는 버킷 정책과 IAM 정책, S3 ACL(Access Control List) 설정, S3 퍼블릭 액세스 차단 설정, S3 암호화 설정, S3 오브젝트 락 설정, VPC(Virtual Private Cloud) 엔드포인트 정책, AWS Organizations 정책, CloudFront 배포 접근, 액세스 포인트 설정, 그리고 요청자 지불 설정입니다. 문서는 Access Denied 메시지 예시 절을 먼저 보고 그 다음에 버킷 정책과 IAM 정책 쪽으로 가라고 적습니다.
가리는 법은 요청자를 먼저 식별하는 것입니다. 서명이 없는 요청이면 IAM 사용자 정책이 없는 익명 요청입니다. 프리사인 URL(Uniform Resource Locator)을 쓴 요청이면 사용자 정책은 그 요청에 서명한 IAM 사용자나 역할의 것과 같습니다. 문서는 올바른 IAM 사용자나 역할을 쓰고 있는지 확인하라고 적습니다.
원 서버에 닿기 전에 앞단이 낼 때
CDN(Content Delivery Network)이나 WAF(Web Application Firewall)를 앞에 두면 403 이 오리진 서버까지 가지 않고 앞단에서 나올 수 있습니다. Cloudflare 문서는 이 둘을 응답 본문의 브랜딩으로 가릅니다. Cloudflare 브랜딩이 없는 403 은 Cloudflare 가 아니라 오리진 웹 서버가 직접 돌려준 것입니다.
flowchart TD
A["403 응답을 받음"] --> B{"본문에 Cloudflare 브랜딩이 있나"}
B -- 없음 --> C["오리진 웹 서버가 냈다"]
B -- 있음 --> D["앞단이 냈다"]
C --> E["권한 규칙 · mod_security · IP 거부 규칙"]
D --> F["WAF 규칙 · Security Level · DDoS 보호 · Browser Integrity Check"]
오리진 쪽으로 갈린 경우에 문서가 드는 흔한 이유는 셋입니다. 오리진 웹 서버에 설정된 권한
규칙, 예를 들어 Apache 의 .htaccess 파일이 첫째입니다. mod_security 규칙이 둘째이고,
특정 IP(Internet Protocol, 인터넷 프로토콜) 대역의 트래픽을 막는 IP 거부 규칙이 셋째입니다.
같은 문서는 Cloudflare 의 IP 대역이 차단돼 있지 않은지 확인하라고 적습니다.
앞단 쪽으로 갈린 경우도 문서가 열거합니다. 요청이 기본 WAF 관리형 규칙이나 존별 커스텀 WAF 관리형 규칙을 위반한 경우입니다. 브랜딩이 붙은 403 은 Challenge 또는 Block 액션이 걸린 WAF 커스텀·관리형 규칙, 기본이 Medium 인 Security Level 설정, DDoS(Distributed Denial of Service) 보호, 대부분의 1xxx Cloudflare 오류 코드, Browser Integrity Check, 검증 확인에서 나올 수 있습니다. DDoS 보호가 기본으로 켜져 있는 범위는 한정돼 있습니다. Cloudflare 에 온보딩된 존, Spectrum 에 온보딩된 IP 애플리케이션, Magic Transit 에 온보딩된 IP 프리픽스입니다.
여기에 갈래가 하나 더 있습니다. Cloudflare 는 특정 경우에 스타일이 없는 403 오류 페이지를 내기도 합니다. 이 오류들은 도메인 설정이 로드되기 전, Cloudflare 인프라의 이른 지점에서 발생하므로 로그에 남지 않습니다. 문서가 드는 예는 SNI(Server Name Indication)입니다. 클라이언트가 보낸 Host 가 SNI 와 일치하지 않으면 403 이 돌아옵니다.
운영
403 은 사유를 안 담아도 되는 값입니다. 그래서 볼 자리는 응답이 아니라 그 값을 낸 쪽의 로그입니다. 낸 쪽이 어디냐에 따라 봐야 할 파일과 손잡이가 갈립니다.
nginx
error_log 지시어가 로깅을 설정합니다. 기본값은 error_log logs/error.log error; 이고
컨텍스트는 main, http, mail, stream, server, location 입니다. 1.5.2 부터 같은 설정 레벨에
여러 벌을 지정할 수 있습니다. 첫 번째 파라미터는 로그를 담을 파일이고, stderr 라는 특별한
값은 표준 오류 파일을 고릅니다.
두 번째 파라미터가 로그 레벨입니다. debug, info, notice, warn, error, crit, alert, emerg 순으로 심각도가 올라갑니다. 어떤 레벨을 정하면 그 레벨과 그보다 심각한 레벨의 메시지가 전부 기록됩니다. 기본 레벨 error 는 error, crit, alert, emerg 를 기록합니다. 그보다 아래인 info · notice · debug · warn 메시지는 기본 설정에서 안 남습니다. 이 파라미터를 빼면 error 가 쓰입니다.
Apache httpd
ErrorLog 가 서버가 마주친 오류를 기록할 파일 이름을 정합니다. 기본값은 유닉스에서
logs/error_log 이고 윈도우와 OS/2 에서는 logs/error.log 입니다. 파일 경로가 절대경로가
아니면 ServerRoot 기준 상대경로로 봅니다. 컨텍스트는 server config 와 virtual host 입니다.
LogLevel 이 그 로그의 상세함을 조절합니다. 기본값은 warn 입니다. 레벨은 중요도가 낮아지는
순으로 emerg, alert, crit, error, warn, notice, info, debug 입니다. 모듈별로도 지정할 수
있습니다.
앞단과 오브젝트 스토리지
S3 는 문서가 든 항목을 다 확인하고도 403 이 남으면 Amazon S3 요청 ID 를 가져와 Support 에 문의하라고 적습니다. 응답에 실려 오는 그 식별자가 지원 쪽이 붙잡는 손잡이입니다.
Cloudflare 쪽은 로그가 비어 있는 경우를 염두에 둬야 합니다. 도메인 설정이 로드되기 전에 발생한 403 은 로그에 남지 않습니다. 로그에 안 보인다는 것이 403 이 안 났다는 뜻은 아닙니다.
경계
로그인은 됐는데 남의 리소스에 접근한 경우
이것도 403 인가. 명세를 기준으로 하면 403 쪽입니다. 401 은 요청이 대상 리소스에 대한 유효한 인증 크리덴셜을 갖고 있지 않아 적용되지 않았음을 가리킵니다. 그렇다고 로그인이 끝난 크리덴셜이 401 을 아예 안 만나는 것은 아닙니다. 같은 절은 요청에 인증 크리덴셜이 실려 있었다면 401 응답이 그 크리덴셜에 대해 인가가 거부됐음을 가리킨다고 적습니다. 403 쪽 정의는 크리덴셜이 실려 있었다면 서버가 그것을 접근을 허용하기에 불충분하다고 본 것이라고 적습니다. 크리덴셜은 유효한데 그것으로는 안 된다는 상황이 여기입니다. 두 값을 가르는 것은 크리덴셜이 실렸느냐가 아니라 응답이 무엇을 함께 보내야 하느냐입니다.
두 값이 클라이언트에게 시키는 것도 다릅니다. 401 을 내는 서버는 대상 리소스에 적용되는
챌린지를 최소 하나 담은 WWW-Authenticate 헤더 필드를 반드시 보내야 합니다(MUST). 다시
인증하라는 신호가 응답 안에 들어 있는 셈입니다. 403 에는 그런 필드 요구가 없고, 클라이언트는
같은 크리덴셜로 요청을 자동으로 되풀이해서는 안 됩니다(SHOULD NOT).
다만 이 판정은 명세가 어느 값을 가리키느냐에 대한 것이고, 실제로 어느 값이 오는지는 서버 설정이 정합니다. Apache httpd 는 인증이 성공하고 인가가 실패했을 때 기본으로 401 을 돌려줍니다.
404 를 받았는데 사실은 금지된 경우
404 를 받았으면 403 은 아닌 것인가. 아닙니다. 금지된 대상 리소스의 현재 존재를 숨기고자 하는 오리진 서버는 403 대신 404 로 응답할 수도 있습니다(MAY). 404 자신의 정의에도 같은 여지가 들어 있습니다. 404 는 오리진 서버가 대상 리소스에 대한 현재 표현을 찾지 못했거나, 그런 것이 존재한다는 것을 밝힐 의사가 없음을 가리킵니다. 그래서 404 를 근거로 리소스가 없다고 단정할 수 없습니다.
두 값의 캐시 취급은 갈립니다. 404 는 휴리스틱하게 캐시 가능합니다. 메서드 정의나 명시적 캐시 제어가 달리 지시하지 않는 한 그렇습니다. 403 은 RFC 9110 15.1 이 휴리스틱하게 캐시 가능하다고 예시한 목록에 없습니다.
요청을 너무 많이 보내서 막혔는데 403 이 온 경우
이것도 403 인가. 맞습니다. 어느 값인지는 상태 줄에 실린 숫자로 갈리고, 막힌 사유로 갈리지 않습니다. 403 의 정의에는 요청 수가 들어 있지 않습니다. 요청 수를 뜻하는 값은 429 로 따로 있고, 그 값이 무엇을 정하는지는 그 항목이 받습니다.
그래서 사유로는 두 값을 못 가릅니다. 403 은 거부의 이유를 안 알려줘도 되는 값이라, 횟수 때문에 막혔다는 사실이 응답 어디에도 안 나타날 수 있습니다. 캐시 규정도 두 값이 갈립니다. 403 에 대해 정해진 것은 저장 금지가 아니라 휴리스틱 캐시 대상이 아니라는 것뿐입니다.
관련 항목
403 값을 정한 표준 문서
RFC 9110 · IANA · RFC 6585 · RFC 7231
403과 경계를 가르는 이웃 상태 코드
HTTP 401 · HTTP 404 · HTTP 429
403이 속하는 상위 분류
403을 실제로 내는 소프트웨어·서비스
nginx · Apache httpd · AWS · S3 · Cloudflare · CDN · WAF
403 여부를 가르는 인가 지시어
mod_authz_core · Require · AuthzSendForbiddenOnFailure
403 원인이 남는 로그
로그 · 메시지 · error_log · ErrorLog · LogLevel
403이 Cloudflare 앞단에서 나는 보호 기능
Security Level · DDoS · Browser Integrity Check · Magic Transit · Spectrum · SNI
403(Access Denied)이 비롯되는 S3 설정
IAM · ACL · VPC · CloudFront · 배포 · 락 · Access Denied · 명시적 거부 · 묵시적 거부 · 버킷 정책 · S3 퍼블릭 액세스 차단 · S3 암호화 · AWS Organizations · 액세스 포인트 · 요청자 지불
이웃 값이 요구하는 응답 헤더
WWW-Authenticate · Retry-After
403 판정을 가르는 접근 통제 개념
403 응답이 오가는 당사자
다른 이름: 403 · Forbidden · 403 Forbidden