관리 메뉴

근묵자흑

Kubescape 런타임 보안(eBPF/Inspektor Gadget)과 MCP AI 연동 동작 방식 본문

AIOps

Kubescape 런타임 보안(eBPF/Inspektor Gadget)과 MCP AI 연동 동작 방식

Luuuuu 2026. 5. 30. 19:55
대상 로직 A:  Runtime Security — eBPF-based runtime monitoring via Inspektor Gadget (kubescape/node-agent)
대상 로직 B:  AI Integration — MCP server for AI assistant integration (kubescape/kubescape cmd/mcpserver)연계 맥락: Cilium Hubble(네트워크 정책 plane), coroot eBPF(관측·서비스맵 plane)와의 대비
코드 기준: node-agent(main, IG fork matthyx 기반), kubescape v3 CLI, cilium(main)
실 데이터: colima k3s(kernel 6.8, BTF)에서 캡처. Kubescape operator v1.40.2, Inspektor Gadget v0.52.

1. 배경 — eBPF를 통한 보안 관측

coroot 문서가 "eBPF로 trace(관측)를 만드는 원리"였다면, 이 문서는 같은 eBPF 기술이 보안에서 무엇을 하는지를 다룹니다. eBPF는 커널에서 syscall·소켓·tracepoint를 후킹하므로, 애플리케이션을 수정하지 않고도 "프로세스가 무슨 행위를 했는가"를 봅니다. Kubescape는 이를 두 단계로 씁니다.

단계 하는 일 산출물
학습(learning) 컨테이너가 정상 동작하는 동안 행위(실행/파일/네트워크/syscall)를 관측해 "정상 baseline"을 만든다 ApplicationProfile CRD
탐지(detection) 이후 이벤트가 baseline에 없거나 시그니처에 맞으면 위협으로 판정 런타임 alert

그리고 이렇게 만든 보안 데이터(취약점·설정 스캔 결과)를 MCP 서버가 AI 어시스턴트에게 도구(tool)로 노출합니다(로직 B).

flowchart LR
  subgraph node["Kubernetes Node (node-agent DaemonSet)"]
    k["커널 eBPF<br/>(Inspektor Gadget gadget)"] --> ev["런타임 이벤트<br/>실행/파일/dns/net/cap/..."]
    ev --> learn["학습 → ApplicationProfile<br/>(정상 baseline)"]
    ev --> detect["탐지 → CEL rule engine<br/>(baseline 대비 anomaly + 시그니처)"]
    detect --> alert["런타임 alert"]
  end
  learn --> stg[("Kubescape storage<br/>aggregated API (CRD)")]
  scan["설정/취약점 스캐너<br/>(kubescape/kubevuln)"] --> stg
  stg --> mcp["MCP server (stdio)<br/>cmd/mcpserver"]
  mcp <--> ai["AI assistant<br/>(Claude/Cursor/codex)"]

핵심 대비 (by coroot): coroot eBPF는 syscall을 트레이싱해 서비스 간 호출 그래프·지연(관측가능성)을 만들고, Hubble은 CNI 데이터패스에서 연결·정책 verdict(네트워크 plane)를 만들며, Kubescape node-agent는 syscall/소켓에서 프로세스 런타임 위협(보안 plane)을 만듭니다. 셋 다 eBPF지만 (관측 층위 × 목적) 평면에서 서로 다른 사분면을 점유합니다(§5).


2. 사전 용어

용어
Inspektor Gadget (IG) eBPF 프로그램을 OCI 이미지("image-based gadget") 로 패키징·배포·실행하는 프레임워크. node-agent가 이걸 라이브러리로 임베드
gadget 하나의 eBPF 프로그램 + 메타데이터를 담은 OCI 이미지 (예: trace_exec, trace_dns)
ApplicationProfile 컨테이너의 "정상 행위 baseline" CRD. 학습된 실행/파일/syscall/network 등
CEL Common Expression Language. 룰을 코드 없이 표현식으로 정의(예: !ap.was_executed(...))
RuntimeRule / RuntimeRuleAlertBinding 탐지 룰과, 그 룰을 워크로드에 바인딩하는 CRD
storage(aggregated API) spdx.softwarecomposition.kubescape.io 그룹의 CRD를 서빙하는 Kubescape 집계 API 서버. node-agent가 프로파일을 쓰고, MCP가 스캔결과를 읽는 곳
MCP Model Context Protocol. AI 어시스턴트가 외부 데이터/도구에 붙는 표준 프로토콜

