---
type: src
tags: [istio, security, authorizationpolicy, mtls, spiffe, egress, rbac]
created: 2026-06-07
---
# AuthorizationPolicy 멘탈모델 — inbound 적용·mTLS identity·HTTP vs TCP·egress (출처: ChatGPT "Istio 운영 노하우" 대화 + Istio 1.30 공식 문서)
> [!abstract] 이 문서가 다루는 것
> AuthorizationPolicy를 "왜 그렇게 평가되는가"의 멘탈모델로 다룬다. 결론부터: **이 정책은 client의 outbound를 막는 게 아니라, selector가 고른 workload의 inbound listener에 RBAC filter를 심어 "그 서버로 들어오는 요청"을 검사한다.** 검사에 쓸 수 있는 조건은 그 inbound Envoy가 *실제로 본 것* — peer cert(mTLS identity)와 복호화된 L7 — 으로만 한정된다. 이 한 장면에서 평가 순서, mTLS 의존성, egress passthrough 한계, TCP DENY 사고가 전부 따라 나온다. 리소스 정의는 [mTLS/SPIFFE 신원](sec__note-mtls-spiffe-identity.html)·[세 리소스 역할 분담](sec__note-security-resource-trio.html)을 정본으로 참조한다.
- **대상환경**: Istio 1.30, sidecar mesh, `security.istio.io/v1`
- **대상독자**: "AuthorizationPolicy가 안 먹는다 / TCP가 통째로 끊겼다"를 겪는 DevOps/SRE
- **범위**: 평가 멘탈모델 — selector 의미, 평가 순서, mTLS 의존 조건, egress/passthrough, HTTP vs TCP 함정
- **선행개념**: sidecar inbound/outbound listener, mTLS, SPIFFE identity(SAN), xDS LDS
## 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를 식별할 수단은 전부 약하다.
```text
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 신원](sec__note-mtls-spiffe-identity.html)이 정본이다.
이 문서는 사용자가 "AuthorizationPolicy를 잘 모르겠다"고 한 지점에서 출발한다. 그래서 정의 → 평가 → identity 토대 → 조건 경계 → egress → TCP 함정 순으로, 운영에서 실제로 터지는 사고까지 한 줄로 잇는다.
## 02. anchor — authz는 "보호받는 서버"의 inbound를 본다
가장 많이 헷갈리는 지점부터 못 박는다. 머리에 하나만 담는다면 이것이다.
> [!key] 한 문장 멘탈모델
> AuthorizationPolicy는 selector가 고른 workload/gateway의 **inbound listener에 RBAC filter를 심어**, "그 서버로 들어오는(inbound) 요청"을 허용/차단한다. 정책의 target은 **보호받는 서버 쪽**이지, client의 outbound를 막는 게 아니다.
평가가 일어나는 지점을 그림으로 박으면 나머지가 따라온다 — 검사는 *server측 sidecar의 inbound*에서 일어난다.
```mermaid
flowchart TD
C[client workload] --> CO[outbound sidecar]
CO --> SI[inbound sidecar
여기서 authz 평가]
SI --> A[server app]
AP[AuthorizationPolicy] -.정책 부착.-> SI
```
예시 정책으로 이 anchor를 확인한다.
```yaml
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/*`인 것만 허용"이다.
- productpage의 outbound를 막는 정책이 **아님**.
- reviews의 inbound를 보호하는 정책**임**.
이 한 줄을 잡으면 "왜 내 정책이 안 먹지?"의 절반이 해결된다. 대부분 정책을 **잘못된 쪽(client) workload에 selector로 붙였기 때문**이다. selector가 client를 가리키면 그 client의 *inbound*만 제어할 뿐 outbound는 손대지 못한다 — 이 함정은 §06에서 따로 다룬다.
## 03. 메커니즘 (1) — 부착 대상 결정과 평가 순서
anchor("inbound에 RBAC filter를 심는다")를 두 갈래로 푼다: **어느 Envoy에 심나(부착)**, **심긴 rule이 어떻게 판정하나(평가)**.
### 부착 대상 — namespace + selector (+ targetRefs)
정책이 어떤 Envoy에 부착되는지는 다음으로 결정된다.
```text
metadata.namespace → 그 namespace의 workload에 적용
+ selector.matchLabels → 해당 label workload로 좁힘
(+ targetRefs) → gateway 등 특정 리소스 지정
```
- namespace에만 두고 selector를 비우면 그 namespace 전체 workload에 적용됨.
- selector를 넣으면 매칭되는 label의 workload로 범위를 좁힘.
- root namespace(istio-system)에 selector 없이 두면 mesh 전역 정책이 됨.
표현을 정확히 하면 "sidecar에 정책을 적용한다"가 아니라, **selector → istiod → Envoy inbound listener의 RBAC filter**라는 변환이다.
```text
AuthorizationPolicy selector가 특정 workload를 선택한다
→ istiod가 그 workload의 Envoy inbound listener에 RBAC filter를 삽입한다
→ 그 workload로 들어오는 traffic을 검사한다
```
이 변환은 xDS의 LDS로 내려간다. 검증하려면 `istioctl proxy-config listener -o json`에서 RBAC filter를 봐야 함 (→ [xDS 계층과 진단](xds__src-xds-layers-and-diagnosis.html)). 즉 AuthorizationPolicy는 추상 정책이 아니라 *Envoy filter chain에 박히는 구체물*이고, "안 먹는다"는 거의 항상 이 filter가 엉뚱한 listener에 박혔거나 아예 안 박힌 상태다.
### 평가 순서 — CUSTOM → DENY → ALLOW
한 inbound 요청이 RBAC filter를 통과할 때의 판정 규칙이다.
> [!tip] 핵심 평가 규칙
> 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건이 어떻게 판정되는지 흐름으로 보면 이렇다.
```mermaid
flowchart TD
REQ[inbound request] --> CUSTOM{CUSTOM deny?}
CUSTOM -->|yes| DENY1[deny]
CUSTOM -->|no| DMATCH{DENY rule match?}
DMATCH -->|yes| DENY2[deny]
DMATCH -->|no| AEXIST{ALLOW policy exists?}
AEXIST -->|no| ALLOW1[allow default]
AEXIST -->|yes| AMATCH{ALLOW rule match?}
AMATCH -->|yes| ALLOW2[allow]
AMATCH -->|no| DENY3[deny]
REQ -.parallel.-> AUDIT{AUDIT rule match?}
AUDIT -->|yes| LOG[log only
no effect on verdict]
```
## 04. 메커니즘 (2) — mTLS가 identity를 만들고, 그게 정책 입력이 된다
`principals` 정책을 이해하려면 mTLS의 본질을 잡아야 한다. 핵심은 "암호화"가 아니라 "증명"이다.
> [!key] 한 문장 멘탈모델
> mTLS는 단순 암호화 옵션이 아니라, Istio에서 **workload identity를 증명하고 그 identity로 정책을 적용하는 기반**이다. authz의 `principal` 조건은 이 증명된 identity를 입력으로 받는다.
일반 TLS는 단방향 인증이다. client만 server를 검증한다.
```text
Client → Server
Client: 너 진짜 server 맞아?
Server: 응, 내 인증서 봐.
Client: OK. 암호화 통신하자.
```
mTLS는 양방향이다. 서버도 client cert를 요구하고, 양측이 한 handshake 안에서 서로의 identity를 검증한다. 이 handshake 한 번에서 server측 Envoy가 "peer가 누구인가"를 확정하기 때문에, 이후 RBAC filter가 그 값을 그대로 쓸 수 있다.
```mermaid
sequenceDiagram
participant C as Client sidecar
participant S as Server sidecar
Note over C,S: TLS 1.3 mutual handshake (한 round-trip 안에서 교환)
C->>S: ClientHello
S->>C: ServerHello, Certificate, CertificateRequest
C->>S: Certificate (client cert)
Note over C,S: 양측이 상대 cert의 SPIFFE SAN을 검증
(순차 질문이 아니라 동일 handshake 내 상호 검증)
S-->>C: Finished — verdict는 이후 RBAC filter에서
```
이렇게 교환된 상대 cert의 **SPIFFE identity**가 authz의 `principals` 입력이 된다. 서버 입장에서 "client가 누구인가"의 답은 peer cert의 SPIFFE SAN이고, Istio는 이를 Kubernetes ServiceAccount에 1:1로 매핑한다.
```text
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:`로 시작하는 모든 정책의 토대다.
```text
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가 있어야 보임).
> [!warning] 흔한 착각
> "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 -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는 안 막힌다**.
```yaml
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 그대로).
> [!warning] 함정
> AuthorizationPolicy를 client workload에 걸어서 "이 서비스가 외부로 못 나가게" 하려는 시도는 동작하지 않는다. authz는 outbound ACL이 아니다.
client의 egress를 통제하려면 별도 메커니즘 조합이 필요하다 — 그리고 그 조합 안에서 authz는 *다른 위치*(egress gateway의 inbound)로 다시 등장한다(§07).
```text
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가 평가 지점이다.)
```text
client pod
outbound sidecar
↓
egress gateway service
↓
egress gateway pod inbound listener ← AuthorizationPolicy 적용 가능
↓
egress gateway outbound
↓
external service
```
```yaml
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를 분리해서 보라
운영에서 가장 자주 혼동되는 구성이다. 상황을 이렇게 가정한다.
```text
app → HTTPS 요청 생성 → sidecar → egress gateway → external HTTPS server
egress gateway는 TLS PASSTHROUGH
sidecar ↔ egress gateway 사이에는 Istio mTLS를 쓰지 않음
```
여기엔 **서로 다른 두 개의 TLS**가 있고, 이를 섞으면 헷갈린다.
```text
[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) 원리대로 가시성이 제한된다.
```text
볼 수 있음: TCP source/destination, port, TLS ClientHello의 SNI
볼 수 없음: HTTP method, HTTP path, HTTP header, request body
```
따라서 mTLS 없는 passthrough 구간에서는 **port / source.ip / connection.sni 기반 정책만** 가능하다.
```yaml
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
```mermaid
flowchart LR
P[client pod] --> SC[sidecar]
SC ==Istio mTLS==> EG[egress gateway
inbound: principal 확인 가능]
EG ==external TLS==> EXT[external service]
```
sidecar↔egress gateway 구간을 Istio mTLS로 만들면 gateway가 source workload identity(`cluster.local/ns/app/sa/client`)를 알 수 있다. 이를 위해 egress gateway를 대상으로 하는 DestinationRule에 `ISTIO_MUTUAL`을 건다.
```yaml
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 기반 정책이 의미를 갖는다.
```yaml
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
```
> [!warning] 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 유무)이 독립적으로 조건 집합을 깎는다.
```text
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)](gw__report-2026-06-08_egress-mtls.html)로 확인.
## 08. 사고 사례 — HTTP vs TCP DENY 비대칭
이건 실제 운영에서 꽤 자주 터지는 사고 유형이고, §05의 "Envoy가 본 것" 원리가 가장 날카롭게 드러나는 지점이다.
원리: AuthorizationPolicy는 적용 workload의 inbound에 걸린다. operation에 `ports`를 안 쓰면 모든 port에 열린 것처럼 동작한다. 그런데 **TCP에는 HTTP method가 없다**. PostgreSQL(5432), Redis(6379), Kafka(9092)는 method라는 개념 자체가 없다.
> [!warning] 핵심 함정
> **HTTP 전용 필드(`methods`/`paths`/`headers`)는 TCP traffic에서 무시(skip)된다.** 그래서 HTTP 필드만 가진 **DENY** rule은 TCP에서 "조건이 통째로 사라진 = 조건 없는 DENY"로 붕괴해 그 connection을 거부한다. 반대로 **ALLOW**는 모든 조건이 매칭돼야 허용하므로, HTTP 필드만 가진 ALLOW는 TCP에서 매칭 자체가 불가능하다 — 같은 필드라도 action에 따라 정반대로 동작한다.
### 사고 재현
workload가 HTTP/TCP 혼재라고 하자.
```text
app=my-service
ports:
8080 HTTP
9432 TCP
```
이 위험한 DENY 정책:
```yaml
spec:
selector:
matchLabels:
app: my-service
action: DENY
rules:
- to:
- operation:
methods: ["POST"] # ports 없음!
```
결과:
```text
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를 좁힌다
```yaml
spec:
selector:
matchLabels:
app: my-service
action: DENY
rules:
- to:
- operation:
ports: ["8080"] # HTTP port로 scope 한정
methods: ["POST"]
```
결과:
```text
HTTP 8080 POST → DENY
HTTP 8080 GET → 안 걸림
TCP 9432 → 안 걸림 (port 불일치로 rule 자체가 매칭 안 됨)
```
> [!warning] 8080에 TCP/HTTP 혼재 시 여전히 위험
> 만약 TCP 서비스도 8080에 같이 있다면 위 패턴도 안전하지 않다. `TCP 8080 → methods 필드가 skip → 조건 없는 DENY로 붕괴 → DENY`. port scope는 맞아도 HTTP 필드가 사라지는 건 동일하므로, 같은 port에 protocol이 섞이면 port scope만으로 안 풀린다.
실전 원칙:
```text
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 붕괴가 전부 이 한 문장의 따름정리다.
- **target = 보호받는 서버**: selector가 고른 workload의 inbound를 검사. client outbound ACL이 아님. "안 먹는다"의 절반은 client에 잘못 붙인 것 — `istioctl proxy-config listener`로 RBAC filter가 어느 listener에 박혔나 확인.
- **평가 순서 CUSTOM → DENY → ALLOW**: ALLOW 0개면 기본 허용, 1개라도 있으면 match만 허용(나머지 거부). AUDIT은 verdict에 무영향 병렬 로그.
- **mTLS = 암호화 + identity(SPIFFE `cluster.local/ns/X/sa/Y`) 증명**: `principal`/`namespace`/`serviceAccounts`의 토대. peer cert가 없으면 이 조건군 전부 불가, `source.ip`/`port`/`connection.sni`/HTTP attr만 가능.
- **조건 경계 = Envoy가 본 layer**: L4(항상) < TLS metadata(ClientHello) < L7(terminate 필요) < identity(peer cert 필요).
- **egress 통제는 gateway의 inbound에 authz**: `selector: istio: egressgateway`. identity 원하면 sidecar↔gateway 구간 `ISTIO_MUTUAL`. PASSTHROUGH면 HTTP method/path/header 불가.
- **TCP 함정**: `DENY + HTTP attr + no ports` = HTTP 필드가 TCP에서 skip → 조건 없는 DENY로 붕괴 → TCP까지 끊김. DENY엔 항상 selector + ports를 좁혀라.
용도별 결론:
```text
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
- **ALLOW 한 개의 비대칭 효과**: ALLOW 정책이 0개일 땐 기본 허용이지만, 첫 ALLOW를 거는 순간 그 workload는 "명시 허용 외 전부 차단"으로 전환된다. STRICT 전환과 함께 ALLOW를 처음 도입할 때 trace 트래픽(health/readiness, telemetry scrape 경로)까지 ALLOW에 포함했는지 확인 필요.
- **authz는 identity를 검증하지만 mTLS 강제는 PeerAuthentication의 몫**: AuthorizationPolicy에 `principals`를 써도, PeerAuthentication이 PERMISSIVE면 평문 요청은 peer cert 없이 들어와 principal 조건에서 그냥 deny될 뿐 "평문 거부"가 명시적으로 되는 게 아니다. 평문 자체를 막으려면 PeerAuthentication STRICT가 별도로 필요함. 이 셋(PeerAuthentication=mTLS 강제, RequestAuthentication=JWT 검증, AuthorizationPolicy=허용/차단)의 역할 분담은 [세 리소스 역할 분담](sec__note-security-resource-trio.html)을 정본으로 참조한다.
- **passthrough egress는 SNI 위조에 취약**: `connection.sni` 기반 정책은 ClientHello의 SNI만 믿는다. 악의적 client가 SNI를 위조하면 우회 가능. 강한 보장이 필요하면 sidecar↔gateway ISTIO_MUTUAL로 source identity를 먼저 고정하고 SNI는 보조 조건으로만 써야 함.
- **HTTP 필드 skip이 ALLOW↔DENY에서 정반대로 작동**: HTTP 전용 필드(methods/paths/headers)는 TCP에서 skip되는데, DENY에서는 "조건이 사라진 무조건 DENY"가 되고 ALLOW에서는 "조건이 매칭 불가 → 매칭 안 됨"이 된다. 그래서 같은 조건이라도 action을 ALLOW↔DENY로 뒤집으면 TCP에 대한 영향이 정반대가 된다 — DENY 정책을 ALLOW로 리팩터링할 때 이 비대칭을 놓치면 의도와 다른 게이트가 생긴다.
## 참조
- [mTLS/SPIFFE 신원](sec__note-mtls-spiffe-identity.html) — SPIFFE 형식·SDS provisioning·일반 TLS와의 차이(정본)
- [세 리소스 역할 분담](sec__note-security-resource-trio.html) — PeerAuthentication / RequestAuthentication / AuthorizationPolicy
- [xDS 계층과 진단](xds__src-xds-layers-and-diagnosis.html) — RBAC filter가 LDS로 내려가는 것 검증
- [Egress mTLS 리포트 — HTTPS over mTLS (ISTIO_MUTUAL)](gw__report-2026-06-08_egress-mtls.html) — egress mTLS 실측