docs: record proxy-bypass closure with before and after evidence
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
3af52bb66a
commit
e1ba9c5626
@@ -0,0 +1,48 @@
|
|||||||
|
수집 시각: 2026-09-03 16:16:48 KST
|
||||||
|
주제: 프록시 우회 경로 차단 (NetworkPolicy)
|
||||||
|
|
||||||
|
=== [1] 차단 전 — 클러스터 안에서 앱에 직접 요청 ===
|
||||||
|
명령: kubectl run ... -- curl http://echo:8081/api/echo \
|
||||||
|
-H 'X-Forwarded-Proto: https' -H 'X-Forwarded-Host: evil.example.com' -H 'X-Forwarded-For: 1.2.3.4'
|
||||||
|
|
||||||
|
scheme https
|
||||||
|
secure True
|
||||||
|
serverName evil.example.com ← 위조 성공
|
||||||
|
remoteAddr 1.2.3.4 ← 위조 성공
|
||||||
|
requestUrl https://evil.example.com/api/echo
|
||||||
|
|
||||||
|
★ Traefik 을 거치지 않으면 헤더 위조가 그대로 통한다.
|
||||||
|
trustedIPs 와 internalProxies 가 둘 다 '대역'을 믿기 때문.
|
||||||
|
|
||||||
|
=== [2] 적용한 것 ===
|
||||||
|
deploy/lab/k8s/traefik-forwarded-headers.yaml — 192.168.122.0/24 제거
|
||||||
|
"--entryPoints.web.forwardedHeaders.trustedIPs=10.42.0.0/16"
|
||||||
|
"--entryPoints.websecure.forwardedHeaders.trustedIPs=10.42.0.0/16"
|
||||||
|
|
||||||
|
deploy/lab/k8s/echo-network-policy.yaml — Traefik 파드에서만 8081 허용
|
||||||
|
[{"from":[{"namespaceSelector":{"matchLabels":{"kubernetes.io/metadata.name":"kube-system"}},"podSelector":{"matchLabels":{"app.kubernetes.io/name":"traefik"}}}],"ports":[{"port":8081,"protocol":"TCP"}]},{"from":[{"ipBlock":{"cidr":"10.42.0.1/32"}},{"ipBlock":{"cidr":"10.42.1.1/32"}}],"ports":[{"port":8081,"protocol":"TCP"}]}]
|
||||||
|
|
||||||
|
=== [3] 차단 후 — 정상 경로 (계속 동작해야 함) ===
|
||||||
|
x-forwarded-proto https
|
||||||
|
x-forwarded-port 443
|
||||||
|
x-forwarded-host app1.hyeonworks.com
|
||||||
|
x-real-ip 100.123.124.30
|
||||||
|
x-forwarded-server traefik-5d6fcf895-wpfhr
|
||||||
|
--- 앱이 해석한 값
|
||||||
|
scheme https
|
||||||
|
secure True
|
||||||
|
serverName app1.hyeonworks.com
|
||||||
|
serverPort 443
|
||||||
|
remoteAddr 100.123.124.30
|
||||||
|
localAddr 10.42.0.14
|
||||||
|
requestUrl https://app1.hyeonworks.com/api/echo
|
||||||
|
|
||||||
|
=== [4] 차단 후 — 우회 시도 ===
|
||||||
|
HTTP 000 / curl exit 7
|
||||||
|
HTTP 000 / curl exit 7
|
||||||
|
|
||||||
|
★ curl exit 7 = Failed to connect. 연결 자체가 성립하지 않는다.
|
||||||
|
|
||||||
|
=== [5] 파드 건강 상태 (probe 가 차단되지 않았는지) ===
|
||||||
|
echo-54dbd94986-8jmdb 1/1 Running restarts=0
|
||||||
|
echo-54dbd94986-lfltk 1/1 Running restarts=0
|
||||||
@@ -164,3 +164,36 @@ curl -s http://192.168.122.11/api/echo \
|
|||||||
업그레이드하는 동안 구 Traefik 파드가 함께 살아 있다.** 이 시점에 측정하면
|
업그레이드하는 동안 구 Traefik 파드가 함께 살아 있다.** 이 시점에 측정하면
|
||||||
옛 파드가 응답해 "고쳤는데 안 바뀌었다"고 오해하게 된다. `x-forwarded-server`
|
옛 파드가 응답해 "고쳤는데 안 바뀌었다"고 오해하게 된다. `x-forwarded-server`
|
||||||
값의 파드 이름으로 어느 파드가 응답했는지 확인해야 한다.
|
값의 파드 이름으로 어느 파드가 응답했는지 확인해야 한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 프록시 우회 차단 (2026-09-03 16:16 KST)
|
||||||
|
|
||||||
|
`05-networkpolicy.txt`
|
||||||
|
|
||||||
|
헤더 신뢰를 켠 뒤 남아 있던 구멍을 실증하고 막았다.
|
||||||
|
|
||||||
|
**차단 전** — 클러스터 안에서 Traefik을 우회해 앱에 직접 요청하면
|
||||||
|
`serverName: evil.example.com`, `remoteAddr: 1.2.3.4`로 **위조가 성립했다.**
|
||||||
|
|
||||||
|
**적용한 것**
|
||||||
|
|
||||||
|
| 파일 | 변경 |
|
||||||
|
|---|---|
|
||||||
|
| `traefik-forwarded-headers.yaml` | `192.168.122.0/24` 제거 (SNAT 때문에 도달 불가한 대역) |
|
||||||
|
| `echo-network-policy.yaml` | Traefik 파드에서만 8081 허용 (라벨 기준) |
|
||||||
|
|
||||||
|
**차단 후**
|
||||||
|
|
||||||
|
```
|
||||||
|
정상 경로 scheme=https, remoteAddr=100.123.124.30 동작
|
||||||
|
우회 시도 HTTP 000 / curl exit 7 연결 거부
|
||||||
|
파드 상태 1/1 Running, restarts=0 probe 정상
|
||||||
|
```
|
||||||
|
|
||||||
|
`exit 7`은 curl의 "Failed to connect"다. HTTP 403이 아니라
|
||||||
|
**TCP 연결 자체가 성립하지 않았다**는 뜻이다.
|
||||||
|
|
||||||
|
`restarts=0`이 중요하다. NetworkPolicy에서 kubelet probe 경로를 빠뜨리면
|
||||||
|
probe가 실패해 파드가 재시작 루프에 빠진다. 노드의 cni0 주소
|
||||||
|
(`10.42.0.1`, `10.42.1.1`)를 `/32`로 허용해 이를 피했다.
|
||||||
|
|||||||
@@ -714,24 +714,82 @@ curl -s https://app1.hyeonworks.com/api/echo \
|
|||||||
`$proxy_add_x_forwarded_for`(덧붙이기)로 바꾸면 클라이언트가 넣은 값이
|
`$proxy_add_x_forwarded_for`(덧붙이기)로 바꾸면 클라이언트가 넣은 값이
|
||||||
사슬 앞부분에 남아 신뢰 경계가 무너진다.
|
사슬 앞부분에 남아 신뢰 경계가 무너진다.
|
||||||
|
|
||||||
### 잔여 위험 — 프록시 우회 경로
|
### 프록시 우회 경로 차단
|
||||||
|
|
||||||
앱이 헤더를 신뢰하게 됐으므로, **Traefik을 거치지 않고 파드에 직접 도달할 수
|
앱이 헤더를 신뢰하게 되면 **Traefik을 거치지 않고 파드에 직접 도달할 수
|
||||||
있으면 위조가 가능하다.** 클러스터 안에서는 Service ClusterIP로 접근할 수
|
있는 경로가 곧 구멍**이 된다. 클러스터 안에서는 Service ClusterIP로 접근할
|
||||||
있다.
|
수 있으므로 실제로 위조가 성립했다.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 클러스터 내부에서 (위조 가능함을 확인)
|
kubectl -n header-lab run t --rm -i --restart=Never --image=curlimages/curl -- \
|
||||||
kubectl -n header-lab run t --rm -it --image=curlimages/curl --restart=Never -- \
|
curl -s http://echo:8081/api/echo \
|
||||||
curl -s http://echo:8081/api/echo -H 'X-Forwarded-Proto: https' -H 'X-Forwarded-For: 1.2.3.4'
|
-H 'X-Forwarded-Proto: https' -H 'X-Forwarded-Host: evil.example.com' -H 'X-Forwarded-For: 1.2.3.4'
|
||||||
```
|
```
|
||||||
|
|
||||||
**NetworkPolicy로 Traefik에서 오는 트래픽만 허용하는 것이 정석이다.**
|
```
|
||||||
이 클러스터는 kube-router 내장 컨트롤러가 있어 적용 가능하다.
|
serverName evil.example.com ← 위조 성공
|
||||||
아직 적용하지 않았으며, `feature/keycloak-header-spoofing-defense`에서
|
remoteAddr 1.2.3.4 ← 위조 성공
|
||||||
다룰 항목이다.
|
requestUrl https://evil.example.com/api/echo
|
||||||
|
```
|
||||||
|
|
||||||
---
|
**두 신뢰 설정이 모두 "대역"을 믿기 때문**이다.
|
||||||
|
|
||||||
|
| 계층 | 신뢰 범위 | 지정 방식 |
|
||||||
|
|---|---|---|
|
||||||
|
| Traefik `trustedIPs` | 파드 대역 전체 | IP 대역 |
|
||||||
|
| 앱 Tomcat `internalProxies` | 사설 대역 전체 (기본 정규식) | IP 정규식 |
|
||||||
|
|
||||||
|
IP로는 Traefik을 특정할 수 없다. **파드 IP가 재시작마다 바뀌기 때문**이다
|
||||||
|
(측정 중 실제로 `10.42.0.8` → `10.42.1.12`로, 노드까지 옮겨갔다).
|
||||||
|
|
||||||
|
**해결 — NetworkPolicy는 IP가 아니라 라벨로 지정한다.**
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| 저장소 파일 | `deploy/lab/k8s/echo-network-policy.yaml` |
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
ingress:
|
||||||
|
- from:
|
||||||
|
- namespaceSelector:
|
||||||
|
matchLabels:
|
||||||
|
kubernetes.io/metadata.name: kube-system
|
||||||
|
podSelector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/name: traefik # ← IP 가 아니라 라벨
|
||||||
|
ports:
|
||||||
|
- protocol: TCP
|
||||||
|
port: 8081
|
||||||
|
```
|
||||||
|
|
||||||
|
`namespaceSelector`와 `podSelector`를 **같은 리스트 항목**에 두면 AND로
|
||||||
|
결합된다. 별개 항목으로 나누면 OR이 되어 kube-system 전체가 허용되므로
|
||||||
|
주의한다.
|
||||||
|
|
||||||
|
**kubelet probe를 위한 규칙이 별도로 필요하다.** readiness/liveness는 파드가
|
||||||
|
아니라 노드에서 오므로 위 규칙에 걸리지 않는다. 빠뜨리면 probe가 실패하고
|
||||||
|
**파드가 재시작 루프에 빠진다.**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- from:
|
||||||
|
- ipBlock: { cidr: 10.42.0.1/32 } # kc-lab-1 의 cni0
|
||||||
|
- ipBlock: { cidr: 10.42.1.1/32 } # kc-lab-2 의 cni0
|
||||||
|
```
|
||||||
|
|
||||||
|
probe의 출발지는 **노드의 flannel 브리지(cni0)** 이고, 각 노드 `/24`의 첫
|
||||||
|
주소다. `/32`로 정확히 지정해야 한다 — `10.42.0.0/16`으로 넓히면 임의의
|
||||||
|
파드가 다시 들어와 정책이 무의미해진다.
|
||||||
|
|
||||||
|
**적용 후 확인**
|
||||||
|
|
||||||
|
```
|
||||||
|
정상 경로 scheme=https, remoteAddr=100.123.124.30 계속 동작
|
||||||
|
우회 시도 HTTP 000 / curl exit 7 연결 자체가 거부됨
|
||||||
|
파드 상태 1/1 Running, restarts=0 probe 정상
|
||||||
|
```
|
||||||
|
|
||||||
|
**"헤더를 믿는다"와 "앞에 반드시 프록시가 있다"는 한 쌍이다.**
|
||||||
|
앞의 것만 하면 이 구멍이 남는다.
|
||||||
|
|
||||||
## 12. 증거
|
## 12. 증거
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user