들어가며

이번 포스트에서는 Cilium의 Security와 eBPF 기반의 런타임 보안 및 관찰 도구인 Tetragon 대해 실습을 통하여 알아보도록 하겠습니다.


실습환경 배포

  • 실습 환경 소개
    • 실습 환경 : k8s(1.33.4), cilium(1.18.1) - 기본 배포 가상 머신 : k8s-ctr, k8s-w1, k8s-w2
    • 이번 실습에는 워커 노드간 보안 정책을 적용하고 통신 암호화 등을 살펴보기 위해서 워커노드를 2개 두는 구성을 하였습니다.
  • 실습 환경 배포
$ mkdir cilium-lab && cd cilium-lab
$ curl -O https://raw.githubusercontent.com/gasida/vagrant-lab/refs/heads/main/cilium-study/8w/Vagrantfile
$ vagrant up
# => ...
#    ==> k8s-w2: Running provisioner: shell...
#        k8s-w2: Running: /var/folders/7k/qy6rsdds57z3tmyn9_7hhd8r0000gn/T/vagrant-shell20250905-16290-5f5n6v.sh
#        k8s-w2: >>>> K8S Node config Start <<<<
#        k8s-w2: [TASK 1] K8S Controlplane Join
#        k8s-w2: >>>> K8S Node config End <<<<
$ vagrant ssh k8s-ctr

Cilium Security 공식문서

Cilium이 제공하는 보안 개요

  • 관련문서
  • Cilium은 크게 다음의 보안 기능을 제공합니다.
    • Identity-Based (Layer3)
    • Port Level (Layer4)
    • Application protocol Level (Layer7)
  • Cilium 보안 정책의 특징
    • 정책이 없는 경우, 기본적으로 모든 트래픽이 허용됩니다.
    • 첫번째 정책 규칙이 적용되면, 허용된 트래픽을 제외한 모든 트래픽이 차단됩니다.
    • 세션 기반 프로토콜에 대해 상태저장 정책이 적용되며, 응답 패킷은 자동으로 허용됩니다. - 관련문서
  • Envoy Proxy 통합 - Docs img.png https://docs.cilium.io/en/stable/security/network/proxy/envoy/
    • Cilium은 Envoy Proxy와 통합되어 Layer7 보안 정책을 적용할 수 있습니다.
    • Envoy Proxy는 L7 프록시로서 HTTP, gRPC, Kafka 등의 프로토콜을 지원합니다.
    • Envoy Proxy를 통해 세밀한 애플리케이션 레벨의 보안 정책을 적용할 수 있습니다.
  • 투명한 암호화 : IPSec or WireGuard 로 Cilium 관리하는 호스트 트래픽과 엔드포인트 간 트래픽의 투명한 암호화 지원합니다. - Docs , Youtube img.png
    • 각 모드의 단점
      • IPSec : BPF 호스트 라우팅에서는 미동작하며, IPsec 터널당 단일 CPU 코어로 제한됩니다. - Docs
      • WireGuard : 터널 모드(VXLAN, GENEVE)에서는 두 번 캡슐화됩니다. - Docs , Youtube
Identity 기반 보안 정책 - Docs

img.png Identity https://docs.cilium.io/en/stable/security/network/identity/

  • Cilium은 Pod에 IP주소가 아닌 Identity를 부여하고 Identity를 기반으로 보안 정책을 적용합니다.
  • Identity는 Namespace, Labels, ServiceAccount 등을 기반으로 생성됩니다.
  • Pod의 IP주소는 변경될 수 있지만, Identity는 변경되기 때문에 보안 정책이 안정적으로 유지될 수 있습니다.
  • 또한 동일한 Security Relevant Labels를 가진 Pod들은 동일한 Identity를 공유합니다.
$ kubectl get ciliumendpoints.cilium.io -n kube-system
# => NAME                              SECURITY IDENTITY   ENDPOINT STATE   IPV4           IPV6
#    coredns-674b8bbfcf-2njqx          <span style="color: green;">15719</span>               ready            172.20.0.37
#    coredns-674b8bbfcf-gwsh5          <span style="color: green;">15719</span>               ready            172.20.0.1
#    hubble-relay-fdd49b976-t5sms      64548               ready            172.20.0.102
#    hubble-ui-655f947f96-vjkrg        3186                ready            172.20.0.197
#    metrics-server-5dd7b49d79-rbbwk   883                 ready            172.20.0.166 

$ kubectl get ciliumidentities.cilium.io 
# => NAME    NAMESPACE            AGE
#    <span style="color: green;">15719</span>   kube-system          57m
#    2887    cilium-monitoring    57m
#    3186    kube-system          57m
#    38953   local-path-storage   57m
#    61576   cilium-monitoring    57m
#    64548   kube-system          57m
#    883     kube-system          57m
  • identity 부여 과정
    • Pod 또는 Container가 생성되면, Cilium은 컨테이너 런타밈에서 수집한 이벤트를 기반으로 네트워크에서 Pod 또는 Container를 나타내는 엔드포인트를 생성합니다.
    • 엔드포인트의 ID는 엔드포인트에서 파생된 Pod 또는 Container와 연관된 레이블을 기반으로 결정됩니다.
    • 다음 단계로 Cilium은 생성된 엔드포인트의 ID를 확인하며, Pod 또는 Container의 레이블이 변경되면 엔드포인트 ID가 재확인 되며 필요에따라 변경됩니다.
Cilium Identity 정보 확인 및 변경 실습
$ kubectl get ciliumidentities.cilium.io 15719 -o yaml | yq
# => {
#      "apiVersion": "cilium.io/v2",
#      "kind": "CiliumIdentity",
#      "metadata": {
#        "creationTimestamp": "2025-09-05T14:14:26Z",
#        "generation": 1,
#        "labels": {
#          "io.kubernetes.pod.namespace": "kube-system"
#        },
#        "name": "15719",
#        "resourceVersion": "796",
#        "uid": "e3b7045e-5a91-47fc-abff-0d8f86330dbf"
#      },
#      "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"
#      }
#    }

$ kubectl exec -it -n kube-system ds/cilium -- cilium identity list
# => ...
#    15719   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
#    ...

$ kubectl get pod -n kube-system -l k8s-app=kube-dns --show-labels
# => NAME                       READY   STATUS    RESTARTS   AGE   LABELS
#    coredns-674b8bbfcf-2njqx   1/1     Running   0          65m   k8s-app=kube-dns,pod-template-hash=674b8bbfcf
#    coredns-674b8bbfcf-gwsh5   1/1     Running   0          65m   k8s-app=kube-dns,pod-template-hash=674b8bbfcf

# 기동 중인 파드에 label 추가 후 cilium id(보안 label)에 반영 될지? : 반영되는가? 얼마나 시간이 걸리는가? ID가 변경되지는 않는가?
$ kubectl label pods -n kube-system -l k8s-app=kube-dns study=8w
# => pod/coredns-674b8bbfcf-2njqx labeled
#    pod/coredns-674b8bbfcf-gwsh5 labeled
$ kubectl exec -it -n kube-system ds/cilium -- cilium identity list
# => ...
#    15719   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
#    <span style="color: green;">38787   k8s:io.cilium.k8s.namespace.labels.kubernetes.io/metadata.name=kube-system</span>
#            <span style="color: green;">k8s:io.cilium.k8s.policy.cluster=default</span>
#            <span style="color: green;">k8s:io.cilium.k8s.policy.serviceaccount=coredns</span>
#            <span style="color: green;">k8s:io.kubernetes.pod.namespace=kube-system</span>
#            <span style="color: green;">k8s:k8s-app=kube-dns</span>
#            <span style="color: green;">k8s:study=8w</span>
#    ...
# <span style="color: green;">👉 study=8w 라벨에 대한 새로운 ID(38787)가 생성되었습니다..</span>

# (위 내용 확인 후 아래 진행) coredns deployment 에 spec.template.metadata.labels에 아래 추가 시 반영 확인
$ kubectl edit deploy -n kube-system coredns
---
...
  template:
    metadata:
      labels:
        app: testing
        k8s-app: kube-dns
...
---
# => deployment.apps/coredns edited

$ kubectl exec -it -n kube-system ds/cilium -- cilium identity list
# => ...
#    <span style="color: green;">30461   k8s:app=testing</span>
#            <span style="color: green;">k8s:io.cilium.k8s.namespace.labels.kubernetes.io/metadata.name=kube-system</span>
#            <span style="color: green;">k8s:io.cilium.k8s.policy.cluster=default</span>
#            <span style="color: green;">k8s:io.cilium.k8s.policy.serviceaccount=coredns</span>
#            <span style="color: green;">k8s:io.kubernetes.pod.namespace=kube-system</span>
#            <span style="color: green;">k8s:k8s-app=kube-dns</span>
#    ...
  • Cilium은 pod update 이벤트를 watch하므로, labels 변경 시 endpoint가 waiting-for-identity 상태로 전환되어 새로운 identity를 할당받습니다. 이로 인해 security labels와 관련된 네트워크 정책도 자동으로 재적용됩니다.
  • 예시:
    • 간단한 pod(simple-pod)를 생성하면 초기 security identity(예: 26830)가 할당됩니다.
    • 이후 kubectl label pod/simple-pod run=not-simple-pod --overwrite로 labels를 변경하면, kubectl get ciliumendpoints 명령에서 identity가 새 값(예: 8710)으로 업데이트된 것을 확인할 수 있습니다. 이는 policy enforcement에 즉시 반영됩니다.
  • 단, 대규모 클러스터에서 자주 labels를 변경하면 identity 할당이 빈번해져 성능 저하가 발생할 수 있으므로, identity-relevant labels를 제한하는 것이 권장됩니다
Special Identities
  • Cilium에서 관리하는 모든 엔드포인트는 ID가 할당 됩니다.
  • Cilium에서 관리하지 않는 엔드포인트의 통신을 허용하기 위해서 특수 ID를 제공합니다.
  • 예약된 특수 ID에는 reserved라는 접두사가 붙습니다.
# 예약된 특수 ID 확인 
$ 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
#    ...
Identity 숫자ID 설명
reserved:unknown 0 ID을 확인할 수 없습니다.
reserved:host 1 로컬 호스트. 로컬 호스트 IP 중 하나에서 시작되거나 지정된 모든 트래픽입니다.
reserved:world 2 클러스터 외부의 모든 네트워크 엔드포인트
reserved:unmanaged 3 Cilium에서 관리하지 않는 엔드포인트, 예를 들어 Cilium이 설치되기 전에 시작된 Kubernetes Pod입니다.
reserved:health 4 Cilium 에이전트가 생성한 상태 확인 트래픽입니다.
reserved:init 5 ID가 아직 확인되지 않은 엔드포인트에는 init ID가 할당됩니다. 이는 보안 ID를 도출하는 데 필요한 일부 메타데이터가 아직 없는 엔드포인트 단계를 나타냅니다. 이는 일반적으로 부트스트래핑 단계에서 발생합니다. init ID는 엔드포인트 생성 시점에 레이블을 알 수 없는 경우에만 할당됩니다. Docker 플러그인의 경우가 여기에 해당할 수 있습니다.
reserved:remote-node 6 모든 원격 클러스터 호스트의 집합입니다. 로컬 노드를 제외한 연결된 클러스터 내 모든 호스트의 IP 중 하나에서 시작되거나 해당 IP로 지정된 모든 트래픽입니다.
reserved:kube-apiserver 7 kube-apiserver를 실행하는 백엔드가 있는 원격 노드입니다.
reserved:ingress 8 Ingress 프록시 연결에 대한 소스 주소로 사용되는 IP에 제공됩니다.
클러스터에서의 Identity 관리

img.png

  • ID는 전체 클러스터에서 유효합니다.
    • 즉, 여러 클러스터 노드에서 여러 개의 포드 또는 컨테이너가 시작되더라도 ID 관련 레이블을 공유하는 경우 모든 포드 또는 컨테이너가 단일 ID를 공유합니다.
    • 이를 위해서는 클러스터 노드 간의 조정이 필요합니다.
  • 엔드포인트 ID를 확인하는 작업은 분산 키-값 저장소를 통해 수행됩니다.
  • 분산 키-값 저장소는 다음 값이 이전에 확인되지 않은 경우 새로운 고유 식별자를 생성하는 형태의 원자적 연산을 수행할 수 있도록 합니다.
  • 이를 통해 각 클러스터 노드는 ID 관련 레이블 하위 집합을 생성한 다음 키-값 저장소를 쿼리하여 ID를 도출할 수 있습니다.
  • 레이블 집합이 이전에 쿼리되었는지 여부에 따라 새 ID가 생성되거나 초기 쿼리의 ID가 반환됩니다.
NetworkPolicy : 3가지 형식 제공 - Docs

img.png https://isovalent.com/blog/post/intro-to-cilium-network-policies/

