NetworkPolicy

개요

쿠버네티스에서 일어나는 트래픽을 제어하는 오브젝트[1]
규칙을 지정해서 어떤 트래픽을 허용하고, 차단할지 지정할 수 있는, 일종의 방화벽이다.
트래픽의 흐름을 L4 레벨에서 관리하고 싶을 때 사용한다.
내부에서도, 외부에 대해서도 사용할 수 있다.

api가 있긴 한데, 지원하는 CNI여야만 사용할 수 있는데, 만들어진지 너무 오래 된 Flannel은 이를 지원하지 않는다.

주의할 점은, 이건 4계층에서 동작한다는 것이다.
그래서 TCP, UDP, SCTP를 통하는 프로토콜에 대해서 동작한다.
그래서 ARP나 ICMP에는 영향이 없다는 것에 유의하자.

기능

기능이 정말 단순해서 할 말이 크게 없다.
말 그대로 어떤 트래픽을 허용하고 차단할지에 대한 네트워크 정책을 지정하는 것이다.

파드 격리(isolation) 종류

트래픽을 분류하는 방식을 먼저 사전지식으로 보자면, 나가는 트래픽인 이그레스(Egress)와 들어오는 트래픽인 인그레스(Ingress)를 들 수 있다.
여기에서 인그레스가 인그레스를 의미하는 건 아니고, 단순히 들어오는 트래픽을 말한다.

클라 기준으로 단순한 예시를 들자.
내 클라에서 웹 서버로 접속을 시도하면, 클라에서 요청 트래픽, 서버에서 응답 트래픽이 발생할 것이다.
이때 클라에서 서버로 나가는 것이 이그레스, 서버의 응답이 클라로 돌아오는 게 인그레스이다.
이걸 서버 기준으로 본다면, 클라에서 들어온 요청이 인그레스, 나가는 응답이 이그레스가 될 것이다.
아무튼 여기에서 표현은 이그와 인그라고 축약하겠다!

네폴의 기능은 인그와 이그 각각에 정책을 설정하는 것이다.

특징

이 세상에 네트워크 트래픽을 관리하는 방법이야 많다.
L7단계에서 어떤 프로토콜을 썼는지, 혹은 어떤 헤더를 가지고 있는지를 통해 제한할 수 있다.
그리고 인터페이스를 타고 들어왔을 시점에 적용하던가(iptables처럼), 애플리케이션 딴에서 관리하는 방법도 있고..
또한 방화벽에는 기본 접근 제한 방식을 어떻게 두냐에 따라 종류를 나누고, 연결추적(conntrack)을 해주냐마냐에 따라서도..

이 정도의 특징을 가진다고 볼 수 있을 것 같다.

주의사항

개인적으로 네폴은 좀 불완전하다고 생각한다.
아무튼 조금 확인해볼 만한 주의사항들을 살펴보자.

클러스터 트래픽 기본 동작

네폴이 만들어지지 않은 네임스페이스에서는 사실 모든 트래픽에 정책이 없다.
즉, 모든 트래픽에 열려있는데 여기에 네폴을 지정하는 순간, 화이트리스트로 동작을 하니 일단 모든 트래픽이 차단된다..
허용할 리스트를 잘 관리하면 되겠지만, 괜히 이거 잘못 지정했다가 난리가 날 수 있다는 것은 감안해야 한다.
그것이 화이트리스트니까..

파드 라이프사이클

새 네폴이 만들어지면, CNI가 이를 적용하는데 시간이 걸릴 수 있다.
그래서 이미 만들어진 파드에 실시간으로 적용이 되지 않을 수 있다는 것에 주의하자.
그래도 새로 만들어진 파드에 대해선 무조건 적용이 보장된다.
CNI는 파드가 시작되기 이전에(초기화 컨테이너에 까지도) 이렇게 동작하도록 보장해야 한다.

kube-apiserver에서는 cni가 네폴을 제대로 적용해주는 정확한 타이밍을 확인할 방법이 없다.
그래서 파드는 다르게 네트워크 구성이 이뤄진 채로 시작하는 것에 대해서도 복원력을 지녀야 한다.
이럴 때 초기화 컨테이너로 시간 벌어주는 게 아주 제격이다.

정책 적용 순서

네폴은 모든 규칙이 웬만해서 실시간으로 이뤄지기는 하겠지만, 그럼에도 순서를 따지자면 일단 모든 트래픽을 차단한 후에 허용 리스트를 풀어주는 방식이다.
어쩌다 최악의 경우에는 허용 리스트가 적용되기 이전에 일단 트래픽이 다 차단돼버리는 상황이 생길 수도 있다..

