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
@@ -714,24 +714,82 @@ curl -s https://app1.hyeonworks.com/api/echo \
|
||||
`$proxy_add_x_forwarded_for`(덧붙이기)로 바꾸면 클라이언트가 넣은 값이
|
||||
사슬 앞부분에 남아 신뢰 경계가 무너진다.
|
||||
|
||||
### 잔여 위험 — 프록시 우회 경로
|
||||
### 프록시 우회 경로 차단
|
||||
|
||||
앱이 헤더를 신뢰하게 됐으므로, **Traefik을 거치지 않고 파드에 직접 도달할 수
|
||||
있으면 위조가 가능하다.** 클러스터 안에서는 Service ClusterIP로 접근할 수
|
||||
있다.
|
||||
앱이 헤더를 신뢰하게 되면 **Traefik을 거치지 않고 파드에 직접 도달할 수
|
||||
있는 경로가 곧 구멍**이 된다. 클러스터 안에서는 Service ClusterIP로 접근할
|
||||
수 있으므로 실제로 위조가 성립했다.
|
||||
|
||||
```bash
|
||||
# 클러스터 내부에서 (위조 가능함을 확인)
|
||||
kubectl -n header-lab run t --rm -it --image=curlimages/curl --restart=Never -- \
|
||||
curl -s http://echo:8081/api/echo -H 'X-Forwarded-Proto: https' -H 'X-Forwarded-For: 1.2.3.4'
|
||||
kubectl -n header-lab run t --rm -i --restart=Never --image=curlimages/curl -- \
|
||||
curl -s 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'
|
||||
```
|
||||
|
||||
**NetworkPolicy로 Traefik에서 오는 트래픽만 허용하는 것이 정석이다.**
|
||||
이 클러스터는 kube-router 내장 컨트롤러가 있어 적용 가능하다.
|
||||
아직 적용하지 않았으며, `feature/keycloak-header-spoofing-defense`에서
|
||||
다룰 항목이다.
|
||||
```
|
||||
serverName evil.example.com ← 위조 성공
|
||||
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. 증거
|
||||
|
||||
|
||||
Reference in New Issue
Block a user