3. 로직 A — Runtime Security: eBPF runtime monitoring via Inspektor Gadget

3.1 전체 아키텍처

node-agent 한 Pod 안에 세 매니저가 있습니다(README 아키텍처 다이어그램 기준).

flowchart TB
  subgraph na["node-agent Pod (privileged, hostPID)"]
    TM["Tracer Manager<br/>17개 IG gadget tracer"]
    PM["Profile Manager<br/>ApplicationProfile 학습"]
    RM["Rule Manager<br/>CEL 평가 + alert"]
    TM -->|enriched event| PM
    TM -->|enriched event| RM
    PM -->|baseline projection| RM
  end
  RM --> EXP["exporters<br/>(stdout/alertmanager/syslog/http)"]
  PM --> STG[("storage / aggregated API")]
  • Tracer Manager: eBPF gadget을 로드·실행해 이벤트를 만든다(§3.2~3.5).
  • Profile Manager: 이벤트를 컨테이너별 baseline으로 학습한다(§3.6).
  • Rule Manager: 이벤트를 baseline·룰과 대조해 alert를 만든다(§3.7~3.8).

node-agent 기동 로그(17개 tracer가 IG gadget 이미지로 로드됨, scenario-data/node-agent-startup.log):

Started tracer trace_network ... Using iouring gadget image ghcr.io/inspektor-gadget/gadget/iouring_new:latest kernelVersion 6.8.0
trace_iouring(6) trace_hardlink(7) trace_http(8) trace_ptrace(9) trace_exec(10) trace_exit(11)
trace_ssh(12) trace_unshare(13) trace_capabilities(14) trace_symlink(15) trace_dns(16) trace_bpf(17)
ContainerWatcher started successfully
RulesWatcher - synced rules from cluster  enabledRules=22
RBCache - ruleBinding added/modified  name=/all-rules-all-pods

3.2 수집 — IG image-based gadget을 실행하는 구조

coroot가 자체 .c eBPF를 컴파일해 넣었다면, node-agent는 eBPF를 OCI 이미지로 분리해 IG 런타임으로 실행합니다. tracer마다 동일 패턴입니다.

// node-agent  pkg/containerwatcher/v2/tracers/exec.go:20~22, 55~80
const execImageName = "ghcr.io/inspektor-gadget/gadget/trace_exec:v0.48.1"

func (et *ExecTracer) Start(ctx context.Context) error {
    et.gadgetCtx = gadgetcontext.New(ctx, execImageName,     // 실행할 gadget OCI 이미지
        gadgetcontext.WithDataOperators(
            et.kubeManager,            // K8s 메타 enrich
            ocihandler.OciHandler,     // 이미지 → eBPF operator 인스턴스화
            NewExecOperator(),         // args 바이트 → 문자열 파싱(custom)
            et.eventOperator()),       // datasource.Subscribe → node-agent 콜백(sink)
        gadgetcontext.WithName(execTraceName),
        gadgetcontext.WithOrasReadonlyTarget(et.ociStore))   // tracers.tar 로컬 OCI store
    go func() { et.runtime.RunGadget(et.gadgetCtx, nil, map[string]string{"operator.oci.ebpf.paths":"true"}) }()
}

중요한 설계 결정 — 네트워크 pull이 아니라 로컬 tarball: 모든 gadget 이미지는 컨테이너에 동봉된 tracers.tar에서 로드됩니다. factory가 한 번만 열어 모든 tracer에 주입합니다.

// node-agent  pkg/containerwatcher/v2/tracers/tracer_factory.go:69
ociStore, err := orasoci.NewFromTar(context.Background(), "tracers.tar")

IG의 oci-handler는 OrasTarget이 주입되면 레지스트리 pull(oci.EnsureImage)과 서명 검증(oci.VerifyGadgetImage)을 건너뜁니다(inspektor-gadget pkg/operators/oci-handler/oci.go:399~417). 따라서 air-gapped 환경에서도 동작합니다.

3.3 datasource → 이벤트 (구조체 디코드 없는 lazy 접근)

