a story

[10] Cilium - Security 본문

Cilium

[10] Cilium - Security

한명 2025. 9. 6. 21:48

이번 게시물에서는 Cilium에서 제공하는 Security 기능에 대해서 살펴보겠습니다.

Cilium의 Security 에 대한 설명 중 Network Security와 Network Policy 부분에서 중점을 두고 설명하도록 하겠습니다.

 

목차

  1. Network Security 개념
  2. Cilium Network Policy

 

1. Network Security 개념

Cilium의 Network Policy를 살펴보기 전 필요한 개념들을 살펴보겠습니다.

Identity based Security

전통적인 보안 아키텍처에서 L3의 보안은 일반적으로 IP 기반의 필터링을 사용합니다.

물론 쿠버네티스 환경에서는 대상의 IP를 식별하기 어렵기 때문에 Network Policy에서는 Label을 기반으로 정책을 생성하고, 이후 IP를 바탕으로 필터링을 하게 됩니다.

 

예를 들어, calico 기반의 Network Policy를 살펴보면 아래와 같습니다.

# Network Policy 생성
cat <<EOF | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-granted-access
spec:
  podSelector:
    matchLabels:
      app: server
  ingress:
  - from:
    - podSelector:
        matchLabels:
          access: granted
    ports:
    - protocol: TCP
      port: 80
EOF

# 노드에서 iptables 확인
root@aks-nodepool1-16223536-vmss000000:/# iptables -S | grep allow-granted-access
-A cali-pi-_-zqXhpUfbm6lphf3Orn -p tcp -m comment --comment "cali:Jz1VzdhBJEh8b5nC" -m comment --comment "Policy default/knp.default.allow-granted-access ingress" -m set --match-set cali40s:ahXLGcJKqoUc6DMM01m_Oja src -m multiport --dports 80 -j MARK --set-xmark 0x10000/0x10000

# 해당 iptables를 확인해보면 ipset에 대한 허용임
root@aks-nodepool1-16223536-vmss000000:/# iptables -L cali-pi-_-zqXhpUfbm6lphf3Orn
Chain cali-pi-_-zqXhpUfbm6lphf3Orn (1 references)
target     prot opt source               destination
MARK       tcp  --  anywhere             anywhere             /* cali:Jz1VzdhBJEh8b5nC */ /* Policy default/knp.default.allow-granted-access ingress */ match-set cali40s:ahXLGcJKqoUc6DMM01m_Oja src multiport dports http MARK or 0x10000

# ipset을 확인해보면 허용된 파드 IP를 확인 가능함
root@aks-nodepool1-16223536-vmss000000:/# ipset list cali40s:ahXLGcJKqoUc6DMM01m_Oja
Name: cali40s:ahXLGcJKqoUc6DMM01m_Oja
Type: hash:net
Revision: 7
Header: family inet hashsize 1024 maxelem 1048576 bucketsize 12 initval 0x4559b4b4
Size in memory: 504
References: 1
Number of entries: 1
Members:
10.224.0.11

# 허용된 Pod IP
$ kubectl get po -owide
NAME                    READY   STATUS    RESTARTS   AGE   IP            NODE                                NOMINATED NODE   READINESS GATES
client-allowed          1/1     Running   0          21m   10.224.0.11   aks-nodepool1-16223536-vmss000000   <none>           <none>

# iptables의 정책은 실제로 server 파드의 인터페이스에서 동작하고 있음
root@aks-nodepool1-16223536-vmss000000:/# iptables -S | grep cali-pi-_-zqXhpUfbm6lphf3Orn
...
-A cali-tw-azv1867ae04537 -m comment --comment "cali:qLBcoqYzO9kdru-z" -j cali-pi-_-zqXhpUfbm6lphf3Orn

# 파드의 veth 인터페이스 확인 -> 19
$ kubectl exec -it server-6f94b4c7-tptnd -- ip a
...
18: eth0@if19: <BROADCAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue qlen 1000
    link/ether a2:33:0f:d3:aa:7e brd ff:ff:ff:ff:ff:ff
    inet 10.224.0.13/16 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::a033:fff:fed3:aa7e/64 scope link
       valid_lft forever preferred_lft forever

# 노드의 인터페이스 확인 -> azv1867ae04537
root@aks-nodepool1-16223536-vmss000000:/# ip a
...
19: azv1867ae04537@if18: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether aa:aa:aa:aa:aa:aa brd ff:ff:ff:ff:ff:ff link-netns cni-d58178f5-2788-69b1-c4f9-c5af88779471
    inet6 fe80::a8aa:aaff:feaa:aaaa/64 scope link
       valid_lft forever preferred_lft forever


# 이때, client Pod IP가 추가되는 경우 -> ipset의 member가 추가됨
root@aks-nodepool1-16223536-vmss000000:/# ipset list cali40s
ipset v7.15: The set with the given name does not exist
root@aks-nodepool1-16223536-vmss000000:/# ipset list cali40s:ahXLGcJKqoUc6DMM01m_Oja
Name: cali40s:ahXLGcJKqoUc6DMM01m_Oja
Type: hash:net
Revision: 7
Header: family inet hashsize 1024 maxelem 1048576 bucketsize 12 initval 0x4559b4b4
Size in memory: 552
References: 1
Number of entries: 2
Members:
10.224.0.38
10.224.0.11