또 cni가 분산 방식으로 구현됐다면, 모든 파드가 한꺼번에 적용되지 않을 수도 있다.
그래서 예측 가능성이 상당히 떨어진다는 것을 감안해야 한다.

호스트 네트워크 파드

데몬셋 파드는 대체로 .spec.hostNetwork=True로 지정되는 경우가 많다.
이러면 파드의 ip가 노드의 ip이기에 생기는 이슈가 있을 수 있다.

네폴은 호스트 네트워크 파드에 대한 동작을 정의하지 않았다.
그래서 아마 cni의 동작은 다음의 두 가지 방식으로 동작할 가능성이 있다.

후자가 가장 흔한 방식이라고 하는데, 가령 이런 케이스가 발생할 수 있다.
어떤 파드에 대해 모든 파드와 네임스페이스에 대한 트래픽을 차단하게 설정했다.
같은 노드에 속한 호스트넷 파드는 노드의 ip를 이용해서 들어오기 때문에 파드, 네임스페이스에 대한 모든 조건을 회피하고 들어올 수도 있다는 거다..
그래서 이럴 땐 아래에서 볼 ipBlock을 이용하여 노드 ip 대역을 설정하는 게 좋다!

ip 대역에 대한 이슈

ip 범위를 통해 규칙을 지정할 ㅓ수도 있다고 하는데, 이게.. 사실 조금 애매하다.
CNI마다 구현 방식이 다른데 어떤 cni는 원본 소스 ip를 그대로 보장해준다.
한편 다른 놈은 로드 밸런서의 ip로 바꿔버리는 경우도 있고..
서비스 외부 트래픽 정책에 따라서 다양한 동작 방식이 있으니 이런 것도 고려할 필요가 있다.

양식 작성법

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: test-network-policy
  namespace: default
spec:
  podSelector:
    matchLabels:
      role: db
  policyTypes:
  - Ingress
  - Egress
  ingress:
  - from:
    - ipBlock:
        cidr: 172.17.0.0/16
        except:
        - 172.17.1.0/24
    - namespaceSelector:
        matchLabels:
          project: myproject
    - podSelector:
        matchLabels:
          role: frontend
    ports:
    - protocol: TCP
      port: 6379
  egress:
  - to:
    - ipBlock:
        cidr: 10.0.0.0/24
    ports:
    - protocol: TCP
      port: 5978

네폴은 다른 오브젝트들처럼 많은 필드를 가지지는 않는다.
대신 헷갈릴 포인트가 있는데 이를 잘 파악해야 한다.

위 예시는 기본 네임스페이스의 role=db 라벨이 붙은 파드들을 대상으로 한다.
그리고 다음 세 가지 중 하나라도 포함 하는 트래픽이 6379 TCP로 들어온다면 허용한다.

이 파드에서 나가는 트래픽은 다음의 조건을 만족하는 트래픽이 5978 TCP 포트로 나갈 때만 허용한다.

이때 중요한 건 들여쓰기와 -의 위치이다.
이것에 따라 조건이 and가 되나 or이 되냐가 바뀐다고 보면 된다.

podSelector

내 정책을 적용하고 싶은 파드를 고른다.
이 값이 없다면 해당 네임스페이스의 모든 파드가 대상이 된다.

policyType

이그나 인그를 써준다.
여기에 해당 트래픽을 쓰게 되는 순간 매칭된 모든 파드의 트래픽은 격리된다.
격리되고 나서 뭘 허용할 지는 이제 아래의 필드들을 이용하면 된다.

참고로 여기에 아무것도 쓰지 않으면 Ingress만 써진 것으로 간주된다.
즉, 네폴을 만드는 순간 모든 인그가 차단된다.
(왜 이렇게 헷갈리게 만들었을까..?)
그러니 대충 이 정도 경우의 수가 있는 것이다.

  policyTypes:
  - Ingress
  - Egress
  policyTypes: {}
  혹은
  policyTypes:
  - Ingress
  policyTypes:
  - Egress

ingress, egress

각각 허용할 인그와 이그 규칙 리스트를 작성하는 필드이다.
둘다 ports 필드를 사용할 수 있고, 인그에 대해선 from, 이그에 대해선 to를 또 사용할 수 있다.

from, to

다음의 세 가지 방식을 사용할 수 있다.

그리고 여기 안 속에도 ports를 사용할 수 있다.
위의 예시가 이런 케이스이다.

근데 여기에서 조심해야 할 것이 바로 and 조건이다.

  - from:
    - namespaceSelector:
        matchLabels:
          user: alice
      podSelector:
        matchLabels:
          role: client