Policy Enforcement Modes by Cilium Network Policy - Docs
  • Policy Enforcement Modes
    • default : 정책이 없는 경우 모든 트래픽이 허용됩니다. 정책이 적용되면 허용된 트래픽을 제외한 모든 트래픽이 차단됩니다.
    • always : 정책이 없는 경우 모든 트래픽이 차단됩니다. 정책이 적용되면 허용된 트래픽을 제외한 모든 트래픽이 차단됩니다.
    • never : 정책이 없는 경우 모든 트래픽이 허용됩니다. 정책이 적용되더라도 모든 트래픽이 허용됩니다.
  • Endpoint default policy
    • 기본적으로 모든 엔드포인트에 대해 모든 송신 및 수신 트래픽이 허용됩니다. 네트워크 정책에 따라 엔드포인트가 선택되면 명시적으로 허용된 트래픽 만 허용되는 기본 거부 상태로 전환됩니다 . 이 상태는 방향별로 적용됩니다.
      • 규칙이 엔드포인트를 선택 하고 해당 규칙에 수신 섹션이 있는 경우, 엔드포인트는 수신에 대해 기본 거부 모드로 전환됩니다.
      • 규칙이 엔드포인트를 선택 하고 규칙에 송신 섹션이 있는 경우, 엔드포인트는 송신에 대해 기본 거부 모드로 전환됩니다.
    • EnableDefaultDeny 7계층 정책 에는 적용되지 않습니다.
      • 7계층 모두 허용이 포함되지 않은 7계층 규칙을 추가하면 default-deny가 명시적으로 비활성화된 경우에도 삭제가 발생합니다.
  • Rule Basics
    • endpointSelector / nodeSelector : 정책 규칙이 적용될 엔드포인트 또는 노드를 선택합니다. 정책 규칙은 선택기에 지정된 레이블과 일치하는 모든 엔드포인트에 적용됩니다.
    • ingress : 엔드포인트의 유입 시, 즉 엔드포인트에 들어오는 모든 네트워크 패킷에 적용해야 하는 규칙 목록입니다.
    • egress : 엔드포인트의 출구에서 적용되어야 하는 규칙 목록, 즉 엔드포인트를 떠나는 모든 네트워크 패킷에 적용되어야 하는 규칙 목록입니다.
    • labels : 레이블은 규칙을 식별하는 데 사용됩니다. 레이블을 사용하여 규칙을 나열하고 삭제할 수 있습니다. Kubernetes를 통해 가져온 정책 규칙에는 NetworkPolicy 또는 CiliumNetworkPolicy 리소스에 지정된 이름에 해당하는 레이블이 자동으로 io.cilium.k8s.policy.name=NAME지정됩니다.
Cilium Endpoint Lifecycle - Docs

img.png https://docs.cilium.io/en/stable/security/policy/lifecycle/

  • restoring : Cilium이 시작되기 전에 엔드포인트가 시작되었으며 Cilium이 네트워킹 구성을 복원하고 있습니다.
  • waiting-for-identity : Cilium이 엔드포인트에 고유한 ID를 할당하고 있습니다.
  • waiting-to-regenerate : 엔드포인트가 ID를 수신했으며 네트워킹 구성이 다시 생성될 때까지 기다리고 있습니다.
  • regenerating : 엔드포인트의 네트워킹 구성이 재생성되고 있습니다. 여기에는 해당 엔드포인트에 대한 eBPF 프로그래밍이 포함됩니다.
  • ready : 엔드포인트의 네트워킹 구성이 성공적으로 다시 생성되었습니다.
  • disconnecting : 엔드포인트가 삭제되고 있습니다.
  • disconnected : 엔드포인트가 삭제되었습니다.
기타 정보

Cilium Security 실습

🔬 Lab1. Locking Down External Access with DNS-Based Policies

  • DNS 기반 보안 정책 - Docs
    • CIDR 또는 IP 기반 정책은 외부 서비스와 연결된 IP가 자주 변경될 수 있으므로 관리가 어렵고 번거롭습니다.
    • Cilium의 DNS 기반 정책은 DNS-IP 매핑 추적과 같은 복잡한 측면을 관리하는 동시에 액세스 제어를 쉽게 지정할 수 있는 메커니즘을 제공합니다.
    • 이 가이드에서는 다음 사항에 대해 알아봅니다.
      • DNS 기반 정책을 사용하여 클러스터 외부 서비스에 대한 이탈 액세스 제어
      • 패턴(또는 와일드카드)을 사용하여 DNS 도메인 하위 집합을 허용 목록에 추가
      • 외부 서비스 접근 제한을 위한 DNS, 포트 및 L7 규칙 결합
데모 애플리케이션 배포
  • Empire의 mediabot pod가 Empire의 git 저장소 관리를 위해 GitHub에 접근해야 하는 간단한 시나리오를 사용하겠습니다.
  • pod는 다른 외부 서비스에 접근할 수 없어야 합니다.
# hubble ui 에 pod name 표기를 위해 app labels 추가 >> 빼고 배포해보고 차이점을 확인해보자.
$ cat << EOF > dns-sw-app.yaml
apiVersion: v1
kind: Pod
metadata:
  name: mediabot
  labels:
    org: empire
    class: mediabot
    app: mediabot
spec:
  containers:
  - name: mediabot
    image: quay.io/cilium/json-mock:v1.3.8@sha256:5aad04835eda9025fe4561ad31be77fd55309af8158ca8663a72f6abb78c2603
EOF
  
$ kubectl apply -f dns-sw-app.yaml
# => pod/mediabot created
$ kubectl wait pod/mediabot --for=condition=Ready
# => pod/mediabot condition met
  
# 확인
$ kubectl exec -it -n kube-system ds/cilium -- cilium identity list
# => ...
#    23762   k8s:app=mediabot
#            k8s:class=mediabot
#            k8s:io.cilium.k8s.namespace.labels.kubernetes.io/metadata.name=default
#            k8s:io.cilium.k8s.policy.cluster=default
#            k8s:io.cilium.k8s.policy.serviceaccount=default
#            k8s:io.kubernetes.pod.namespace=default
#            k8s:org=empire
#    ...
$ kubectl get pods
# => NAME       READY   STATUS    RESTARTS   AGE
#    mediabot   1/1     Running   0          86s
  
# 외부 통신 확인 : hubble ui 에서 확인
$ kubectl exec mediabot -- curl -I -s https://api.github.com | head -1
# => HTTP/2 200
$ kubectl exec mediabot -- curl -I -s --max-time 5 https://support.github.com | head -1 
# => HTTP/2 302

img.png

DNS Egress 정책 적용 1 : mediabot포드가 api.github.com에만 액세스하도록 허용
#
$ 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"
  - toEndpoints:
    - matchLabels:
        "k8s:io.kubernetes.pod.namespace": kube-system
        "k8s:k8s-app": kube-dns
    toPorts:
    - ports:
      - port: "53"
        protocol: ANY
      rules:
        dns:
        - matchPattern: "*"
EOF
# => ciliumnetworkpolicy.cilium.io/fqdn created

# 확인
$ kubectl get cnp
# => NAME   AGE   VALID
#    fqdn   11s   True

$ kubectl exec -it -n kube-system ds/cilium -- cilium policy selectors 
# => SELECTOR                                                                                                                                                                      LABELS         USERS   IDENTITIES
#    &LabelSelector{MatchLabels:map[string]string{any.class: mediabot,any.org: empire,k8s.io.kubernetes.pod.namespace: default,},MatchExpressions:[]LabelSelectorRequirement{},}   default/fqdn   1       23762 

$ cilium config view | grep -i dns
# => dnsproxy-enable-transparent-mode                  true
#    dnsproxy-socket-linger-timeout                    10
#    hubble-metrics                                    dns drop tcp flow port-distribution icmp httpV2:exemplars=true;labelsContext=source_ip,source_namespace,source_workload,destination_ip,destination_namespace,destination_workload,traffic_direction
#    tofqdns-dns-reject-response-code                  refused
#    tofqdns-enable-dns-compression                    true
#    tofqdns-endpoint-max-ip-per-hostname              1000
#    tofqdns-idle-connection-grace-period              0s
#    tofqdns-max-deferred-connection-deletes           10000
#    tofqdns-preallocate-identities                    true
#    tofqdns-proxy-response-max-delay                  100ms

# 외부 통신 확인 : hubble ui 에서 확인
$ kubectl exec mediabot -- curl -I -s https://api.github.com | head -1
# => HTTP/2 200

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

img.png api.github.com으로만 접속이 허용된 상태


# cilium-agent 내에 go 로 구현된 lightweight proxy 가 DNS 쿼리/응답 감시와 ~~캐싱 처리~~ : 기본 30초?
$ cilium hubble port-forward&
# => ℹ️   Hubble Relay is available at 127.0.0.1:4245
$ hubble observe --pod mediabot
# => ...
#    Sep  6 07:57:02.807: default/mediabot:59702 (ID:23762) <- kube-system/coredns-674b8bbfcf-hcdsq:53 (ID:41039) dns-response proxy FORWARDED (DNS Answer "20.200.245.245" TTL: 30 (Proxy api.github.com. A))
#    ...
#    Sep  6 07:57:02.809: default/mediabot:59458 (ID:23762) -> api.github.com:443 (ID:16777217) policy-verdict:L3-Only EGRESS ALLOWED (TCP Flags: SYN)
# <span style="color: green;">👉 api.github.com 으로의 접속은 허용됨</span>
#    ...
#    Sep  6 07:48:26.004: default/mediabot:43181 (ID:23762) <- kube-system/coredns-674b8bbfcf-nxkpb:53 (ID:41039) dns-response proxy FORWARDED (DNS Answer  TTL: 4294967295 (Proxy support.github.com. AAAA))
#    ...
#    Sep  6 07:48:26.025: default/mediabot:34138 (ID:23762) <> support.github.com:443 (world) policy-verdict:none EGRESS DENIED (TCP Flags: SYN)
# <span style="color: red;">👉 support.github.com 으로의 접속은 차단됨</span>
#    ...

# (옵션) coredns 로그로 직접 확인해보기
# k9s -> configmap (coredns) : log 추가

## 로깅 활성화
$ kubectl logs -n kube-system -l k8s-app=kube-dns -f
# => [INFO] 172.20.1.42:46910 - 17176 "A IN api.github.com.default.svc.cluster.local. udp 58 false 512" NXDOMAIN qr,aa,rd 151 0.001380815s
#    [INFO] 172.20.1.42:46910 - 10271 "AAAA IN api.github.com.default.svc.cluster.local. udp 58 false 512" NXDOMAIN qr,aa,rd 151 0.000691012s
#    [INFO] 172.20.1.42:59040 - 34206 "A IN api.github.com.svc.cluster.local. udp 50 false 512" NXDOMAIN qr,aa,rd 143 0.001186301s
#    [INFO] 172.20.1.42:59040 - 38304 "AAAA IN api.github.com.svc.cluster.local. udp 50 false 512" NXDOMAIN qr,aa,rd 143 0.001266842s
#    [INFO] 172.20.1.42:47633 - 16595 "AAAA IN api.github.com.cluster.local. udp 46 false 512" NXDOMAIN qr,aa,rd 139 0.007288392s
#    [INFO] 172.20.1.42:47633 - 42706 "A IN api.github.com.cluster.local. udp 46 false 512" NXDOMAIN qr,aa,rd 139 0.032957929s
#    [INFO] 172.20.1.42:48160 - 48028 "AAAA IN api.github.com. udp 32 false 512" NOERROR qr,rd,ra 116 0.046812603s
#    [INFO] 172.20.1.42:48160 - 49818 "A IN api.github.com. udp 32 false 512" NOERROR qr,rd,ra 62 0.043482105s
#    ...