IG datasource는 컬럼형(필드별 바이트)입니다. node-agent는 이벤트마다 Go 구조체로 unmarshal하지 않고, 단일 타입 DatasourceEvent가 16개 이벤트 인터페이스를 모두 구현하며 getter 호출 시점에만 필드를 추출합니다(쓰지 않는 필드 비용 0).

// node-agent  pkg/utils/datasource_event.go (DatasourceEvent 타입 + getFieldAccessor)
type DatasourceEvent struct { Data datasource.Data; Datasource datasource.DataSource; EventType EventType; ... }
var _ ExecEvent = (*DatasourceEvent)(nil)   // DNSEvent/NetworkEvent/... 동일 타입이 전부 구현
func (e *DatasourceEvent) GetContainerID() string {
    v,_ := e.getFieldAccessor("runtime.containerId").String(e.Data); return v }   // "proc.pid","proc.comm" 동일

operator 우선순위로 파싱 순서를 제어합니다: custom parse operator(prio 0/1) 가 먼저 raw를 가공(예: 실행 인자의 NUL 구분 바이트를 ArgsSeparator(non-breaking space)로 join), sink operator(prio 50000) 가 나중에 datasource.Subscribe로 가공된 데이터를 node-agent 콜백에 push합니다(execoperator.go).

3.4 자체 eBPF gadget — 시그니처 기반 위협 탐지

IG 표준 gadget 외에, node-agent는 보안 특화 eBPF를 pkg/ebpf/gadgets/에 직접 만들어 같은 image-based 포맷으로 빌드합니다. 핵심은 "포트/이름이 아니라 행위·바이트 패턴"으로 탐지한다는 점입니다.

gadget 후킹 탐지 로직
randomx FPU 비활성화 tracepoint MXCSR 반올림모드(RC 비트 0x6000)가 비표준값이면 RandomX(모네로 채굴) 의심. mntns별 5초/5회 디바운싱 (randomx/program.bpf.c:152,29~47)
ssh SEC("socket1") 소켓필터 TCP payload 첫 8바이트가 SSH-2.0- 배너인지 (포트 무관) (ssh/program.bpf.c:132)
ptrace ptrace tracepoint 전체가 아니라 PTRACE_SETREGS/POKETEXT/POKEDATA(메모리·레지스터 변조=코드 인젝션)만 (ptrace/program.bpf.c:46~61)
kmod init_module/finit_module 커널 모듈 적재 탐지, finit은 fd→경로 해석 (kmod/program.bpf.c:40,76)

공통 헬퍼 has_upper_layer()는 overlayfs upper layer 여부로 컨테이너 내에 새로 드롭된 바이너리(drift/fileless) 를 판정합니다(randomx/upper_layer.h:9~32).

3.5 이벤트 파이프라인 — 큐 → enrich → 핸들러 라우팅

flowchart LR
  cb["tracer sink 콜백<br/>(필터: error/qr/comm)"] --> q["OrderedEventQueue<br/>(timestamp min-heap)"]
  q -->|"50ms 틱 · full-alert"| pb["processQueueBatch"]
  pb --> en["EventEnricher<br/>process tree enrich"]
  en --> wp["worker pool<br/>(ants)"]
  wp --> ph["EventHandlerFactory.ProcessEvent<br/>container lookup + IgnoreContainer + dedup"]
  ph --> prof["containerProfile<br/>(학습)"]
  ph --> rule["ruleManager<br/>(탐지)"]
  ph --> etc["malware / dns / networkStream / metrics"]
  • 이벤트는 timestamp 우선순위 큐로 시간순 재정렬 후 50ms 배치로 처리(container_watcher.go, ordered_event_queue.go).
  • ProcessEvent는 컨테이너ID로 K8s 메타를 조회(없으면 drop), IgnoreContainer(kubescape 자기자신 등) 필터, dedup 후 EventType별 핸들러로 라우팅(event_handler_factory.go).
  • 라우팅 예: Execve → [containerProfile, ruleManager, malware, metrics, rulePolicy], Dns → [dnsManager, ruleManager, networkStream, metrics].

컨테이너→Pod 매핑이 전제: 이 라우팅은 이벤트의 컨테이너ID를 Pod/namespace로 해석할 수 있어야 동작합니다. 이 해석은 IG container-collection이 CRI 소켓으로 수행하는데, colima k3s에서는 소켓 경로 불일치로 막혔다가 해결했습니다(§6).

