Istio 심화 — 왜 그렇게 흐르는가 · AuthorizationPolicy 가 RBAC 필터 두 개가 되는 방식 · 讲解
인가 정책은 필터 사슬로 번역된다
한 줄 요약
AuthorizationPolicy 는 워크로드 인바운드 리스너의 envoy.filters.http.rbac 필터가 된다. DENY 정책들은 필터 하나, ALLOW 정책들은 또 하나로 모이고 DENY 필터가 앞에 선다. 두 필터는 사슬로 이어져 둘 다 통과해야 요청이 앱에 닿는다. dry-run 은 같은 정책이 rules 가 아니라 shadow_rules 에 들어가는 것이다.
왜 이게 필요했나
인가 규칙은 앱마다 코드로 짜면 언어마다 다르게 구현되고, 감사할 때 어디에 무엇이 있는지 모른다. Istio 는 이것을 프록시로 옮겼다. 규칙은 쿠버네티스 리소스로 한곳에 쓰고, 집행은 모든 파드 앞의 Envoy 가 똑같은 방식으로 한다.
그런데 사람이 쓰는 규칙은 '이건 막고(DENY) 저건 허용(ALLOW)' 이 섞여 있고, Envoy 의 RBAC 필터 하나는 action 을 딱 하나만 갖는다. 그래서 번역에 규칙이 필요하다. 이 규칙을 모르면 운영에서 흔히 보는 두 사고를 설명할 수 없다. ALLOW 정책 하나를 추가했더니 나머지 트래픽이 전부 403 이 되는 사고, 그리고 경로만 적은 DENY 정책 하나가 데이터베이스 연결까지 끊는 사고다.
어떻게 동작하나
Istio 문서의 평가 순서는 다섯 줄이다.
| 순서 | 조건 | 판정 |
| --- | --- | --- |
| 1 | CUSTOM 정책이 거부 | 거부 |
| 2 | DENY 정책에 매치 | 거부 |
| 3 | 이 워크로드에 ALLOW 정책이 하나도 없음 | 허용 |
| 4 | ALLOW 정책에 매치 | 허용 |
| 5 | 그 밖 | 거부 |
istiod 는 이 표를 필터 사슬로 옮긴다. 2번 줄은 action: DENY 인 RBAC 필터, 4·5번 줄은 action: ALLOW 인 RBAC 필터다. ALLOW 필터는 '매치하는 정책이 있어야 통과' 이므로 매치하지 않으면 그 자리에서 거부한다 — 5번 줄이 따로 필요 없는 이유다. 3번 줄은 필터를 만들지 않는 것으로 구현된다. RBAC 필터의 rules 에 정책이 하나도 없으면 모든 요청을 거부하기 때문에, ALLOW 정책이 없는 워크로드에는 ALLOW 필터 자체를 두지 않는다.
필터 사슬에서 각 필터가 할 수 있는 일은 '다음으로 넘기기' 와 '그 자리에서 거부' 둘뿐이다. 다음 필터를 건너뛰고 곧장 허용하는 길은 없다. 그러니 요청이 앱에 닿으려면 DENY 필터도 ALLOW 필터도 통과해야 한다. 논리로는 AND 다. 그래서 두 필터의 순서를 바꿔도 판정은 같다. 순서가 바뀌면 달라지는 것은 통계다. 두 필터가 같은 http.<stat_prefix>.rbac.allowed/denied 를 함께 올리므로, 한 요청이 첫 필터에서 allowed 를 올리고 둘째에서 denied 를 올릴 수 있다.
정책 속성은 대부분 그대로 옮겨진다. paths: ["/admin*"] 는 url_path.path.prefix: /admin 이 되고 — 그래서 /administrator 도 걸린다 — methods: ["GET"] 는 :method 헤더의 정확 매치가 된다. 정책마다 ns[default]-policy[deny-admin]-rule[0] 같은 이름이 붙어서, 거부 로그나 config_dump 에서 원래 리소스를 찾아갈 수 있다.
dry-run 은 정책에 istio.io/dry-run: "true" 애너테이션을 붙이는 것이다. istiod 는 그 정책을 shadow_rules 에 넣는다. RBAC 필터는 shadow 쪽을 평가해 shadow_allowed/shadow_denied 통계와 동적 메타데이터만 남기고 판정에는 쓰지 않는다. rules 가 없는 필터는 아무것도 거부하지 않는다. shadow_rules_stat_prefix 를 주면 통계 이름 가운데에 접두사가 한 칸 끼어든다.
현장에서 만나는 모습
ALLOW 하나 추가했더니 전부 막혔다. 3번 줄 때문이다. ALLOW 정책이 0개일 때는 '다 허용' 이던 워크로드가, 1개가 되는 순간 '매치한 것만 허용' 으로 바뀐다. 헬스체크 경로나 다른 서비스의 호출이 새 ALLOW 에 없으면 곧바로 403 이 된다. 본문이 RBAC: access denied 면 프록시가 막은 것이고, 앱의 403 과는 본문으로 가른다.
DENY 정책이 TCP 서비스를 끊었다. DENY 는 요청에 없는 속성을 매치로 본다. 막는 쪽이 빈틈을 두지 않으려는 설계다. 그래서 경로만 적은 DENY 는 경로라는 개념이 없는 TCP 연결을 전부 매치해 끊는다. istioctl validate 가 이것을 경고하며 ports 로 좁히라고 권한다. 종료 코드는 0 이라 CI 에서 놓치기 쉽다.
새 정책을 바로 켜기 무섭다. dry-run 으로 먼저 넣고 shadow_denied 가 예상한 요청에서만 오르는지 본 뒤 애너테이션을 뗀다. 운영 대시보드에서 이 통계를 보려면 접두사까지 포함한 정확한 이름을 알아야 한다.
공식 문서: [Authorization Policy](https://istio.io/latest/docs/reference/config/security/authorization-policy/) · [Envoy RBAC filter](https://www.envoyproxy.io/docs/envoy/v1.38.3/api-v3/extensions/filters/http/rbac/v3/rbac.proto)
다음 실습에서 할 것
DENY 와 ALLOW 정책을 쓰고 istioctl 경고를 읽은 뒤, 평가 순서로 네 요청의 결과를 먼저 예측한다. 두 정책을 RBAC 필터 두 개로 옮겨 Envoy 로 띄우고 예측과 대조한다. 필터 순서를 바꾸고, ALLOW 필터를 빼고, DENY 를 shadow_rules 로 옮겨 가며 평가 순서의 세 줄을 직접 확인한다.