- Published on
nginx HTTPS 강제 리다이렉트 무한루프 해결
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버에서 HTTP를 HTTPS로 강제 리다이렉트했는데 브라우저가 계속 같은 URL로 튕기며 접속이 안 되는 경우가 있습니다. 흔히 ERR_TOO_MANY_REDIRECTS로 보이고, 서버 로그에는 301 또는 302가 반복됩니다. 이 문제는 단순히 return 301 https://$host$request_uri; 한 줄로 끝나지 않는 경우가 많습니다. 특히 CDN, L7 로드밸런서(ALB, NLB+TLS), Ingress, 리버스 프록시 체인이 끼면 클라이언트가 실제로는 HTTPS로 접속했는데 nginx가 HTTP로 인식하거나, 반대로 업스트림 앱이 다시 HTTP로 되돌리는 형태로 루프가 생깁니다.
이 글에서는 nginx 기준으로 무한 리다이렉트 루프의 원인을 유형별로 나누고, 운영에서 안전하게 쓰는 설정 패턴과 점검 방법을 제공합니다.
무한루프가 생기는 전형적인 구조
대부분 아래 흐름 중 하나입니다.
- 클라이언트
https://example.com접속 - CDN 또는 로드밸런서에서 TLS 종료 후 nginx로는
http로 전달 - nginx는
$scheme이http라고 판단해서 HTTPS로 리다이렉트 - 클라이언트는 다시 HTTPS로 접속
- 2~4 반복
또는 앱(예: Spring, Next.js, Django)이 X-Forwarded-Proto를 못 받아서 자신이 HTTP라고 생각하고 HTTPS로 리다이렉트하거나, 반대로 잘못된 설정으로 HTTP로 되돌리는 경우도 있습니다.
1) $scheme만 믿고 리다이렉트하는 설정의 함정
많이들 아래처럼 작성합니다.
if ($scheme = http) {
return 301 https://$host$request_uri;
}
문제는 TLS가 nginx 앞단에서 종료되는 구조에서는 nginx가 실제 연결을 http로 받기 때문에 $scheme이 항상 http가 될 수 있다는 점입니다. 즉, 클라이언트는 HTTPS로 들어오는데 nginx는 계속 HTTP로 받아서 계속 리다이렉트하게 됩니다.
해결 방향
- nginx가 직접 TLS를 종료하는 구조라면
80포트 서버 블록에서만 HTTPS로 넘기면 됩니다. - nginx 앞단에서 TLS를 종료한다면, nginx는 헤더 기반으로 원래 프로토콜을 판단해야 합니다.
2) 가장 안전한 기본 패턴: 80은 무조건 301, 443은 서비스
nginx가 직접 TLS를 처리하는 경우, 가장 깔끔한 패턴은 아래입니다.
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name example.com www.example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
location / {
proxy_pass http://app_upstream;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
이 구조에서는 80에서만 리다이렉트가 발생하고, 443에서는 리다이렉트 로직이 없으니 루프가 생기기 어렵습니다.
자주 하는 실수
443서버 블록에도 동일한 리다이렉트if가 들어가 있는 경우server_name이 겹치고 우선순위가 꼬여서443요청이 의도치 않은 서버 블록으로 매칭되는 경우
3) 로드밸런서나 CDN 뒤에서 터지는 루프: X-Forwarded-Proto 처리
TLS 종료가 로드밸런서에서 일어나면 nginx는 HTTP만 보게 됩니다. 이때는 로드밸런서가 보통 X-Forwarded-Proto: https를 붙여줍니다. nginx는 이 값을 신뢰해서 리다이렉트 여부를 판단해야 합니다.
권장 패턴: map으로 원래 스킴 복원
아래는 X-Forwarded-Proto가 있으면 그 값을, 없으면 $scheme을 쓰는 방식입니다.
map $http_x_forwarded_proto $origin_scheme {
default $scheme;
https https;
http http;
}
server {
listen 80;
server_name example.com;
# 로드밸런서 뒤에서 nginx가 80으로만 받더라도,
# 원래가 https면 리다이렉트하지 않도록 제어 가능
if ($origin_scheme = http) {
return 301 https://$host$request_uri;
}
location / {
proxy_pass http://app_upstream;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $origin_scheme;
}
}
핵심은 리다이렉트 조건이 $scheme이 아니라 $http_x_forwarded_proto를 반영한 값이어야 한다는 점입니다.
보안 주의
X-Forwarded-Proto는 클라이언트도 임의로 보낼 수 있습니다. 따라서 인터넷에서 직접 nginx에 접근 가능한 구조라면, 이 헤더를 무조건 신뢰하면 안 됩니다.
운영에서는 보통 다음 중 하나로 정리합니다.
- nginx는 로드밸런서에서만 접근 가능(보안그룹, 방화벽)하게 만들고 헤더를 신뢰
- 또는 로드밸런서 IP 대역에서만 헤더를 신뢰하는 추가 방어를 구성
4) 업스트림 앱이 다시 리다이렉트하는 경우(프레임워크 설정)
nginx에서 HTTPS로 잘 넘겼는데도 루프가 계속된다면, 실제로는 업스트림 애플리케이션이 리다이렉트를 생성하고 있을 수 있습니다.
예를 들어 앱이 자신을 HTTP로 인식하면 다음 같은 일이 생깁니다.
- 앱이
http로 canonical URL을 만들고 다시https로 보내거나 - 반대로
https강제 옵션이 앱에도 켜져 있고, nginx도 켜져 있어 이중 리다이렉트 체인이 생김
nginx에서 업스트림에 반드시 전달할 헤더
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $origin_scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
Spring Boot라면 server.forward-headers-strategy 또는 프록시 헤더 처리 설정이 중요하고, Next.js나 Express도 trust proxy 같은 옵션이 영향을 줍니다. 이 부분은 앱 스택마다 다르지만, 공통적으로 원래 스킴 전달이 핵심입니다.
관련해서 프록시/빌드 환경 이슈를 다루는 글도 함께 보면 운영 디버깅 감이 빨리 잡힙니다.
5) proxy_redirect와 Location 헤더가 꼬이는 문제
업스트림이 Location: http://...로 응답하면, 브라우저는 HTTP로 이동하고 nginx가 다시 HTTPS로 보내면서 루프가 생길 수 있습니다.
이때는 업스트림이 올바른 X-Forwarded-Proto를 받아서 Location을 HTTPS로 만들도록 하는 것이 1순위고, 부득이하면 nginx에서 proxy_redirect로 보정합니다.
location / {
proxy_pass http://app_upstream;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $origin_scheme;
# 업스트림이 http Location을 내보낼 때 https로 교정
proxy_redirect http:// https://;
}
다만 proxy_redirect는 증상만 가리는 경우가 있어, 가능하면 업스트림의 프록시 인식 설정을 먼저 바로잡는 것을 권장합니다.
6) 실제 원인을 빠르게 좁히는 점검 방법
1) curl로 리다이렉트 체인 확인
아래처럼 -I와 -L을 조합하면 어디서 무슨 응답이 반복되는지 빠르게 보입니다.
curl -I http://example.com
curl -IL http://example.com
curl -IL https://example.com
Location이 계속 같은 곳을 가리키는지, http와 https가 번갈아 나오는지 확인하세요.
2) nginx access log에 스킴과 포워드 헤더 남기기
로그 포맷을 잠시 확장하면 원인 파악이 훨씬 쉬워집니다.
log_format with_proto '$remote_addr - $host "$request" '
'status=$status scheme=$scheme '
'xfp="$http_x_forwarded_proto" '
'ua="$http_user_agent"';
access_log /var/log/nginx/access.log with_proto;
여기서 scheme=http인데 xfp=https가 반복된다면, nginx가 TLS 종료 지점이 아니며 헤더를 기준으로 판단해야 한다는 신호입니다.
3) 로드밸런서 설정 확인
- HTTPS 리스너가 있는지
- 타겟 그룹으로 전달 시
X-Forwarded-Proto가 설정되는지 - 헬스체크 경로가 리다이렉트를 따라가다가 실패하지는 않는지
운영에서 병목을 추적할 때 관측 지표를 잘 쌓는 것이 중요하듯, 리다이렉트 루프도 결국 “관측”이 해결의 시작입니다. 성격은 다르지만 원인 추적 접근법은 비슷하니 아래 글도 참고할 만합니다.
7) 실전 예시: ALB에서 TLS 종료, nginx는 80만 수신
상황:
- 클라이언트는 HTTPS로 접속
- ALB가 TLS 종료 후 nginx 인스턴스
80으로 전달 - nginx에서 HTTPS 강제하려다 무한루프
해결 설정 예시는 다음처럼 정리하는 편이 안전합니다.
map $http_x_forwarded_proto $origin_scheme {
default http;
https https;
}
server {
listen 80;
server_name example.com;
# 원래 요청이 http였을 때만 https로 올림
if ($origin_scheme = http) {
return 301 https://$host$request_uri;
}
location / {
proxy_pass http://app_upstream;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $origin_scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
여기서 default http;로 둔 이유는, 로드밸런서가 헤더를 안 붙여주는 환경에서 애매하게 동작하지 않도록 하기 위함입니다. 실제로는 인프라 표준에 맞춰 default $scheme;로 두고, 보안그룹으로 로드밸런서만 접근하도록 제한하는 구성이 더 흔합니다.
체크리스트: 루프를 끊는 최소 점검 항목
80서버 블록은return 301 https://...로 단순화되어 있는가443서버 블록에는 리다이렉트 로직이 없는가- TLS 종료 지점이 nginx인지, 앞단(LB, CDN)인지 명확한가
- 앞단이 붙이는
X-Forwarded-Proto를 nginx와 업스트림 앱이 모두 올바르게 처리하는가 - 업스트림이
Location을http로 만들고 있지 않은가 curl -IL로 리다이렉트 체인이http와https를 번갈아 반복하지 않는가
마무리
nginx의 HTTPS 강제 리다이렉트 무한루프는 설정 한 줄의 문제가 아니라, “TLS가 어디서 종료되는지”와 “원래 스킴이 어디에서 어떻게 전달되는지”를 끝까지 추적해야 해결됩니다. 가장 안정적인 접근은 80과 443의 역할을 명확히 분리하고, 프록시 체인이 있는 경우 X-Forwarded-Proto를 표준화해 nginx와 애플리케이션이 동일한 기준으로 URL을 생성하도록 만드는 것입니다.