## 호출
$ kubectl exec mediabot -- curl -I -s https://api.github.com | head -1
# => HTTP/2 200
$ kubectl exec mediabot -- curl -I -s https://api.github.com | head -1
# => HTTP/2 200
$ hubble observe --pod mediabot
# => Sep  6 08:01:46.195: default/mediabot:39968 (ID:23762) -> kube-system/coredns-6c95f59f49-fgbmq:53 (ID:41039) to-endpoint FORWARDED (UDP)
#    Sep  6 08:01:46.197: default/mediabot:39968 (ID:23762) <> kube-system/coredns-6c95f59f49-fgbmq (ID:41039) pre-xlate-rev TRACED (UDP)
#    Sep  6 08:01:46.198: default/mediabot:39968 (ID:23762) <- kube-system/coredns-6c95f59f49-fgbmq:53 (ID:41039) to-network FORWARDED (UDP)
#    Sep  6 08:01:46.198: default/mediabot:39968 (ID:23762) <> kube-system/coredns-6c95f59f49-fgbmq (ID:41039) pre-xlate-rev TRACED (UDP)
#    Sep  6 08:01:46.228: default/mediabot:49139 (ID:23762) -> kube-system/coredns-6c95f59f49-fgbmq:53 (ID:41039) to-endpoint FORWARDED (UDP)
#    Sep  6 08:01:46.230: default/mediabot:49139 (ID:23762) <> kube-system/coredns-6c95f59f49-fgbmq (ID:41039) pre-xlate-rev TRACED (UDP)
#    Sep  6 08:01:46.238: default/mediabot:49139 (ID:23762) <> kube-system/coredns-6c95f59f49-fgbmq (ID:41039) pre-xlate-rev TRACED (UDP)
#    Sep  6 08:01:46.238: default/mediabot:49139 (ID:23762) <- kube-system/coredns-6c95f59f49-fgbmq:53 (ID:41039) to-network FORWARDED (UDP)
#    Sep  6 08:01:46.409: default/mediabot:49139 (ID:23762) <- kube-system/coredns-6c95f59f49-fgbmq:53 (ID:41039) dns-response proxy FORWARDED (DNS Answer  TTL: 4294967295 (Proxy api.github.com. AAAA))
#    Sep  6 08:01:46.410: default/mediabot:49139 (ID:23762) <- kube-system/coredns-6c95f59f49-fgbmq:53 (ID:41039) dns-response proxy FORWARDED (DNS Answer "20.200.245.245" TTL: 1 (Proxy api.github.com. A))
#    Sep  6 08:01:46.410: kube-system/coredns-6c95f59f49-fgbmq:53 (ID:41039) <> default/mediabot (ID:23762) pre-xlate-rev TRACED (UDP)
#    Sep  6 08:01:46.410: kube-system/kube-dns:53 (world) <> default/mediabot (ID:23762) post-xlate-rev TRANSLATED (UDP)
#    Sep  6 08:01:46.410: kube-system/coredns-6c95f59f49-fgbmq:53 (ID:41039) <> default/mediabot (ID:23762) pre-xlate-rev TRACED (UDP)
#    Sep  6 08:01:46.410: kube-system/kube-dns:53 (world) <> default/mediabot (ID:23762) post-xlate-rev TRANSLATED (UDP)
#    Sep  6 08:01:46.412: default/mediabot:46244 (ID:23762) -> api.github.com:443 (ID:16777217) policy-verdict:L3-Only EGRESS ALLOWED (TCP Flags: SYN)
#    Sep  6 08:01:46.412: default/mediabot:46244 (ID:23762) -> api.github.com:443 (ID:16777217) to-network FORWARDED (TCP Flags: SYN)
#    Sep  6 08:01:46.421: default/mediabot:46244 (ID:23762) <- api.github.com:443 (ID:16777217) to-endpoint FORWARDED (TCP Flags: SYN, ACK)
#    Sep  6 08:01:46.422: api.github.com:443 (ID:16777217) <> default/mediabot (ID:23762) pre-xlate-rev TRACED (TCP)
#    Sep  6 08:01:46.422: default/mediabot:46244 (ID:23762) -> api.github.com:443 (ID:16777217) to-network FORWARDED (TCP Flags: ACK)
#    Sep  6 08:01:46.424: default/mediabot:46244 (ID:23762) -> api.github.com:443 (ID:16777217) to-network FORWARDED (TCP Flags: ACK, PSH)
#    Sep  6 08:01:46.433: default/mediabot:46244 (ID:23762) <- api.github.com:443 (ID:16777217) to-endpoint FORWARDED (TCP Flags: ACK, PSH)
#    Sep  6 08:01:46.464: api.github.com:443 (ID:16777217) <> default/mediabot (ID:23762) pre-xlate-rev TRACED (TCP)
#    Sep  6 08:01:46.465: api.github.com:443 (ID:16777217) <> default/mediabot (ID:23762) pre-xlate-rev TRACED (TCP)
#    Sep  6 08:01:46.685: default/mediabot:46244 (ID:23762) -> api.github.com:443 (ID:16777217) to-network FORWARDED (TCP Flags: ACK, FIN)
#    Sep  6 08:01:46.703: default/mediabot:46244 (ID:23762) <- api.github.com:443 (ID:16777217) to-endpoint FORWARDED (TCP Flags: ACK, FIN)
#    Sep  6 08:01:46.703: default/mediabot:46244 (ID:23762) -> api.github.com:443 (ID:16777217) to-network FORWARDED (TCP Flags: RST)
#    Sep  6 08:01:46.703: default/mediabot:46244 (ID:23762) -> api.github.com:443 (ID:16777217) to-network FORWARDED (TCP Flags: RST)
#    Sep  6 08:01:46.705: default/mediabot:46244 (ID:23762) <- api.github.com:443 (ID:16777217) to-endpoint FORWARDED (TCP Flags: ACK, RST)

## cilium 파드 이름
$ export CILIUMPOD0=$(kubectl get -l k8s-app=cilium pods -n kube-system --field-selector spec.nodeName=k8s-ctr -o jsonpath='{.items[0].metadata.name}')
$ export CILIUMPOD1=$(kubectl get -l k8s-app=cilium pods -n kube-system --field-selector spec.nodeName=k8s-w1  -o jsonpath='{.items[0].metadata.name}')
$ export CILIUMPOD2=$(kubectl get -l k8s-app=cilium pods -n kube-system --field-selector spec.nodeName=k8s-w2  -o jsonpath='{.items[0].metadata.name}')
$ echo $CILIUMPOD0 $CILIUMPOD1 $CILIUMPOD2
# => cilium-p8r4t cilium-vrh7l cilium-rmd6z

## 단축키(alias) 지정
$ alias c0="kubectl exec -it $CILIUMPOD0 -n kube-system -c cilium-agent -- cilium"
$ alias c1="kubectl exec -it $CILIUMPOD1 -n kube-system -c cilium-agent -- cilium"
$ alias c2="kubectl exec -it $CILIUMPOD2 -n kube-system -c cilium-agent -- cilium"

##
$ c0 fqdn cache list
# => Endpoint   Source   FQDN   TTL   ExpirationTime   IPs
$ c1 fqdn cache list
# => Endpoint   Source       FQDN                  TTL   ExpirationTime             IPs
#    1039       connection   support.github.com.   0     2025-09-06T08:26:39.259Z   185.199.108.133
#    1039       connection   support.github.com.   0     2025-09-06T08:26:39.259Z   185.199.110.133
#    1039       connection   support.github.com.   0     2025-09-06T08:26:39.259Z   185.199.109.133
#    1039       connection   support.github.com.   0     2025-09-06T08:26:39.259Z   185.199.111.133
#    1039       connection   api.github.com.       0     2025-09-06T08:26:39.259Z   20.200.245.245
$ c2 fqdn cache list
# => Endpoint   Source   FQDN   TTL   ExpirationTime   IPs

$ c0 fqdn names
# => {
#      "DNSPollNames": null,
#      "FQDNPolicySelectors": []
#    }
$ c1 fqdn names
# => {
#      "DNSPollNames": null,
#      "FQDNPolicySelectors": [
#        {
#          "regexString": "^api[.]github[.]com[.]$",
#          "selectorString": "MatchName: api.github.com, MatchPattern: "
#        }
#      ]
#    }
$ c2 fqdn names
# => {
#      "DNSPollNames": null,
#      "FQDNPolicySelectors": []
#    }
  • cilium-agent 내에 go 로 구현된 lightweight proxy 가 DNS 쿼리/응답 감시와 캐싱 처리 - Code
DNS Egress 정책 적용 2 : 모든 GitHub 하위 도메인(예: 패턴)에 액세스.
# fqdn 캐시 초기화 및 정책 삭제
$ kubectl delete cnp fqdn
# => ciliumnetworkpolicy.cilium.io "fqdn" deleted
$ c1 fqdn cache clean -f
# => FQDN proxy cache cleared
$ c2 fqdn cache clean -f
# => FQDN proxy cache cleared

# dns-pattern.yaml 내용
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "fqdn"
spec:
  endpointSelector:
    matchLabels:
      org: empire
      class: mediabot
  egress:
  - toFQDNs:
    - matchName: "*.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: "*"

$ kubectl apply -f https://raw.githubusercontent.com/cilium/cilium/1.18.1/examples/kubernetes-dns/dns-pattern.yaml
# => ciliumnetworkpolicy.cilium.io/fqdn created
$ c1 fqdn names
# => {
#      "DNSPollNames": null,
#      "FQDNPolicySelectors": [
#        {
#          "regexString": "^[-a-zA-Z0-9_]*[.]github[.]com[.]$",
#          "selectorString": "MatchName: , MatchPattern: *.github.com"
#        }
#      ]
#    }
$ c2 fqdn names
# => {
#      "DNSPollNames": null,
#      "FQDNPolicySelectors": []
#    }
$ c1 fqdn cache list
# => Endpoint   Source   FQDN   TTL   ExpirationTime   IPs
$ c2 fqdn cache list
# => Endpoint   Source   FQDN   TTL   ExpirationTime   IPs

# 확인
$ kubectl get cnp
# => NAME   AGE   VALID
#    fqdn   59s   True

# 외부 통신 확인 : hubble ui 에서 확인 >> github.com 은 공식 문서 설명대로라면 안되야됨..
##  It is important to note and test that this doesn’t allow access to github.com because the *. 
## in the pattern requires one subdomain to be present in the DNS name
$ kubectl exec mediabot -- curl -I -s https://support.github.com | head -1
# => HTTP/2 302
$ kubectl exec mediabot -- curl -I -s https://gist.github.com | head -1
# => HTTP/2 302
$ kubectl exec mediabot -- curl -I -s --max-time 5 https://github.com | head -1
# => HTTP/2 200
$ kubectl exec mediabot -- curl -I -s --max-time 5 https://cilium.io| head -1
# => command terminated with exit code 28
# <span style="color: green;">👉 *.github.com 으로의 접속만 허용되기 때문에 cilium.io 접속은 차단됨</span>

img.png

DNS Egress 정책 적용 3 : DNS, Port 조합 적용
# dns-port.yaml 내용
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "fqdn"
spec:
  endpointSelector:
    matchLabels:
      org: empire
      class: mediabot
  egress:
  - toFQDNs:
    - matchPattern: "*.github.com"
    toPorts:
    - ports:
      - port: "443"
        protocol: TCP
  - toEndpoints:
    - matchLabels:
        "k8s:io.kubernetes.pod.namespace": kube-system
        "k8s:k8s-app": kube-dns
    toPorts:
    - ports:
      - port: "53"
        protocol: ANY
      rules:
        dns:
        - matchPattern: "*"

$ kubectl apply -f https://raw.githubusercontent.com/cilium/cilium/1.18.1/examples/kubernetes-dns/dns-port.yaml
# => ciliumnetworkpolicy.cilium.io/fqdn configured
$ c1 fqdn names
# => {
#      "DNSPollNames": null,
#      "FQDNPolicySelectors": [
#        {
#          "regexString": "^[-a-zA-Z0-9_]*[.]github[.]com[.]$",
#          "selectorString": "MatchName: , MatchPattern: *.github.com"
#        }
#      ]
#    }
$ c2 fqdn names
# => {
#      "DNSPollNames": null,
#      "FQDNPolicySelectors": []
#    }
$ c1 fqdn cache list
# => Endpoint   Source       FQDN                  TTL   ExpirationTime             IPs
#    1039       connection   support.github.com.   0     2025-09-06T08:26:39.259Z   185.199.110.133
#    1039       connection   support.github.com.   0     2025-09-06T08:26:39.259Z   185.199.109.133
#    1039       connection   support.github.com.   0     2025-09-06T08:26:39.259Z   185.199.111.133
#    1039       connection   gist.github.com.      0     2025-09-06T08:26:39.259Z   20.200.245.247
#    1039       connection   github.com.           0     2025-09-06T08:26:39.259Z   20.200.245.247
#    1039       connection   cilium.io.            0     2025-09-06T08:26:39.259Z   104.198.14.52
#    1039       connection   support.github.com.   0     2025-09-06T08:26:39.259Z   185.199.108.133
$ c2 fqdn cache list
# => Endpoint   Source   FQDN   TTL   ExpirationTime   IPs
$ c1 fqdn cache clean -f
# => FQDN proxy cache cleared
$ c2 fqdn cache clean -f
# => FQDN proxy cache cleared

# 외부 통신 확인 : hubble ui 에서 확인 
$ kubectl exec mediabot -- curl -I -s https://support.github.com | head -1
# => HTTP/2 302
$ kubectl exec mediabot -- curl -I -s --max-time 5 http://support.github.com | head -1
# => command terminated with exit code 28

img.png *.github.com의 443 포트만 허용되는 상태

실습 리소스 삭제
#
$ kubectl delete -f https://raw.githubusercontent.com/cilium/cilium/1.18.1/examples/kubernetes-dns/dns-sw-app.yaml
# => pod "mediabot" deleted
$ kubectl delete cnp fqdn
# => ciliumnetworkpolicy.cilium.io "fqdn" deleted
샘플 애플리케이션 배포 및 통신 문제 확인
  • 샘플 애플리케이션 배포
