본문으로 바로가기 본문 바로가기
WIKI 기술 지식 베이스

호스트 보호 튜토리얼

원문 보기 위키 갱신

Kubernetes 클러스터 관리자가 클러스터 밖에서 들어오는 트래픽으로부터 클러스터를 최대한 안전하게 보호하고 싶은 상황을 상상해 보세요. 클러스터는 Kubernetes NodePort, 즉 클러스터의 어떤 노드에서든 접근 가능한 잘 알려진 TCP 포트 번호 형태로 여러 유용한 서비스를 노출하지만, 관리자는 그중 일부 NodePort만 외부 트래픽에 개방하고 싶어 해요. 이 튜토리얼은 pre-DNAT 정책을 사용해 기본적으로 외부 트래픽을 막고 특정 NodePort만 선별적으로 허용하는 방법을 안내해요.

출처: 문서

본문

Kubernetes 클러스터의 관리자가 클러스터 밖에서 들어오는 트래픽으로부터 클러스터를 최대한 안전하게 보호하고 싶다고 상상해 보세요. 다만 이 클러스터는 Kubernetes NodePort로 노출된 여러 유용한 서비스를 제공하고, 즉 클러스터의 어떤 노드에서든 접근 가능한 잘 알려진 TCP 포트 번호 형태로 서비스가 제공된다고 가정하죠. 관리자는 그중 일부 NodePort는 외부 트래픽에 개방하고 싶어 해요.

이 예제에서는 각 클러스터 노드의 외부 인터페이스에 pre-DNAT 정책을 적용해서:

  • 일반적으로는 클러스터 밖에서 들어오는 트래픽을 허용하지 않고,

  • 특정 NodePort로 들어오는 트래픽만 허용해요.

일반 호스트 엔드포인트 정책이 아니라 pre-DNAT 정책을 이 목적으로 사용하는 이유는 다음과 같아요.

  1. 일반 외부 트래픽에 대한 보호가 그 트래픽의 목적지와 무관하게 적용되길 원해요. 예를 들어 로컬에서 호스팅되는 pod, 다른 노드에 있는 pod, 호스트 자체에서 실행 중인 로컬 서버 프로세스가 목적지일 수 있죠. 우리가 원하는 대로 pre-DNAT 정책은 이 모든 경우에 적용되지만, 일반 호스트 엔드포인트 정책은 로컬 pod로 가는 트래픽에는 적용되지 않아요.

  2. 이 정책을 내부적으로 변환되는 포트 번호가 아니라 광고된 NodePort 기준으로 작성하고 싶어요. ingress 노드의 kube-proxy는 DNAT를 사용해 NodePort 번호와 IP 주소를 해당 서비스를 백킹하는 pod 중 하나의 값으로 바꿔요. 따라서 정책은 그 DNAT 이전에 적용되어야 하고, 그 말은 곧 pre-DNAT 정책이어야 한다는 뜻이에요.

참고: 이 튜토리얼은 이름이 지정된 호스트 엔드포인트, 즉 interfaceName이 특정 인터페이스 이름으로 설정된 호스트 엔드포인트를 사용한다고 가정해요. 이 튜토리얼은 interfaceName: "*"인 호스트 엔드포인트에는 그대로는 동작하지 않아요.

다음은 일반적인 외부 ingress 트래픽을 막기 위해 필요한 pre-DNAT 정책이에요:

calicoctl apply -f - <<EOF
apiVersion: projectcalico.org/v3
kind: GlobalNetworkPolicy
metadata:
  name: allow-cluster-internal-ingress
spec:
  order: 10
  preDNAT: true
  applyOnForward: true
  ingress:
    - action: Allow
      source:
        nets: [10.240.0.0/16, 192.168.0.0/16]
  selector: has(host-endpoint)
---
apiVersion: projectcalico.org/v3
kind: GlobalNetworkPolicy
metadata:
  name: drop-other-ingress
spec:
  order: 20
  preDNAT: true
  applyOnForward: true
  ingress:
    - action: Deny
  selector: has(host-endpoint)
EOF

구체적으로 이 정책은 클러스터 내부로 알려진 IP 주소에서 오는 트래픽은 허용하고, 다른 출처의 트래픽은 거부해요. 이 예제의 클러스터 내부 IP 주소는 노드 자체 IP 주소로 10.240.0.0/16, Kubernetes가 pod에 할당할 IP 주소로 192.168.0.0/16을 가정해요. 물론 여러분 클러스터에서 실제 사용 중인 CIDR에 맞게 조정해야 해요.

