Cilium - Cilium Security

Gyullbb·2025년 9월 6일
0

K8S

목록 보기
22/22

Cilium은 여러 수준에서 보안을 제공하는데, 각 수준은 개별적으로 사용될 수도 있고, 함께 결합하여 사용할 수도 있다.
각 수준에 대해 알아본다.

Layer 3 (Identity-Based)

엔드포인트간 연결 정책을 정의하는 방식이다.
전통적인 Kubernetes에서 사용하는 IP 기반 보안 모델은 파드가 생성·삭제될 때마다 모든 노드의 보안 규칙을 갱신해야 하기에 대규모 환경에서 한계가 존재하였다.
Cilium은 이러한 한계를 극복하기 위해 Identity를 도입하여, 보안을 IP와 완전히 분리하고 Identity 기반으로 통신 규칙을 정의하는 방안을 도입하였다.

전통적인 Kubernetes

  • 파드마다 할당된 IP 주소 기반으로 보안 정책 적용
  • 파드가 생성·삭제될 때마다 모든 노드의 보안 규칙(IP 필터)을 갱신해야 함
  • 대규모 환경에서 확장성·유연성에 한계 존재

Cilium

  • 라벨 기반 아이덴티티(Identity) 를 도입해 보안 정책 적용
  • 파드의 생성·삭제 시 보안 규칙 재설정 불필요, 아이덴티티 해석만 수행
  • 확장성과 관리 효율성 향상

Identity

Identity란 모든 엔드포인트에 할당되는 객체로, 엔드포인트 간 기본 연결성을 강제하며 Layer 3 수준의 보안 적용에 사용된다.

Cilium은 엔드포인트의 보안 관련 라벨(Security Relevant Labels)에 따라, 각 엔드포인트에 클러스터 전체에서 고유한 식별자를 부여한다.
동일한 라벨 집합을 가진 엔드포인트는 동일한 Identity를 공유하게 된다. 이 Identity 개념을 통해 application을 확장하더라도 동일한 보안 라벨 집합에 속하는 엔드포인트는 모두 동일한 Identity를 공유하기 때문에, 정책 적용을 매우 큰 규모로 확장할 수 있다.

파드나 컨테이너의 라벨이 변경되면 아이덴티티도 다시 확인되며, 필요 시 자동으로 수정된다.

Cilium Code

코드와 함께 Identity가 엔드포인트에 할당되는 과정을 확인해본다.

아래 코드는 엔드포인트 보안 라벨 변경을 확인하고, Identity를 할당하는 코드이다.
Identity에는 Local Identity와 Global Identity가 있는데, Local Identity는 단일 노드 내에서만 고유한 ID를 부여받는 엔드포인트이며, Global Identity는 클러스터 전체에서 고유한 ID를 부여받는 엔드포인트이다.
Local Identity의 경우, 컨트롤러 동기화 및 KV Store 확인 없이 노드 내에서 바로 Identity를 할당한다.
Global Identity의 경우, 컨트롤러 동기화를 수행하며, 컨트롤러에서는 Identity 할당을 진행한다.

//cilium/pkg/endpoint/endpoint.go
func (e *Endpoint) runIdentityResolver(ctx context.Context, blocking bool, updateJitter time.Duration) (regenTriggered bool) {
...
	newLabels := e.labels.IdentityLabels()
	e.runlock()
...
  //Local Identity의 경우, 컨트롤러 동기화 및 KV Store 확인 없이 노드 내에서 바로 Identity를 할당한다.
	if blocking || identity.IdentityAllocationIsLocal(newLabels) {
    scopedLog.Info("Resolving identity labels (blocking)")
		regenTriggered, err = e.identityLabelsChanged(ctx)
    ...
  }
  ...
  ctrlName := resolveIdentity + "-" + strconv.FormatUint(uint64(e.ID), 10)
  //Global Identity의 경우, 컨트롤러 동기화를 수행하며, 컨트롤러에서는 Identity 할당을 진행한다.
	e.controllers.UpdateController(ctrlName,
		controller.ControllerParams{
			Group: resolveIdentityControllerGroup,
			DoFunc: func(ctx context.Context) error {
				_, err := e.identityLabelsChanged(ctx)
...
			},
...
		},
	)
}

