Add platform infrastructure configuration
This commit is contained in:
@@ -0,0 +1,287 @@
|
||||
# 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 상태를 다시 확인한다.
|
||||
|
||||
```sh
|
||||
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`과 `web` entrypoint를 명시한다.
|
||||
- 공개 TLS는 Host Nginx가 종료하므로 애플리케이션 Ingress에 `spec.tls`를 넣지 않는다.
|
||||
- `websecure` NodePort `30443`은 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 연결에 성공했다는 뜻이므로 실패다.
|
||||
|
||||
```sh
|
||||
nc -vz -w 3 192.168.0.107 30080
|
||||
nc -vz -w 3 192.168.0.107 30443
|
||||
```
|
||||
|
||||
반대로 서버 loopback에서는 두 포트가 listening 상태여야 하며 Host 기반 Gitea
|
||||
health가 통과해야 한다.
|
||||
|
||||
```sh
|
||||
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의 역할
|
||||
|
||||
```text
|
||||
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가
|
||||
발생했다.
|
||||
|
||||
```text
|
||||
기존: NodePort web=30080, websecure=30443
|
||||
변경: LoadBalancer web=31251, websecure=30997
|
||||
```
|
||||
|
||||
이어진 loopback listener 검사가 실패했다. 당시 실패 처리도 새
|
||||
`HelmChartConfig`를 삭제했을 뿐, desired state에 없던 수동 Service spec은 복원하지
|
||||
못했다. Gitea·PostgreSQL·PV/PVC는 건드리지 않고 Traefik Service만 다음 명령으로
|
||||
즉시 원래 경계에 복구했다.
|
||||
|
||||
```sh
|
||||
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를 선언한다.
|
||||
- `baseline` overlay를 추가해 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와 자격 증명은
|
||||
로그에 남기지 않았다.
|
||||
|
||||
```sh
|
||||
bash infrastructure/networking/traefik/scripts/observe-client-host.sh
|
||||
```
|
||||
|
||||
확인 결과는 다음과 같다.
|
||||
|
||||
```text
|
||||
ClientHost: 10.42.0.1
|
||||
Minimum trusted CIDR: 10.42.0.1/32
|
||||
```
|
||||
|
||||
Pod CIDR 전체, loopback 전체 또는 LAN CIDR을 추정해 넓히지 않고 이 한 주소만 trust
|
||||
overlay에 기록했다. 적용 명령과 승인 문자열은 다음과 같았다.
|
||||
|
||||
```sh
|
||||
bash infrastructure/networking/traefik/scripts/apply-trust.sh \
|
||||
--observed-client-host '10.42.0.1' \
|
||||
--execute
|
||||
```
|
||||
|
||||
```text
|
||||
APPLY default TRUST 10.42.0.1/32
|
||||
```
|
||||
|
||||
현재 runtime에는 다음 trust 인자 하나만 존재한다.
|
||||
|
||||
```text
|
||||
--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의 정적 계약은 다음 명령으로 검증한다.
|
||||
|
||||
```sh
|
||||
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`, `websecure` trust, `LoadBalancer`가 없다.
|
||||
|
||||
새 설치처럼 `HelmChartConfig`가 없거나 이미 observe 상태인 경우에는 다음 guarded
|
||||
script로 observe 구성을 확인하거나 적용한다.
|
||||
|
||||
```sh
|
||||
bash infrastructure/networking/traefik/scripts/apply-observe.sh --execute
|
||||
# 승인: APPLY <현재-context> OBSERVE
|
||||
```
|
||||
|
||||
현재 live trust에서 다시 관측하려면 먼저 아래 표준 롤백으로 observe를 적용한 뒤
|
||||
probe를 실행한다. trust 상태에서 `apply-observe.sh`를 바로 실행하지 않는다.
|
||||
|
||||
```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를 적용한다.
|
||||
|
||||
```sh
|
||||
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을 적용한다.
|
||||
|
||||
```sh
|
||||
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을 함께 통과해야 한다.
|
||||
|
||||
```sh
|
||||
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](https://docs.k3s.io/helm),
|
||||
[k3s 내장 Traefik](https://docs.k3s.io/networking/networking-services),
|
||||
[Traefik forwarded headers](https://doc.traefik.io/traefik/reference/install-configuration/entrypoints/),
|
||||
[Traefik access log](https://doc.traefik.io/traefik/observe/logs-and-access-logs/),
|
||||
[Traefik Chart 40.1.0 values](https://github.com/traefik/traefik-helm-chart/blob/v40.1.0/traefik/values.yaml)다.
|
||||
|
||||
첫 실패, 수동 복구, 영구 보완, 관측값과 trust 적용의 전체 명령·출력은
|
||||
[중앙 실행 기록](../../../../docs/platform/runbooks/2026-07-23-traefik-forwarded-header-trust-boundary.md)에
|
||||
보존한다.
|
||||
Reference in New Issue
Block a user