호스트 보호 튜토리얼
Kubernetes 클러스터 관리자가 클러스터 밖에서 들어오는 트래픽으로부터 클러스터를 최대한 안전하게 보호하고 싶은 상황을 상상해 보세요. 클러스터는 Kubernetes NodePort, 즉 클러스터의 어떤 노드에서든 접근 가능한 잘 알려진 TCP 포트 번호 형태로 여러 유용한 서비스를 노출하지만, 관리자는 그중 일부 NodePort만 외부 트래픽에 개방하고 싶어 해요. 이 튜토리얼은 pre-DNAT 정책을 사용해 기본적으로 외부 트래픽을 막고 특정 NodePort만 선별적으로 허용하는 방법을 안내해요.
출처: 문서
본문
Kubernetes 클러스터의 관리자가 클러스터 밖에서 들어오는 트래픽으로부터 클러스터를 최대한 안전하게 보호하고 싶다고 상상해 보세요. 다만 이 클러스터는 Kubernetes NodePort로 노출된 여러 유용한 서비스를 제공하고, 즉 클러스터의 어떤 노드에서든 접근 가능한 잘 알려진 TCP 포트 번호 형태로 서비스가 제공된다고 가정하죠. 관리자는 그중 일부 NodePort는 외부 트래픽에 개방하고 싶어 해요.
이 예제에서는 각 클러스터 노드의 외부 인터페이스에 pre-DNAT 정책을 적용해서:
-
일반적으로는 클러스터 밖에서 들어오는 트래픽을 허용하지 않고,
-
특정 NodePort로 들어오는 트래픽만 허용해요.
일반 호스트 엔드포인트 정책이 아니라 pre-DNAT 정책을 이 목적으로 사용하는 이유는 다음과 같아요.
-
일반 외부 트래픽에 대한 보호가 그 트래픽의 목적지와 무관하게 적용되길 원해요. 예를 들어 로컬에서 호스팅되는 pod, 다른 노드에 있는 pod, 호스트 자체에서 실행 중인 로컬 서버 프로세스가 목적지일 수 있죠. 우리가 원하는 대로 pre-DNAT 정책은 이 모든 경우에 적용되지만, 일반 호스트 엔드포인트 정책은 로컬 pod로 가는 트래픽에는 적용되지 않아요.
-
이 정책을 내부적으로 변환되는 포트 번호가 아니라 광고된 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 규칙이 어떤 트래픽을 항상 허용하는지 미리 확인해 두면 운영 중에 낭패를 보는 일을 피할 수 있어요.