# 샘플 애플리케이션 배포
$ 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
# => deployment.apps/webpod created
#    service/webpod created

# k8s-ctr 노드에 curl-pod 파드 배포
$ cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: curl-pod
  labels:
    app: curl
spec:
  nodeName: k8s-ctr
  containers:
  - name: curl
    image: nicolaka/netshoot
    command: ["tail"]
    args: ["-f", "/dev/null"]
  terminationGracePeriodSeconds: 0
EOF
# => pod/curl-pod created
  • 통신 확인
# 배포 확인
$ kubectl get deploy,svc,ep webpod -owide
# => NAME                     READY   UP-TO-DATE   AVAILABLE   AGE   CONTAINERS   IMAGES           SELECTOR
#    deployment.apps/webpod   2/2     2            2           47s   webpod       traefik/whoami   app=webpod
#    
#    NAME             TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE   SELECTOR
#    service/webpod   ClusterIP   10.96.194.188   <none>        80/TCP    47s   app=webpod
#    
#    NAME               ENDPOINTS                         AGE
#    endpoints/webpod   172.20.1.252:80,172.20.2.124:80   47s
$ kubectl get endpointslices -l app=webpod
# => NAME           ADDRESSTYPE   PORTS   ENDPOINTS                   AGE
#    webpod-mr2xg   IPv4          80      172.20.1.252,172.20.2.124   59s
$ kubectl get ciliumendpoints # IP 확인
# => NAME                      SECURITY IDENTITY   ENDPOINT STATE   IPV4           IPV6
#    curl-pod                  1846                ready            172.20.0.190
#    webpod-697b545f57-p9hhs   4449                ready            172.20.2.124
#    webpod-697b545f57-rcgf8   4449                ready            172.20.1.252

# 통신 문제 확인
$ kubectl exec -it curl-pod -- curl -s --connect-timeout 1 webpod | grep Hostname
# => Hostname: webpod-697b545f57-rcgf8
$ kubectl exec -it curl-pod -- sh -c 'while true; do curl -s --connect-timeout 1 webpod | grep Hostname; echo "---" ; sleep 1; done'
# => Hostname: webpod-697b545f57-rcgf8
#    ---
#    Hostname: webpod-697b545f57-rcgf8
#    ---
#    Hostname: webpod-697b545f57-rcgf8
#    ---
#    Hostname: webpod-697b545f57-p9hhs
#    ...

# cilium-dbg, map
$ kubectl exec -n kube-system ds/cilium -- cilium-dbg ip list
$ kubectl exec -n kube-system ds/cilium -- cilium-dbg endpoint list
$ kubectl exec -n kube-system ds/cilium -- cilium-dbg service list
$ kubectl exec -n kube-system ds/cilium -- cilium-dbg bpf lb list
$ kubectl exec -n kube-system ds/cilium -- cilium-dbg bpf nat list
$ kubectl exec -n kube-system ds/cilium -- cilium-dbg map list | grep -v '0             0'
$ kubectl exec -n kube-system ds/cilium -- cilium-dbg map get cilium_lb4_services_v2
$ kubectl exec -n kube-system ds/cilium -- cilium-dbg map get cilium_lb4_backends_v3
$ kubectl exec -n kube-system ds/cilium -- cilium-dbg map get cilium_lb4_reverse_nat
$ kubectl exec -n kube-system ds/cilium -- cilium-dbg map get cilium_ipcache_v2

Transparent Encryption with WireGuard

  • 각 노드는 자체 암호화 키 쌍을 자동으로 생성하고 Kubernetes CiliumNode 커스텀 리소스 객체의 io.cilium.network.wg-pub-key 어노테이션을 통해 공개 키를 배포합니다.
  • 각 노드의 공개키는 해당 노드에서 실행 중인 Cilium 관리 엔드포인트의 트래픽을 암호화 및 암호 해독하는 데 다른 노드에서 사용됩니다.
  • 한 노드 내에서 파드(엔드포인트)간 통신 시에는 암호화 되지 않습니다.
  • WireGuard 터널 엔드포인트는 각 노드의 UDP 포트 51871에서 노출됩니다.
  • 제한 사항 : L7 정책 시행 및 가시성, eBPF 기반 호스트 라우팅

img.png

20250907_cilium_w8_14.gif Wireguard를 이용한 암호화

20250907_cilium_w8_15.svg

🔬 Lab2. WireGuard 설정 및 실습

  • 터널모드는 두 번 캡슐화 됩니다. - Docs, Youtube
  • WireGuard 터널 엔드포인트는 51871각 노드의 UDP 포트에 노출됩니다.
  • WireGuard 설정
# [커널 구성 옵션] CONFIG_WIREGUARD=m on Linux 5.6 and newe 
$ uname -ar
# => Linux k8s-ctr 6.8.0-71-generic #71-Ubuntu SMP PREEMPT_DYNAMIC Tue Jul 22 16:44:45 UTC 2025 aarch64 aarch64 aarch64 GNU/Linux
$ grep -E 'CONFIG_WIREGUARD=m' /boot/config-$(uname -r)
# => CONFIG_WIREGUARD=m

# 설정 전 기본 정보 확인
$ ip -c addr
$ ip -c route
$ ip rule show

# 설정
$ helm upgrade cilium cilium/cilium --version 1.18.1 --namespace kube-system --reuse-values \
  --set encryption.enabled=true --set encryption.type=wireguard
# => Release "cilium" has been upgraded. Happy Helming!
#    ...

$ kubectl -n kube-system rollout restart ds/cilium
# => daemonset.apps/cilium restarted

# 확인
$ cilium config view | grep -i wireguard
# => enable-wireguard                                  true
#    wireguard-persistent-keepalive                    0s
$ kubectl exec -it -n kube-system ds/cilium -- cilium encrypt status
# => Encryption: Wireguard
#    Interface: cilium_wg0
#            Public key: chlVHrcv7vafkretDiAyIn0+1bU4xVDzxhSxEUBx9lw=
#            Number of peers: 2

$ kubectl exec -it -n kube-system ds/cilium -- cilium status | grep Encryption
# => Encryption:              Wireguard   [NodeEncryption: Disabled, cilium_wg0 (Pubkey: chlVHrcv7vafkretDiAyIn0+1bU4xVDzxhSxEUBx9lw=, Port: 51871, Peers: 2)]   

$ kubectl exec -it -n kube-system ds/cilium -- cilium debuginfo --output json
$ kubectl exec -it -n kube-system ds/cilium -- cilium debuginfo --output json | jq .encryption
# => {
#      "wireguard": {
#        "interfaces": [
#          {
#            "listen-port": 51871,
#            "name": "cilium_wg0",
#            "peer-count": 2,
#            "peers": [
#              {
#                "allowed-ips": [
#                  "172.20.0.0/24",
#                  "192.168.10.100/32",
#                  "172.20.0.103/32",
#                  "172.20.0.93/32",
#                  "172.20.0.36/32",
#                  "172.20.0.5/32",
#                  "172.20.0.168/32",
#                  "172.20.0.27/32",
#                  "172.20.0.190/32"
#                ],
#                "endpoint": "192.168.10.100:51871",
#                "last-handshake-time": "0001-01-01T00:00:00.000Z",
#                "public-key": "9Pva2PmR3ETq7N/GVRHD4IsmmSYEoJTEFzXg7Qe/FGo="
#              },
#              {
#                "allowed-ips": [
#                  "192.168.10.101/32",
#                  "172.20.1.93/32",
#                  "172.20.1.252/32",
#                  "172.20.1.0/24",
#                  "172.20.1.105/32",
#                  "172.20.1.6/32"
#                ],
#                "endpoint": "192.168.10.101:51871",
#                "last-handshake-time": "0001-01-01T00:00:00.000Z",
#                "public-key": "ZkvKJ95K9Bo6xE6DnkFfRPUHNCsuRAnIejCP7OqObB8="
#              }
#            ],
#            "public-key": "chlVHrcv7vafkretDiAyIn0+1bU4xVDzxhSxEUBx9lw="
#          }
#        ],
#        "node-encryption": "Disabled"
#      }
#    }

$ ip -d -c addr show cilium_wg0
# => 24: cilium_wg0: <POINTOPOINT,NOARP,UP,LOWER_UP> mtu 1420 qdisc noqueue state UNKNOWN group default
#        link/none  promiscuity 0  allmulti 0 minmtu 0 maxmtu 2147483552
#        wireguard numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535 tso_max_size 65536 tso_max_segs 65535 gro_max_size 65536
$ ip rule show
# => 9:      from all fwmark 0x200/0xf00 lookup 2004
#    10:     from all fwmark 0xa00/0xf00 lookup 2005
#    100:    from all lookup local
#    32766:  from all lookup main
#    32767:  from all lookup default 

# wg 정보 확인
$ wg -h
$ wg show
# => interface: cilium_wg0
#      public key: 9Pva2PmR3ETq7N/GVRHD4IsmmSYEoJTEFzXg7Qe/FGo=
#      private key: (hidden)
#      listening port: 51871
#      fwmark: 0xe00
#    
#    peer: chlVHrcv7vafkretDiAyIn0+1bU4xVDzxhSxEUBx9lw=
#      endpoint: 192.168.10.102:51871
#      allowed ips: 192.168.10.102/32, 172.20.2.243/32, 172.20.2.226/32, 172.20.2.125/32, 172.20.2.124/32, 172.20.2.140/32, 172.20.2.0/24, 172.20.2.171/32
#    
#    peer: ZkvKJ95K9Bo6xE6DnkFfRPUHNCsuRAnIejCP7OqObB8=
#      endpoint: 192.168.10.101:51871
#      allowed ips: 172.20.1.0/24, 172.20.1.105/32, 172.20.1.93/32, 172.20.1.6/32, 192.168.10.101/32, 172.20.1.252/32

$ wg show all public-key
# => cilium_wg0      9Pva2PmR3ETq7N/GVRHD4IsmmSYEoJTEFzXg7Qe/FGo=
$ wg show all private-key
$ wg show all preshared-keys
$ wg show all endpoints
# => cilium_wg0      chlVHrcv7vafkretDiAyIn0+1bU4xVDzxhSxEUBx9lw=    192.168.10.102:51871
#    ZkvKJ95K9Bo6xE6DnkFfRPUHNCsuRAnIejCP7OqObB8=    192.168.10.101:51871
$ wg show all transfer
# => cilium_wg0      chlVHrcv7vafkretDiAyIn0+1bU4xVDzxhSxEUBx9lw=    0       0
#    cilium_wg0      ZkvKJ95K9Bo6xE6DnkFfRPUHNCsuRAnIejCP7OqObB8=    0       0

# 퍼블릭 키 확인
$ kubectl get cn -o yaml | grep annotations -A1
# =>     annotations:
#          network.cilium.io/wg-pub-key: 9Pva2PmR3ETq7N/GVRHD4IsmmSYEoJTEFzXg7Qe/FGo=
#    --
#        annotations:
#          network.cilium.io/wg-pub-key: ZkvKJ95K9Bo6xE6DnkFfRPUHNCsuRAnIejCP7OqObB8=
#    --
#        annotations:
#          network.cilium.io/wg-pub-key: chlVHrcv7vafkretDiAyIn0+1bU4xVDzxhSxEUBx9lw=
  • 통신 확인
# curl 호출
$ kubectl exec -it curl-pod -- curl webpod
# => Hostname: webpod-697b545f57-rcgf8
#    ...
$ kubectl exec -it curl-pod -- curl webpod
# => Hostname: webpod-697b545f57-p9hhs
#    ...

# tcpdump
$ tcpdump -i cilium_wg0 -n
$ tcpdump -eni any udp port 51871
$ tcpdump -eni any udp port 51871 -w /tmp/wg.pcap  # vagrant scp k8s-ctr:/tmp/wg.pcap . > wireshark 로 확인