보안 라벨이 변경되었거나, 신규로 필요할 경우, 신규 Identity를 할당한다.

//cilium/pkg/endpoint/endpoint.go
func (e *Endpoint) identityLabelsChanged(ctx context.Context) (regenTriggered bool, err error) {
  ...
	allocatedIdentity, _, err := e.allocator.AllocateIdentity(ctx, newLabels, notifySelectorCache, identity.InvalidIdentity)
}

Global Identity의 경우, Controller에 의해 KV store에서 idPool 중 사용 가능한 ID를 할당받는다.

//cilium/pkg/identity/cache/allocator.go
func (m *CachingIdentityAllocator) AllocateIdentity(ctx context.Context, lbls labels.Labels, notifyOwner bool, oldNID identity.NumericIdentity) (id *identity.Identity, allocated bool, err error) {
  	idp, allocated, isNewLocally, err := m.IdentityAllocator.Allocate(ctx, &key.GlobalIdentity{LabelArray: lbls.LabelArray()})
}
//cilium/pkg/allocator/allocator.go
func (a *Allocator) Allocate(ctx context.Context, key AllocatorKey) (idpool.ID, bool, bool, error) {
  ...
    //KV store에서 idPool 중 사용 가능한 ID를 할당받는다.
  	kvstore.Trace(a.logger, "Allocating from kvstore", fieldKey, key)
		value, isNew, firstUse, err = a.lockedAllocate(ctx, key)
    if err == nil {
			a.mainCache.insert(key, value)
    }
...
}

Local Identity의 경우, Reserved Identity 여부 및 Well-Known Identity를 먼저 확인한다.
Reserved Identity인 경우 추가 할당 없이 예약된 Identity를 그대로 반환한다.

