LabHub
배우기 러닝패스 코스

CCA — 실리움 인증 어소시에이트 · 보안 정책 사고 조사: DNS와 허용 경계 · 이론

정책을 더했는데 왜 더 넓게 열릴까

LabHub 에서 이어서 보기

한 줄 요약

정책 파일을 더 많이 만들었다고 통신이 더 안전해지는 것은 아닙니다. 어떤 파드를 어느 방향으로 제한하는지, 여러 허용 규칙이 어떻게 합쳐지는지, 이름 해석에 필요한 별도 통신이 남아 있는지 확인해야 합니다. 이 모듈은 보안 작업 뒤 발생한 두 장애를 실제 요청과 Cilium 관측 기록으로 구분합니다.

왜 이게 필요했나

파란 팀은 사내 주문 API를 관리합니다. 보안 담당자가 외부 연결을 줄여 달라고 하자 팀은 클라이언트의 egress를 API의 8080 포트로 제한했습니다. 정책 적용은 성공했고 API 파드도 Ready였습니다. 그런데 애플리케이션에서는 주문이 멈췄습니다. API 파드 IP로 직접 요청하면 응답하지만 서비스 이름으로 요청하면 기다리다 끝납니다.

처음에는 API 장애라고 생각해 서버를 재시작했습니다. 그러나 서버를 바꾸어도 결과는 같았습니다. 여기서 통제군이 중요합니다. 같은 출발지에서 같은 서버의 같은 포트로 가되, 주소를 해석하는 경로만 바꾸었을 때 결과가 갈렸습니다. 한 번에 여러 설정을 바꾸지 말고 이 차이가 어느 의존성을 드러내는지 먼저 질문해야 합니다.

다른 팀에서는 협력 조직에 API를 열기 위해 허용 정책을 추가했습니다. 기존 정책이 여전히 남아 있으니 새 정책은 추가 심사 조건이라고 생각했습니다. 하지만 두 허용 정책은 두 개의 출입문이 될 수 있습니다. 요구 사항을 AND로 적어 두고 구현은 OR로 만든 셈입니다. 보안 검토는 파일 개수가 아니라 누가 어느 문을 통과할 수 있는지 확인하는 일입니다.

어떻게 동작하나

1. API가 표현하는 의도와 실행기를 분리한다

기본 Kubernetes NetworkPolicy도 podSelector와 namespaceSelector를 지원합니다. IP 주소만 나열하는 정책이 아닙니다. 정책의 최상위 podSelector는 보호할 대상을, ingress의 from은 들어오는 상대를, egress의 to는 나가는 상대를 고릅니다. 같은 peer 안에 namespaceSelector와 podSelector를 두면 두 조건에 함께 맞는 상대를 뜻하고, 별도 목록 항목으로 나누면 서로 다른 허용 경로가 됩니다.

