🏠 목록 AuthorizationPolicy 멘탈모델 — inbound 적용·mTLS identity·HTTP vs TCP·egress (출처: ChatGPT "Istio 운영 노하우" 대화 + Istio 1.30 공식 문서) 📄 MD 원본 🌓 테마
istiosecurityauthorizationpolicymtlsspiffeegressrbac

AuthorizationPolicy 멘탈모델 — inbound 적용·mTLS identity·HTTP vs TCP·egress (출처: ChatGPT "Istio 운영 노하우" 대화 + Istio 1.30 공식 문서)

ℹ 이 문서가 다루는 것

AuthorizationPolicy를 "왜 그렇게 평가되는가"의 멘탈모델로 다룬다. 결론부터: 이 정책은 client의 outbound를 막는 게 아니라, selector가 고른 workload의 inbound listener에 RBAC filter를 심어 "그 서버로 들어오는 요청"을 검사한다. 검사에 쓸 수 있는 조건은 그 inbound Envoy가 실제로 본 것 — peer cert(mTLS identity)와 복호화된 L7 — 으로만 한정된다. 이 한 장면에서 평가 순서, mTLS 의존성, egress passthrough 한계, TCP DENY 사고가 전부 따라 나온다. 리소스 정의는 mTLS/SPIFFE 신원·세 리소스 역할 분담을 정본으로 참조한다.

01. 배경 — authz는 어떤 문제를 푸는가, 왜 mTLS가 전제인가

먼저 "왜 이 리소스가 존재하나"부터. mesh 안에서 서비스 A가 서비스 B를 호출할 때, B는 한 가지 질문에 답해야 한다: "지금 나에게 들어온 이 요청을 처리해도 되나?" network reachability(누가 나에게 패킷을 보낼 수 있나)는 L3/L4 방화벽의 영역이고, AuthorizationPolicy는 그보다 한 층 위 — application 수준의 인가(authorization) — 를 담당한다. "POST /admin은 admin SA만", "payments namespace에서 온 GET만" 같은 규칙이다.

그러면 B는 "누가 호출했는가"를 무엇으로 믿을까? 여기서 mTLS가 전제로 들어온다. 평문 네트워크에서 server가 client를 식별할 수단은 전부 약하다.

IP?        믿기 어려움. NAT, proxy, pod 재생성, spoofing 문제.
Header?    app이 위조 가능.
JWT?       end-user identity에는 좋지만 service-to-service identity와는 별도.
mTLS cert? sidecar가 workload identity로 발급받고 handshake에서 검증.

그래서 Istio의 service-to-service 인가는 mTLS로 증명된 workload identity 위에 선다. authz가 보는 "누가"의 정본 출처는 peer certificate의 SPIFFE SAN이다. 이 의존 관계 — mTLS가 없으면 principal 조건을 쓸 수 없다 — 가 이 문서 전체의 골격이고, §03·§04에서 메커니즘으로 풀어낸다. SPIFFE 형식·SDS provisioning의 디테일은 mTLS/SPIFFE 신원이 정본이다.

이 문서는 사용자가 "AuthorizationPolicy를 잘 모르겠다"고 한 지점에서 출발한다. 그래서 정의 → 평가 → identity 토대 → 조건 경계 → egress → TCP 함정 순으로, 운영에서 실제로 터지는 사고까지 한 줄로 잇는다.

02. anchor — authz는 "보호받는 서버"의 inbound를 본다

가장 많이 헷갈리는 지점부터 못 박는다. 머리에 하나만 담는다면 이것이다.

★ 한 문장 멘탈모델

AuthorizationPolicy는 selector가 고른 workload/gateway의 inbound listener에 RBAC filter를 심어, "그 서버로 들어오는(inbound) 요청"을 허용/차단한다. 정책의 target은 보호받는 서버 쪽이지, client의 outbound를 막는 게 아니다.

평가가 일어나는 지점을 그림으로 박으면 나머지가 따라온다 — 검사는 server측 sidecar의 inbound에서 일어난다.

client workloadoutbound sidecarinbound sidecarauthz 평가server appAuthorizationPolicy정책 부착
그림 1. AuthorizationPolicy는 server 쪽 inbound sidecar에 부착되어 거기서 authz를 평가. 즉 정책의 평가 지점은 항상 수신측 사이드카.

