문제

인증서가 유효하고 https://도 열리니 끝났다고 생각합니다. 그런데 누군가 도메인만 입력하거나, 3년 전 메일에 있던 낡은 http:// 링크를 누르면 평문 버전 사이트로 떨어집니다 — 자물쇠 아이콘 없이, 검색결과에서 보안 버전과 뒤섞인 채, 같은 카페 Wi-Fi에 있는 누구나 조용히 읽을 수 있는 상태로요. HTTPS가 가능한 것과 HTTPS를 강제하는 것은 다릅니다. 모든 HTTP 요청이 HTTPS로 튕겨나가기 전까지, 당신의 사이트는 두 벌이고 그중 안전하지 않은 쪽은 여전히 열려 있습니다.

리다이렉트를 거는 것 자체는 5분짜리 일입니다. 까다로운 건 세 가지가 동시에 맞아떨어져야 한다는 점입니다 — 리다이렉트 상태 코드, HSTS 정책, 그리고 오리진 앞에 있는 프록시나 CDN. 이 셋이 어긋나면 사이트가 여전히 평문으로 열리거나, 아예 안 열립니다.

‘HTTPS 강제’가 실제로 뜻하는 것

두 개의 층이 있는데, 사람들이 자주 뒤섞습니다.

리다이렉트는 서버 쪽입니다. 80번 포트(http://)로 요청이 들어오면, 페이지를 주는 대신 3xx 상태와 https:// 버전을 가리키는 Location: 헤더로 답합니다. 브라우저는 그걸 따라갑니다. 모든 클라이언트(오래된 것, 봇 포함)에 통하지만, 평문 요청이 이미 나간 뒤에야 작동합니다.

HSTS는 클라이언트 쪽 기억입니다. HTTPS로 보내는 헤더로, 브라우저에게 “앞으로 N초 동안 이 도메인엔 http://를 시도조차 하지 말고, 네트워크에 닿기 전에 https://로 고쳐라”라고 말합니다. 평문 요청 자체를 없애지만, 이미 한 번 HTTPS로 방문한 브라우저에만 적용됩니다.

둘 다 필요합니다. 리다이렉트는 모두를 잡고, HSTS는 재방문자를 그 위험한 첫 평문 홉에서 보호합니다. 어느 하나만으로는 완전하지 않습니다.

리다이렉트 코드 고르기: 302 말고 301 (그리고 308이 필요할 때)

301 Moved Permanently를 쓰세요. HTTP에서 HTTPS로의 이동은 영구적입니다 — 되돌아갈 일이 없으니 — 그래서 301이 정직하고, 검색엔진은 HTTP URL을 HTTPS로 접어 표준으로 삼으며, 브라우저는 리다이렉트를 캐시해 그만 묻습니다. 302(임시)는 평문 URL이 돌아올 수도 있다고 모두에게 알리는 셈이라 틀렸고, 방문마다 왕복을 한 번씩 낭비합니다.

진짜 함정은 POST 요청입니다. RFC 9110은 여전히 클라이언트가 301·302를 GET으로 바꾸는 걸 허용하고, 역사적으로 많은 클라이언트가 그렇게 했습니다. 호스트가 폼 전송이나 API 호출도 받는다면, HTTP 엔드포인트에 닿은 클라이언트는 리다이렉트에서 메서드와 본문을 조용히 잃을 수 있습니다. 그게 위험하다면 308 Permanent Redirect(§15.4.9)를 쓰세요 — 메서드와 본문을 반드시 보존하는 영구 리다이렉트입니다. 어림잡아 콘텐츠 사이트는 301, 같은 호스트가 POST도 받으면 308입니다.

전체를 망가뜨리는 함정: 리다이렉트 루프

가장 절박한 검색을 만들어내는 실패인데, 정작 당신의 리다이렉트 규칙 버그인 경우는 거의 없습니다. “모든 HTTP를 HTTPS로 보내기”를 켰더니 브라우저가 **ERR_TOO_MANY_REDIRECTS**를 띄우고 아무것도 안 엽니다.

오리진 앞에서 TLS를 종단하고 오리진과는 평문 HTTP로 대화하는 무언가가 있으면 벌어집니다. 대표적인 게 Cloudflare의 “Flexible” SSL 모드입니다.

  1. 브라우저가 Cloudflare에 HTTPS로 붙습니다. 여기까진 좋습니다.
  2. Flexible 모드의 Cloudflare가 요청을 오리진에 평문 HTTP로 넘깁니다.
  3. 오리진은 HTTP 요청을 보고 충실하게 301 → https://로 답합니다.
  4. Cloudflare가 그 리다이렉트를 브라우저에 돌려주고, 브라우저는 HTTPS로 재시도하고…
  5. …Cloudflare는 그걸 또 오리진에 HTTP로 넘깁니다. 3번으로. 무한히.

해결책은 리다이렉트를 지우는 게 아닙니다. 앞단도 오리진과 HTTPS로 말하게 하는 겁니다 — Cloudflare의 SSL/TLS 모드를 **Full (Strict)**로 설정하세요. TLS를 종단하는 리버스 프록시나 로드밸런서(nginx, AWS ALB, 쿠버네티스 ingress)에서도 같은 모양이 나옵니다. 거기선 원시 연결 스킴으로 리다이렉트하지 말고, 프록시가 붙이는 X-Forwarded-Proto 헤더를 읽으세요. 값이 https면 원래 요청이 이미 안전했으니 리다이렉트하지 않고, http일 때만 리다이렉트합니다.

프록시를 거치지 않은 요청엔 이 헤더가 없으니, 헤더가 없으면 HTTP로 보고 리다이렉트하면 됩니다. 실수는 로컬 소켓을 믿는 것 — TLS 종단 프록시 뒤에서는 로컬 소켓이 항상 평문으로 보입니다.

작동한 뒤: 리다이렉트에서 멈추지 마세요

HSTS를 추가하세요. HTTPS가 강제되면 HTTPS 응답에 Strict-Transport-Security: max-age=31536000; includeSubDomains를 실어 보내세요. 이제 재방문자는 평문 요청을 아예 건너뜁니다 — 브라우저가 http://를 메모리 안에서 https://로 바꿉니다(DevTools에 네트워크 왕복이 아니라 Non-Authoritative-Reason: HSTS가 붙은 307 Internal Redirect로 보입니다). 아무것도 깨지지 않는지 확인하는 동안엔 짧은 max-age로 시작하고, 이후 올리세요. 모든 서브도메인이 HTTPS를 할 수 있다고 확신할 때만 preload를 붙이고 브라우저 preload 목록에 등록하세요 — preload는 빠르게 되돌리기 어렵습니다.

혼합 콘텐츠를 살피세요. 페이지가 HTTPS로 뜨는 순간, 페이지가 끌어오는 http:// 이미지·스크립트·스타일시트는 혼합 콘텐츠가 됩니다 — 브라우저는 능동 혼합 콘텐츠(스크립트)를 아예 차단하고 나머지엔 경고를 냅니다. 리다이렉트는 완벽한데 하드코딩된 http:// 자산 하나 때문에 자물쇠가 깨져 보일 수 있습니다.

확인하는 법

맞아야 하는 설정 파일이 아니라, 브라우저나 봇이 보는 그대로 바깥에서 검증하세요.

  1. 평문 URL을 요청해 상태를 읽으세요. http://yourdomain.com을 쳐서, Location이 정확히 대응하는 https://(같은 경로, 홈으로 새는 우회 없이)인 단일 301(또는 308)이 오는지 확인합니다.
  2. 홉 수를 세세요. http://에서 최종 https:// URL로 리다이렉트는 한 번이어야 합니다. http → https → https-with-www → …가 보이면 리다이렉트 규칙을 겹쳐 쌓은 것이고, 추가 홉마다 지연이자 유출 기회입니다.
  3. HSTS 헤더가 실제로 있는지 확인하세요. 리다이렉트가 아니라 https:// 응답의 헤더를 보세요 — 리다이렉트 응답은 브라우저가 신뢰할 헤더를 못 실어 보내는 경우가 많습니다.
  4. www와 apex, 그리고 깊은 경로를 시험하세요. 홈페이지만 리다이렉트하고 http://yourdomain.com/old/page가 루트로 버려지지 않고 https://yourdomain.com/old/page로 가야 한다는 걸 잊는 경우가 많습니다.

DechoNet으로 확인하기

  • HTTP 점검은 http://부터 리다이렉트 체인을 따라가 각 홉의 정확한 상태 코드, 최종 URL, 응답 헤더를 보여줍니다 — 깔끔한 단일 301/308인지, HTTPS 응답에 Strict-Transport-Security 헤더가 있는지 확인할 수 있습니다.
  • SSL 점검은 모두를 몰아넣는 그 인증서가 실제로 유효하고 완전한지 확인합니다 — 깨졌거나 만료된 인증서로 강제 리다이렉트하는 건 에러를 옮길 뿐 고치는 게 아닙니다.

해결 체크리스트

  • 모든 http:// 트래픽을 대응하는 https://로 301(호스트가 POST도 받으면 308)로 보냅니다. 전체 경로를 보존하고, 모두를 홈으로 버리지 마세요.
  • CDN이나 TLS 종단 프록시 뒤라면 앞단이 오리진과 HTTPS로 말하게(Cloudflare: Full (Strict)) 하거나, 원시 소켓이 아니라 X-Forwarded-Proto 기준으로 리다이렉트하세요 — 이게 ERR_TOO_MANY_REDIRECTS를 막습니다.
  • HTTPS 응답에 **Strict-Transport-Security**를 보냅니다. 적당한 max-age로 시작하고, 모든 서브도메인이 HTTPS를 하면 includeSubDomains를, 확신이 설 때만 preload를 붙이세요.
  • 혼합 콘텐츠를 고치세요 — 하드코딩된 http:// 자산을 찾아내 자물쇠를 깨끗하게 만듭니다.
  • 바깥에서 검증하세요: 깔끔한 단일 리다이렉트 홉, 올바른 최종 URL, HTTPS 응답의 HSTS. www·apex·깊은 경로를 모두 시험하세요.

언제 전문가에게

  • CDN 뒤가 아닌데 ERR_TOO_MANY_REDIRECTS가 뜬다면, HTTPS를 강제하려는 두 층을 찾으세요 — 애플리케이션 규칙과 웹서버 규칙이 서로 부딪히거나, HTTP→HTTPS 규칙이 apex↔www 규칙과 싸우는 경우입니다. 한쪽 리다이렉트를 끄고 한 주체가 담당하게 하세요.
  • HSTS를 걸었는데도 브라우저가 여전히 HTTP로 온다면, HSTS는 HTTPS로 한 번 성공한 뒤에야 적용된다는 걸 기억하세요 — 그리고 일단 preload되면 브라우저 벤더가 캐시해 제거가 느립니다. 서브도메인이 모두 HTTPS를 못 하는 도메인은 preload하지 마세요.
  • 리다이렉트는 맞는데 인증서 자체가 실패한다면(이름 불일치, 만료, 중간 인증서 누락) 그건 리다이렉트가 아니라 SSL 문제입니다 — 깨진 인증서로 모두를 몰아넣는 건 시작할 때의 평문보다 나쁩니다.

내 도메인에서 바로 확인

무료, 가입 불필요. 이 가이드가 다루는 항목을 바로 검사하고 조치 방법을 알려드립니다.