$ termshark /tmp/wg.pcap
# => termshark 2.4.0  |  wg.pcap                                                                                                                                      Analysis    Misc
#    ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
#    ┃Filter:                                                                                                                                                         <Apply> <Recent> ┃
#    ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
#     No. -   Time -       Source -              Dest -                Proto -        Length -    Info -                                                                               ▲
#     <span style="background-color: #2080fd; color: #fff;">1       0.000000     192.168.10.100        192.168.10.102        WireGuard      192         Transport Data, receiver=0x12FBD5BA, counter=28, datalen=112                         █</span>
#     2       0.001455     192.168.10.100        192.168.10.102        WireGuard      192         Transport Data, receiver=0x12FBD5BA, counter=29, datalen=112
#     3       0.026152     192.168.10.102        192.168.10.100        WireGuard      240         Transport Data, receiver=0xF6874881, counter=22, datalen=160
#     4       0.026152     192.168.10.102        192.168.10.100        WireGuard      288         Transport Data, receiver=0xF6874881, counter=23, datalen=208
#    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
#    [+] Frame 1: 192 bytes on wire (1536 bits), 192 bytes captured (1536 bits)
#    [+] Linux cooked capture v2
#    [+] Internet Protocol Version 4, Src: 192.168.10.100, Dst: 192.168.10.102
#    [+] User Datagram Protocol, Src Port: 51871, Dst Port: 51871
#    [-] WireGuard Protocol
#          Type: Transport Data (4)
#          Reserved: 000000
#          Receiver: 0x12fbd5ba
#          Counter: 28
#    <span style="background-color: #2080fd; color: #fff;">      Encrypted Packet [=]          </span>
#    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
#     0000   08 00 00 00 00 00 00 03  00 01 04 06 08 00 27 5d   ........ ......']
#     0010   09 97 00 00 45 00 00 ac  70 7e 00 00 40 11 73 a8   ....E... p~..@.s.
#     0020   c0 a8 0a 64 c0 a8 0a 66  ca 9f ca 9f 00 98 96 c4   ...d...f ........
#     0030   04 00 00 00 ba d5 fb 12  1c 00 00 00 00 00 00 00   ........ ........
#     0040   e6 <span style="background-color: #2080fd; color: #fff;">08 0f da 17 ab 05 18  ad a8 92 a6 53 17 5e 27</span>   ........ ....S.^'
#     0050   <span style="background-color: #2080fd; color: #fff;">79 e8 ce 06 41 63 c2 9b  e1 51 3b b7 a1 a9 82 09</span>   y...Ac.. .Q;.....
#     0060   <span style="background-color: #2080fd; color: #fff;">f5 71 8a f3 f9 ad aa 78  40 01 cd e6 22 43 cb 6d</span>   .q.....x @..."C.m
#     0070   <span style="background-color: #2080fd; color: #fff;">3e 06 60 40 67 27 5e 96  8b 6f 43 87 32 89 bb 54</span>   >.`@g'^. .oC.2..T
#     0080   <span style="background-color: #2080fd; color: #fff;">cc dd 61 ff 0b 52 32 31  7a e0 9f 1f de 50 6e d5</span>   ..a..R21 z....Pn.
#     0090   <span style="background-color: #2080fd; color: #fff;">0a 69 d4 d3 9c 73 8d a3  60 ff 34 2b 51 40 65 48</span>   .i...s.. `.4+Q@eH
#     00a0   <span style="background-color: #2080fd; color: #fff;">19 8d 8c fb e3 41 40 13  63 06 b8 1a 96 64 10 58</span>   .....A@. c....d.X
#     00b0   <span style="background-color: #2080fd; color: #fff;">44 3b fa 0c 7d f8 09 98  f3 50 98 7c 64 04 cd 59</span>   D;..}... .P.|d..Y
# <span style="color: green;">👉 패킷이 암호화 되어 있는 것을 확인할 수 있습니다.</span>

# query the flow API and look for flows
$ hubble observe --pod curl-pod
  • Node-to-Node Encryption (beta) : 아직 베타 기능이지만 --set encryption.nodeEncryption=true 옵션을 통해 노드 - 노드 , 노드 - 파드 간 트래픽도 암호화 할 수 있습니다.
  • 설정 원복
#
$ helm upgrade cilium cilium/cilium --version 1.18.1 --namespace kube-system --reuse-values \
  --set encryption.enabled=false
# => Release "cilium" has been upgraded. Happy Helming!
#    ...

$ kubectl -n kube-system rollout restart ds/cilium
# => daemonset.apps/cilium restarted

# 확인
$ cilium config view | grep -i wireguard
# => (없음)
$ kubectl exec -it -n kube-system ds/cilium -- cilium encrypt status
# => Encryption: Disabled
$ kubectl exec -it -n kube-system ds/cilium -- cilium status | grep Encryption
# => Encryption:              Disabled
# <span style="color: green;">👉 WireGuard 인터페이스 및 설정이 제거된 것을 확인할 수 있습니다.</span>

Inspecting TLS Encrypted Connections

img.png https://docs.cilium.io/en/stable/security/tls-visibility/

  • 쿠버네티스 환경에서 네트워크 트래픽은 보안을 위해 TLS(Transport Layer Security)로 암호화되는 경우가 많습니다.
  • 그러나 암호화된 연결은 정책 적용이나 가시성 확보 측면에서 제약이 생길 수 있습니다.
  • Cilium은 이러한 문제를 해결하기 위해 TLS 암호화된 연결도 투명하게 검사할 수 있는 기능을 제공합니다.
소개
  • Cilium은 HTTPS와 같은 TLS 보호 연결에서도 API 인식 가시성과 정책 적용을 지원합니다.
  • 기존 하드웨어 방화벽과 유사하지만, 전적으로 쿠버네티스 워커 노드의 소프트웨어 방식으로 동작합니다.
  • 네트워크 보안팀은 내부 인증 기관(CA)을 생성하여 클라이언트가 이를 신뢰하도록 설정합니다.
  • Cilium은 TLS 연결을 중간에서 종료하고 내부 CA 서명 인증서를 사용하여 대상 서비스와 새로운 TLS 연결을 맺습니다.
  • 이를 통해 애플리케이션 계층 데이터 검사정책 적용이 가능해집니다.
TLS 검사 구성 단계

TLS 검사를 구현하려면 CA 기반 모델을 따라야 하며, 기본 절차는 다음과 같습니다:

  1. 내부 CA 생성: CA 인증서를 생성하여 내부인증기관을 만듭니다.
  2. 대상 서비스 인증서 요청: DNS 이름과 일치하는 서명 요청(CSR: Certificate Signing Request)을 생성합니다.
  3. CA 서명 인증서 발급: 내부 CA로 서명된 인증서를 만듭니다.
  4. 클라이언트 신뢰 체인 구성: 클라이언트에 CA 인증서를 설치합니다.
  5. Cilium 구성: 새로운 TLS 연결을 생성할 때 사용할 신뢰할 CA 집합을 지정합니다.

⚠️ 운영 환경에서는 CA 개인 키 보관이 매우 중요합니다.
개인 키가 유출되면 TLS 트래픽을 누구나 가로채어 검사할 수 있기 때문입니다.
반대로 인증서는 공개 정보이므로 민감하지 않습니다.

TLS 검사 동작 방식
  • Cilium은 클라이언트와의 원래 TLS 연결을 종료하고, Envoy를 통해 대상 서비스와 새로운 TLS 연결을 수립합니다.
  • 네트워크 정책에는 terminatingTLSoriginatingTLS 설정이 필요합니다.
  • 이 과정에서 핵심은 Envoy에 인증서를 전달하는 방식입니다.
  • NPDS(원래의 버전)와 SDS(새롭고 향상된 버전) 두 가지 방식을 제공하고 있으며 각각 장단점이 있습니다.

  • NPDS (Network Policy Discovery Service)
    • 인증서와 키를 Base64 텍스트로 인라인 전송합니다.
    • 단순하지만 동일 인증서를 여러 정책에서 사용할 경우 Envoy 메모리에 중복 저장됩니다.
    • 대규모 환경에서는 메모리 사용량이 급증할 수 있는 단점이 있습니다.
  • SDS (Secret Discovery Service)
    • Cilium 1.17부터 도입된 개선된 방식입니다.
    • Kubernetes 시크릿을 참조하여 Envoy SDS API를 통해 전달합니다.
    • 인증서가 참조 기반으로 전달되므로 중복 저장이 사라집니다.
    • 메모리 효율성과 보안이 뛰어나며, Cilium 1.17 이후 기본값으로 권장됩니다.
TLS Interception 구성 (Cilium 1.17+)
  • Cilium은 세 가지 방법으로 TLS Interception을 구성할 수 있습니다:
  1. SDS (권장 방식)
    • 정책에서 참조한 시크릿을 cilium-secrets 네임스페이스에 복사 후 SDS로 동기화합니다.
    • 기본값이자 가장 안전하고 효율적인 방식입니다.
tls:
  readSecretsOnlyFromSecretsNamespace: true
  secretsNamespace:
    name: cilium-secrets # This setting is optional, as it is the default
  secretSync:
    enabled: true
  1. NPDS 인라인 방식 (비권장)
    • 클러스터 내 어느 네임스페이스든 시크릿을 저장할 수 있고, Cilium이 직접 읽어 Envoy에 전달합니다.
    • 보안 범위가 크게 확장되므로 권장되지 않습니다.
tls:
  readSecretsOnlyFromSecretsNamespace: false
  secretSync:
    enabled: false
  1. 하위 호환 모드
    • cilium-secrets 네임스페이스를 직접 참조합니다.
    • 업그레이드 호환성을 위해 유지되는 옵션입니다.
tls:
  readSecretsOnlyFromSecretsNamespace: true
  secretsNamespace:
    name: cilium-secrets # This setting is optional, as it is the default
  secretSync:
    enabled: false
  • Helm 설정 주요 항목
    • tls.secretsNamespace.name : 정책 비밀 네임스페이스 (기본값: cilium-secrets)
    • tls.readSecretsOnlyFromSecretsNamespace : 지정 네임스페이스에서만 비밀 읽기 (기본값: true)
    • tls.secretSync.enabled : sSDS 사용을 위한 비밀 동기화 활성화 (기본값: true)
Cilium TLS Interception 설정 (SDS Mode)
#
$ kubectl get all,secret,cm -n cilium-secrets
# => NAME                         DATA   AGE
#    configmap/kube-root-ca.crt   1      3h33m

#
$ cat << EOF > tls-config.yaml
tls:
  readSecretsOnlyFromSecretsNamespace: true
  secretsNamespace:
    name: cilium-secrets # This setting is optional, as it is the default
  secretSync:
    enabled: true
EOF

$ helm upgrade cilium cilium/cilium --version 1.18.1 --namespace kube-system --reuse-values \
  -f tls-config.yaml
# => Release "cilium" has been upgraded. Happy Helming!
#    ...

$ kubectl -n kube-system rollout restart deploy/cilium-operator
# => deployment.apps/cilium-operator restarted
$ kubectl -n kube-system rollout restart ds/cilium
# => daemonset.apps/cilium restarted

# 확인
$ cilium config view | grep -i secret
# => enable-ingress-secrets-sync                       true
#    enable-policy-secrets-sync                        true
#    ingress-secrets-namespace                         cilium-secrets
#    policy-secrets-namespace                          cilium-secrets
#    policy-secrets-only-from-secrets-namespace        true

🔬 Lab3. TLS Interception 실습

  • 관련문서

  • TLS 가로채기를 시연하기 위해 DNS 인식 정책 예제에 사용한 것과 동일한 mediabot애플리케이션을 사용할 것입니다. 이 애플리케이션은 HTTPS를 사용하여 Star Wars API 서비스에 액세스합니다. 이는 일반적으로 Cilium과 같은 네트워크 계층 메커니즘이 통신의 HTTP 계층 세부 정보를 볼 수 없음을 의미합니다. 모든 애플리케이션 데이터는 네트워크로 전송되기 전에 TLS를 사용하여 암호화되기 때문입니다.
  • 다음의 항목을 실습하면서 TLS 가로채기 기능을 이해해 보겠습니다.
    • TLS 가로채기를 활성화하기 위해 내부 인증 기관(CA)과 해당 CA가 서명한 관련 인증서 생성
    • DNS 기반 정책 규칙을 사용하여 가로챌 트래픽을 선택하려면 Cilium 네트워크 정책을 사용
    • cilium monitor를 사용하여 HTTP 요청의 세부 사항을 검사
단일 포드 mediabot 애플리케이션을 생성
# 
$ cat << EOF > dns-sw-app.yaml
apiVersion: v1
kind: Pod
metadata:
  name: mediabot
  labels:
    org: empire
    class: mediabot
    app: mediabot
spec:
  containers:
  - name: mediabot
    image: quay.io/cilium/json-mock:v1.3.8@sha256:5aad04835eda9025fe4561ad31be77fd55309af8158ca8663a72f6abb78c2603
EOF

$ kubectl apply -f dns-sw-app.yaml
# => pod/mediabot created
$ kubectl wait pod/mediabot --for=condition=Ready
# => pod/mediabot condition met

# 확인
$ kubectl get pods
# => NAME                      READY   STATUS    RESTARTS   AGE
#    mediabot                  1/1     Running   0          16s
$ kubectl exec mediabot -- curl -I -s https://api.github.com | head -1
# => HTTP/2 200
$ kubectl exec mediabot -- curl -I -s --max-time 5 https://support.github.com | head -1
# => HTTP/2 302
TLS 키 및 인증서 생성 및 설치
  • 아래의 이미지는 생성되거나 복사되는 암호화 데이터가 포함된 다양한 파일과 시스템의 어떤 구성 요소가 해당 파일에 액세스해야 하는지 설명합니다.