# server 파드가 증가하는 경우 -> iptables의 veth interface 정책이 추가됨
$ kubectl exec -it server-6f94b4c7-7tf2x -- ip a
...
24: eth0@if25: <BROADCAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue qlen 1000
    link/ether 2a:18:44:33:78:64 brd ff:ff:ff:ff:ff:ff
    inet 10.224.0.21/16 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::2818:44ff:fe33:7864/64 scope link
       valid_lft forever preferred_lft forever

root@aks-nodepool1-16223536-vmss000000:/# ip a
25: azv0a40327c417@if24: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether aa:aa:aa:aa:aa:aa brd ff:ff:ff:ff:ff:ff link-netns cni-b934579e-7782-1c9b-511f-98381ee5263c
    inet6 fe80::a8aa:aaff:feaa:aaaa/64 scope link
       valid_lft forever preferred_lft forever

# 인터페이스에 대한 정책이 추가됨
root@aks-nodepool1-16223536-vmss000000:/# iptables -S | grep cali-pi-_-zqXhpUfbm6lphf3Orn
-N cali-pi-_-zqXhpUfbm6lphf3Orn
-A cali-pi-_-zqXhpUfbm6lphf3Orn -p tcp -m comment --comment "cali:Jz1VzdhBJEh8b5nC" -m comment --comment "Policy default/knp.default.allow-granted-access ingress" -m set --match-set cali40s:ahXLGcJKqoUc6DMM01m_Oja src -m multiport --dports 80 -j MARK --set-xmark 0x10000/0x10000
-A cali-tw-azv0a40327c417 -m comment --comment "cali:it2wtgChKCWF3MNr" -j cali-pi-_-zqXhpUfbm6lphf3Orn # 추가됨
-A cali-tw-azv1867ae04537 -m comment --comment "cali:qLBcoqYzO9kdru-z" -j cali-pi-_-zqXhpUfbm6lphf3Orn

 

이러한 방식은 새로운 파드가 생성되거나 혹은 변경되는 경우 규칙에서 IP를 추가하거나 제거하는 방식으로 업데이트가 이뤄지는데, 대규모 분산 애플리케이션이라고 하면 다수 노드에 많은 업데이트가 발생할 수 있습니다. (물론 Calico 에서도 ipset을 사용하는 방식 등으로 Iptables를 최대한 최적화를 한 것 같긴 합니다)

 

Cilium에서는 기존 방식에서 유연성을 주기 위해서 보안을 네트워크 주소라는 방식에서 분리하여, Identity 방식으로 제공합니다. 실제로는 숫자 형식의 Identity를 사용하지만, 아래 그림과 같은 Label 기반으로 구분된 파드를 동일한 Identity로 구분합니다.

../../../_images/identity.png

 

출처: https://docs.cilium.io/en/stable/security/network/identity/

 

만약 Label이 일치하는 신규 파드가 생성되면, 동일한 frontends라는 Identity를 부여 받게 되고, 이후 backends라는 Identity로 연결이 가능해 집니다. 이로써 모든 노드를 업데이트해야 할 필요가 없어집니다.

 

이와 같이 cilium에서는 파드의 Label 기반으로 보안 식별자(identity)를 가지고 식별하게 됩니다. 이후 NetworkPolicy에서는 endpointSelector 와 같은 형태로 파드(endpoint)를 식별해 identity로 인식하고 이를 바탕으로 통신을 허용하는 방식으로 동작한다.

 

실제 Cilium 클러스터에서 확인해보겠습니다.

# Cilium의 엔드포인트의 Identity 확인
kubectl get ciliumendpoints.cilium.io -n kube-system

# 같은 label을 가지는 파드들은 가은 identity를 가지고 있다, 같은 보안 정책을 공유함
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl get ciliumendpoints.cilium.io -n kube-system
NAME                              SECURITY IDENTITY   ENDPOINT STATE   IPV4           IPV6
coredns-674b8bbfcf-pcdst          30494               ready            172.20.0.242
coredns-674b8bbfcf-pn8k7          30494               ready            172.20.0.100
hubble-relay-fdd49b976-r548j      17545               ready            172.20.0.160
hubble-ui-655f947f96-f6vhr        11820               ready            172.20.0.114
metrics-server-5dd7b49d79-bj65g   601                 ready            172.20.0.138


# Namespace 별 Security Identity 확인
kubectl get ciliumidentities.cilium.io 

