| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 | 20 | 21 |
| 22 | 23 | 24 | 25 | 26 | 27 | 28 |
- windows
- ipam
- 쿠버네티스
- KEDA
- Timeout
- gateway api
- curl
- vscode
- Karpenter
- ansible
- HPA
- Object Storage
- kubernetes
- aws
- ubuntu
- EKS
- VPA
- go
- AKS
- AutoScaling
- 묘공단
- Azure
- 업그레이드
- upgrade
- minIO
- cilium
- directpv
- 컨테이너
- calico
- WSL
- Today
- Total
a story
[10] Cilium - Security 본문
이번 게시물에서는 Cilium에서 제공하는 Security 기능에 대해서 살펴보겠습니다.
Cilium의 Security 에 대한 설명 중 Network Security와 Network Policy 부분에서 중점을 두고 설명하도록 하겠습니다.

목차
- Network Security 개념
- 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로 구분합니다.

출처: 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.enabled를 true 로 설정해서 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 |