img.png 암호화 데이터와 시스템의 구성요소간의 액세스 관계도

  • 로컬 시스템에 openssl이 이미 설치되어 있다면 사용할 수 있지만, 그렇지 않은 경우 간단한 단축키를 사용하여 cilium 포드 내에서 실행한 후 결과 명령을 실행할 수 있습니다.
  • Kubernetes 시크릿을 생성하거나 포드에 복사할 때 cilium 포드에서 결과 파일을 복사하는 데 사용할 수 있습니다 .
내부 인증 기관(CA) 만들기
# 'myCA.key'라는 이름의 CA 개인 키를 생성합니다.
$ openssl genrsa -des3 -out myCA.key 2048
# => Enter PEM pass phrase: qwe123          # <span style="color: green;">CA 인증서에서 사용할 비밀번호 입력</span>
#    Verifying - Enter PEM pass phrase: qwe123

$ ls *.key
# => myCA.key

# 개인 키에서 CA 인증서를 생성
$ openssl req -x509 -new -nodes -key myCA.key -sha256 -days 1825 -out myCA.crt
# => Enter pass phrase for myCA.key: qwe123   # <span style="color: green;">개인 키 비밀번호 입력</span>
#    You are about to be asked to enter information that will be incorporated
#    into your certificate request.
#    What you are about to enter is what is called a Distinguished Name or a DN.
#    There are quite a few fields but you can leave some blank
#    For some fields there will be a default value,
#    If you enter '.', the field will be left blank.
#    -----
#    Country Name (2 letter code) [AU]:KR
#    State or Province Name (full name) [Some-State]:Seoul
#    Locality Name (eg, city) []:Seoul
#    Organization Name (eg, company) [Internet Widgits Pty Ltd]:cloudneta
#    Organizational Unit Name (eg, section) []:study
#    Common Name (e.g. server FQDN or YOUR name) []:cloudneta.net
#    Email Address []:

$ ls *.crt
# => myCA.crt

$ openssl x509 -in myCA.crt -noout -text
# => ...
#            Issuer: C = KR, ST = Seoul, L = Seoul, O = cloudneta, OU = study, CN = cloudneta.net
#            Validity
#                Not Before: Sep  6 10:55:56 2025 GMT
#                Not After : Sep  5 10:55:56 2030 GMT
#            Subject: C = KR, ST = Seoul, L = Seoul, O = cloudneta, OU = study, CN = cloudneta.net
#    ...
#                X509v3 Basic Constraints: critical
#                    CA:TRUE
#    ...
지정된 DNS 이름에 대한 개인 키 및 인증서 서명 요청 생성
# 검사를 위해 가로채려는 대상 서비스의 DNS 이름과 일치하는 일반 이름을 사용하여 내부 개인 키와 인증서 서명을 생성합니다
## 이 예에서는 httpbin.org

# 먼저 개인 키를 만듭니다.
$ openssl genrsa -out internal-httpbin.key 2048
$ ls internal-httpbin.key
# => internal-httpbin.key

# 다음으로, 인증서 서명 요청을 생성하고, 메시지가 표시되면 일반 이름 필드에 대상 서비스의 DNS 이름을 지정합니다. 
# 다른 모든 메시지에는 원하는 값을 입력할 수 있습니다.
# 특정 값이어야 하는 유일한 필드는 클라이언트에 제공될 정확한 DNS 대상을 보장하는 것입니다. Common Name: httpbin.org
# 이 예제 워크플로는 정책 YAML(아래)의 toFQDNs 규칙도 인증서의 DNS 이름과 일치하도록 업데이트되는 한 모든 DNS 이름에 적용됩니다.
$ openssl req -new -key internal-httpbin.key -out internal-httpbin.csr
# => ...
#    Common Name (e.g. server FQDN or YOUR name) []:httpbin.org
#    ...

$ ls internal-httpbin.csr
# => internal-httpbin.csr
CA를 사용하여 DNS 이름에 대한 서명된 인증서 생성
# 내부 CA 개인 키를 사용하여 httpbin.org에 대한 서명된 인증서를 생성합니다 internal-httpbin.crt
$ openssl x509 -req -days 360 -in internal-httpbin.csr -CA myCA.crt -CAkey myCA.key -CAcreateserial -out internal-httpbin.crt -sha256
# => Certificate request self-signature ok
#    subject=C = KR, ST = Seoul, L = Seoul, O = cloudneta, OU = study, CN = httpbin.org
#    Enter pass phrase for myCA.key: qwe123     <span style="color: green;">CA 개인 키 비밀번호 입력</span>
  
$ ls internal-httpbin.crt
# => internal-httpbin.crt
$ openssl x509 -in internal-httpbin.crt -noout -text
# => ...
#            Issuer: C = KR, ST = Seoul, L = Seoul, O = cloudneta, OU = study, CN = cloudneta.net   # <span style="color: green;">내부 CA 인증서 정보</span>
#            Validity
#                Not Before: Sep  6 11:00:18 2025 GMT
#                Not After : Sep  1 11:00:18 2026 GMT
#            Subject: C = KR, ST = Seoul, L = Seoul, O = cloudneta, OU = study, CN = httpbin.org    # <span style="color: green;">대상 서비스 인증서 정보</span>
  
# 다음으로 대상 서비스에 대한 개인 키와 서명된 인증서를 모두 포함하는 Kubernetes 비밀을 생성합니다.
$ kubectl create secret tls httpbin-tls-data -n kube-system --cert=internal-httpbin.crt --key=internal-httpbin.key
# => secret/httpbin-tls-data created
$ kubectl get secret -n kube-system  httpbin-tls-data
# => NAME               TYPE                DATA   AGE
#    httpbin-tls-data   kubernetes.io/tls   2      10s  
클라이언트 포드 내에서 신뢰할 수 있는 CA로 내부 CA 추가
  • CA 인증서가 클라이언트 포드에 저장된 후에도 애플리케이션에서 사용하는 TLS 라이브러리가 CA 파일을 인식하는지 확인해야 합니다.
    • 대부분의 Linux 애플리케이션은 Linux 배포판과 함께 제공되는 신뢰할 수 있는 CA 인증서 세트를 자동으로 사용합니다.
      • 이 가이드에서는 Ubuntu 컨테이너를 클라이언트로 사용하므로 Ubuntu별 지침에 따라 업데이트합니다. 다른 Linux 배포판은 다른 메커니즘을 사용합니다.
    • 개별 애플리케이션은 OS 인증서 저장소를 사용하는 대신 자체 인증서 저장소를 활용할 수 있습니다. 자세한 내용은 애플리케이션 또는 애플리케이션 런타임 설명서를 참조하십시오.
#
$ kubectl exec -it mediabot -- ls -l /usr/local/share/ca-certificates/
# => total 0
  
# Ubuntu의 경우 먼저 추가 CA 인증서를 클라이언트 Pod 파일 시스템에 복사합니다.
$ kubectl cp myCA.crt default/mediabot:/usr/local/share/ca-certificates/myCA.crt
$ kubectl exec -it mediabot -- ls -l /usr/local/share/ca-certificates/
# => total 4
#    -rw-r--r-- 1 root root 1342 Sep  6 11:03 myCA.crt
  
# /etc/ssl/certs/ca-certificates.crt에 있는 신뢰할 수 있는 인증 기관의 글로벌 세트에 이 인증서를 추가하는 Ubuntu 전용 유틸리티 실행
$ kubectl exec -it mediabot -- ls -l /etc/ssl/certs/ca-certificates.crt # 사이즈, 생성/수정 날짜 확인
# => -rw-r--r-- 1 root root 213777 Jan  9  2024 /etc/ssl/certs/ca-certificates.crt
$ kubectl exec mediabot -- update-ca-certificates
# => Updating certificates in /etc/ssl/certs...
#    rehash: warning: skipping ca-certificates.crt,it does not contain exactly one certificate or CRL
#    1 added, 0 removed; done.
#    Running hooks in /etc/ca-certificates/update.d...
#    done.
$ kubectl exec -it mediabot -- ls -l /etc/ssl/certs/ca-certificates.crt # 사이즈, 생성/수정 날짜 확인
# => -rw-r--r-- 1 root root 215119 Sep  6 11:04 /etc/ssl/certs/ca-certificates.crt
# <span style="color: green;">👉 신규 CA 인증서가 추가되어 파일 크기와 수정 날짜가 변경된 것을 확인할 수 있습니다.</span>
Cilium에 신뢰할 수 있는 CA 목록 제공
  • 다음으로, Cilium이 보조 TLS 연결을 시작할신뢰해야 하는 CA 세트를 제공합니다. 이 목록은 조직에서 신뢰하는 표준 글로벌 CA 세트와 일치해야 합니다. 논리적인 옵션 중 하나는 운영 체제에서 신뢰하는 표준 CA 세트입니다. 이는 TLS 검사 도입 이전에 사용되던 CA 세트이기 때문입니다.
  • 간단하게 설명하기 위해 이 예에서 우리는 mediabot 포드의 Ubuntu 파일 시스템에서 이 목록을 복사할 것입니다. 하지만 이 신뢰할 수 있는 CA 목록은 특정 TLS 클라이언트나 서버에만 국한되지 않는다는 점을 이해하는 것이 중요합니다. 따라서 TLS 검사에 참여하는 TLS 클라이언트나 서버의 수에 관계없이 이 단계는 한 번만 수행하면 됩니다.
#
$ kubectl cp default/mediabot:/etc/ssl/certs/ca-certificates.crt ca-certificates.crt
  
#  이 인증서 번들을 사용하여 Kubernetes 비밀을 생성하여 Cilium이 인증서 번들을 읽고 이를 사용하여 나가는 TLS 연결을 검증할 수 있도록 합니다.
$ kubectl create secret generic tls-orig-data -n kube-system --from-file=ca.crt=./ca-certificates.crt
# => secret/tls-orig-data created
$ kubectl get secret -n kube-system tls-orig-data
# => NAME            TYPE     DATA   AGE
#    tls-orig-data   Opaque   1      7s  
Apply DNS and TLS-aware Egress Policy
  • 지금까지 TLS 검사를 활성화하기 위한 키와 인증서를 생성했지만, Cilium에 어떤 트래픽을 가로채서 검사할지 알려주지 않았습니다. 이 작업은 다른 Cilium 네트워크 정책에 사용되는 것과 동일한 Cilium 네트워크 정책 구조를 사용하여 수행됩니다.
  • 다음 Cilium 네트워크 정책은 Cilium이 mediabot포드와 . 사이의 통신에 대해 HTTP 인식 검사를 수행해야 함을 나타냅니다 httpbin.org.
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "l7-visibility-tls"
spec:
  description: L7 policy with TLS
  endpointSelector:
    matchLabels:
      org: empire
      class: mediabot
  egress:
  - toFQDNs:
    - matchName: "httpbin.org"
    toPorts:
    - ports:
      - port: "443"
        protocol: "TCP"
      terminatingTLS:
        secret:
          namespace: "kube-system"
          name: "httpbin-tls-data"
      originatingTLS:
        secret:
          namespace: "kube-system"
          name: "tls-orig-data"
      rules:
        http:
        - {}
  - toPorts:
    - ports:
      - port: "53"
        protocol: ANY
      rules:
        dns:
          - matchPattern: "*"
  • 즉 , 이 정책은 출구 접근 권한이 있는 endpointSelector라벨이 있는 포드에만 적용됩니다 .class: mediabot, org:empire
  • 첫 번째 출구 섹션에서는 사양을 사용하여 TCP 포트 443 출구를 허용합니다 .toFQDNs: matchNamehttpbin.org
  • httptoFQDNs 규칙 아래의 섹션은 이러한 연결이 모든 요청을 허용하는 정책에 따라 HTTP로 구문 분석되어야 함을 나타 냅니다 {}.
  • 및 섹션은 TLS 가로채기를 사용하여 mediabot에서의 초기 TLS 연결을 종료하고 .에 대한 새로운 아웃바운드 TLS 연결을 시작해야 함을 나타 terminatingTLS냅니다 .originatingTLShttpbin.org
  • 두 번째 출구 섹션은 mediabot포드가 kube-dns서비스에 접근할 수 있도록 합니다. Cilium이 지정된 패턴과 일치하는 DNS 조회를 검사하고 허용하도록 지시합니다. 이 경우 모든 DNS 쿼리를 검사하고 허용합니다.rules: dns
# 모니터링 : hubble cli 나 ui 에서 https 이니 L7 정보 확인 불가능
$ hubble observe --pod mediabot -f
  