3.6 학습 — ApplicationProfile baseline lifecycle

컨테이너가 시작되면 node-agent가 "sniffing(학습) 윈도우" 동안 행위를 관측해 컨테이너별 ContainerProfile로 누적하고, 학습이 끝나면 status=completed로 굳혀 워크로드 단위 ApplicationProfile이 됩니다.

flowchart TB
  ev["실행/파일/cap/syscall/net/http 이벤트"] -->|"Report*"| cd["containerData<br/>surface별 set/map dedup 누적"]
  cd -->|"UpdateDataTicker: initialDelay 2m → updateDataPeriod"| save["saveProfile<br/>ContainerProfileSpec 직렬화"]
  save -->|"증분 delta, 저장 후 emptyEvents"| q["persistent queue"]
  q --> stg[("storage")]
  timer["maxSniffingTime 도달 / 컨테이너 종료"] -->|"status=Completed 최종 save"| save
  stg -->|"reconciler refresh 1m"| cache["containerprofilecache<br/>Completed·TooLarge만 accept"]
  cache -->|"rule 요구 surface만 투영"| proj["ProjectedContainerProfile<br/>= rule 평가용 정상 baseline"]

저장되는 baseline(ContainerProfileSpec, container_operations.go:193~211): Capabilities, Execs{Path,Args}, Opens{Path,Flags}, Syscalls, Endpoints(HTTP), PolicyByRuleId, IdentifiedCallStacks, Egress/Ingress(NetworkNeighbor), SeccompProfile.

핵심 게이트: 소비 측 캐시는 Completed/TooLarge 상태만 "확정 baseline"으로 accept합니다(containerprofilecache.go:704~713). 학습 중(ready)이면 reject — 이게 "정상 baseline 확정"의 분기점입니다.

container=worker  execs=[/bin/busybox, /usr/bin/curl]  caps=[CAP_SETGID, CAP_SETUID]  opens=57
container=app     execs=[]  (nginx는 학습 중 자식 프로세스를 실행하지 않음)  syscalls=0  opens=1

3.7 탐지 — CEL rule engine

enriched 이벤트 1건이 들어오면, 워크로드에 바인딩된 룰들을 (저렴→비쌈) 게이트로 통과시킵니다.

flowchart TD
  ev["enriched event"] --> rules["ListRulesForPod(ns,pod)<br/>RuntimeRuleAlertBinding 기반"]
  rules --> g1{"enabled & context &<br/>(profile Required면 존재?)"}
  g1 -->|no| skip1[suppress]
  g1 -->|yes| pf{"Prefilter.ShouldSkip?<br/>(path/comm/port 저비용)"}
  pf -->|yes| skip2[prefiltered]
  pf -->|no| pol{"policy allowlist<br/>(PolicyByRuleId)?"}
  pol -->|yes| skip3[suppress]
  pol -->|no| cel["CEL 평가<br/>예: !ap.was_executed(cid, exePath)"]
  cel --> apc[("ProjectedContainerProfile<br/>Execs/Syscalls/...")]
  cel --> sa{모든 표현식 true?}
  sa -->|no| skip4[no alert]
  sa -->|yes| cd{"ShouldCooldown?"}
  cd -->|no| send["CreateRuleFailure → exporter.SendRuleAlert"]
  • 룰은 코드 하드코딩이 아니라 RuntimeRule CRD에서 동적 로딩됩니다(RulesWatcherRuleCreator.SyncRules). R0001/R0003 같은 ID는 소스에 없고 CRD에 정의됩니다.
  • 대표 판정 helper ap.was_executed(containerID, exePath): baseline Execs.Values(정확) + Execs.Patterns(dynamic 경로) + PodSpec command/lifecycle에 있으면 true, 없으면 false. 룰은 보통 !ap.was_executed(...)baseline에 없으면 true=alert (cel/libraries/applicationprofile/exec.go:15~56).
  • 프로파일이 아직 없으면(ProfileNotAvailableErr) 결과를 false로 변환해 alert하지 않음 → 학습 미완성 시 오탐 방지.
  • 중복 억제 3계층: RulePolicy allowlist(CEL 이전) → RuleCooldown(count 기반) → alertLogDedup(OTEL 로그 60s).
  • v1 한계: wasExecutedWithArgs는 args를 실제 매칭하지 않음(타입만 검증, wasExecuted와 동치). flags/args/methods/ports 복합키 미지원.