//cilium/pkg/identity/cache/allocator.go
func (m *CachingIdentityAllocator) AllocateLocalIdentity(lbls labels.Labels, notifyOwner bool, oldNID identity.NumericIdentity) (id *identity.Identity, allocated bool, err error) {

	// If this is a reserved, pre-allocated identity, just return that and be done
	if reservedIdentity := identity.LookupReservedIdentityByLabels(lbls); reservedIdentity != nil {
		m.logger.Debug(
			"Resolving reserved identity",
			logfields.Identity, reservedIdentity.ID,
			logfields.IdentityLabels, lbls,
			logfields.New, false,
		)
		return reservedIdentity, false, nil
	}

Reserved Identity

위 코드에서 보듯이, Cilium에는 Reserved Identity가 존재한다.
이는 Cilium이 네트워크 통신을 수행할 때 반드시 필요하거나, 보안 신원이 명확히 정의된 잘 알려진 엔드포인트에 할당된다.
Reserved Identity의 Numeric ID는 아래 코드에 정의되어 있다.

//cilium/pkg/identity/numericidentity.go
const (
	// IdentityUnknown represents an unknown identity
	IdentityUnknown NumericIdentity = iota

	// ReservedIdentityHost represents the local host
	ReservedIdentityHost

	// ReservedIdentityWorld represents any endpoint outside of the cluster
	ReservedIdentityWorld

	// ReservedIdentityUnmanaged represents unmanaged endpoints.
	ReservedIdentityUnmanaged

	// ReservedIdentityHealth represents the local cilium-health endpoint
	ReservedIdentityHealth

	// ReservedIdentityInit is the identity given to endpoints that have not
	// received any labels yet.
	ReservedIdentityInit

	// ReservedIdentityRemoteNode is the identity given to all nodes in
	// local and remote clusters except for the local node.
	ReservedIdentityRemoteNode

	// ReservedIdentityKubeAPIServer is the identity given to remote node(s) which
	// have backend(s) serving the kube-apiserver running.
	ReservedIdentityKubeAPIServer

	// ReservedIdentityIngress is the identity given to the IP used as the source
	// address for connections from Ingress proxies.
	ReservedIdentityIngress

	// ReservedIdentityWorldIPv4 represents any endpoint outside of the cluster
	// for IPv4 address only.
	ReservedIdentityWorldIPv4

	// ReservedIdentityWorldIPv6 represents any endpoint outside of the cluster
	// for IPv6 address only.
	ReservedIdentityWorldIPv6

	// ReservedEncryptedOverlay represents overlay traffic which must be IPSec
	// encrypted before it leaves the host
	ReservedEncryptedOverlay
)

// Special identities for well-known cluster components
// Each component has two identities. The first one is used for Kubernetes <1.21
// or when the NamespaceDefaultLabelName feature gate is disabled. The second
// one is used for Kubernetes >= 1.21 and when the NamespaceDefaultLabelName is
// enabled.
const (
	DeprecatedETCDOperator NumericIdentity = iota + 100
	DeprecatedCiliumKVStore

	// ReservedKubeDNS is the reserved identity used for kube-dns.
	ReservedKubeDNS

	// ReservedEKSKubeDNS is the reserved identity used for kube-dns on EKS
	ReservedEKSKubeDNS

	// ReservedCoreDNS is the reserved identity used for CoreDNS
	ReservedCoreDNS

	// ReservedCiliumOperator is the reserved identity used for the Cilium operator
	ReservedCiliumOperator

	// ReservedEKSCoreDNS is the reserved identity used for CoreDNS on EKS
	ReservedEKSCoreDNS

	DeprecatedCiliumEtcdOperator

	// Second identities for all above components
	DeprecatedETCDOperator2
	DeprecatedCiliumKVStore2
	ReservedKubeDNS2
	ReservedEKSKubeDNS2
	ReservedCoreDNS2
	ReservedCiliumOperator2
	ReservedEKSCoreDNS2
	DeprecatedCiliumEtcdOperator2
)
Reserved IdentityNumeric ID설명
IdentityUnknown0알 수 없는 Identity
ReservedIdentityHost1로컬 호스트
ReservedIdentityWorld2클러스터 외부 엔드포인트
ReservedIdentityUnmanaged3Cilium이 관리하지 않는 엔드포인트
ReservedIdentityHealth4로컬 cilium-health 엔드포인트
ReservedIdentityInit5아직 라벨을 받지 않은 엔드포인트
ReservedIdentityRemoteNode6로컬 노드를 제외한 모든 노드
ReservedIdentityKubeAPIServer7kube-apiserver 백엔드가 있는 원격 노드
ReservedIdentityIngress8Ingress 프록시에서 소스 IP로 사용되는 엔드포인트
ReservedIdentityWorldIPv49IPv4 클러스터 외부 엔드포인트
ReservedIdentityWorldIPv610IPv6 클러스터 외부 엔드포인트
ReservedEncryptedOverlay11호스트를 떠나기 전에 IPSec으로 암호화해야 하는 오버레이 트래픽

Component IdentityNumeric ID설명
ReservedKubeDNS102kube-dns
ReservedEKSKubeDNS103EKS kube-dns
ReservedCoreDNS104CoreDNS
ReservedCiliumOperator105Cilium Operator
ReservedEKSCoreDNS106EKS CoreDNS

Layer 3 정책 메소드

Layer 3은 여러 방식으로 통신 연결 규칙을 설정할 수 있다.

Endpoints based

Cilium이 관리하는 두 엔드포인트의 라벨을 기반으로 통신 관계를 정의한다.

목표 : role: frontendrole: backend 통신 허용

배포 yaml

# frontend pod
apiVersion: v1
kind: Pod
metadata:
  name: frontend
  labels:
    role: frontend
spec:
  containers:
  - name: nginx
    image: nginx
    ports:
    - containerPort: 80
---
# other pod
apiVersion: v1
kind: Pod
metadata:
  name: other
  labels:
    role: other
spec:
  containers:
  - name: nginx
    image: nginx
    ports:
    - containerPort: 80
---
# backend pod
apiVersion: v1
kind: Pod
metadata:
  name: backend
  labels:
    role: backend
spec:
  containers:
  - name: backend
    image: nginx
    ports:
    - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: backend
spec:
  selector:
    role: backend
  ports:
    - port: 80
      targetPort: 80
---
#NetworkPolicy
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: allow-frontend-to-backend
spec:
  endpointSelector:
    matchLabels:
      role: backend
  ingress:
  - fromEndpoints:
    - matchLabels:
        role: frontend

정책 적용 확인

(|HomeLab:N/A) root@k8s-ctr:~# kubectl -n kube-system exec ds/cilium -- cilium policy get
[
  {
    "endpointSelector": {
      "matchLabels": {
        "any:role": "backend",
        "k8s:io.kubernetes.pod.namespace": "default"
      }
    },
    "ingress": [
      {
        "fromEndpoints": [
          {
            "matchLabels": {
              "any:role": "frontend",
              "k8s:io.kubernetes.pod.namespace": "default"
            }
          }
        ]
      }
    ],
    ...

통신 확인
frontend -> backend 통신은 가능하나, other -> backend 통신은 불가능한 것을 확인할 수 있다.

(|HomeLab:N/A) root@k8s-ctr:~# kubectl exec -it frontend -- curl -s http://backend
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

(|HomeLab:N/A) root@k8s-ctr:~# kubectl exec -it other -- curl -sv http://backend
*   Trying 10.96.27.61:80...

^Ccommand terminated with exit code 130

Services based

Service 개념을 활용하여 통신 관계를 정의한다.

목표 : role: frontendService: backend 통신 허용

role: frontend 파드에서 backend라는 이름을 가진 Service와 통신을 할 수 있는 CiliumNetworkPolicy를 배포한다.
이 때, 주의할 점은 CiliumNetworkPolicy에 backend서비스만 지정을 해서 배포하면 coredns통신이 되지 않는다는 점이다.
kube-dns로도 통신을 할 수 있도록 설정을 해야지 정상적인 backend서비스로의 통신이 가능하다.

배포 yaml

#NetworkPolicy (Service based)
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: allow-frontend-to-backend-service
spec:
  endpointSelector:
    matchLabels:
      role: frontend
  egress:
  - toServices:
    - k8sService:
        serviceName: backend
        namespace: default
    - k8sService:
        serviceName: kube-dns
        namespace: kube-system

정책 적용 확인

(|HomeLab:N/A) root@k8s-ctr:~# kubectl -n kube-system exec ds/cilium -- cilium policy get
[
[
  {
    "endpointSelector": {
      "matchLabels": {
        "any:role": "backend",
        "k8s:io.kubernetes.pod.namespace": "default"
      }
    },
    "ingress": [
      {
        "fromEndpoints": [
          {
            "matchLabels": {
              "any:role": "frontend",
              "k8s:io.kubernetes.pod.namespace": "default"
            }
          }
        ]
      }
    ],
...
  {
    "endpointSelector": {
      "matchLabels": {
        "any:role": "frontend",
        "k8s:io.kubernetes.pod.namespace": "default"
      }
    },
    "egress": [
      {
        "toEndpoints": [
          {
            "matchLabels": {
              "any:role": "backend",
              "k8s:io.kubernetes.pod.namespace": "default"
            }
          },
          {
            "matchLabels": {
              "any:k8s-app": "kube-dns",
              "k8s:io.kubernetes.pod.namespace": "kube-system"
            }
          }
        ],
        "toServices": [
          {
            "k8sService": {
              "serviceName": "backend",
              "namespace": "default"
            }
          },
          {
            "k8sService": {
              "serviceName": "kube-dns",
              "namespace": "kube-system"
            }
          }
        ]
      }
    ],

통신 확인
frontend -> backend 서비스 통신은 가능하나, other -> backend 서비스 통신은 불가능한 것을 확인할 수 있다.

(|HomeLab:N/A) root@k8s-ctr:~# kubectl exec -it frontend -- curl -sv http://backend
*   Trying 10.96.65.95:80...
* Connected to backend (10.96.65.95) port 80 (#0)
> GET / HTTP/1.1
> Host: backend
> User-Agent: curl/7.88.1
> Accept: */*
>
< HTTP/1.1 200 OK

(|HomeLab:N/A) root@k8s-ctr:~# kubectl exec -it other -- curl -sv http://backend
*   Trying 10.96.65.95:80...
^Ccommand terminated with exit code 130

Entities based

fromEntities와 toEntities를 사용하여 Pod 통신을 실제 IP 대신 엔티티(논리 그룹) 단위로 제어할 수 있다.

Entity설명
host로컬 호스트(노드) 및 hostNetwork 모드 컨테이너
remote-node다른 노드 및 원격 노드의 hostNetwork 모드 컨테이너
kube-apiserverKubernetes API 서버 (내부/외부 배포 모두 포함)
ingressCilium Envoy 인그레스 프록시 (Pod-to-Pod hairpin 포함)
cluster클러스터 내 모든 엔드포인트 (host, remote-node, init 포함)
initidentity 미할당 초기 부트스트랩 단계 엔드포인트
healthCilium health check 엔드포인트
unmanagedCilium이 관리하지 않는 엔드포인트 (cluster 엔티티에도 포함됨)
world클러스터 외부(인터넷 포함, CIDR 0.0.0.0/0과 동일)
allcluster + world 모든 엔드포인트 전체

목표 : pod에서 로컬 호스트 및 hostNetwork 모드 컨테이너에만 접근 가능

배포 yaml

# dev Pod
apiVersion: v1
kind: Pod
metadata:
  name: dev-pod
  labels:
    role: dev
spec:
  containers:
  - name: curl
    image: curlimages/curl
    command: ["sleep", "3600"]
---
# Allow dev → host
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: dev-to-host
spec:
  endpointSelector:
    matchLabels:
      role: dev
  egress:
    - toEntities:
      - host
---
# same-pod (nginx, hostNetwork 사용, k8s-w1에 고정)
apiVersion: v1
kind: Pod
metadata:
  name: same-pod
  labels:
    app: same
spec:
  nodeName: k8s-w1       # 특정 노드에 고정
  hostNetwork: true      # hostNetwork 모드 활성화
  dnsPolicy: ClusterFirstWithHostNet
  containers:
  - name: nginx
    image: nginx:1.25
    ports:
    - containerPort: 80 
---
# same-pod (nginx, hostNetwork 사용, k8s-w2에 고정)
apiVersion: v1
kind: Pod
metadata:
  name: diff-pod
  labels:
    app: same
spec:
  nodeName: k8s-w2       # 특정 노드에 고정
  hostNetwork: true      # hostNetwork 모드 활성화
  dnsPolicy: ClusterFirstWithHostNet
  containers:
  - name: nginx
    image: nginx:1.25
    ports:
    - containerPort: 80  

통신 확인
통신을 확인해보면, 동일한 노드에 hostNetwork로 뜬 Pod와는 정상통신이 되고, 타 노드에 hostNetwork로 뜬 Pod와는 정상통신이 되지 않음을 확인할 수 있다.

(|HomeLab:N/A) root@k8s-ctr:~# kubectl get pod -o wide
NAME       READY   STATUS    RESTARTS   AGE   IP               NODE     NOMINATED NODE   READINESS GATES
dev-pod    1/1     Running   0          5s    172.20.2.232     k8s-w2   <none>           <none>
diff-pod   1/1     Running   0          44s   192.168.10.102   k8s-w2   <none>           <none>
same-pod   1/1     Running   0          44s   192.168.10.101   k8s-w1   <none>           <none>

(|HomeLab:N/A) root@k8s-ctr:~# kubectl exec -it dev-pod -- curl http://192.168.10.102
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>

(|HomeLab:N/A) root@k8s-ctr:~# kubectl exec -it dev-pod -- curl http://192.168.10.101

^Ccommand terminated with exit code 130

Node based

특정 노드만 허용하거나 차단하도록 통신 관계를 정의한다.

IP/CIDR based

외부 서비스와의 통신 관계를 IP 주소나 서브넷을 이용해 정의한다.

DNS based

DNS 이름을 통해 IP로 변환하여 클러스터 외부 peer와의 통신 관계를 정의한다.

Layer 4 (Restriction of accessible ports)

엔드포인트가 접근할 수 있는 포트 범위와 프로토콜(TCP/UDP 등)을 제한하는 정책이다.
L3 정책과 함께 적용되어, L3 정책으로 통신 가능 여부를 결정한 이후, 포트 단위로 더 세밀하게 제어할 때 사용된다.

Layer 7 (Application protocol level)

애플리케이션 프로토콜 단위로 트래픽을 제어하는 정책이다.
L3(Layer3)와 L4(Layer4) 정책으로 허용된 트래픽 중, 애플리케이션 레벨에서 세밀한 접근 제어를 수행할 때 사용된다.

0개의 댓글