실제로 패킷을 제어하는 것은 정책을 지원하는 네트워크 플러그인입니다. 이 실습에서는 Cilium이 표준 NetworkPolicy와 CiliumNetworkPolicy를 모두 처리합니다. 그러므로 API 객체가 저장됐다는 사실만 보지 말고 선택된 엔드포인트와 실제 요청 결과도 확인합니다. 자세한 선택자 의미는 [Kubernetes 공식 설명](https://kubernetes.io/docs/concepts/services-networking/network-policies/#behavior-of-to-and-from-selectors)과 대조하세요.

2. 이름이 같은 클라이언트라도 같은 신원은 아니다

Cilium identity는 보안 관련 라벨 집합에 대응합니다. 파란 팀 client와 빨간 팀 client가 모두 role=client라고 해도 namespace 관련 라벨은 다릅니다. 숫자 ID만 외우거나 app 라벨 하나만 비교하지 마세요. CEP의 identity.id와 identity.labels를 나란히 읽어 어떤 집합을 구분하는지 확인해야 합니다. 이 숫자를 애플리케이션의 영구 고객 번호처럼 저장해서도 안 됩니다. [Cilium 용어 설명](https://docs.cilium.io/en/stable/gettingstarted/terminology/#identity)이 출발점입니다.

CNP의 endpointSelector는 그 정책이 속한 네임스페이스의 대상을 선택합니다. 다른 네임스페이스의 출발지를 허용하려면 그 경계를 명시적으로 표현해야 합니다. 이 모듈은 Kubernetes에 등록한 namespaced CNP를 사용합니다. 직접 정책 API에 넣은 객체나 클러스터 전역 정책까지 같은 범위로 일반화하지 않습니다. [네임스페이스 경계 안내](https://docs.cilium.io/en/stable/security/policy/kubernetes/)를 함께 읽으세요.

3. ingress를 막는 것과 egress를 막는 것은 다르다

이 환경의 policyEnforcementMode는 default입니다. 아무 정책도 선택하지 않은 방향과, 제한 규칙이 선택한 방향을 구별해야 합니다. 서버 ingress를 좁혔다고 클라이언트 egress까지 자동으로 같은 범위가 되는 것은 아닙니다. 반대로 API로 나가는 통신을 허용했다고 이름 해석 서버로 나가는 통신까지 허용한 것도 아닙니다.

이 실습은 API 8080만 허용한 클라이언트와 DNS도 허용한 클라이언트를 따로 남깁니다. 전자는 IP 직접 요청의 성공을 유지하면서 이름 요청이 실패하고, 후자는 두 요청이 모두 성공해야 합니다. DNS 복구에는 kube-system의 kube-dns 엔드포인트와 UDP/TCP 53을 지정합니다. 모든 egress를 열어 오류를 없애면 과제의 범위를 벗어납니다. default 외에 always와 never, enableDefaultDeny와 L7의 예외도 있으므로 여기서 본 현상을 모든 설정에 그대로 적용하지 마세요. [정책 강제 모드](https://docs.cilium.io/en/stable/security/policy/intro/)에서 조건을 확인합니다.

4. 허용의 합집합과 명시적 거부를 구분한다

union 서버에는 파란 팀 client를 허용하는 CNP가 있습니다. 여기에 빨간 팀 client를 허용하는 표준 NetworkPolicy를 추가하면 두 팀 모두 통과하는 상태를 만들 수 있습니다. 새 정책이 기존 허용을 더 좁히는 필터라고 읽으면 안 됩니다. 같은 파드에 적용된 모든 허용 경로를 펼쳐 봅니다.

deny 서버에는 위와 같은 허용 경로를 두고 빨간 팀을 명시적으로 거부하는 CNP를 추가합니다. Cilium의 명시적 deny는 표준 NetworkPolicy를 포함한 allow보다 우선합니다. 이는 단순히 허용 규칙이 없어서 차단되는 default-deny와 다른 개념입니다. 이 기능으로 특정 URL이나 FQDN을 deny할 수 있다고 확대 해석하지 마세요. [명시적 거부의 우선순위와 제한](https://docs.cilium.io/en/stable/security/policy/deny/)을 참고합니다.

| 대상 | 파란 client | 파란 stranger | 빨간 client | 비교 이유 |
| --- | --- | --- | --- | --- |
| baseline | 허용 | 허용 | 허용 | 서버 자체가 응답하는 통제군 |
| native | 허용 | 거부 | 거부 | 표준 NP의 같은 네임스페이스 선택 |
| ingress | 허용 | 거부 | 거부 | CNP의 출발지 선택 |
| union | 허용 | 거부 | 허용 | 서로 다른 API의 허용 합집합 |
| deny | 허용 | 거부 | 거부 | 빨간 팀 allow보다 우선하는 deny |

이 표는 앞으로 구성할 격리 실습의 목표 상태입니다. 초기 상태부터 정답 정책이 전부 적용돼 있다는 뜻이 아닙니다. 실제 정책을 읽고 요청을 보내 목표와 다른 칸을 좁혀 갑니다.

현장에서 만나는 모습

HTTP 000은 서버가 보낸 상태 코드가 아닙니다. curl이 HTTP 응답 코드를 얻지 못한 경우에도 보입니다. timeout이라는 이유만으로 네트워크 정책 때문이라고 단정하지 마세요. 서버 미기동, 잘못된 주소, 연결 경로 문제도 같은 모양으로 보일 수 있습니다. 따라서 서버 Ready, IP 직접 통제군, 클라이언트 위치, 정책 spec, Hubble verdict를 함께 봅니다.

Hubble의 DROPPED 한 줄을 찾는 것만으로는 부족합니다. 관측 버퍼에는 이전 실험과 다른 파드의 요청도 있습니다. 출발 파드와 네임스페이스, IP, 목적지 포트, 고유 source port, 해당 시각을 맞추면 조사 범위를 줄일 수 있습니다. CEP identity까지 대조하면 같은 이름의 다른 엔드포인트를 혼동할 위험도 줄어듭니다. TCP SYN이 전달됐다는 사실과 HTTP 200을 받았다는 사실도 구분합니다. 전자는 전달 경로 관찰이고 후자는 애플리케이션 응답입니다. [Hubble CLI 안내](https://docs.cilium.io/en/stable/observability/hubble/hubble-cli/)의 필터를 이용해 직접 범위를 좁혀 보세요.

실제 사전 탐침에서는 22요청 중 DNS 해석에서 멈춘 한 요청에 TCP 흐름이 없었습니다. 이를 TCP 로그 누락이라고 판정하면 관측기가 틀립니다. 그 요청은 API 연결 전 단계에서 멈췄기 때문입니다. DNS의 UDP 53 드롭과 IP 직접 요청의 성공을 함께 보아야 합니다. 당시 DNS 증거는 시간대와 발신 파드의 비교이며 query ID까지 연결한 패킷 증명은 아니었습니다. 증거의 범위를 적는 것도 장애 보고서의 일부입니다.

다음 실습에서 할 것

준비된 격리 VM에서 먼저 신원과 통제군을 확인합니다. 이어 표준 NP와 CNP의 선택자, DNS가 빠진 egress와 복구된 egress, allow 합집합과 deny 우선순위를 각기 다른 대상에 구성합니다. 단계가 끝나도 이전 비교 대상은 남겨 두세요. 마지막에는 정책과 관측 결과를 대조한 사고 보고서를 작성합니다.

이 실습에서 일부러 열어 두는 union과 baseline은 비교를 위한 격리 대상입니다. 그대로 운영 환경에 복사하지 마세요. 작업물을 보관하려면 세션을 종료하기 전에 내보내야 합니다. 이 모듈의 실험은 한 VM 안의 Cilium 정책 동작이며 다중 클러스터 연결, BGP 라우팅, L7 URL 제한까지 검증하는 것은 아닙니다.