# 
$ kubectl get pods
# => NAME                      READY   STATUS    RESTARTS   AGE
#    mediabot                  1/1     Running   0          19m
$ kubectl exec -it mediabot -- curl -sL 'https://httpbin.org/anything'
# => {
#      "args": {},
#      "data": "",
#      "files": {},
#      "form": {},
#      "headers": {
#        "Accept": "*/*",
#        "Host": "httpbin.org",
#        "User-Agent": "curl/7.88.1",
#        "X-Amzn-Trace-Id": "Root=1-68bc1691-6a809d304ab6834b5a89a5ca"
#      },
#      "json": null,
#      "method": "GET",
#      "origin": "211.229.62.113",
#      "url": "https://httpbin.org/anything"
#    }
$ kubectl exec -it mediabot -- curl -sL 'https://httpbin.org/headers' -v # 서버 인증서 확인
# => ...
#    * Server certificate:
#    *  subject: CN=httpbin.org
#    *  start date: Jul 20 00:00:00 2025 GMT
#    *  expire date: Aug 17 23:59:59 2026 GMT
#    *  subjectAltName: host "httpbin.org" matched cert's "httpbin.org"
#    *  issuer: <span style="color: green;">C=US; O=Amazon; CN=Amazon RSA 2048 M03</span> # <span style="color: green;">👉 원래의 인증기관(CA)에서 발급한 인증서</span>
#    *  SSL certificate verify ok.
  
# 정책 적용
$ kubectl create -f https://raw.githubusercontent.com/cilium/cilium/1.18.1/examples/kubernetes-tls-inspection/l7-visibility-tls.yaml
# => ciliumnetworkpolicy.cilium.io/l7-visibility-tls created
$ kubectl get cnp
# => NAME                AGE   VALID
#    l7-visibility-tls   7s    True  
Demonstrating TLS Inspection
  • 우리가 푸시한 정책은 에서 mediabot로의 모든 HTTPS 요청을 허용 httpbin.org하지만 모든 데이터는 HTTP 계층에서 구문 분석한다는 점을 기억하세요. 즉, cilium monitor는 모든 HTTP 요청과 응답을 보고합니다.
  • 이를 확인하려면 새 창을 열고 다음 명령을 실행하여 mediabot포드와 동일한 Kubernetes 워커 노드에서 실행 중인 cilium 포드의 이름(예: cilium-97s78)을 식별합니다.
# 모니터링 : hubble cli 나 ui 에서 https 이니 L7 정보 확인 불가능
$ hubble observe --pod mediabot -f
  
#
$ kubectl exec -it mediabot -- curl -sL 'https://httpbin.org/anything'
$ kubectl exec -it mediabot -- curl -sL 'https://httpbin.org/headers' -v # 서버 인증서 확인
# => ...
#    * Server certificate:
#    *  subject: C=KR; ST=Seoul; L=Seoul; O=cloudneta; OU=study; CN=httpbin.org
#    *  start date: Sep  6 11:00:18 2025 GMT
#    *  expire date: Sep  1 11:00:18 2026 GMT
#    *  common name: httpbin.org (matched)
#    *  issuer: <span style="color: green;">C=KR; ST=Seoul; L=Seoul; O=cloudneta; OU=study; CN=cloudneta.net</span> 
#  <span style="color: green;">👉 내부 인증기관(CA)에서 발급한 인증서로 교체되어 있습니다!</span>
#  <span style="color: green;">    즉, 우리가 만든 CA에서 발급한 인증서로 TLS 연결이 맺어진 것을 확인할 수 있으며</span>
#  <span style="color: green;">    TLS 가로채기가 성공적으로 이루어진 것입니다.</span>
#    *  SSL certificate verify ok.

img.png

  • 원래는 알 수 없었던 L7 정보가 인증서 교체후 “GET /anything” 으로 확인되는 것을 확인할 수 있습니다.
실습 리소스 삭제
$ kubectl delete -f https://raw.githubusercontent.com/cilium/cilium/1.18.1/examples/kubernetes-dns/dns-sw-app.yaml
# => pod "mediabot" deleted
$ kubectl delete cnp l7-visibility-tls
# => ciliumnetworkpolicy.cilium.io "l7-visibility-tls" deleted
$ kubectl delete secret -n kube-system tls-orig-data
# => secret "tls-orig-data" deleted
$ kubectl delete secret -n kube-system httpbin-tls-data
# => secret "httpbin-tls-data" deleted

Tetragon

  • Tetragon 소개 Youtube 영상 : eCHO Episode 194: Tetragon Everywhere
  • Tetragon Resources - Link

Tetragon 소개

  • TetragoneBPF 기반의 런타임 보안 및 관찰 도구입니다. - Docs
  • Linux 커널의 모든 함수에 연결하여 인수, 반환 값, Tetragon이 프로세스에 대해 수집하는 관련 메타데이터(예: 실행 파일 이름), 파일 및 기타 속성을 필터링할 수 있습니다.
  • Kubernetes 환경을 인식하며, 네임스페이스, 포드 등의 Kubernetes ID를 이해하므로 개별 작업 부하와 관련하여 보안 이벤트 감지를 구성할 수 있습니다.

img.png

  • Tetragon은 다음과 같은 보안에 중요한 이벤트를 감지하고 대응할 수 있습니다.
    • Process 실행 및 종료 events
    • System 콜 호출 및 반환
    • 네트워크와 파일 액세스를 포함한 I/O 활동
  • Kubernetes 환경에서 Tetragon을 사용하면 Kubernetes를 인식합니다.
    • 즉, 네임스페이스, 포드 등의 Kubernetes ID를 이해하므로 개별 작업 부하와 관련하여 보안 이벤트 감지를 구성할 수 있습니다.
기능 개요
  • eBPF 실시간 처리
    • 모든 정책과 필터링을 커널 내부 eBPF에서 직접 수행합니다.
      • 이벤트를 유저 공간으로 보내지 않고 커널에서 차단/반응 처리 → 성능 향상.
    • 고빈도 이벤트(send, read, write 등)도 커널에서 바로 필터링하므로 관측 오버헤드가 감소됩니다.
    • 파일, 소켓, 바이너리 이름, 네임스페이스, 권한(capabilities) 등 세부 필터링을 지원합니다.
    • 이렇게 지정된 이벤트만 유저 공간으로 전달하므로 자원 사용 효율적입니다.
  • eBPF 유연한 추적
    • Tetragon은 리눅스 커널의 모든 함수에 연결(hook)이 가능하고, 이를 통해 함수의 인자, 반환값, 프로세스/파일 메타데이터 등 다양한 기준으로 필터링합니다.
    • 사용자가 직접 트레이싱 정책(tracing policy)을 작성해 보안/관측 시나리오를 해결 가능합니다.
    • 저장소에는 여러 예시가 제공되며, 사용자는 이를 기반으로 자신만의 정책을 만들 수 있습니다.
    • 특정 커널 함수 추적도 가능하며, 엔진에 하드코딩된 제한 없습니다.
    • 시스템콜 트레이싱에서 흔히 발생하는 문제(잘못된 데이터 읽기, 악의적 조작, 페이지 폴트 누락 등)를 커널 깊숙한 곳에서 안정적으로 훅킹하여 회피합니다.
    • 개발자 다수가 커널 개발 경험을 갖고 있어, 다양한 보안 및 관측 유스케이스를 지원하는 정책 세트를 제공합니다.
  • eBPF을 통한 커널 인식
    • eBPF를 통해 Tetragon은 리눅스 커널 상태에 직접 접근합니다.
    • 이를 쿠버네티스 정보나 사용자 정책과 결합해 실시간 커널 레벨 규칙 적용이 가능합니다.
    • 예를 들어, 애플리케이션이 권한을 변경할 때, 해당 프로세스가 시스템 호출을 완료하고 추가 시스템 호출을 실행하기 전에 경고를 발생시키거나 프로세스를 종료하는 정책을 생성할 수 있습니다.

Tetragon 설치

# /proc 파일 시스템 액세스 필요.
# 기본적으로 Tetragon은 이벤트 로그의 노이즈를 줄이기 위해 kube-system 이벤트를 필터링합니다. 
$ helm repo add cilium https://helm.cilium.io
$ helm repo update
# => ...Successfully got an update from the "cilium" chart repository
#    Update Complete. ⎈Happy Helming!⎈
$ helm install tetragon cilium/tetragon -n kube-system
# => NAME: tetragon
#    LAST DEPLOYED: Sat Sep  6 21:14:53 2025
#    NAMESPACE: kube-system
#    STATUS: deployed
#    REVISION: 1
#    TEST SUITE: None
$ kubectl rollout status -n kube-system ds/tetragon -w
# => daemon set "tetragon" successfully rolled out

# 확인
$ k -n kube-system get deploy tetragon-operator -owide
# => NAME                READY   UP-TO-DATE   AVAILABLE   AGE   CONTAINERS          IMAGES                                    SELECTOR
#    tetragon-operator   1/1     1            1           37s   tetragon-operator   quay.io/cilium/tetragon-operator:v1.5.0   app.kubernetes.io/instance=tetragon,app.kubernetes.io/name=tetragon-operator
$ k -n kube-system get cm tetragon-operator-config tetragon-config
# => NAME                       DATA   AGE
#    tetragon-operator-config   9      45s
#    tetragon-config            33     45s

$ k -n kube-system get ds tetragon -owide
# => NAME       DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR   AGE   CONTAINERS               IMAGES                                                                      SELECTOR
#    tetragon   3         3         3       3            3           <none>          54s   export-stdout,tetragon   quay.io/cilium/hubble-export-stdout:v1.1.0,quay.io/cilium/tetragon:v1.5.0   app.kubernetes.io/instance=tetragon,app.kubernetes.io/name=tetragon
$ k -n kube-system get svc,ep tetragon
# => NAME               TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)    AGE
#    service/tetragon   ClusterIP   10.96.248.15   <none>        2112/TCP   75s
#    
#    NAME                 ENDPOINTS                                                     AGE
#    endpoints/tetragon   192.168.10.100:2112,192.168.10.101:2112,192.168.10.102:2112   75s
$ k -n kube-system get svc,ep tetragon-operator-metrics
# => NAME                                TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
#    service/tetragon-operator-metrics   ClusterIP   10.96.105.200   <none>        2113/TCP   83s
#    
#    NAME                                  ENDPOINTS          AGE
#    endpoints/tetragon-operator-metrics   172.20.1.32:2113   83s

$ k -n kube-system get pod -l app.kubernetes.io/part-of=tetragon -owide
$ k -n kube-system get pod -l app.kubernetes.io/name=tetragon
$ kc -n kube-system describe pod -l app.kubernetes.io/name=tetragon
# => ...
#    Containers:
#      export-stdout:
#        Container ID:  containerd://eb8abd117200c08e26b7efc02ae15ff14a618d1fa282b30530db973468ad1dcf
#        Image:         quay.io/cilium/hubble-export-stdout:v1.1.0
#    ...
#      tetragon:
#        Container ID:  containerd://7b283b22db614b6288a01c09e302349720d85e64ab85d449173abec9597ddce8
#        Image:         quay.io/cilium/tetragon:v1.5.0
#    ...
  • 데모 애플리케이션 배포
#
$ kubectl create -f https://raw.githubusercontent.com/cilium/cilium/v1.18.1/examples/minikube/http-sw-app.yaml
# => service/deathstar created
#    deployment.apps/deathstar created
#    pod/tiefighter created
#    pod/xwing created

# 확인
$ kubectl get pods
# => NAME                         READY   STATUS    RESTARTS   AGE
#    deathstar-86f85ffb4d-2z9dv   1/1     Running   0          23s
#    deathstar-86f85ffb4d-djnhm   1/1     Running   0          23s
#    tiefighter                   1/1     Running   0          23s
#    xwing                        1/1     Running   0          23s

실행 모니터링

  • 시스템의 모든 실행을 추적합니다. - Docs, Blog

img.png https://isovalent.com/blog/post/top-tetragon-use-cases/

img_1.png https://yuki-nakamura.com/2024/05/23/tetragon-process-lifecycle-observation-tetragon-agent-part/

  • Tetragon은 실행 이벤트를 JSON 로그와 GRPC 스트림을 통해 공개합니다. 사용자는 이를 통해 시스템의 모든 실행 과정을 관찰할 수 있습니다.
# 여러 노드가 있는 클러스터에서는 사용하는 Tetragon Pod가 "xwing" Pod와 동일한 노드에 있어야 실행 이벤트를 캡처할 수 있습니다.
# 이 명령을 사용하면 "**xwing**" Pod와 동일한 Kubernetes **노드**에 있는 **Tetragon Pod의 이름을** 가져올 수 있습니다.
$ POD=$(kubectl -n kube-system get pods -l 'app.kubernetes.io/name=tetragon' -o name --field-selector spec.nodeName=$(kubectl get pod xwing -o jsonpath='{.spec.nodeName}'))
$ echo $POD
# => pod/tetragon-fwccq
  
# 터미널1
# 일치하는 Pod를 식별한 후 해당 Pod를 대상으로 지정하여 명령을 실행합니다 : Tetragon에서 캡처한 실행 이벤트를 반환
$ kubectl exec -ti -n kube-system $POD -c tetragon -- tetra -h
# 압축 실행 이벤트에는 이벤트 유형, 포드 이름, 바이너리, 인수가 포함됩니다. 종료 이벤트에는 반환 코드가 포함됩니다.
$ kubectl exec -ti -n kube-system $POD -c tetragon -- tetra getevents -o compact --pods xwing
  