예시 정책으로 이 anchor를 확인한다.

apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
  name: allow-productpage
  namespace: default
spec:
  selector:
    matchLabels:
      app: reviews          # 이 정책의 보호 대상 = reviews
  action: ALLOW
  rules:
  - from:
    - source:
        principals:
        - cluster.local/ns/default/sa/productpage
    to:
    - operation:
        methods: ["GET"]
        paths: ["/reviews/*"]

이 정책의 의미는 "reviews workload로 들어오는 요청 중 source principal이 productpage SA이고 GET /reviews/*인 것만 허용"이다.

이 한 줄을 잡으면 "왜 내 정책이 안 먹지?"의 절반이 해결된다. 대부분 정책을 잘못된 쪽(client) workload에 selector로 붙였기 때문이다. selector가 client를 가리키면 그 client의 inbound만 제어할 뿐 outbound는 손대지 못한다 — 이 함정은 §06에서 따로 다룬다.

03. 메커니즘 (1) — 부착 대상 결정과 평가 순서

anchor("inbound에 RBAC filter를 심는다")를 두 갈래로 푼다: 어느 Envoy에 심나(부착), 심긴 rule이 어떻게 판정하나(평가).

부착 대상 — namespace + selector (+ targetRefs)

정책이 어떤 Envoy에 부착되는지는 다음으로 결정된다.

metadata.namespace      → 그 namespace의 workload에 적용
+ selector.matchLabels  → 해당 label workload로 좁힘
(+ targetRefs)          → gateway 등 특정 리소스 지정

표현을 정확히 하면 "sidecar에 정책을 적용한다"가 아니라, selector → istiod → Envoy inbound listener의 RBAC filter라는 변환이다.

AuthorizationPolicy selector가 특정 workload를 선택한다
→ istiod가 그 workload의 Envoy inbound listener에 RBAC filter를 삽입한다
→ 그 workload로 들어오는 traffic을 검사한다

이 변환은 xDS의 LDS로 내려간다. 검증하려면 istioctl proxy-config listener <pod> -o json에서 RBAC filter를 봐야 함 (→ xDS 계층과 진단). 즉 AuthorizationPolicy는 추상 정책이 아니라 Envoy filter chain에 박히는 구체물이고, "안 먹는다"는 거의 항상 이 filter가 엉뚱한 listener에 박혔거나 아예 안 박힌 상태다.

평가 순서 — CUSTOM → DENY → ALLOW

한 inbound 요청이 RBAC filter를 통과할 때의 판정 규칙이다.

✓ 핵심 평가 규칙
  1. CUSTOM (ext_authz 등) 먼저 평가
  2. DENY rule 평가 — 매칭되면 즉시 거부
  3. ALLOW rule 평가
  4. ALLOW policy가 하나도 없으면 → 기본 허용
  5. ALLOW policy가 하나라도 있으면 → 매칭되는 요청만 허용 (나머지는 거부)

AUDIT은 이 ALLOW/DENY 판정 트랙과 별개다. AUDIT rule이 매칭돼도 허용/차단 결정에는 영향을 주지 않고 매칭 사실을 로그로만 남긴다(병렬·비간섭 평가 트랙). 따라서 위 순서에 끼지 않고 곁가지로 동작한다.

4번과 5번의 비대칭이 운영에서 중요하다. ALLOW 정책을 처음 하나 추가하는 순간, 그 workload는 "기본 허용"에서 "명시 허용 외 전부 차단"으로 바뀐다. 좁은 ALLOW 하나를 잘못 걸면 멀쩡하던 트래픽이 한꺼번에 막힌다 — 이 "첫 ALLOW의 비대칭"은 STRICT 전환과 겹칠 때 가장 위험하다(§정리/What you might be missing).

요청 1건이 어떻게 판정되는지 흐름으로 보면 이렇다.