참고: drop-other-ingress 정책은 allow-cluster-internal-ingress보다 높은 order 값을 갖기 때문에, allow-cluster-internal-ingress 이후에 적용돼요. 명시적인 drop-other-ingress 정책이 필요한 이유는 pre-DNAT 정책에는 자동 default-drop 의미가 없기 때문이에요. 일반 호스트 엔드포인트 정책에는 default-drop 의미가 있지만, 앞에서 언급했듯이 일반 호스트 엔드포인트 정책은 항상 적용되는 게 아니에요.

또한 각 노드의 외부 인터페이스를 통과하는 egress 트래픽을 허용하는 정책도 필요해요. 그렇지 않으면 이 인터페이스들에 대해 호스트 엔드포인트를 정의하는 순간, 로컬 프로세스에서 나가는 egress 트래픽이 failsafe 규칙이 허용하는 트래픽을 제외하고 전부 막혀요. 포워딩 트래픽에 대한 default-deny 규칙은 없기 때문에, 호스트 엔드포인트에서 포워딩 트래픽은 허용돼요.

calicoctl apply -f - <<EOF
apiVersion: projectcalico.org/v3
kind: GlobalNetworkPolicy
metadata:
  name: allow-outbound-external
spec:
  order: 10
  egress:
    - action: Allow
  selector: has(host-endpoint)
EOF

참고: 이 egress 규칙들은 pre-DNAT가 아니라 일반 호스트 엔드포인트 정책으로 정의해요. pre-DNAT 정책은 egress 규칙을 지원하지 않기 때문이에요. (pre-DNAT 정책은 Linux 네트워킹 스택에서 패킷의 나가는 인터페이스가 아직 결정되지 않은 지점에서 적용되기 때문이죠.) 이 정책들은 포워딩 트래픽에는 적용되지 않는(applyOnForward가 false) 일반 호스트 엔드포인트 정책이므로, 로컬 pod에서 보내는 트래픽에는 적용되지 않아요. 위 정책은 pod가 아니라 노드 자체에서 실행되는 애플리케이션이나 서버 프로세스가 어떤 목적지로든 아웃바운드 연결을 할 수 있게 허용해요. 특정 IP 주소로 제한할 use case가 있다면, 대응하는 destination 스펙을 추가해서 구현할 수 있어요.

이제 각 노드의 외부 방향 인터페이스에 대해 호스트 엔드포인트를 정의할 수 있어요. 위의 정책들은 모두 host-endpoint 레이블이 있는 엔드포인트에 적용되도록 셀렉터를 갖고 있으니, 정의에 이 레이블을 포함해야 해요. 예를 들어 node1의 eth0이라면:

calicoctl apply -f - <<EOF
apiVersion: projectcalico.org/v3
kind: HostEndpoint
metadata:
  name: node1-eth0
  labels:
    host-endpoint: ingress
spec:
  interfaceName: eth0
  node: node1
EOF

각 노드에 호스트 엔드포인트를 정의하고 나면, 클러스터 내부 통신은 여전히 평소처럼 잘 동작하는 반면(예를 들어 calicoctl get hep, calicoctl get pol 같은 명령이 성공적으로 실행돼요), 클러스터 밖에서는 (failsafe 규칙이 허용하는 트래픽을 제외하면) 접근할 수 없게 되는 걸 확인할 수 있어요. 예를 들어 클러스터에 NodePort 31852로 노출된 Kubernetes 서비스가 있다면, 이 시점에서 그 NodePort는 클러스터 내부에서는 동작하지만 외부에서는 동작하지 않아요.

해당 NodePort에 외부 접근용 핀홀을 열려면 다음과 같은 pre-DNAT 정책을 구성하면 돼요:

calicoctl apply -f - <<EOF
apiVersion: projectcalico.org/v3
kind: GlobalNetworkPolicy
metadata:
  name: allow-nodeport
spec:
  preDNAT: true
  applyOnForward: true
  order: 10
  ingress:
    - action: Allow
      protocol: TCP
      destination:
        selector: has(host-endpoint)
        ports: [31852]
  selector: has(host-endpoint)
EOF

그 NodePort를 특정 노드를 통해서만 접근 가능하게 만들고 싶다면, 해당 노드들에 특별한 host-endpoint 레이블을 부여하고:

host-endpoint: <special-value>

allow-nodeport 정책의 셀렉터로 has(host-endpoint) 대신 host-endpoint=='<special-value>'를 사용하면 돼요.

더 알아보기 (Learn more)

pre-DNAT 정책과 호스트 엔드포인트를 조합하면 클러스터 외부 접근을 NodePort 단위로 정밀하게 제어할 수 있어요. failsafe 규칙이 어떤 트래픽을 항상 허용하는지 미리 확인해 두면 운영 중에 낭패를 보는 일을 피할 수 있어요.