3.8 alert export

판정된 alert는 CreateRuleFailure로 메타데이터(룰ID/이름, InfectedPID, 실행 path/args, process tree comm/pid/ppid/cmdline, 컨테이너/Pod/노드, 프로파일 상태)를 채워 exporter.SendRuleAlert로 내보냅니다. exporter 종류: stdout(기본), AlertManager, syslog, HTTP. 이 환경은 stdoutExporter: true라 alert가 node-agent 로그로 나옵니다.

3.9  라이브 eBPF 이벤트와 런타임 alert

(a) IG trace_exec (라이브 eBPF, K8s enrich) — standalone Inspektor Gadget으로 18초간 794건 캡처. 모든 이벤트가 namespace/pod/container로 enrich됩니다.

K8S.NAMESPACE  K8S.PODNAME                  K8S.CONTAINERNAME  COMM   ARGS
runtime-demo   victim-app-f87cbcfcd-msctl   app                cat    /bin/cat /etc/passwd     ← 유발한 실행
coroot-poc     load-gen-648f569fcb-9hwsq    curl               curl   /usr/bin/curl ...
traffic-gen    flaky-client-784686bddb-q9d7b client             wget   /bin/wget ...

네임스페이스 분포(794건): coroot-poc 662, traffic-gen 90, coroot-scenario 18, runtime-demo 16, gadget 8.

(b) 런타임 alert (baseline 대비 anomaly) — 학습 완료(completed) 후 nginx app 컨테이너에서 baseline에 없는 프로세스(/bin/sh -c whoami / wget / cat /etc/shadow)를 실행 → 25건의 R0003 alert(scenario-data/runtime_alerts.jsonl). app의 syscall baseline이 비어 있었으므로, 주입된 프로세스의 모든 syscall이 "unexpected"로 발화:

{"RuleID":"R0003","BaseRuntimeMetadata":{"alertName":"Syscalls Anomalies in container",
 "arguments":{"message":"Unexpected system call detected: execve with PID 0","syscall":"execve"},
 "severity":1,"profileMetadata":{"status":"completed","completion":"partial","failOnProfile":true}},
 "RuntimeK8sDetails":{"clusterName":"colima","namespace":"runtime-demo","podName":"victim-app-f87cbcfcd-msctl",
 "containerName":"app","image":"nginx:1.27-alpine","workloadKind":"Deployment","workloadName":"victim-app"},
 "RuntimeProcessDetails":{"processTree":{"comm":"app"}}, "message":"Unexpected system call detected: execve with PID 0"}

탐지된 distinct syscall(25종): execve, connect, socket, openat, mmap, mprotect, clone, wait4, write, read, getuid, ioctl, recvfrom, nanosleep ...


4. 로직 B — AI Integration: MCP server

4.1 구조

MCP 서버는 kubescape CLI의 서브커맨드(kubescape mcpserver)이며, 코드는 cmd/mcpserver/(3파일)로 작고 자기완결적입니다.

flowchart LR
  ai["AI assistant<br/>Claude·Cursor·codex"] <-->|"stdio JSON-RPC"| mcp["Kubescape MCP Server v0.0.1<br/>mark3labs/mcp-go, ServeStdio"]
  mcp -->|"spdxv1beta1 client (KUBECONFIG)"| api[("kube-apiserver<br/>aggregation")]
  api --> stg[("storage pod<br/>spdx.softwarecomposition CRD")]
// kubescape  cmd/mcpserver/mcpserver.go:515~533
s := server.NewMCPServer("Kubescape MCP Server", "0.0.1", server.WithToolCapabilities(false), server.WithRecovery())
ksServer := &KubescapeMcpserver{ s: s, ksClient: client }
createVulnerabilityToolsAndResources(ksServer)
createConfigurationsToolsAndResources(ksServer)
server.ServeStdio(s)   // transport = stdio

연결은 표준 client-go로 KUBECONFIG(없으면 in-cluster)에서 만들어, aggregated API를 통해 CRD를 읽습니다. 즉 로컬에서 kubescape mcpserver를 띄우면 현재 kubeconfig context의 클러스터에 붙습니다(ksinit/ksinit.go:14~40).

4.2 노출하는 tool / resource