inbound requestCUSTOM deny?DENY match?ALLOW exists?ALLOW match?denydenyallow (default)allowdenyyesnoyesnonoyesyesnoAUDIT match → logverdict 무관parallel
그림 2. AuthZ 우선순위: CUSTOM deny → DENY rule → (ALLOW 정책 없으면 기본 allow / 있으면 match면 allow, 아니면 deny). AUDIT은 병렬로 로깅만 하고 판정에 영향 없음.

04. 메커니즘 (2) — mTLS가 identity를 만들고, 그게 정책 입력이 된다

principals 정책을 이해하려면 mTLS의 본질을 잡아야 한다. 핵심은 "암호화"가 아니라 "증명"이다.

★ 한 문장 멘탈모델

mTLS는 단순 암호화 옵션이 아니라, Istio에서 workload identity를 증명하고 그 identity로 정책을 적용하는 기반이다. authz의 principal 조건은 이 증명된 identity를 입력으로 받는다.

일반 TLS는 단방향 인증이다. client만 server를 검증한다.

Client → Server
Client: 너 진짜 server 맞아?
Server: 응, 내 인증서 봐.
Client: OK. 암호화 통신하자.

mTLS는 양방향이다. 서버도 client cert를 요구하고, 양측이 한 handshake 안에서 서로의 identity를 검증한다. 이 handshake 한 번에서 server측 Envoy가 "peer가 누구인가"를 확정하기 때문에, 이후 RBAC filter가 그 값을 그대로 쓸 수 있다.

Client sidecarServer sidecarTLS 1.3 mutual handshake (한 RTT 내 교환)ClientHelloServerHello, Certificate, CertReqCertificate (client cert)양측 SPIFFE SAN 상호 검증 (동일 handshake 내)Finished — verdict는 이후 RBAC
그림 3. mTLS는 동일 handshake 안에서 양측 SPIFFE SAN을 상호 검증(순차 질문 아님). 인증 성립 후 verdict(ALLOW/DENY)는 별도 RBAC filter에서 결정.

이렇게 교환된 상대 cert의 SPIFFE identity가 authz의 principals 입력이 된다. 서버 입장에서 "client가 누구인가"의 답은 peer cert의 SPIFFE SAN이고, Istio는 이를 Kubernetes ServiceAccount에 1:1로 매핑한다.

cluster.local/ns/default/sa/productpage
cluster.local/ns/payments/sa/payment-api
cluster.local/ns/inference/sa/model-gateway

이 SAN이 곧 source.principal 조건과 비교되는 값이다.

게다가 client side Envoy는 secure naming check까지 수행한다. 즉 "연결 대상 service의 server cert SAN(SPIFFE identity)이, 그 service에 기대되는 service account와 일치하는가"를 검증한다 — DNS/IP가 엉뚱한 workload로 hijack돼도 cert SAN이 기대값과 다르면 거부된다. 이로써 identity 증명은 server측(누가 들어왔나)뿐 아니라 client측(내가 옳은 server에 붙었나)에서도 양방향으로 닫힌다.

한 줄 요약 — 이것이 principals:로 시작하는 모든 정책의 토대다.

mTLS = 암호화 + 상대 workload identity 증명
AuthorizationPolicy = 그 identity를 보고 허용/차단

05. 메커니즘 (3) — "Envoy가 본 것"이 조건의 경계를 긋는다

§04에서 따라 나오는 핵심 원리: Envoy는 자신이 실제로 본 것만으로 판단한다. mTLS handshake가 없으면 peer cert가 없으니 identity를 모르고, TLS를 terminate 안 하면 HTTP를 못 보니 method/path를 모른다. 그래서 authz 조건은 "peer certificate가 있어야만 쓸 수 있는 것"과 "L4/TLS 정보만으로 되는 것"으로 갈린다. 이 경계를 모르면 mTLS 없는 구간에 principal 정책을 걸어놓고 "왜 안 먹지"로 헤맨다.

카테고리 조건 키 mTLS 필요? 추가 전제
workload identity source.principal 필요 peer cert에서 파생
source.namespace 필요 peer cert에서 파생
serviceAccounts / trustDomain 필요 peer cert에서 파생
L4 source.ip 불필요
destination.port (to.operation.ports) 불필요
TLS metadata connection.sni TLS 필요 TLS ClientHello가 보여야 함
L7 (HTTP) methods / paths / request.headers TLS terminate 필요 proxy가 HTTP를 복호화해 봐야 함
end-user JWT request.auth.* RequestAuthentication + HTTP TLS terminate 필요

