Add platform infrastructure configuration

This commit is contained in:
donghyeon-ka
2026-08-28 17:35:41 +09:00
parent fa76531e5b
commit 16c337bcc9
302 changed files with 83259 additions and 1 deletions
+287
View File
@@ -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)에
보존한다.