HTTP 301이 뜨는 이유 — ALB ssl-redirect

curl로 API를 호출했을 때 301이 뜬 이유를 파고들다 ALB의 ssl-redirect annotation까지 닿았다.

왜 궁금해졌는가

curl work-api.test-nudge-eap.io/readyz 를 날렸더니 정상 응답 대신 이게 돌아왔다.

<html>
<head><title>301 Moved Permanently</title></head>
<body>
<center><h1>301 Moved Permanently</h1></center>
</body>
</html>

반면 https://를 명시해서 치면:

{"status":"ready"}

같은 엔드포인트인데 왜 한쪽은 301이고 한쪽은 200인가.


탐구

301이 정확히 무엇인가

301 Moved Permanently는 리소스가 영구적으로 다른 URL로 이동했다는 뜻이다.

  • 클라이언트에게 앞으로는 새 URL을 쓰라고 알림
  • 브라우저는 이 결과를 캐싱함
  • 응답에 Location 헤더가 포함됨

curl -v 로 확인하면:

< HTTP/1.1 301 Moved Permanently
< Location: https://work-api.test-nudge-eap.io/readyz

http://로 보낸 요청을 https://로 리다이렉트하고 있다.

누가 301을 보내는가

EKS + ALB + Ingress 구조라면 트래픽 경로는 이렇다.

Route53
  ↓
ALB (HTTP 80 → HTTPS 443 redirect)
  ↓
Ingress
  ↓
Service
  ↓
Pod

301을 보내는 주체 후보는 세 가지다.

  1. ALB Listener Rule — 80 → 443 redirect 설정
  2. nginx ingress annotationnginx.ingress.kubernetes.io/force-ssl-redirect: "true"
  3. Application 레벨 — 앱 코드에서 직접 redirect

이 경우는 ALB 레벨에서 발생한다.

Ingress annotation이 원인이었다

실제 Ingress 설정을 보면:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ingress
  annotations:
    alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:ap-northeast-2:...
    alb.ingress.kubernetes.io/healthcheck-path: /readyz
    alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS": 443}]'
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/ssl-policy: ELBSecurityPolicy-TLS13-1-2-2021-06
    alb.ingress.kubernetes.io/ssl-redirect: '443'    # ← 이것
    alb.ingress.kubernetes.io/target-type: ip

alb.ingress.kubernetes.io/ssl-redirect: '443' 이 annotation 때문이다.

ssl-redirect annotation의 동작 원리

AWS Load Balancer Controller는 이 annotation을 보고 ALB를 다음과 같이 구성한다.

80 리스너:

  • 기본 action: Redirect
  • 대상 포트: 443
  • 상태 코드: HTTP_301

443 리스너:

  • TargetGroup으로 forward

실제 트래픽 흐름:

HTTP 80 요청
   ↓
ALB Listener (301 redirect)
   ↓
HTTPS 443
   ↓
TargetGroup → Pod

→ 301은 Ingress나 Pod가 아니라 ALB 레벨에서 발생한다.

그런데 브라우저에서는 왜 안 보이는가

브라우저로 http://work-api.test-nudge-eap.io/readyz를 열면 301이 뜨지 않고 바로 응답이 온다. 301이 사라진 게 아니라, 브라우저가 301을 받고 자동으로 따라가기 때문이다.

HTTP 클라이언트마다 리다이렉트를 처리하는 방식이 다르다.

클라이언트301 수신 시 동작사용자에게 보이는 것
브라우저Location 헤더의 URL로 자동 재요청최종 응답(200)만 보임
curl (기본)301 응답에서 멈춤301 HTML 그대로 출력
curl -L브라우저처럼 자동으로 따라감최종 응답(200)이 출력됨

브라우저가 자동으로 따라가는 이유는 HTTP 스펙상 301은 클라이언트가 새 URL로 재요청해야 한다고 정의되어 있고, 브라우저는 이를 UX 차원에서 자동 처리하기 때문이다. 추가로 브라우저는 301 결과를 캐싱하기 때문에, 같은 URL을 다시 열면 서버에 요청조차 보내지 않고 바로 https://로 간다.

curl은 기본적으로 이 자동 처리를 하지 않는다. API 클라이언트나 스크립트는 리다이렉트를 명시적으로 인지해야 하는 경우가 많기 때문에 기본 동작이 멈추는 것이다.

# 301에서 멈춤 (기본)
curl http://work-api.test-nudge-eap.io/readyz
 
# 브라우저처럼 자동으로 따라감
curl -L http://work-api.test-nudge-eap.io/readyz

이해한 것

301은 오류가 아니라 의도된 보안 설정이다.

요청결과이유
http://301HTTPS 강제 리다이렉트
https://200정상

HTTP는 평문이기 때문에 인증 토큰·쿠키가 노출될 위험이 있다. 그래서 HTTP 요청을 HTTPS로 강제 전환하는 게 보안 표준이다.

ssl-redirect: '443' annotation을 제거하면 301이 사라지고 HTTP 직접 접근이 가능해지지만, 보안상 권장되지 않는다.

curl에서 리다이렉트를 따라가려면 -L 옵션을 쓴다. 이 옵션이 없으면 301에서 멈추고, 있으면 브라우저처럼 자동으로 최종 응답까지 따라간다.


참고 자료