표는 결국 "Envoy가 그 정보를 볼 수 있는 layer"로 정렬돼 있다 — L4(언제나 보임) → TLS metadata(ClientHello만 보면 됨) → L7(복호화해야 보임) → identity(peer cert가 있어야 보임).

⚠ 흔한 착각

"mTLS가 없으면 authz를 아예 못 쓴다"는 틀림. source.ip, destination.port, connection.sni, HTTP method/path/header(평문 또는 terminate된 TLS) 기반 정책은 가능하다. 불가능한 것은 workload identity(principal/namespace/serviceAccounts) 기반 정책뿐이다 — 이것만 peer certificate를 요구한다.

검증법: peer cert 유무·HTTP 가시성은 istioctl proxy-config listener <pod> -o json의 inbound filter chain에서 확인한다(mTLS면 transport_socket에 TLS context, RBAC filter에 principal 조건이 보임). 실제 거부 원인은 Envoy access log의 RBAC 거부(RBAC: access denied)와 대조해 어느 rule이 걸렸는지 좁힌다.

06. anchor 적용 — client에 걸어도 outbound는 안 막힌다

이제 anchor를 실전 함정에 적용한다. 첫 번째: "이 서비스가 외부로 못 나가게" 하려고 client workload에 정책을 걸면? 답은 outbound는 안 막힌다.

metadata:
  namespace: app
spec:
  selector:
    matchLabels:
      app: client          # client라는 workload를 selector로 골라도...

이 정책은 "client app으로 들어오는 inbound traffic"을 제어한다. client app이 api.vendor.com으로 나가는 outbound를 직접 막지 못한다. selector가 고른 workload의 inbound만 검사하기 때문이다(§02 anchor 그대로).

⚠ 함정

AuthorizationPolicy를 client workload에 걸어서 "이 서비스가 외부로 못 나가게" 하려는 시도는 동작하지 않는다. authz는 outbound ACL이 아니다.

client의 egress를 통제하려면 별도 메커니즘 조합이 필요하다 — 그리고 그 조합 안에서 authz는 다른 위치(egress gateway의 inbound)로 다시 등장한다(§07).

1. outboundTrafficPolicy: REGISTRY_ONLY     # 미등록 외부 차단
2. ServiceEntry로 허용 목적지만 등록
3. Sidecar egress.hosts로 config scope 제한
4. egress gateway로 외부 통신 강제
5. egress gateway workload에 AuthorizationPolicy 적용  ← 여기서 authz가 다시 등장
6. Kubernetes NetworkPolicy / CNI egress policy
7. 별도 firewall / NAT gateway policy

주의: Sidecar egress.hosts프록시 설정량을 줄이는 scope 장치이지 보안 차단 장치가 아니다. scope 밖 목적지가 반드시 차단되는 건 아니라는 점을 구분해야 함.

07. anchor 적용 — egress gateway도 "하나의 workload"다

5번 항목이 핵심이다. egress gateway는 "mesh 밖으로 나가는 출구"지만, Envoy 관점에서는 그냥 하나의 workload다. anchor를 그대로 적용하면: egress traffic을 authz로 통제하려면 egress gateway workload에 inbound 정책으로 건다. (client의 outbound가 아니라 gateway의 inbound가 평가 지점이다.)

client pod
  outbound sidecar
      ↓
egress gateway service
      ↓
egress gateway pod inbound listener  ← AuthorizationPolicy 적용 가능
      ↓
egress gateway outbound
      ↓
external service
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
  name: allow-specific-client-to-egress-gateway
  namespace: istio-system
spec:
  selector:
    matchLabels:
      istio: egressgateway      # gateway도 하나의 workload
  action: ALLOW
  rules:
  - from:
    - source:
        principals:
        - cluster.local/ns/app/sa/client
    to:
    - operation:
        ports: ["443"]

의미: "egressgateway로 들어오는 요청 중 source principal이 app/client SA인 것만 허용". 이것이 가능하려면 — §04의 원리상 — sidecar↔egress gateway 구간에 mTLS가 있어야 principal을 알 수 있다. 그 mTLS가 없는 흔한 구성이 다음 절의 passthrough다.

