Traefik 경계 계약과 적용 기록
Traefik은 k3s가 관리하는 클러스터 내부 Ingress Controller다. 공개 요청은 반드시
Host Nginx에서 TLS를 종료한 뒤 loopback NodePort를 통해 Traefik의 web
entrypoint로 들어온다. 이 저장소는 두 번째 Ingress Controller를 설치하지 않으며,
애플리케이션 Ingress에 클러스터 내부 TLS를 중복 구성하지 않는다.
2026-07-23 현재 kube-system/traefik HelmChartConfig에는 trust overlay가 실제로
적용돼 있다. Host Nginx 경유 관측에서 확인한 ClientHost 10.42.0.1 한 주소만
10.42.0.1/32로 신뢰하며, Gitea의 site manifest도 외부 HTTPS URL을 생성한다.
현재 live 상태
| 항목 | 확인된 값 |
|---|---|
| k3s Traefik Chart | 40.1.3+up40.1.0 |
| Traefik 이미지 | v3.7.4 |
HelmChartConfig |
trust overlay와 일치, live 적용됨 |
| Service 유형 | NodePort, externalTrafficPolicy: Cluster |
web |
Service 80, NodePort 30080 |
websecure |
Service 443, NodePort 30443 |
| NodePort bind 범위 | 127.0.0.0/8 |
| JSON access log | 활성화, request header 기록 제외 |
관측 ClientHost |
10.42.0.1 |
web trusted CIDR |
10.42.0.1/32 |
websecure forwarded-header trust |
없음 |
forwardedHeaders.insecure |
없음 |
| Gitea site manifest | start_url과 icon URL 모두 https://git.learn.hyeonworks.com/ 기준 |
| 실제 ingress 경로 | Host Nginx :443 -> 127.0.0.1:30080 -> Traefik web |
다음 명령으로 변할 수 있는 live 상태를 다시 확인한다.
kubectl -n kube-system get helmchartconfig.helm.cattle.io/traefik
kubectl -n kube-system get service/traefik \
-o custom-columns='NAME:.metadata.name,TYPE:.spec.type,PORTS:.spec.ports[*].port,NODEPORTS:.spec.ports[*].nodePort'
kubectl -n kube-system get deployment/traefik -o json |
jq -r '.spec.template.spec.containers[] | select(.name == "traefik") | .args[]'
web=30080, websecure=30443, Service NodePort 중 하나라도 다르면 Host Nginx를
새 포트로 임의 변경하지 말고 중지한다. 선언과 live 상태가 왜 달라졌는지 먼저
확인한다.
트래픽과 노출 경계
- 애플리케이션 Ingress가 hostname에서 Service로 이어지는 routing을 소유한다.
- 모든 Ingress는
spec.ingressClassName: traefik과webentrypoint를 명시한다. - 공개 TLS는 Host Nginx가 종료하므로 애플리케이션 Ingress에
spec.tls를 넣지 않는다. websecureNodePort30443은 Service 계약상 고정하지만 현재 Host Nginx upstream은 사용하지 않는다. 이 entrypoint에는 forwarded-header trust도 설정하지 않는다.- Traefik Dashboard와 관리 endpoint는 공개하지 않는다.
- k3s drop-in의
nodeport-addresses=127.0.0.0/8이 LAN에서 NodePort에 직접 접근하는 우회 경로를 차단한다.
서버 node IP와 별도 LAN 클라이언트에서는 다음 연결이 거부되거나 timeout이어야
한다. Traefik 404도 TCP 연결에 성공했다는 뜻이므로 실패다.
nc -vz -w 3 192.168.0.107 30080
nc -vz -w 3 192.168.0.107 30443
반대로 서버 loopback에서는 두 포트가 listening 상태여야 하며 Host 기반 Gitea health가 통과해야 한다.
nc -vz -w 3 127.0.0.1 30080
nc -vz -w 3 127.0.0.1 30443
curl --fail-with-body \
--header 'Host: git.learn.hyeonworks.com' \
http://127.0.0.1:30080/api/healthz
UFW는 현재 inactive다. 인터넷 측 고포트 차단 여부는 LAN 결과에서 추론하지 않고 router 규칙 또는 별도 외부망 검사로 확인한다.
선언 구조와 각 overlay의 역할
infrastructure/networking/traefik/
├── base/
│ └── helm-chart-config.yaml
├── overlays/
│ ├── baseline/
│ │ └── service-boundary-only-patch.yaml
│ ├── observe/
│ └── trust/
│ └── trusted-proxy-cidr-patch.yaml
└── scripts/
├── apply-observe.sh
├── observe-client-host.sh
├── apply-trust.sh
├── rollback-to-observe.sh
└── validate.sh
세 overlay는 모두 Service NodePort, externalTrafficPolicy: Cluster와
30080/30443을 명시적으로 소유한다.
baseline: Service 경계만 남긴다. access log와 forwarded-header trust는 없다.observe: Service 경계와 header를 버리는 JSON access log를 적용한다. trust는 없다.trust: observe 설정에web.forwardedHeaders.trustedIPs=10.42.0.1/32만 추가한다.
루트 kustomization.yaml은 의도적으로 안전한 observe overlay를 가리킨다. 현재
live 상태는 trust이므로 루트에 단순히 kubectl apply -k를 실행하면 trust 제거를
요청하게 된다. 상태 전환은 아래 guarded script와 정확한 overlay를 사용한다.
Chart 원본이나 k3s가 소유한 HelmChart는 직접 수정하지 않는다.
첫 observe 적용 실패와 복구
첫 observe 적용 때 HelmChartConfig에는 access log만 있고 Traefik Service values가
없었다. k3s Helm Controller가 전체 Chart를 기본값으로 다시 조정하면서 다음 drift가
발생했다.
기존: NodePort web=30080, websecure=30443
변경: LoadBalancer web=31251, websecure=30997
이어진 loopback listener 검사가 실패했다. 당시 실패 처리도 새
HelmChartConfig를 삭제했을 뿐, desired state에 없던 수동 Service spec은 복원하지
못했다. Gitea·PostgreSQL·PV/PVC는 건드리지 않고 Traefik Service만 다음 명령으로
즉시 원래 경계에 복구했다.
kubectl -n kube-system patch service traefik \
--type=merge \
--patch '{"spec":{"type":"NodePort","externalTrafficPolicy":"Cluster","ports":[{"name":"web","port":80,"protocol":"TCP","targetPort":"web","nodePort":30080},{"name":"websecure","port":443,"protocol":"TCP","targetPort":"websecure","nodePort":30443}]}}'
그 뒤 다음을 영구 보완했다.
base,baseline,observe,trust가 Service type과 정확한 NodePort를 선언한다.baselineoverlay를 추가해 access log나 trust 없이도 NodePort desired state를 유지한다.- observe 실패 시
HelmChartConfig를 삭제하지 않고 baseline을 적용한다. - trust 실패 또는 표준 trust 롤백 시 observe를 적용한다.
- rollout 뒤 NodePort listener와 Gitea health가 수렴할 때까지 bounded wait를 한다.
- 검증기는 세 overlay에서 LoadBalancer 부재와
30080/30443을 강제한다.
따라서 HelmChartConfig 삭제는 더 이상 롤백 방법이 아니다. 삭제하면 Chart 기본값이
다시 Service를 소유해 같은 drift를 재발시킬 수 있다.
전달 헤더 최소 신뢰 적용 결과
Host Nginx는 외부 요청의 기존 forwarded chain을 이어 붙이지 않고 신뢰 경계에서 다음 값을 새로 만든다.
Host는 선택한 공개 hostname으로 고정한다.X-Real-IP와X-Forwarded-For는 Nginx가 실제로 본 client address로 교체한다.X-Forwarded-Proto는https,X-Forwarded-Port는443으로 고정한다.
observe 단계에서 다음 probe가 Host Nginx를 반드시 통과하는 고유 요청을 만들고 Traefik JSON access log의 한 router 기록만 읽었다. request header와 자격 증명은 로그에 남기지 않았다.
bash infrastructure/networking/traefik/scripts/observe-client-host.sh
확인 결과는 다음과 같다.
ClientHost: 10.42.0.1
Minimum trusted CIDR: 10.42.0.1/32
Pod CIDR 전체, loopback 전체 또는 LAN CIDR을 추정해 넓히지 않고 이 한 주소만 trust overlay에 기록했다. 적용 명령과 승인 문자열은 다음과 같았다.
bash infrastructure/networking/traefik/scripts/apply-trust.sh \
--observed-client-host '10.42.0.1' \
--execute
APPLY default TRUST 10.42.0.1/32
현재 runtime에는 다음 trust 인자 하나만 존재한다.
--entryPoints.web.forwardedHeaders.trustedIPs=10.42.0.1/32
entryPoints.websecure.forwardedHeaders.*와 forwardedHeaders.insecure 인자는 없다.
적용 후 /assets/site-manifest.json의 start_url과 두 icon URL이 모두 HTTPS로
확인됐고 Gitea health의 status·database·cache 검사도 통과했다.
검증과 상태 전환
소스와 세 overlay의 정적 계약은 다음 명령으로 검증한다.
cd /home/donghyeon/workspace/platform
bash infrastructure/networking/traefik/scripts/validate.sh
검증기는 다음 조건을 강제한다.
- 세 overlay의 Service가
NodePort,externalTrafficPolicy: Cluster,30080/30443을 유지한다. - observe와 trust access log는 JSON이고 request header를 기록하지 않는다.
- trust CIDR은 관측한 단일 host
/32또는/128형식이다. forwardedHeaders.insecure,websecuretrust,LoadBalancer가 없다.
새 설치처럼 HelmChartConfig가 없거나 이미 observe 상태인 경우에는 다음 guarded
script로 observe 구성을 확인하거나 적용한다.
bash infrastructure/networking/traefik/scripts/apply-observe.sh --execute
# 승인: APPLY <현재-context> OBSERVE
현재 live trust에서 다시 관측하려면 먼저 아래 표준 롤백으로 observe를 적용한 뒤
probe를 실행한다. trust 상태에서 apply-observe.sh를 바로 실행하지 않는다.
bash infrastructure/networking/traefik/scripts/rollback-to-observe.sh --execute
bash infrastructure/networking/traefik/scripts/observe-client-host.sh
관측값이 달라지면 기존 CIDR을 넓히지 말고 trust patch를 exact host CIDR로 갱신한 뒤
apply-trust.sh를 실행한다. Chart, 이미지, context, API server, NodePort 경계 또는
재관측 값이 기대와 다르면 스크립트가 적용을 중단한다.
롤백
trust만 제거하고 JSON access log를 남기는 표준 롤백은 observe overlay를 적용한다.
bash infrastructure/networking/traefik/scripts/rollback-to-observe.sh --execute
# 승인: ROLLBACK <현재-context> OBSERVE
observe 적용 자체가 실패하면 apply-observe.sh가 NodePort-only baseline overlay를
적용한다. access log와 trust를 모두 제거해야 하는 명시적 유지보수에서는 live
context와 대상 overlay를 재확인한 뒤 baseline을 적용한다.
kubectl apply --kustomize \
infrastructure/networking/traefik/overlays/baseline
어느 경우에도 HelmChartConfig를 삭제해 롤백하지 않는다. baseline 또는 observe를
적용해 Service 30080/30443을 계속 desired state로 남긴다.
k3s·kube-proxy·CNI·Service traffic policy나 Host Nginx 경로를 바꾸면
ClientHost가 달라질 수 있다. 이때는 observe로 돌아가 다시 관측하고 정확한 한
주소만 trust한다.
종단 간 인수 조건
다음 로컬 검사는 Host Nginx와 Traefik을 함께 통과해야 한다.
curl --fail-with-body \
--resolve git.learn.hyeonworks.com:443:127.0.0.1 \
https://git.learn.hyeonworks.com/api/healthz
curl --fail-with-body \
--resolve git.learn.hyeonworks.com:443:127.0.0.1 \
https://git.learn.hyeonworks.com/assets/site-manifest.json |
jq -e '
.start_url == "https://git.learn.hyeonworks.com/" and
([.icons[].src | startswith("https://git.learn.hyeonworks.com/")] | all)
'
별도 LAN 클라이언트에서 192.168.0.107:30080/30443이 거부되는지 다시 확인하고,
독립 외부망에서는 공개 HTTP→HTTPS redirect와 두 서비스의 HTTPS 응답을 검사한다.
서버에서 공인 FQDN으로 향하는 NAT hairpin timeout만으로 공개 실패를 판정하지 않는다.
구현 근거는 k3s HelmChartConfig, k3s 내장 Traefik, Traefik forwarded headers, Traefik access log, Traefik Chart 40.1.0 values다.
첫 실패, 수동 복구, 영구 보완, 관측값과 trust 적용의 전체 명령·출력은 중앙 실행 기록에 보존한다.