근묵자흑
Kubescape 런타임 보안(eBPF/Inspektor Gadget)과 MCP AI 연동 동작 방식 본문
대상 로직 A: Runtime Security — eBPF-based runtime monitoring via Inspektor Gadget (kubescape/node-agent)
대상 로직 B: AI Integration — MCP server for AI assistant integration (kubescape/kubescapecmd/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"]
- 룰은 코드 하드코딩이 아니라
RuntimeRuleCRD에서 동적 로딩됩니다(RulesWatcher→RuleCreator.SyncRules). R0001/R0003 같은 ID는 소스에 없고 CRD에 정의됩니다. - 대표 판정 helper
ap.was_executed(containerID, exePath): baselineExecs.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 |
'AIOps' 카테고리의 다른 글
| Kubescape MCP × LLM Agentic RCA (0) | 2026.06.06 |
|---|---|
| Coroot 코드로 읽는 LLM RCA 컨텍스트 — 한 인시던트가 Cloud 전송까지 가는 경로 (0) | 2026.05.24 |
| AIOps 스터디 6주차 — coroot의 데이터 흐름과 RCA 책임 분담 (0) | 2026.05.16 |
| AIOps 스터디 5주차 - RCA 평가지표 정리 (1) | 2026.05.10 |
| AIOps 스터디 5주차 - RCA (0) | 2026.05.09 |