(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl get ciliumidentities.cilium.io
NAME    NAMESPACE            AGE
10477   local-path-storage   12m
11451   cilium-monitoring    12m
11820   kube-system          12m
17545   kube-system          12m
30494   kube-system          12m
601     kube-system          12m
62283   cilium-monitoring    12m

이러한 identity는 서로 security label로 구분됩니다. ciliumIdentity를 조회해보면 어떤 security label로 구성되어 있는지 확인할 수 있습니다.

kubectl get ciliumidentities.cilium.io 14735 -o yaml | yq

(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl get ciliumidentities.cilium.io 30494 -o yaml | yq
{
  "apiVersion": "cilium.io/v2",
  "kind": "CiliumIdentity",
  "metadata": {
    "creationTimestamp": "2025-09-04T13:13:50Z",
    "generation": 1,
    "labels": {
      "io.kubernetes.pod.namespace": "kube-system"
    },
    "name": "30494",
    "resourceVersion": "902",
    "uid": "b59af7e9-83f3-4221-ba5c-e8e3db15f33e"
  },
  "security-labels": {
    "k8s:io.cilium.k8s.namespace.labels.kubernetes.io/metadata.name": "kube-system",
    "k8s:io.cilium.k8s.policy.cluster": "default",
    "k8s:io.cilium.k8s.policy.serviceaccount": "coredns",
    "k8s:io.kubernetes.pod.namespace": "kube-system",
    "k8s:k8s-app": "kube-dns"
  }
}

# cilium agent에서 identity list 확인
kubectl exec -it -n kube-system ds/cilium -- cilium identity list

(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl exec -it -n kube-system ds/cilium -- cilium identity list
ID      LABELS
1       reserved:host
        reserved:kube-apiserver
2       reserved:world
3       reserved:unmanaged
4       reserved:health
5       reserved:init
6       reserved:remote-node
7       reserved:kube-apiserver
        reserved:remote-node
8       reserved:ingress
9       reserved:world-ipv4
10      reserved:world-ipv6
601     k8s:app.kubernetes.io/instance=metrics-server
        k8s:app.kubernetes.io/name=metrics-server
        k8s:io.cilium.k8s.namespace.labels.kubernetes.io/metadata.name=kube-system
        k8s:io.cilium.k8s.policy.cluster=default
        k8s:io.cilium.k8s.policy.serviceaccount=metrics-server
        k8s:io.kubernetes.pod.namespace=kube-system
10477   k8s:app=local-path-provisioner
        k8s:io.cilium.k8s.namespace.labels.kubernetes.io/metadata.name=local-path-storage
        k8s:io.cilium.k8s.policy.cluster=default
        k8s:io.cilium.k8s.policy.serviceaccount=local-path-provisioner-service-account
        k8s:io.kubernetes.pod.namespace=local-path-storage
11451   k8s:app=prometheus
        k8s:io.cilium.k8s.namespace.labels.kubernetes.io/metadata.name=cilium-monitoring
        k8s:io.cilium.k8s.policy.cluster=default
        k8s:io.cilium.k8s.policy.serviceaccount=prometheus-k8s
        k8s:io.kubernetes.pod.namespace=cilium-monitoring
11820   k8s:app.kubernetes.io/name=hubble-ui
        k8s:app.kubernetes.io/part-of=cilium
        k8s:io.cilium.k8s.namespace.labels.kubernetes.io/metadata.name=kube-system
        k8s:io.cilium.k8s.policy.cluster=default
        k8s:io.cilium.k8s.policy.serviceaccount=hubble-ui
        k8s:io.kubernetes.pod.namespace=kube-system
        k8s:k8s-app=hubble-ui
17545   k8s:app.kubernetes.io/name=hubble-relay
        k8s:app.kubernetes.io/part-of=cilium
        k8s:io.cilium.k8s.namespace.labels.kubernetes.io/metadata.name=kube-system
        k8s:io.cilium.k8s.policy.cluster=default
        k8s:io.cilium.k8s.policy.serviceaccount=hubble-relay
        k8s:io.kubernetes.pod.namespace=kube-system
        k8s:k8s-app=hubble-relay
30494   k8s:io.cilium.k8s.namespace.labels.kubernetes.io/metadata.name=kube-system
        k8s:io.cilium.k8s.policy.cluster=default
        k8s:io.cilium.k8s.policy.serviceaccount=coredns
        k8s:io.kubernetes.pod.namespace=kube-system
        k8s:k8s-app=kube-dns
62283   k8s:app=grafana
        k8s:io.cilium.k8s.namespace.labels.kubernetes.io/metadata.name=cilium-monitoring
        k8s:io.cilium.k8s.policy.cluster=default
        k8s:io.cilium.k8s.policy.serviceaccount=default
        k8s:io.kubernetes.pod.namespace=cilium-monitoring


# 파드가 모든 라벨을 가지는 것은 아니며, cilium이 추가로 security label을 추가하는 것으로 보임
kubectl get pod -n kube-system -l k8s-app=kube-dns --show-labels

(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl get pod -n kube-system -l k8s-app=kube-dns --show-labels
NAME                       READY   STATUS    RESTARTS   AGE   LABELS
coredns-674b8bbfcf-pcdst   1/1     Running   0          19m   k8s-app=kube-dns,pod-template-hash=674b8bbfcf
coredns-674b8bbfcf-pn8k7   1/1     Running   0          19m   k8s-app=kube-dns,pod-template-hash=674b8bbfcf

 

참고로 파드에 label을 추가하면 새로운 Identity로 생성되는 것을 알 수 있습니다. (대규모 클러스터에서는 자주 Label을 변경하면 Identity 할당으로 성능 저하가 발생할 수 있음)

kubectl label pods -n kube-system -l k8s-app=kube-dns study=security

(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl label pods -n kube-system -l k8s-app=kube-dns study=security
pod/coredns-674b8bbfcf-pcdst labeled
pod/coredns-674b8bbfcf-pn8k7 labeled

kubectl exec -it -n kube-system ds/cilium -- cilium identity list

(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl exec -it -n kube-system ds/cilium -- cilium identity list
...
30494   k8s:io.cilium.k8s.namespace.labels.kubernetes.io/metadata.name=kube-system
        k8s:io.cilium.k8s.policy.cluster=default
        k8s:io.cilium.k8s.policy.serviceaccount=coredns
        k8s:io.kubernetes.pod.namespace=kube-system
        k8s:k8s-app=kube-dns

# 잠시후 새로운 Identity로 변경된 것으로 확인됨
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl exec -it -n kube-system ds/cilium -- cilium identity list
...
33353   k8s:io.cilium.k8s.namespace.labels.kubernetes.io/metadata.name=kube-system
        k8s:io.cilium.k8s.policy.cluster=default
        k8s:io.cilium.k8s.policy.serviceaccount=coredns
        k8s:io.kubernetes.pod.namespace=kube-system
        k8s:k8s-app=kube-dns
        k8s:study=security

 

Proxy Injection

L7 정책의 통제는 envoy 프록시를 통해서 이뤄집니다. Cilium에서 L7 기능을 활성화하면, Cilium Agent는 Envoy Proxy를 별도의 프로세스로 실행 해야 합니다. 혹은 envoy.enabledtrue 로 설정해서 Envoy 프록시를 독립적인 라이프 사이클을 가지는 DaemonSet 형태로 실행할 수 있으며 이 경우 cilium-envoy라는 파드가 실행됩니다.

 

아래는 대략적인 아키텍처와 트래픽 흐름의 예시로, Cilium-agent와 Envoy에 정책 설정을 내리게 되며, 실제 트래픽은 eBPF를 통해서 Envoy 프록시를 거쳐서 실제 서비스 파드로 향하게 됩니다.

출처: https://docs.cilium.io/en/stable/security/network/proxy/envoy/

 

이제 Cilium Network Policy를 통해서 살펴보겠습니다.

 

 

2. Cilium Network Policy

Cilium에서는 쿠버네티스 Network Policy에서 확장된 CiliumNetworkPolicy를 CRD로 제공하고 있습니다.

아래 그림에서 쿠버네티스의 Network Policy와 Cilium Network Policy의 차이점을 확인할 수 있으며, Cilium Network Policy에서 보다 풍부한 조건의 Network Policy를 구현할 수 있습니다. CNCF에서는 주관하는 CKS 시험에서도 과거에는 기본적인 NetworkPolicy을 다뤘다면, 리뉴얼된 시험에서는 CiliumNetworkPolicy에 대한 문제도 추가되었습니다.

출처: https://isovalent.com/blog/post/intro-to-cilium-network-policies/

 

쿠버네티스의 Network Policy가 3, 4계층의 Network Policy를 지원하는데 반해 Cilium Network Policy는 3~7계층에서 송/수신 정책을 지원합니다. 또한 Cilium Clusterwide Network Policy를 통해서 클러스터 범위의 정책을 지원하는 CRD도 있습니다.

Network Policy를 생성하지 않으면 기본적으로 모든 엔드포인트에 대해 모든 송신 및 수신 트래픽이 허용되는 상태입니다. 이때, 네트워크 정책을 생성하여 엔드포인트가 선택되면 명시적으로 허용된 트래픽만 허용되고 나머지 통신은 default Deny 상태로 전환됩니다.

 

참고로, 아래의 Network Policy 정책 Editor를 통해서 Network Policy나 Cilium Network Policy를 연습해볼 수 있습니다.

참고: https://editor.networkpolicy.io/?id=kDEN8z93C4bi0Yzn

 

각 레이어별 Network Policy의 예제를 살펴보고 간단히 실습도 진행해보겠습니다.

 

L3 Policy

CiliumNetworkPolicy 에서 L3 계층의 정책은 Endpoint, Service, Entity, Node, IP/CIDR, DNS 기반으로 동작하는 정책을 생성할 수 있습니다.

참고: https://docs.cilium.io/en/stable/security/policy/language/#layer-3-examples

 

Endpoint based

CiliumNetworkPolicy의 L3 정책은 아래와 같습니다.

# CiliumNetworkPolicy
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "l3-rule"
spec:
  endpointSelector:
    matchLabels:
      role: backend
  ingress:
  - fromEndpoints:
    - matchLabels:
        role: frontend

 

이를 쿠버네티스 NetworkPolicy와 비교해보면 아래와 같습니다. Cilium에서는 파드를 Cilium Endpoint로 인식합니다. 얼핏 보면 podSelector를 endpointSelector로 변경하고, 조금 더 간결한 문법을 사용합니다.

# NetworkPolicy
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-frontend-to-backend
spec:
  podSelector:
    matchLabels:
      role: backend
  policyTypes:
  - Ingress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          role: frontend

Endpoint based 정책은 기존 쿠버네티스 Network Policy에서도 유사하게 사용할 수 있습니다. 이후 살펴볼 정책들은 CiliumNetworkPolicy에서만 가능합니다.

 

Service based

CiliumNetworkPolicy 에서는 Service의 이름이나, Service에 지정된 Label로 정책을 생성할 수 있습니다.

apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "service-rule"
spec:
  endpointSelector:
    matchLabels:
      id: app2
  egress:
  - toServices:
    # Services may be referenced by namespace + name
    - k8sService:
        serviceName: myservice
        namespace: default
    # Services may be referenced by namespace + label selector
    - k8sServiceSelector:
        selector:
          matchLabels:
            env: staging
        namespace: another-namespace

 

Entities based

Entities based 정책은 정의된 엔티티에 대한 네트워크 정책을 정의할 수 있도록 합니다.

host, remote-node, kube-apiserver, ingress, cluster, init, health, unmanaged, world, all 과 같은 Entities를 사용할 수 있습니다.

  • host: 로컬 호스트. 여기에는 로컬 호스트의 호스트 네트워킹 모드에서 실행되는 모든 컨테이너도 포함됩니다.
  • remote-node: 로컬 호스트 이외의 연결된 클러스터에 있는 모든 노드입니다.
  • kube-apiserver: 클러스터의 kube-apiserver를 나타냅니다.
  • ingress: 수신 L7 트래픽을 처리하는 Cilium Envoy 인스턴스를 나타냅니다.
  • cluster: 로컬 클러스터 내부의 모든 네트워크 엔드포인트의 논리적 그룹입니다.
  • init: 부트스트랩 단계의 모든 엔드포인트가 포함되어 seuciry identity가 아직 확인되지 않은 상태입니다.
  • health: 클러스터 연결 상태를 확인하는 데 사용되는 상태 엔드포인트를 나타냅니다.
  • unmanaged: Cilium에서 관리하지 않는 엔드포인트를 나타냅니다.
  • world: 클러스터 외부의 모든 엔드포인트에 해당합니다. world에 허용하는 것은 CIDR 0.0.0.0/0에 허용하는 것과 동일합니다.
  • all: 알려진 모든 클러스터의 조합과 world를 나타내며 모든 통신을 화이트리스트에 추가합니다.

 

기존 네트워크 정책에서는 없는 개념이라, 몇가지 Entity는 어떤 의미인지 정확하지 않기도 합니다. 이해하기로는 정책의 대상을 단순히 쿠버네티스 리소스에 국한하지 않고, 보다 포괄적이면서 구분 가능한 개체 혹은 집합의 개념을 도입한 것 같습니다. 이를 통해 쿠버네티스 클러스터 내부 리소스 간의 보안이 아닌, 클러스터의 바운더리를 확장한 네트워크에서 보안을 제공하는 것으로 보입니다.

 

아래 Entitiy에 대해서 테스트 해보겠습니다. 샘플 애플리케이션과 dev 목적의 pod를 실행합니다.

cat << EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: webpod
spec:
  replicas: 2
  selector:
    matchLabels:
      app: webpod
  template:
    metadata:
      labels:
        app: webpod
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app
                operator: In
                values:
                - sample-app
            topologyKey: "kubernetes.io/hostname"
      containers:
      - name: webpod
        image: traefik/whoami
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: webpod
  labels:
    app: webpod
spec:
  selector:
    app: webpod
  ports:
  - protocol: TCP
    port: 80
    targetPort: 80
  type: ClusterIP
EOF

# k8s-w1 노드에 dev 파드 배포
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: dev-pod
  labels:
    env: dev
spec:
  nodeName: k8s-w1
  containers:
  - name: netshoot
    image: nicolaka/netshoot
    command: ["tail"]
    args: ["-f", "/dev/null"]
  terminationGracePeriodSeconds: 0
EOF

 

생성된 파드를 확인해보고, 통신 테스트를 수행해 보겠습니다.

(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl get no -owide
NAME      STATUS   ROLES           AGE   VERSION   INTERNAL-IP      EXTERNAL-IP   OS-IMAGE             KERNEL-VERSION     CONTAINER-RUNTIME
k8s-ctr   Ready    control-plane   43h   v1.33.4   192.168.10.100   <none>        Ubuntu 24.04.2 LTS   6.8.0-64-generic   containerd://1.7.27
k8s-w1    Ready    <none>          43h   v1.33.4   192.168.10.101   <none>        Ubuntu 24.04.2 LTS   6.8.0-64-generic   containerd://1.7.27
k8s-w2    Ready    <none>          43h   v1.33.4   192.168.10.102   <none>        Ubuntu 24.04.2 LTS   6.8.0-64-generic   containerd://1.7.27

(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl get svc
NAME         TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
kubernetes   ClusterIP   10.96.0.1       <none>        443/TCP   43h
webpod       ClusterIP   10.96.220.228   <none>        80/TCP    5m26s

# 통신 테스트
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl exec -it dev-pod -- bash
dev-pod:~# curl webpod
Hostname: webpod-697b545f57-xc9cc
IP: 127.0.0.1
IP: ::1
IP: 172.20.2.48
IP: fe80::e4b8:eff:fe4b:f81e
RemoteAddr: 172.20.1.194:33880
GET / HTTP/1.1
Host: webpod
User-Agent: curl/8.14.1
Accept: */*

dev-pod:~# ping -c 1 192.168.10.100
PING 192.168.10.100 (192.168.10.100) 56(84) bytes of data.
64 bytes from 192.168.10.100: icmp_seq=1 ttl=63 time=1.53 ms

--- 192.168.10.100 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 1.530/1.530/1.530/0.000 ms

 

테스트 파드에서 통신을 테스트 해보면 특이사항이 없습니다.

아래와 같이 정책을 만들어 보겠습니다. (entities based 정책을 만드는 것 자체가 그 외의 통신을 거부하도록 강제하지 않는 것 같습니다. default deny 정책도 추가해서 테스트 했습니다)

# 기본 차단 정책
cat <<EOF | kubectl apply -f -
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "default-deny-dev"
spec:
  endpointSelector:
    matchLabels:
      env: dev
  egress: []
EOF

# host에 대해서만 허용
cat << EOF | kubectl apply -f -
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "dev-to-host"
spec:
  endpointSelector:
    matchLabels:
      env: dev
  egress:
    - toEntities:
      - host
EOF

 

해당 파드가 위치한 노드 외에는 모드 차단되는 것을 알 수 있습니다.

# 허용됨
dev-pod:~# ping -c 1 192.168.10.101
PING 192.168.10.101 (192.168.10.101) 56(84) bytes of data.
64 bytes from 192.168.10.101: icmp_seq=1 ttl=64 time=2.66 ms

--- 192.168.10.101 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms

# 차단됨
dev-pod:~# curl -m 1 webpod
curl: (28) Resolving timed out after 1003 milliseconds
dev-pod:~# ping -c 1 192.168.10.100
PING 192.168.10.100 (192.168.10.100) 56(84) bytes of data.
^C
--- 192.168.10.100 ping statistics ---
1 packets transmitted, 0 received, 100% packet loss, time 0ms

rtt min/avg/max/mdev = 2.655/2.655/2.655/0.000 ms
dev-pod:~# ping -c 1 192.168.10.102
PING 192.168.10.102 (192.168.10.102) 56(84) bytes of data.
^C
--- 192.168.10.102 ping statistics ---
1 packets transmitted, 0 received, 100% packet loss, time 0ms

 

정책을 추가해보겠습니다.

# host에 대해서만 허용
cat << EOF | kubectl apply -f -
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "dev-to-remote-node"
spec:
  endpointSelector:
    matchLabels:
      env: dev
  egress:
    - toEntities:
      - remote-node
EOF

 

이제 다른 노드도 통신이 가능해집니다.

dev-pod:~# ping -c 1 192.168.10.100
PING 192.168.10.100 (192.168.10.100) 56(84) bytes of data.
64 bytes from 192.168.10.100: icmp_seq=1 ttl=63 time=4.02 ms

--- 192.168.10.100 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 4.021/4.021/4.021/0.000 ms
dev-pod:~# ping -c 1 192.168.10.102
PING 192.168.10.102 (192.168.10.102) 56(84) bytes of data.
64 bytes from 192.168.10.102: icmp_seq=1 ttl=63 time=1.88 ms

--- 192.168.10.102 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 1.884/1.884/1.884/0.000 ms

 

이 보다 세부적인 노드간 통신 제어를 Node based 정책에서 가능하게 됩니다.

 

Node based

Entity 기반의 host 혹은 remote-node를 확장해서 Label 기반으로 노드 통신을 제어할 수 있습니다.

예를 들어, 노드 그룹(노드 풀)로 prd-front 노드 그룹과 prd-backend 노드 그룹을 기타 dev-*로 노드 그룹을 가지는 경우, prd 노드 그룹 간의 통신만 허용하는 시나리오가 있을 수 있습니다.

apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "to-prod-from-control-plane-nodes"
spec:
  endpointSelector:
    matchLabels:
      env: prod
  ingress:
    - fromNodes:
        - matchLabels:
            node-role.kubernetes.io/control-plane: ""

 

IP/CIDR based

IP나 혹은 CIDR을 허용 해줄 수 있습니다.

apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "cidr-rule"
spec:
  endpointSelector:
    matchLabels:
      app: myService
  egress:
  - toCIDR:
    - 20.1.1.1/32
  - toCIDRSet:
    - cidr: 10.0.0.0/8
      except:
      - 10.96.0.0/12

 

이 정책도 쿠버네티스의 기본 Network Policy에서 유사하게 생성 가능합니다.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-cidr
spec:
  podSelector:
    matchLabels:
      app: myService
  policyTypes:
  - Egress
  egress:
  - to:
    - ipBlock:
        cidr: 20.1.1.1/32
  - to:
    - ipBlock:
        cidr: 10.0.0.0/8
        except:
        - 10.96.0.0/12

 

DNS based

Cilium의 DNS-Based Policy는 L7 수준으로 통제 되는 것은 아니며, Cilium Agent 내부의 DNS Proxy를 통해서 처리됩니다.

먼저 DNS-Based Policy가 만들어지면, 파드가 DNS 요청을 보낼 때, DNS Proxy가 요청을 가로채고, DNS=프록시는 요청을 DNS 서버로 전달해서 응답을 파드에 전달하기 전에 응답 IP를 내부적으로 저장합니다. 결국 toFQDNs에 명시된 도메인과 일치하는 응답 IP만 egress 트래픽을 대상으로 허용되므로, 이러한 동작은 L3 수준의 통제가 됩니다.

파드가 실제로 DNS 응답에서 받은 IP로 연결을 시도하면 Cilium은 해당 IP가 정책에 의해 허용된 것인지 확인하여 허용 혹은 차단합니다.

 

아래와 같이 실습을 통해서 살펴보겠습니다.

참고: https://docs.cilium.io/en/stable/security/dns/

 

mediabot이라는 파드를 생성하겠습니다. 이 시나리오에서 mediabot은 GitHub 리파지터리를 관리하기 위해 github에 접근해야하고, 다른 서비스에서는 접근을 하면 안됩니다.

$ kubectl create -f https://raw.githubusercontent.com/cilium/cilium/1.18.1/examples/kubernetes-dns/dns-sw-app.yaml
$ kubectl wait pod/mediabot --for=condition=Ready
$ kubectl get pods

(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl get po
NAME                      READY   STATUS    RESTARTS   AGE
mediabot                  1/1     Running   0          49s

 

아래와 같이 정책을 배포 합니다.

cat << EOF | kubectl apply -f -
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "fqdn"
spec:
  endpointSelector:
    matchLabels:
      org: empire
      class: mediabot
  egress:
  - toFQDNs:
    - matchName: "api.github.com" # *.github.com 도 등록 가능함
  - toEndpoints:
    - matchLabels:
        "k8s:io.kubernetes.pod.namespace": kube-system
        "k8s:k8s-app": kube-dns
    toPorts:
    - ports:
      - port: "53"
        protocol: ANY
      rules:
        dns:
        - matchPattern: "*"
EOF

 

아래와 같이 테스트 해보면 toFQDNs로 허용한 api.github.com만 접근이 되는 것을 알 수 있습니다.

$ kubectl exec mediabot -- curl -I -s https://api.github.com | head -1
$ kubectl exec mediabot -- curl -I -s --max-time 5 https://support.github.com | head -1

(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl exec mediabot -- curl -I -s https://api.github.com | head -1
HTTP/2 200
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl exec mediabot -- curl -I -s --max-time 5 https://support.github.com | head -1
command terminated with exit code 28

 

L4 Policy

CiliumNetworkPolicy 에서 L4 계층의 정책은 L3 정책과 함께 사용할 수도 있고, 독립적으로 사용할 수도 있습니다.

참고: https://docs.cilium.io/en/stable/security/policy/language/#layer-4-examples

 

L4 계층이므로, 특정 프로토콜과 포트를 지정하여 패킷을 필터링 할 수 있습니다.

apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "l4-rule"
spec:
  endpointSelector:
    matchLabels:
      app: myService
  egress:
    - toPorts:
      - ports:
        - port: "80"
          protocol: TCP

 

L7 Policy

Cilium에서는 L7 정책을 제공하며, 이 정책은 노드에 실행 중인 Envoy 인스턴스를 통해서 트래픽을 프록시하여 동작합니다.

참고: https://docs.cilium.io/en/stable/security/policy/language/#layer-7-examples

 

3, 4 계층의 정책과 다르게 L7 계층의 정책은 패킷의 손실을 의미하지 않습니다. 가능한 경우 애플리케이션 프로토콜 별 접근 거부 메시지를 작성하여 반환한다는 차이가 있습니다. 예를 들어, HTTP 요청에 대해서는 HTTP 403 access denied가 DNS 요청의 경우에는 DNS REFUSED와 같은 응답을 받습니다.

 

HTTP 프로토콜을 예를 들면, 특정 path를 허용할 수 있습니다.

apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "rule1"
spec:
  description: "Allow HTTP GET /public from env=prod to app=service"
  endpointSelector:
    matchLabels:
      app: service
  ingress:
  - fromEndpoints:
    - matchLabels:
        env: prod
    toPorts:
    - ports:
      - port: "80"
        protocol: TCP
      rules:
        http:
        - method: "GET"
          path: "/public"

 

아래와 같이 샘플 애플리케이션을 생성해보겠습니다.

cat << EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: service-pod
  labels:
    app: service
spec:
  containers:
  - name: nginx
    image: nginx
    volumeMounts:
    - name: html
      mountPath: /usr/share/nginx/html
    - name: config
      mountPath: /etc/nginx/conf.d/default.conf
      subPath: default.conf
  volumes:
  - name: html
    configMap:
      name: service-html
  - name: config
    configMap:
      name: nginx-config
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx-config
data:
  default.conf: |
    server {
      listen 80;
      location /public {
        rewrite ^/public$ /public.html break;
        root /usr/share/nginx/html;
      }
      location /private {
        rewrite ^/private$ /private.html break;
        root /usr/share/nginx/html;
      }
    }
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: service-html
data:
  public.html: |
    <html><body><h1>Hello from /public</h1></body></html>
  private.html: |
    <html><body><h1>Hello from /private</h1></body></html>
EOF

# Service 생성
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Service
metadata:
  name: service-svc
spec:
  selector:
    app: service
  ports:
  - name: http
    port: 80
    targetPort: 80
  type: ClusterIP
EOF

# client pod 생성
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: client-prod
  labels:
    env: prod
spec:
  containers:
  - name: netshoot
    image: nicolaka/netshoot
    command: ["tail"]
    args: ["-f", "/dev/null"]
  terminationGracePeriodSeconds: 0
EOF

cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: client-dev
  labels:
    env: dev
spec:
  containers:
  - name: netshoot
    image: nicolaka/netshoot
    command: ["tail"]
    args: ["-f", "/dev/null"]
  terminationGracePeriodSeconds: 0
EOF

 

아래와 같이 정의한 /public, /private path가 잘 호출되는지 확인해보겠습니다.

(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl exec -it client-prod -- curl -m 1 service-svc/public
<html><body><h1>Hello from /public</h1></body></html>
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl exec -it client-prod -- curl -m 1 service-svc/private
<html><body><h1>Hello from /private</h1></body></html>

(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl exec -it client-dev -- curl -m 1 service-svc/public
<html><body><h1>Hello from /public</h1></body></html>
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl exec -it client-dev -- curl -m 1 service-svc/private
<html><body><h1>Hello from /private</h1></body></html>

 

이제 path 기준으로 정책을 생성하겠습니다. 여기서는 /public 만 허용합니다.

cat <<EOF | kubectl apply -f -
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "allow-public-from-prod"
spec:
  description: "Allow HTTP GET /public from env=prod to app=service"
  endpointSelector:
    matchLabels:
      app: service
  ingress:
  - fromEndpoints:
    - matchLabels:
        env: prod
    toPorts:
    - ports:
      - port: "80"
        protocol: TCP
      rules:
        http:
        - method: "GET"
          path: "/public"
EOF

 

결과를 확인해보면, app=service를 가진 서비스로 호출하는데 아래와 같이 결과에 차이가 있습니다.

# env=prod는 /private에 대해서 'Access denied'로 실패함
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl exec -it client-prod -- curl -m 1 service-svc/public
<html><body><h1>Hello from /public</h1></body></html>
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl exec -it client-prod -- curl -m 1 service-svc/private
Access denied

# env=dev는 모든 요청에 실패함
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl exec -it client-dev -- curl -m 1 service-svc/public
curl: (28) Connection timed out after 1002 milliseconds
command terminated with exit code 28
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl exec -it client-dev -- curl -m 1 service-svc/private
curl: (28) Connection timed out after 1002 milliseconds
command terminated with exit code 28

 

L7 정책에서는 특정 header를 가진 요청에 대해서 허용할 수 도 있습니다. 아래 예시에서는 X-My-Header: true헤더가 있는 경우 요청이 허용됩니다.

apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "l7-rule"
spec:
  endpointSelector:
    matchLabels:
      app: myService
  ingress:
  - toPorts:
    - ports:
      - port: '80'
        protocol: TCP
      rules:
        http:
        - method: GET
          path: "/path1$"
        - method: PUT
          path: "/path2$"
          headers:
          - 'X-My-Header: true'

이상 Cilium의 Network Policy에서 기존 쿠버네티스 Network 보다 확장된 기능을 살펴봤습니다.

 

마치며

Cilium의 Security를 다루며 Cilium Network security에서 필요한 개념과 CiliumNetworkPolicy에 대해서 살펴봤습니다. Cilium에서는 그 외에 노드 간 네트워크 패킷 암호화를 위한 Transparent Encryption 기능도 제공하고 있습니다.

 

본 게시물은 CloudNet에서 진행하는 Cilium 스터디를 참여하면서, 제공해주신 가이드를 바탕으로 학습한 내용을 정리한 내용입니다. 지난 8주 간 Cilium이 제공하는 기본적인 CNI Plugin의 역할뿐 아니라, Observability, 외부 라우팅 연동, Multi Cluster, Service Mesh, CiliumNetworkPolicy와 같은 다양한 주제를 살펴봤습니다.

 

살펴보기로 Cilium은 기본적인 CNI Plugin의 역할이 아닌, '네트워크'의 범주에서 필요한 모든 기능을 추가 Addon이 필요없이 Cilium에서 All-in-One으로 제공하기 위해 확장하고 있는 것으로 보입니다.

다만 제공되는 기능이 넓어지다보니 beta 수준의 기능이 많다는 점과 한편 클라우드의 매니지드 쿠버네티스 서비스에서는 이러한 모든 기능을 활용하기 어려울 수 있다는 점이 여전히 한계로 느껴집니다. 그러함에도 확장된 기능이 아닌 CNI Plugin 자체로 Cilium과 eBPF가 주는 효율성은 대규모 클러스터에서는 여전히 좋은 선택지가 될 것으로 보입니다.

'Cilium' 카테고리의 다른 글

AKS의 Azure CNI Powered by Cilium  (0) 2025.08.29
[9] Cilium - ServiceMesh  (0) 2025.08.23
[8] Cilium - Cluster Mesh  (0) 2025.08.14
[7] Cilium - BGP Control Plane  (0) 2025.08.14
[6] Cilium - LoadBalancer IPAM, L2 Announcement  (0) 2025.08.08