Traefik IngressRoute 설정했는데 404만 뜬다 - tls 한 줄이 HTTP 라우터를 조용히 지워버리는 함정
집에서 돌리는 k3s 서버에 도메인을 붙이고 Let's Encrypt 인증서까지 자동으로 받게 설정했다. https://로 접속하면 잘 뜬다. 그런데 주소창에 그냥 도메인만 치거나 http://로 들어가면 하얀 화면에 딱 한 줄이 나온다.
404 page not found
앱이 내는 404가 아니라 Traefik이 "이 요청을 보낼 곳이 없다"며 내는 기본 404다. 더 답답한 건 Traefik 로그에 에러가 하나도 없다는 점이었다. 설정 문법도 맞고, 적용도 잘 됐고, HTTPS는 멀쩡하다. 한참 헤맨 끝에 원인은 IngressRoute의 tls: 블록이었다. 이 글은 그 원인과 재현 실험, 해결 방법 두 가지를 정리한 기록이다.
환경
- k3s v1.36 단일 노드, 기본 내장 Traefik 3.7
- entryPoint 두 개:
web(80) /websecure(443) - 인증서: Traefik ACME(Let's Encrypt),
certResolver: letsencrypt - 라우팅: Kubernetes 기본 Ingress가 아니라 Traefik CRD인
IngressRoute사용
문제의 설정
처음 작성한 IngressRoute는 이랬다. 80이든 443이든 둘 다 받고 싶어서 entryPoints에 둘 다 적었고, 인증서를 받으려고 tls를 붙였다. 누가 봐도 "80과 443 양쪽에서 이 앱으로 보내라"로 읽힌다.
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: mini-portal
namespace: portal
spec:
entryPoints:
- web # 80
- websecure # 443
routes:
- kind: Rule
match: Host(`k0.co.kr`)
services:
- name: mini-portal
port: 5000
tls:
certResolver: letsencrypt
결과는 HTTPS만 200, HTTP는 404였다.
재현 실험 - tls 한 줄 차이
정말 tls 때문인지 확인하려고, 운영 중인 라우트는 건드리지 않고 테스트용 호스트 이름(demo.example.test)으로 IngressRoute를 하나 따로 만들어 실험했다. Host 헤더만 바꿔서 노드의 80/443으로 직접 요청을 보내면 DNS 없이도 테스트할 수 있다.
# HTTP (80)
curl -s -o /dev/null -w "%{http_code}\n" -H "Host: demo.example.test" http://127.0.0.1/
# HTTPS (443) - 인증서 검증은 생략(-k)
curl -sk -o /dev/null -w "%{http_code}\n" \
--resolve demo.example.test:443:127.0.0.1 https://demo.example.test/
결과
- entryPoints: [web, websecure] + tls 있음 — HTTP
404❌ / HTTPS200⭕ - entryPoints: [web, websecure] + tls 없음 — HTTP
200⭕ / HTTPS200⭕
나머지는 완전히 같고 tls: 블록 하나만 뺐을 뿐인데 HTTP가 살아났다. (tls를 뺐는데도 HTTPS가 되는 이유는 뒤에서 따로 설명한다.)
Traefik 액세스 로그를 JSON으로 켜두면 결정적인 단서가 보인다. 404가 난 요청과 200이 난 요청을 나란히 놓아보면 이렇다(필드 일부만 발췌).
// tls 있음 → HTTP 요청
{"RequestHost":"demo.example.test","RequestScheme":"http","entryPointName":"web",
"DownstreamStatus":404, "OriginStatus":0}
// tls 없음 → HTTP 요청
{"RequestHost":"demo.example.test","RequestScheme":"http","entryPointName":"web",
"DownstreamStatus":200, "RouterName":"portal-tls-demo-e0292e...@kubernetescrd",
"ServiceURL":"http://10.42.0.144:5000"}
404 쪽에는 RouterName 필드가 아예 없다. 요청이 web entryPoint까지는 들어왔지만 매칭되는 라우터가 하나도 없었다는 뜻이다. 라우터가 에러를 낸 게 아니라, 애초에 web 쪽에는 라우터가 만들어지지 않은 것이다. 에러 로그가 없었던 이유도 이것이다. Traefik 입장에서는 잘못된 설정이 아니라 의도된 동작이기 때문이다.
원인 - tls가 붙은 라우터는 "HTTPS 전용"이다
Traefik 공식 문서의 라우터 TLS 항목에 이 동작이 명시돼 있다. 요지는 이렇다.
라우터에 TLS 섹션이 지정되면, Traefik은 그 라우터를 HTTPS 요청 전용으로 취급하고 HTTP(TLS가 아닌) 요청은 무시한다.
즉 tls:는 "인증서를 이걸로 써라"라는 옵션이 아니라 "이 라우터는 TLS 요청만 받는다"는 선언이다. 그래서 entryPoints에 web을 적어도, TLS가 아닌 web 쪽에서는 라우터가 조용히 빠진다. 설정 파일만 봐서는 절대 짐작할 수 없는 부분이다.
정리하면
- IngressRoute 하나 = entryPoint마다 라우터 하나씩 생성
- 그런데
tls가 있으면 TLS가 아닌 entryPoint(web)의 라우터는 요청을 받지 않음 - 에러도 경고도 없음 → 80으로 들어온 요청은 매칭 실패 → Traefik 기본 404
해결 방법 1 - HTTP용 IngressRoute를 따로 만들고 HTTPS로 리다이렉트
내가 적용한 방법이다. HTTPS용과 HTTP용을 두 개로 쪼갠다. HTTP용은 tls 없이 web에만 붙이고, 앱으로 보내는 대신 HTTPS로 돌려보내는 미들웨어를 건다. 어차피 HTTP로 서비스할 생각은 없었으니, 80으로 들어오면 443으로 안내하는 게 맞다.
① 리다이렉트 미들웨어
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: redirect-to-https
namespace: portal
spec:
redirectScheme:
scheme: https
permanent: true
② HTTPS 전용 IngressRoute (websecure만, tls 있음)
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: mini-portal
namespace: portal
spec:
entryPoints:
- websecure
routes:
- kind: Rule
match: Host(`k0.co.kr`)
services:
- name: mini-portal
port: 5000
tls:
certResolver: letsencrypt
③ HTTP 전용 IngressRoute (web만, tls 없음, 리다이렉트)
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: mini-portal-http-redirect
namespace: portal
spec:
entryPoints:
- web
routes:
- kind: Rule
match: Host(`k0.co.kr`)
middlewares:
- name: redirect-to-https
services: # 미들웨어가 먼저 응답하므로 실제로 요청이 가지는 않는다
- name: mini-portal
port: 5000
적용 후 확인하면 이렇게 나온다.
$ curl -sI http://k0.co.kr/blog
HTTP/1.1 308 Permanent Redirect
Location: https://k0.co.kr/blog
301이 아니라 308이 나오는 것도 눈여겨볼 만하다. 둘 다 "영구 이동"이지만, 308은 원래 요청의 메서드와 본문을 그대로 유지하라는 뜻이다. 301은 역사적으로 브라우저가 POST를 GET으로 바꿔 다시 보내는 경우가 있어서, 폼 전송 같은 요청이 리다이렉트를 거치며 깨질 수 있다. 이 버전의 Traefik은 permanent: true일 때 308을 돌려줬다. 검색엔진은 두 코드를 똑같이 영구 이동으로 취급한다.
여러 앱을 한 클러스터에 올린다면 앱마다 이 쌍을 만들어야 한다는 게 단점이다. 대신 앱별로 HTTP 처리를 다르게 할 수 있다는 장점이 있다. 예를 들어 사내망 전용 서비스 하나는 HTTP 그대로 두고 싶을 때도 문제없다.
해결 방법 2 - entryPoint 단에서 전역 리다이렉트
"80으로 들어오는 건 무조건 443으로"가 원칙이라면 라우트마다 쪼갤 필요 없이 Traefik 자체 설정(정적 설정)에서 한 번에 처리할 수 있다. k3s 내장 Traefik은 Helm 차트로 설치되므로, HelmChartConfig로 실행 인자를 추가한다.
# /var/lib/rancher/k3s/server/manifests/traefik-config.yaml
apiVersion: helm.cattle.io/v1
kind: HelmChartConfig
metadata:
name: traefik
namespace: kube-system
spec:
valuesContent: |-
additionalArguments:
- "--entryPoints.web.http.redirections.entryPoint.to=websecure"
- "--entryPoints.web.http.redirections.entryPoint.scheme=https"
- "--entryPoints.web.http.redirections.entryPoint.permanent=true"
이 파일을 k3s의 manifests 디렉터리에 두면 k3s가 알아서 Traefik을 다시 배포한다. 이후로는 IngressRoute를 websecure + tls로만 만들면 되고, HTTP용 라우트는 필요 없다.
어느 쪽을 고를까
- 방법 1 (라우트 분리) — 앱별로 제어 가능, Traefik 재배포 불필요. 대신 앱마다 YAML이 두 배.
- 방법 2 (전역 리다이렉트) — 설정 한 번으로 끝, 실수할 여지가 적음. 대신 80에서 HTTP로 서비스해야 하는 앱이 생기면 예외 처리가 번거롭다.
새로 구성한다면 방법 2를 추천한다. 이번 같은 함정에 빠질 여지 자체가 사라진다. 이미 방법 1로 잘 돌고 있는 앱을 굳이 바꿀 필요는 없다.
덤 1 - tls를 빼도 HTTPS가 되는 이유 (k3s 한정)
재현 실험에서 tls를 뺐는데도 HTTPS가 200으로 떴다. 이상해서 Traefik 실행 인자를 확인해보니 k3s 내장 Traefik에는 이 옵션이 기본으로 들어 있었다.
$ kubectl -n kube-system get deploy traefik \
-o jsonpath='{.spec.template.spec.containers[0].args}' | tr ',' '\n' | grep http.tls
"--entryPoints.websecure.http.tls=true"
websecure entryPoint 자체에 TLS가 켜져 있어서, 그 위에 올라가는 라우터는 tls 블록이 없어도 자동으로 HTTPS 라우터가 된다(액세스 로그의 라우터 이름에도 websecure- 접두사가 붙어 있었다). 다만 이때는 certResolver를 지정하지 않았으니 해당 도메인용 인증서를 따로 받아둔 게 없다면 Let's Encrypt 인증서가 아니라 Traefik 기본 자체서명 인증서가 나간다. 브라우저에서는 경고가 뜬다. 그러니 tls.certResolver는 여전히 필요하다. 원리를 알고 나면 "tls는 HTTPS 라우터에만 붙인다"는 규칙이 자연스럽게 이해된다.
덤 2 - 인증서 발급은 왜 멀쩡했나
80번이 404를 내는 동안에도 Let's Encrypt 인증서는 정상적으로 발급·갱신됐다. 인증서 리졸버가 tlschallenge(TLS-ALPN-01) 방식이라 도메인 소유 확인을 443 포트에서 하기 때문이다.
--certificatesresolvers.letsencrypt.acme.tlschallenge=true
만약 httpchallenge(HTTP-01)를 썼다면 이야기가 다르다. Let's Encrypt가 http://도메인/.well-known/acme-challenge/...로 확인하러 오기 때문에, 80번 라우팅이 깨지면 인증서 갱신까지 실패한다. 당장은 멀쩡하다가 90일 뒤에 갑자기 사이트 전체에 인증서 만료 경고가 뜨는, 훨씬 고약한 형태로 터진다. HTTP-01을 쓰고 있다면 80번 라우팅을 꼭 확인하자.
덤 3 - 그래도 80이 안 된다면, 공유기를 의심하자
Traefik 설정을 다 고쳤는데 밖에서 http://로 접속하면 여전히 이상한 경우가 있었다. 집 안에서는 되고, 휴대폰 LTE로 접속하면 공유기 관리자 로그인 페이지가 떴다. 공유기가 외부에서 들어오는 80번을 자기 원격 관리 페이지로 가로채고 있었던 것이다. 443은 포트포워딩이 제대로 돼 있어서 HTTPS만 멀쩡했으니, 증상만 보면 Traefik 문제와 구분이 안 된다.
실제로 이것 때문에 애드센스 사이트 심사에서 "사이트에 접속할 수 없음"으로 떨어진 적도 있다. 구분하는 방법은 간단하다.
- 서버 안에서
curl -H "Host: 도메인" http://127.0.0.1/→ 이게 되면 Traefik은 정상 - 외부망(LTE)에서
curl -I http://도메인/→ 여기서 다른 응답이 오면 공유기나 ISP 쪽 문제 - 공유기 설정에서 원격 관리 포트가 80으로 잡혀 있지 않은지, 80번 포트포워딩 규칙이 있는지 확인
트러블슈팅 체크리스트 - Traefik이 404를 낼 때
- 액세스 로그에 RouterName이 있는가? — 없으면 라우터 매칭 실패. Host 규칙, entryPoint, tls 여부를 본다. 있는데 404면 앱이 낸 404다.
- tls가 붙은 라우트의 entryPoints에 web이 들어 있지 않은가? — 들어 있다면 이 글의 함정이다. HTTP용 라우트를 분리하거나 전역 리다이렉트를 쓴다.
- Host 규칙이 실제 요청 도메인과 정확히 같은가? —
www유무, 서브도메인 오타를 확인한다. - IngressRoute와 Service가 같은 네임스페이스인가? — Traefik CRD는 기본적으로 다른 네임스페이스의 서비스를 참조하지 못한다.
- 서버 안에서는 되는데 밖에서만 안 되는가? — 공유기, 방화벽, ISP의 80번 차단을 의심한다.
- 인증서 방식이 HTTP-01인가? — 그렇다면 80번 라우팅이 깨졌을 때 갱신도 같이 깨진다.
마무리
이 문제가 고약한 이유는 설정이 틀리지 않았다는 데 있다. 문법 오류도 없고, 에러 로그도 없고, HTTPS는 잘 된다. entryPoints: [web, websecure]라고 적어놓은 걸 보면 당연히 양쪽 다 될 거라고 믿게 된다. 문서에는 적혀 있지만, 문제를 겪기 전에는 그 문장을 찾아 읽을 일이 없다.
그래서 이런 류의 문제는 "설정을 다시 읽는 것"보다 작게 재현해서 한 줄씩 빼보는 것이 훨씬 빠르다. 테스트용 호스트 이름으로 IngressRoute를 하나 만들고 Host 헤더만 바꿔 curl을 날려보면, 운영 서비스를 건드리지 않고 몇 분 만에 원인을 좁힐 수 있다. 참고로 이 글은 Traefik 3.7 기준이다. 옵션 이름이나 기본값은 버전마다 조금씩 다를 수 있으니, 적용 전에 쓰는 버전의 공식 문서를 한 번 확인하시길.
댓글 0
로그인 후 댓글을 작성할 수 있습니다.