## JSON 형식으로 전체 실행 이벤트를 보려면 명령 -o compact에서 옵션을 제거
$ kubectl exec -ti -n kube-system $POD -c tetragon -- tetra getevents --pods xwing
  
# 터미널2
$ kubectl exec -ti xwing -- bash -c 'curl https://ebpf.io/applications/#tetragon'

# 터미털 1
$ kubectl exec -ti -n kube-system $POD -c tetragon -- tetra getevents -o compact --pods xwing
# => 🚀 process default/xwing /usr/bin/bash -c "curl https://ebpf.io/applications/#tetragon"
#    🚀 process default/xwing /usr/bin/curl https://ebpf.io/applications/#tetragon
#    💥 exit    default/xwing /usr/bin/curl https://ebpf.io/applications/#tetragon 0 

# 터미널2
$ kubectl exec -ti xwing -- bash -c 'curl https://httpbin.org'

# 터미털 1
$ kubectl exec -ti -n kube-system $POD -c tetragon -- tetra getevents -o compact --pods xwing
# => 🚀 process default/xwing /usr/bin/bash -c "curl https://httpbin.org"
#    🚀 process default/xwing /usr/bin/curl https://httpbin.org
#    💥 exit    default/xwing /usr/bin/curl https://httpbin.org 0

# 터미널2
$ kubectl exec -ti xwing -- bash -c 'cat /etc/passwd'

# 터미털 1
$ kubectl exec -ti -n kube-system $POD -c tetragon -- tetra getevents -o compact --pods xwing
# => 🚀 process default/xwing /usr/bin/bash -c "cat /etc/passwd"
#    🚀 process default/xwing /usr/bin/cat /etc/passwd
#    💥 exit    default/xwing /usr/bin/cat /etc/passwd 0

파일 접속 모니터링

  • 추적 정책으로 민감 파일 모니터링 - Docs, Blog

img.png https://isovalent.com/blog/post/file-monitoring-with-ebpf-and-tetragon-part-1/

img_1.png Figure 3: In-kernel vs user-space filtering

  • YAML 구성 파일을 통해 Tetragon에 추적 정책을 추가하면 Tetragon의 기본 실행 추적 기능을 확장할 수 있습니다.
  • 이러한 정책은 커널에서 필터링을 수행하여 커널에서 실행 중인 BPF 프로그램에서 관심 있는 이벤트사용자 공간에 게시되도록 합니다.
  • 이를 통해 사용량이 많은 시스템에서도 오버헤드를 낮게 유지할 수 있습니다.
  • 아래의 Manifest는 실행 모니터링 의 예시를 확장하여 Linux에서 민감한 파일을 모니터링하는 정책을 적용합니다.
  • 민감 파일 모니터링 정책 적용
# file_monitoring.yaml
apiVersion: cilium.io/v1alpha1
kind: TracingPolicy
metadata:
  name: "file-monitoring-filtered"
spec:
  kprobes:
  - call: "security_file_permission"
    syscall: false
    return: true
    args:
    - index: 0
      type: "file" # (struct file *) used for getting the path
    - index: 1
      type: "int" # 0x04 is MAY_READ, 0x02 is MAY_WRITE
    returnArg:
      index: 0
      type: "int"
    returnArgAction: "Post"
    selectors:
    - matchArgs:      
      - index: 0
        operator: "Prefix"
        values:
        - "/boot"           # Reads to sensitive directories
        - "/root/.ssh"      # Reads to sensitive files we want to know about
        - "/etc/shadow"
        - "/etc/profile"
        - "/etc/sudoers"
        - "/etc/pam.conf"   # Reads global shell configs bash/csh supported
        - "/etc/bashrc"
        - "/etc/csh.cshrc"
        - "/etc/csh.login"  # Add additional sensitive files here
      - index: 1
        operator: "Equal"
        values:
        - "4" # MAY_READ

# Manifest 적용
$ kubectl apply -f https://raw.githubusercontent.com/cilium/tetragon/main/examples/quickstart/file_monitoring.yaml
# => tracingpolicy.cilium.io/file-monitoring-filtered created
$ kubectl get tracingpolicy
# => NAME                       AGE
#    file-monitoring-filtered   55s
  • Tetragon 파일 액세스 이벤트 관찰
# 터미널2 : 이벤트를 생성하기위애 정책에 참조된 중요한(민감한) 파일을 읽어보세요
$ kubectl exec -ti xwing -- bash -c 'cat /etc/shadow'

# 터미널1
$ POD=$(kubectl -n kube-system get pods -l 'app.kubernetes.io/name=tetragon' -o name --field-selector spec.nodeName=$(kubectl get pod xwing -o jsonpath='{.spec.nodeName}'))
$ kubectl exec -ti -n kube-system $POD -c tetragon -- tetra getevents -o compact --pods xwing
# => 🚀 process default/xwing /usr/bin/bash -c "cat /etc/shadow"
#    🚀 process default/xwing /usr/bin/cat /etc/shadow
#    📚 read    default/xwing /usr/bin/cat /etc/shadow
#    📚 read    default/xwing /usr/bin/cat /etc/shadow
#    📚 read    default/xwing /usr/bin/cat /etc/shadow
#    📚 read    default/xwing /usr/bin/cat /etc/shadow
#    💥 exit    default/xwing /usr/bin/cat /etc/shadow 0  

# 터미널2 : 추적 정책에 따라 Tetragon은 민감한 디렉토리에 쓰기를 시도하는 경우(예: /etc디렉토리에 쓰기를 시도하는 경우)에 대한 응답으로 쓰기 이벤트를 생성합니다.
$ kubectl exec -ti xwing -- bash -c 'echo foo >> /etc/bar'

# 터미널1
$ kubectl exec -ti -n kube-system $POD -c tetragon -- tetra getevents -o compact --pods xwing
# => 🚀 process default/xwing /usr/bin/bash -c "echo foo >> /etc/bar"
#    📝 write   default/xwing /usr/bin/bash /etc/bar
#    📝 write   default/xwing /usr/bin/bash /etc/bar
#    💥 exit    default/xwing /usr/bin/bash -c "echo foo >> /etc/bar" 0 

# 정책 삭제
$ kubectl delete -f https://raw.githubusercontent.com/cilium/tetragon/main/examples/quickstart/file_monitoring.yaml
# => tracingpolicy.cilium.io "file-monitoring-filtered" deleted

Policy Enforcement

  • 커널 수준에서 정책 제한을 적용관련 내용을 살펴보겠습니다. - Docs
  • Tetragon의 추적 정책은 파일 액세스 이벤트나 네트워크 연결 이벤트와 같은 이벤트를 보고하기 위해 커널 함수를 모니터링하고, 해당 커널 함수에 제한을 적용할 수 있도록 지원합니다.
  • Tetragon에서 커널 내 필터링을 사용하면 커널에서 사용자 공간으로 전송되는 이벤트를 제한하여 성능을 크게 향상시킬 수 있습니다.
  • 또한, 커널 내 필터링을 통해 Tetragon은 커널 수준에서 정책 제한을 적용할 수 있습니다.
  • 예를 들어, SIGKILL정책 위반이 감지되었을 때 프로세스에 를 실행하면 해당 프로세스는 더 이상 실행되지 않습니다.
  • 정책 적용이 시스템 호출을 통해 트리거되는 경우, 애플리케이션은 시스템 호출에서 반환되지 않고 종료됩니다.
  • 이 섹션에서는 이 시작 가이드에서 이미 배포한 Tetragon 기능(실행, 파일 추적 및 네트워크 추적 정책)에 아래의 네트워크 및 파일 정책 적용을 추가합니다.
    • Kubernetes 클러스터에서 나가는 네트워크 트래픽을 제한하는 정책 적용
    • 민감한 파일에 블록 쓰기 및 읽기 작업 적용
  • 구체적인 구현 세부 사항은 시행 개념 섹션을 참조하세요.
파일 액세스 제한 적용
  • 다음은 파일 액세스 모니터링 의 예시를 적용하여 민감한 파일이 읽히지 않도록 확장하는 방법입니다.
  • 사용되는 정책은 이며 file_monitoring_enforce.yaml, 필요에 따라 검토하고 확장할 수 있습니다.
  • 관찰 정책과 적용 정책의 유일한 차이점은 애플리케이션에 작업 블록을 추가 SIGKILL하고 작업 시 오류를 반환한다는 것입니다.
# file_monitoring_enforce.yaml : matchaction 확인
apiVersion: cilium.io/v1alpha1
kind: TracingPolicyNamespaced
metadata:
  name: "file-monitoring-filtered"
spec:
  kprobes:
  - call: "security_file_permission"
    syscall: false
    return: true
    args:
    - index: 0
      type: "file" # (struct file *) used for getting the path
    - index: 1
      type: "int" # 0x04 is MAY_READ, 0x02 is MAY_WRITE
    returnArg:
      index: 0
      type: "int"
    returnArgAction: "Post"
    selectors:
    - matchArgs:
      - index: 0
        operator: "Prefix"
        values:
        - "/boot"           # Reads to sensitive directories
        - "/root/.ssh"      # Reads to sensitive files we want to know about
        - "/etc/shadow"
        - "/etc/profile"
        - "/etc/sudoers"
        - "/etc/pam.conf"   # Reads global shell configs bash/csh supported
        - "/etc/bashrc"
        - "/etc/csh.cshrc"
        - "/etc/csh.login"  # Add additional sensitive files here
      - index: 1
        operator: "Equal"
        values:
        - "4" # MAY_READ
      matchActions:
      - action: Sigkill
...
$ kubectl apply -f https://raw.githubusercontent.com/cilium/tetragon/main/examples/quickstart/file_monitoring_enforce.yaml
# => tracingpolicynamespaced.cilium.io/file-monitoring-filtered created
$ kubectl get tracingpolicynamespaced
# => NAME                       AGE
#    file-monitoring-filtered   11s
  
# 터미널1
$ POD=$(kubectl -n kube-system get pods -l 'app.kubernetes.io/name=tetragon' -o name --field-selector spec.nodeName=$(kubectl get pod xwing -o jsonpath='{.spec.nodeName}'))
$ kubectl exec -ti -n kube-system $POD -c tetragon -- tetra getevents -o compact --pods xwing
# => 🚀 process default/xwing /usr/bin/bash -c "cat /etc/shadow"
#    🚀 process default/xwing /usr/bin/cat /etc/shadow
#    📚 read    default/xwing /usr/bin/cat /etc/shadow
#    📚 read    default/xwing /usr/bin/cat /etc/shadow
#    💥 exit    default/xwing /usr/bin/cat /etc/shadow <span style="color: green;">SIGKILL</span>
  
# 터미널2 : 정의된 정책에 포함된 파일 중 하나인 민감한 파일을 읽어보세요
$ kubectl exec -ti xwing -- bash -c 'cat /etc/shadow'
# => command terminated with exit code 137
# <span style="color: green;">👉 이번에는 "cat /etc/shadow" 명령이 SIGKILL로 인해 종료된 것을 확인할 수 있습니다.</span>
  
# 시행된 파일 정책에 포함되지 않은 파일을 읽거나 쓰려는 시도는 영향을 받지 않습니다.
$ kubectl exec -ti xwing -- bash -c 'echo foo > /tmp/test.txt'

$ kubectl exec -ti -n kube-system $POD -c tetragon -- tetra getevents -o compact --pods xwing
# => 🚀 process default/xwing /usr/bin/bash -c "echo foo > /tmp/test.txt"
#    💥 exit    default/xwing /usr/bin/bash -c "echo foo > /tmp/test.txt" 0  

마치며

Cilium Security와 Tetragon에 대해 알아보았습니다. Cilium 자체도 다양한 보안 기능을 제공하고 있는데 Tetragon까지 더해지니 다양한 보안 요구사항을 충족시킬 수 있을 것 같습니다. TLS Interception 기능도 흥미로웠는데 완전 MITM 공격인것 같으면서도 트러블 슈팅에 큰 도움이 될 수 있을 것 같습니다.

벌써 Cilium 스터디가 끝났습니다. Cilium 설치와 기본 설정에서 부터, Hubble을 비롯한 관측성과 파드 네트워크 상세, BGP 라우팅, 서비스메시, Performance, Security와 Tetragon 등등 따라가기도 벅찼지만 많은 것을 배울 수 있었던 시간이었습니다. 매주 스터디 마다 느낀 점은 Cilium의 생태계는 정말 넓고 깊다는 것입니다. 앞으로도 꾸준히 공부하고 실습하면서 익혀나가야 할 것 같습니다.

실습할때 복잡한 Kubernetes 구성이 필요한 경우도 많았는데 매번 편리하게 환경을 구축할 수 있도록 해주셔서 환경 구성에는 어려움없이 진행할 수 있었습니다. 매주 실습자료 준비와 진행에 애써주신 가시다 님께 감사드립니다. :bow: