Cilium은 여러 수준에서 보안을 제공하는데, 각 수준은 개별적으로 사용될 수도 있고, 함께 결합하여 사용할 수도 있다.
각 수준에 대해 알아본다.
엔드포인트간 연결 정책을 정의하는 방식이다.
전통적인 Kubernetes에서 사용하는 IP 기반 보안 모델은 파드가 생성·삭제될 때마다 모든 노드의 보안 규칙을 갱신해야 하기에 대규모 환경에서 한계가 존재하였다.
Cilium은 이러한 한계를 극복하기 위해 Identity를 도입하여, 보안을 IP와 완전히 분리하고 Identity 기반으로 통신 규칙을 정의하는 방안을 도입하였다.
전통적인 Kubernetes
Cilium
Identity란 모든 엔드포인트에 할당되는 객체로, 엔드포인트 간 기본 연결성을 강제하며 Layer 3 수준의 보안 적용에 사용된다.
Cilium은 엔드포인트의 보안 관련 라벨(Security Relevant Labels)에 따라, 각 엔드포인트에 클러스터 전체에서 고유한 식별자를 부여한다.
동일한 라벨 집합을 가진 엔드포인트는 동일한 Identity를 공유하게 된다. 이 Identity 개념을 통해 application을 확장하더라도 동일한 보안 라벨 집합에 속하는 엔드포인트는 모두 동일한 Identity를 공유하기 때문에, 정책 적용을 매우 큰 규모로 확장할 수 있다.
파드나 컨테이너의 라벨이 변경되면 아이덴티티도 다시 확인되며, 필요 시 자동으로 수정된다.
코드와 함께 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
}
위 코드에서 보듯이, 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 Identity | Numeric ID | 설명 |
|---|---|---|
| IdentityUnknown | 0 | 알 수 없는 Identity |
| ReservedIdentityHost | 1 | 로컬 호스트 |
| ReservedIdentityWorld | 2 | 클러스터 외부 엔드포인트 |
| ReservedIdentityUnmanaged | 3 | Cilium이 관리하지 않는 엔드포인트 |
| ReservedIdentityHealth | 4 | 로컬 cilium-health 엔드포인트 |
| ReservedIdentityInit | 5 | 아직 라벨을 받지 않은 엔드포인트 |
| ReservedIdentityRemoteNode | 6 | 로컬 노드를 제외한 모든 노드 |
| ReservedIdentityKubeAPIServer | 7 | kube-apiserver 백엔드가 있는 원격 노드 |
| ReservedIdentityIngress | 8 | Ingress 프록시에서 소스 IP로 사용되는 엔드포인트 |
| ReservedIdentityWorldIPv4 | 9 | IPv4 클러스터 외부 엔드포인트 |
| ReservedIdentityWorldIPv6 | 10 | IPv6 클러스터 외부 엔드포인트 |
| ReservedEncryptedOverlay | 11 | 호스트를 떠나기 전에 IPSec으로 암호화해야 하는 오버레이 트래픽 |
| Component Identity | Numeric ID | 설명 |
|---|---|---|
| ReservedKubeDNS | 102 | kube-dns |
| ReservedEKSKubeDNS | 103 | EKS kube-dns |
| ReservedCoreDNS | 104 | CoreDNS |
| ReservedCiliumOperator | 105 | Cilium Operator |
| ReservedEKSCoreDNS | 106 | EKS CoreDNS |
Layer 3은 여러 방식으로 통신 연결 규칙을 설정할 수 있다.
Cilium이 관리하는 두 엔드포인트의 라벨을 기반으로 통신 관계를 정의한다.
role: frontend → role: 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
Service 개념을 활용하여 통신 관계를 정의한다.
role: frontend → Service: 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
fromEntities와 toEntities를 사용하여 Pod 통신을 실제 IP 대신 엔티티(논리 그룹) 단위로 제어할 수 있다.
| Entity | 설명 |
|---|---|
host | 로컬 호스트(노드) 및 hostNetwork 모드 컨테이너 |
remote-node | 다른 노드 및 원격 노드의 hostNetwork 모드 컨테이너 |
kube-apiserver | Kubernetes API 서버 (내부/외부 배포 모두 포함) |
ingress | Cilium Envoy 인그레스 프록시 (Pod-to-Pod hairpin 포함) |
cluster | 클러스터 내 모든 엔드포인트 (host, remote-node, init 포함) |
init | identity 미할당 초기 부트스트랩 단계 엔드포인트 |
health | Cilium health check 엔드포인트 |
unmanaged | Cilium이 관리하지 않는 엔드포인트 (cluster 엔티티에도 포함됨) |
world | 클러스터 외부(인터넷 포함, CIDR 0.0.0.0/0과 동일) |
all | cluster + world 모든 엔드포인트 전체 |
배포 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
특정 노드만 허용하거나 차단하도록 통신 관계를 정의한다.
외부 서비스와의 통신 관계를 IP 주소나 서브넷을 이용해 정의한다.
DNS 이름을 통해 IP로 변환하여 클러스터 외부 peer와의 통신 관계를 정의한다.
엔드포인트가 접근할 수 있는 포트 범위와 프로토콜(TCP/UDP 등)을 제한하는 정책이다.
L3 정책과 함께 적용되어, L3 정책으로 통신 가능 여부를 결정한 이후, 포트 단위로 더 세밀하게 제어할 때 사용된다.
애플리케이션 프로토콜 단위로 트래픽을 제어하는 정책이다.
L3(Layer3)와 L4(Layer4) 정책으로 허용된 트래픽 중, 애플리케이션 레벨에서 세밀한 접근 제어를 수행할 때 사용된다.