egress TLS passthrough — 두 개의 TLS를 분리해서 보라

운영에서 가장 자주 혼동되는 구성이다. 상황을 이렇게 가정한다.

app → HTTPS 요청 생성 → sidecar → egress gateway → external HTTPS server

egress gateway는 TLS PASSTHROUGH
sidecar ↔ egress gateway 사이에는 Istio mTLS를 쓰지 않음

여기엔 서로 다른 두 개의 TLS가 있고, 이를 섞으면 헷갈린다.

[Istio mTLS]      client sidecar ↔ egress gateway     ← source identity의 출처
[External HTTPS]  app or gateway ↔ api.vendor.com      ← 외부 서버 인증

PASSTHROUGH면 gateway는 application HTTPS를 복호화하지 않고 SNI로만 route해 as-is로 forward한다. "Envoy가 본 것"(§05) 원리대로 가시성이 제한된다.

볼 수 있음:  TCP source/destination, port, TLS ClientHello의 SNI
볼 수 없음:  HTTP method, HTTP path, HTTP header, request body

따라서 mTLS 없는 passthrough 구간에서는 port / source.ip / connection.sni 기반 정책만 가능하다.

apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
  name: allow-sni-to-vendor
  namespace: istio-system
spec:
  selector:
    matchLabels:
      istio: egressgateway
  action: ALLOW
  rules:
  - to:
    - operation:
        ports: ["443"]
    when:
    - key: connection.sni
      values:
      - api.vendor.com

하지만 이 정책은 "어느 service account가 호출했는지"를 검증하지 못한다. mTLS가 없어 peer cert가 없기 때문이다(§04 토대 부재).

source identity를 원하면 — sidecar↔gateway 구간 ISTIO_MUTUAL

client podsidecaregress gatewayinbound: principal 확인external serviceIstio mTLSexternal TLS
그림 4. client→sidecar는 Istio mTLS라 egress gateway의 inbound에서 source.principal을 확인해 authz 가능. gateway→external은 외부 TLS라 메시 신원이 없으므로 principal 기반 정책 불가.

sidecar↔egress gateway 구간을 Istio mTLS로 만들면 gateway가 source workload identity(cluster.local/ns/app/sa/client)를 알 수 있다. 이를 위해 egress gateway를 대상으로 하는 DestinationRule에 ISTIO_MUTUAL을 건다.

apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata:
  name: egressgateway-for-vendor
  namespace: app
spec:
  host: istio-egressgateway.istio-system.svc.cluster.local
  trafficPolicy:
    portLevelSettings:
    - port:
        number: 443
      tls:
        mode: ISTIO_MUTUAL
        sni: api.vendor.com

그러면 gateway에서 principal 기반 정책이 의미를 갖는다.

apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
  name: allow-client-to-egress-vendor
  namespace: istio-system
spec:
  selector:
    matchLabels:
      istio: egressgateway
  action: ALLOW
  rules:
  - from:
    - source:
        principals:
        - cluster.local/ns/app/sa/client
    to:
    - operation:
        ports: ["443"]
    when:
    - key: connection.sni
      values:
      - api.vendor.com
⚠ passthrough의 마지막 함정

gateway가 application HTTPS를 PASSTHROUGH로만 처리하면, mTLS로 source identity를 얻더라도 HTTP method/path/header 기반 정책은 여전히 불가능하다. 그 정보는 복호화해야 보이기 때문. method/path까지 정책으로 쓰려면 gateway가 TLS를 terminate하거나, app이 평문 HTTP로 gateway에 보내고 gateway가 TLS origination을 해야 한다. 단 PASSTHROUGH + TLS origination을 잘못 조합하면 double encryption이 되니 주의.

정리하면 두 축(mTLS 유무 × TLS terminate 유무)이 독립적으로 조건 집합을 깎는다.

sidecar↔egress gateway에 mTLS 없음
  → principal 불가, port/source.ip/connection.sni 만 가능
PASSTHROUGH
  → HTTP method/path/header 불가 (복호화 안 함)