tool 입력 백엔드 CRD
list_vulnerability_manifests namespace?, level(image/workload/both) VulnerabilityManifest (label로 image/workload 구분)
list_vulnerabilities_in_manifest manifest_name VulnerabilityManifest.Spec.Payload.Matches[].Vulnerability
list_vulnerability_matches_for_cve manifest_name, cve_id 위에서 cve_id 필터
list_configuration_security_scan_manifests namespace? WorkloadConfigurationScan
get_configuration_security_scan_manifest manifest_name WorkloadConfigurationScan 상세

resource template 2종: kubescape://vulnerability-manifests/{ns}/{name}[/cve_list|/cve_details/{cve}], kubescape://configuration-manifests/{ns}/{name}.

4.3 데이터 출처와 함의

MCP 서버는 읽기 전용이며, 데이터는 다른 컴포넌트(kubevuln=취약점 스캔, kubescape=설정 스캔)가 storage에 적재한 CRD입니다. MCP 서버 자신은 스캔하지 않고 노출만 합니다.

4.4 라이브 MCP 세션

kubescape mcpserver를 stdio로 띄우고 MCP JSON-RPC 클라이언트(scenario-data/mcp_client.py)로 구동한 실제 세션(scenario-data/mcp_transcript.json):

initialize → serverInfo: {"name":"Kubescape MCP Server","version":"0.0.1"}, capabilities:{resources,tools}
tools/list → 5 tools (위 표)
resources/templates/list → 2 templates (kubescape://...)
tools/call list_vulnerability_manifests {level:both} → 실제 8개 manifest:
  - docker.io/library/busybox:1.36
  - docker.io/bitnamilegacy/clickhouse:24.12.3
  - ghcr.io/coroot/coroot-cluster-agent:1.4.0
  - ghcr.io/coroot/coroot-node-agent:1.25.3
  - docker.io/jimmidyson/configmap-reload:v0.5.0  ...

AI 어시스턴트가 "이 클러스터 취약점 알려줘"라고 물으면 tools/call → aggregated API → CRD → JSON으로 위 데이터가 그대로 흐릅니다.


5. 연계 — Hubble(정책 plane) vs node-agent(위협 plane), coroot 대비

같은 "eBPF 네트워크 관측"이라도 세 시스템의 관측 층위와 목적이 직교합니다.

Cilium Hubble Kubescape node-agent coroot eBPF
관측 plane CNI 데이터패스(tc/xdp) + L7 프록시 syscall/소켓 IG gadget syscall(connect/read/write)
1차 단위 Flow(L3/L4/L7 + verdict) 위협 이벤트(실행/ptrace/dns/...) L7 호출 span
L7 원천 Envoy/dnsproxy access-log(메타) eBPF raw 페이로드 직접 파싱 소켓 페이로드 앞부분 파싱
verdict/정책 있음(FORWARDED/DROPPED + NetworkPolicy) 없음(baseline 이상탐지) 없음
주체 식별 Endpoint(security identity/labels) 프로세스(PID/comm/ancestry) 서비스(cgroup→workload)
목적 네트워크 정책·가시성 런타임 위협탐지 관측·서비스맵·SLO
전제 Cilium CNI(+L7 프록시) CNI 무관, 런타임만 CNI 무관
  • Hubble: "파드 A→B가 정책상 허용/차단됐나? 클러스터 east-west 흐름은?" — Flow는 pkg/hubble/parser/threefour가 datapath의 Drop/Trace/PolicyVerdict notify를 파싱해 만들고, L7은 프록시 access-log에서 옵니다(parser/seven/parser.go:91). 멀티노드는 Hubble Relay가 fan-out 집계.
  • node-agent: "어떤 프로세스가 비정상 실행/ptrace/페이로드를 냈나?" — Hubble HTTP가 Envoy 메타데이터 수준인 반면, node-agent http tracer는 raw 바이트를 http.ReadRequest로 직접 복원해 페이로드까지 봅니다(httpparse.go).
  • 겹치는 네트워크/DNS 영역에서도 의미가 갈립니다: Hubble=정책 plane, node-agent=위협 plane. 실전에선 "Hubble로 허용된 연결인지 + node-agent로 그 연결을 만든 프로세스가 정상인지"로 상호 보완합니다.

5.1  Hubble + 같은 연결의 두 plane 대조

이 비교를 코드뿐 아니라 테스트 환경에서 확인하기 위해, colima k3s의 CNI를 flannel → Cilium v1.19.3(kube-proxy 대체 모드)으로 바꾸고 Hubble을 켰습니다(scenario-data-cilium/). 데모: server(nginx) + client(app=client) + attacker(app=attacker), 그리고 CiliumNetworkPolicy L7으로 app=client만 server에 HTTP GET / 허용:

# policy-l7.yaml (발췌)
kind: CiliumNetworkPolicy
spec:
  endpointSelector: { matchLabels: { app: server } }
  ingress:
  - fromEndpoints: [{ matchLabels: { app: client } }]
    toPorts:
    - ports: [{ port: "80", protocol: TCP }]
      rules: { http: [{ method: "GET", path: "/" }] }

트래픽 결과: client→server HTTP 200, attacker→server 000(timeout, 차단).

(a) Hubble (정책·연결 plane) — verdict 분포 FORWARDED 497 / DROPPED 34 (hubble_all.txt, 600 flow 기준):

# 정책 차단 (attacker)
attacker(app=attacker) <> server:80  Policy denied DROPPED (TCP Flags: SYN)        drop_reason=POLICY_DENIED
# 허용 + L7 (client) — Envoy 프록시 경유(to-proxy = REDIRECTED)
client → server:80  to-proxy FORWARDED (SYN)
client → server:80  http-request  FORWARDED (HTTP/1.1 GET http://server.hubble-demo.svc.cluster.local/)
client ← server:80  http-response FORWARDED (HTTP/1.1 200 0ms (GET ...))

즉 Hubble은 verdict(FORWARDED/DROPPED) + 정책 사유(POLICY_DENIED) + L7 메타데이터(method/url/status/latency) 를 endpoint(파드/라벨) 단위로 보여줍니다. L7은 §5 표대로 Envoy access-log에서 옵니다(코드 parser/seven/parser.go:91 ↔ 실측 to-proxy/http-request 일치).

(b) node-agent / IG (프로세스·위협 plane) — 같은 연결을 IG trace_tcp로 본 모습:

hubble-demo/client/curl    COMM=curl PID=10030 → server:80  connect / close
hubble-demo/attacker/curl  COMM=curl PID=10104 → server:80  connect

같은 통신을 프로세스 정체(curl, PID) + 소켓으로 보지만 verdict·정책은 없습니다.

→ 핵심 대조 (같은 attacker→server 연결): IG는 *"attacker 파드의 curl(PID 10104) 프로세스가 connect를 시도했다"(syscall 발생)를 보고, Hubble은 *"그 패킷이 NetworkPolicy로 DROP됐다"(POLICY_DENIED)를 봅니다. 둘 다 eBPF지만 한쪽은 누가(프로세스), 한쪽은 정책상 어떻게(verdict) 를 답해 상호 보완합니다. (Hubble 소스 분석은 codex-notes/notes_C1_hubble.md.)


부록 A. 코드 위치

단계 파일 핵심
gadget 실행 node-agent pkg/containerwatcher/v2/tracers/exec.go gadgetcontext.New, RunGadget, WithOrasReadonlyTarget
로컬 OCI store .../tracers/tracer_factory.go:69 orasoci.NewFromTar("tracers.tar")
이벤트 lazy 접근 node-agent pkg/utils/datasource_event.go DatasourceEvent, getFieldAccessor
자체 eBPF node-agent pkg/ebpf/gadgets/{randomx,ssh,ptrace,kmod}/program.bpf.c MXCSR/SSH배너/PTRACE_POKE 시그니처
이벤트 파이프라인 .../v2/container_watcher.go, ordered_event_queue.go 큐→enrich→worker pool
학습 node-agent pkg/containerprofilemanager/v1/*, objectcache/containerprofilecache/* ContainerProfile, isTerminalCPStatus
탐지 node-agent pkg/rulemanager/*, .../cel/* ReportEnrichedEvent, ap.was_executed
alert node-agent pkg/exporters/* SendRuleAlert
MCP kubescape cmd/mcpserver/mcpserver.go, pkg/ksinit/ksinit.go ServeStdio, 5 tool, aggregated API
Hubble cilium pkg/hubble/parser/{threefour,seven}/parser.go Flow 파싱, verdict