podSelector 앞에는 -가 안 붙어 있는데, 이는 네임스페이스와 파드를 같이 고려한다는 뜻이다.
user=alice가 붙은 네임스페이스의 role=client가 붙은 파드를 허용한다는 것이다.
json으로 치면,

{
	from: [
		{ namespace:{}, pod:{} }
	]
}

인 꼴이다.

  - from:
    - namespaceSelector:
        matchLabels:
          user: alice
    - podSelector:
        matchLabels:
          role: client

그럼 이것은?

{
	from: [
		{ namespace:{} }, 
		{ pod:{} }
	]
}

이런 뜻이라서, user=alice가 붙은 네임스페이스의 모든 파드나, 같은 네임스페이스의 role=client가 붙은 모든 파드를 허용한다는 것이다.
그래서 정책을 지정할 때는 -를 정말 유의해서 작성해야 한다.

ports

from, to 안에 넣지 않고, 모든 리스트에 대해 일괄적으로 포트를 지정할 수도 있다.
아니면 다른 조건 없이 ports로만 조건을 거는 것도 된다..

  - ports:
    - protocol: TCP
      port: 5978
      endPort: 6000

포트의 범위를 지정하는 것도 가능한데, 이렇게 endPort라고 써주면 된다.
endPort 값은 무조건 port값보다는 커야 하고, 숫자로 지정돼야 한다.
당연히 port 없이 endPort만 쓰는 건 안 된다.

이거 어케 되나?

  egress:
  - to:
    - ipBlock:
        cidr: 10.0.0.0/24
    ports:
    - protocol: TCP
      port: 5978
  egress:
  - to:
    - ipBlock:
        cidr: 10.0.0.0/24
  - ports:
    - protocol: TCP
      port: 5978
  egress:
  - to:
    - ipBlock:
        cidr: 10.0.0.0/24
  ports:
  - protocol: TCP
    port: 5978

예시

사실 위의 글만 이해했다면 굳이 예시까지 싶은데, 문서에 있는 거 옮긴다는 차원에서만 적어본다.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-ingress
spec:
  podSelector: {}
  policyTypes:
  - Ingress

파드를 지정하지 않았으니 기본 네임스페이스의 모든 파드가 대상이 되고, .spec.ingress로 허용할 리스트를 지정하지 않았으니 전부 차단!
이그는 지정되지 않았으니 영향 없다.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-all-ingress
spec:
  podSelector: {}
  ingress:
  - {}
  policyTypes:
  - Ingress

.spec.ingress에 규칙으로 지정하지 않고 그냥 빈 칸으로 두면 마찬가지로 모든 게 대상이 된다.

    - namespaceSelector:
        matchExpressions:
        - key: namespace
          operator: In
          values: ["frontend", "backend"]

이건 라벨 셀렉터 부분에서 다뤄야 하는 건데 굳이..
네임스페이스를 이름으로 지정하고 싶다면 kubernetes.io/metadata.name 라벨을 사용하면 된다.

kind: NetworkPolicy
apiVersion: networking.k8s.io/v1
metadata:
  namespace: default
  name: deny-from-other-namespaces
spec:
  podSelector:
    matchLabels:
  ingress:
  - from:
    - podSelector: {}

인그 규칙은 있는데, 파드셀렉터에 대해서 비어있으니 같은 네임스페이스에서의 파드들이 전부 선택된다.

kind: NetworkPolicy
apiVersion: networking.k8s.io/v1
metadata:
  name: web-allow-external
spec:
  podSelector:
    matchLabels:
      app: web
  ingress:
  - {}

이렇게 하면 같은 네임스페이스에서 셀렉되지 않는 모든 파드에 대한 인그레스가 차단?

제한

아직 안 되거나, 구현되지 않을 것이 있으니 아래의 케이스에 대해선 다른 솔루션을 사용하라.

진짜?

관련 문서

이름 noteType created
NetworkPolicy knowledge 2025-01-09

참고

개인적인 생각에, 이 오브젝트는 바뀌거나 삭제돼야 할 것 같다.
사용성도 안 좋고, 제한도 많고.
차라리 cni마다 crd도 구현케 하던가 조금 더 면밀한 제한사항을 요구하며 cni가 구현하도록 해야 한다.
커넥션 보장도 안 해줘, 언제 적용될지도 보장 안 해줘, 세밀한 조정도 안 된다.
네폴을 만든 순간부터 트래픽이 제한된다는 것도 웃긴다.
사실 다른 방화벽들도 다 비슷하긴 하다만, 너무 책임 없게 느껴진다.

개선 방향성


  1. https://kubernetes.io/docs/concepts/services-networking/network-policies/ ↩︎