source identity 원함
  → sidecar↔gateway 구간 ISTIO_MUTUAL 필요

관련 실측은 Egress mTLS 리포트 — HTTPS over mTLS (ISTIO_MUTUAL)로 확인.

08. 사고 사례 — HTTP vs TCP DENY 비대칭

이건 실제 운영에서 꽤 자주 터지는 사고 유형이고, §05의 "Envoy가 본 것" 원리가 가장 날카롭게 드러나는 지점이다.

원리: AuthorizationPolicy는 적용 workload의 inbound에 걸린다. operation에 ports를 안 쓰면 모든 port에 열린 것처럼 동작한다. 그런데 TCP에는 HTTP method가 없다. PostgreSQL(5432), Redis(6379), Kafka(9092)는 method라는 개념 자체가 없다.

⚠ 핵심 함정

HTTP 전용 필드(methods/paths/headers)는 TCP traffic에서 무시(skip)된다. 그래서 HTTP 필드만 가진 DENY rule은 TCP에서 "조건이 통째로 사라진 = 조건 없는 DENY"로 붕괴해 그 connection을 거부한다. 반대로 ALLOW는 모든 조건이 매칭돼야 허용하므로, HTTP 필드만 가진 ALLOW는 TCP에서 매칭 자체가 불가능하다 — 같은 필드라도 action에 따라 정반대로 동작한다.

사고 재현

workload가 HTTP/TCP 혼재라고 하자.

app=my-service
ports:
  8080 HTTP
  9432 TCP

이 위험한 DENY 정책:

spec:
  selector:
    matchLabels:
      app: my-service
  action: DENY
  rules:
  - to:
    - operation:
        methods: ["POST"]        # ports 없음!

결과:

HTTP 8080 POST → DENY  (method 매칭)
HTTP 8080 GET  → 안 걸림 (method 불일치 → POST 아님)
TCP 9432       → DENY됨! HTTP 필드(methods)가 TCP에서 skip되어
                 조건 없는 DENY로 붕괴 → connection 거부

HTTP인 GET은 안 걸리는데 method 개념조차 없는 TCP(9432)는 막히는 비대칭이 사고의 본질이다. DB로 가던 9432 TCP가 통째로 끊긴다.

안전 패턴 — DENY엔 항상 ports를 좁힌다

spec:
  selector:
    matchLabels:
      app: my-service
  action: DENY
  rules:
  - to:
    - operation:
        ports: ["8080"]          # HTTP port로 scope 한정
        methods: ["POST"]

결과:

HTTP 8080 POST → DENY
HTTP 8080 GET  → 안 걸림
TCP 9432       → 안 걸림 (port 불일치로 rule 자체가 매칭 안 됨)
⚠ 8080에 TCP/HTTP 혼재 시 여전히 위험

만약 TCP 서비스도 8080에 같이 있다면 위 패턴도 안전하지 않다. TCP 8080 → methods 필드가 skip → 조건 없는 DENY로 붕괴 → DENY. port scope는 맞아도 HTTP 필드가 사라지는 건 동일하므로, 같은 port에 protocol이 섞이면 port scope만으로 안 풀린다.

실전 원칙:

DENY + HTTP attribute(methods/paths/hosts) = 반드시 HTTP port로 scope를 좁힌다.
TCP workload와 HTTP workload가 같은 Pod에 섞이면 AuthorizationPolicy를 더 조심해서 나눈다.
가능하면 selector를 app 단위보다 더 좁힌다.

핵심 정리

멘탈모델 한 줄: selector가 고른 server의 inbound listener에 RBAC filter가 박히고, 그 Envoy가 실제로 본 것(peer cert·복호화된 L7)만으로 판정한다. 평가 순서·mTLS 의존·passthrough 한계·TCP 붕괴가 전부 이 한 문장의 따름정리다.

용도별 결론:

server workload 보호        → server workload selector에 authz
egress gateway 사용 권한 제어 → egress gateway selector에 authz
client의 모든 outbound ACL    → authz만으로 부적합.
                              ServiceEntry/Sidecar/outboundTrafficPolicy/
                              egress gateway/NetworkPolicy 조합

What you might be missing

참조