근묵자흑

Learning eBPF: 개인 프로젝트 - Datadog + KubeScent 본문

k8s/learning-ebpf

Learning eBPF: 개인 프로젝트 - Datadog + KubeScent

Luuuuu 2026. 10. 24. 18:57

KubeScent는 Kubernetes의 권한·마운트 설정과 실제 동작 기록을 연결해, 가능한 접근 경로와 관측된 행동을 함께 조회하는 보안 분석 도구입니다.

 

이번 구현에서는 Kubernetes 구성을 읽어 공격 후보 그래프를 만들고, Datadog 보안 이벤트를 graphdb Neo4j에 연결했습니다. 

1. 개요와 전체 그래프 모델

그래프에서 원은 컨테이너·파일·계정 같은 대상의 종류이고, 화살표는 대상 사이에 저장하는 관계의 방향입니다. Neo4j는 이런 정점과 관계를 저장하고 조회하는 데이터베이스입니다. 공격 후보 경로는 설정상 검토할 접근 가능성을 뜻하며, 실제 침해 여부는 관측 기록과 함께 조사해야 합니다.

 

 

Container → Volume → Identity 순서로 조회하면 컨테이너에 마운트된 토큰과 해당 계정을 찾습니다. Identity → PermissionSet은 계정에 부여된 권한입니다. PermissionSet → Pod의 POD_EXEC는 그 권한으로 명령 실행을 검토할 Pod를 연결합니다. Pod → Container는 Pod에 포함된 컨테이너를 나타냅니다.

 

Node는 Pod가 배치된 호스트이고, Endpoint는 네트워크 진입점입니다. CE_PRIV_MOUNT는 privileged 설정이 있는 컨테이너에서 노드로 연결합니다. ENDPOINT_EXPLOIT는 선언된 포트나 EndpointSlice의 포트 대응으로 연결합니다.

 

EvaluationRun에는 평가 입력과 모델 버전을 기록합니다. GENERATED는 평가 실행과 생성된 자산 정점을 연결합니다. 자산 7종 중 그림에는 Container만 표시했습니다. SNAPSHOT_OF는 이 Container를 실제 실행 구간인 EntityInterval에 연결합니다.

 

Evidence는 Datadog에서 받은 동작 기록입니다. OBSERVED_IN으로 컨테이너 실행 구간에 연결하고, ACTOR로 동작을 수행한 Process에 연결합니다. Process에서 File이나 IPAddress를 조회하면 접근 대상을 확인할 수 있습니다. 그림의 FILE_OPEN / EXECUTED_FILE은 파일 열기와 파일 실행이라는 두 관계를 묶어 표시했습니다.

모델의 부분 역할 현재 구현 상태
EvaluationRun → 후보 자산 특정 입력·모델 버전에서 가능한 경로를 계산합니다. 자산 정점 7종·후보 관계 9종을 구현했습니다.
Container → EntityInterval ← Evidence 구성의 컨테이너와 관측된 실행을 연결합니다. 전체 runtime ID로 연결합니다. 사건 당시 구성의 동일성은 미검증입니다.
Evidence → Process → File / IPAddress 실행·파일 열기·연결의 출처와 결과를 보관합니다. exec/open/connect를 지원합니다. Process·File·IPAddress는 증거 범위로 식별합니다.
ObjectState / CollectionCoverage 구성 변경 이력과 센서·정책의 관측 공백을 설명합니다. 확장 예정입니다. 현재 DB에 이 두 정점 모델을 구현하지 않았습니다.

 

2. 개발 배경과 기본 개념 (Background)

Kubernetes에서 애플리케이션은 컨테이너로 실행되고, 하나 이상의 컨테이너를 Pod로 묶어 관리합니다. 보안 점검에서는 애플리케이션이 업무에 필요한 파일과 권한만 사용하는지 확인합니다. 예를 들어 설정을 조회하는 프로그램에 다른 Pod 안에서 명령까지 실행할 권한이 필요한지 검토할 수 있습니다.

 

ServiceAccount는 Pod의 프로그램이 Kubernetes API를 이용할 때 사용하는 계정입니다. 토큰은 이 계정으로 인증할 때 제시하는 자격 증명입니다. API 서버는 인증으로 요청 주체를 확인하고, 권한 검사로 그 주체가 요청한 작업을 수행해도 되는지 판단합니다. 따라서 토큰을 사용할 수 있다는 사실만으로 모든 관리 작업이 허용되지는 않습니다.

 

컨테이너 안에서 ServiceAccount 토큰 파일이 열렸다고 가정해 보겠습니다. 파일 접근 기록으로는 어떤 프로세스가 언제 접근했는지 확인할 수 있습니다. 그 토큰이 어느 ServiceAccount와 연결되고, 그 계정이 어떤 Pod에 접근할 수 있는지는 Kubernetes 구성을 함께 봐야 합니다.

 

구성만으로는 실제 행동 여부를 알 수 없으므로 두 데이터를 함께 조회합니다.

용어 이 실습에서의 의미
Pod / Container Pod는 Kubernetes의 실행 관리 단위이고, Container는 그 안에서 프로그램을 실행하는 환경입니다.
namespace Pod·계정 등의 리소스를 나눠 관리하는 논리적 범위입니다. 같은 이름도 namespace가 다르면 다른 대상일 수 있습니다.
Volume / mount Volume은 컨테이너에 제공할 파일·저장 공간이고, mount는 이를 컨테이너 내부 경로에서 사용할 수 있게 연결하는 설정입니다.
RBAC 계정에 역할을 연결해 어떤 리소스에 어떤 작업을 허용할지 정하는 권한 관리 방식입니다.
프로세스 / 이벤트 프로세스는 실행 중인 프로그램입니다. 이벤트는 프로그램 실행·파일 열기처럼 관측된 동작 한 건을 표현한 기록입니다.
candidate 구성에서 계산한 공격 후보. 실제 공격 발생 기록은 아닙니다.
Evidence Datadog에서 받은 이벤트를 필요한 필드로 정규화한 증거 레코드입니다.
EvaluationRun 입력과 모델 버전에 연결된 평가 실행입니다. 공개 완료된 실행을 조회합니다.
EntityInterval 전체 runtime container ID와 Pod UID 등으로 구분하는 컨테이너 실행 구간입니다.
Identity / PermissionSet ServiceAccount 같은 주체와 바인딩을 통해 연결되는 권한 묶음입니다.
unresolved 증거를 특정 실행 구간에 유일하게 연결할 수 없는 상태입니다.
coverage 어떤 정책과 센서가 어느 구간을 관측했는지에 관한 정보입니다. 기간별 관리 기능은 아직 구현하지 않았습니다.

동작 원리 (Intuition Before Details)

KubeScent는 구성에서 계산한 접근 경로에 관측 기록을 연결해 조사할 대상을 좁힙니다.

 

건물의 도면에 출입 가능한 방과 문이 표시돼 있고, 별도로 출입 기록이 있다고 생각하면 됩니다. 도면으로 이동 가능성을 조사하고, 기록으로 특정 행동을 확인합니다. 두 자료를 연결할 때는 같은 방인지뿐 아니라 같은 시기의 방인지도 확인해야 합니다. Pod 이름이 같아도 재시작한 컨테이너는 다른 실행일 수 있습니다.

 

3. 전체 아키텍처와 실습 환경

flowchart LR
  K[Kubernetes API] --> C[KubeScent 수집·후보 계산]
  D[Datadog 보안 이벤트] --> L[실습용 local intake]
  L --> E[KubeScent 정규화·실행 식별]
  C --> N[(Neo4j)]
  E --> N
  N --> I[사건 → 관련 후보 경로]
  N --> A[후보 자산 → 관측된 행동]

 

4. 수집하는 정보와 해석 범위

두 입력은 별도로 수집한 뒤 컨테이너 실행 식별자로 연결합니다.

입력 이번 구현에서 사용하는 정보 확인할 수 있는 내용
Kubernetes API Pod, 컨테이너 마운트, ServiceAccount, 역할·바인딩, 노드와 엔드포인트 구성 어떤 계정과 파일이 연결돼 있고 어떤 접근 후보가 있는지
Datadog exec 실행 시각, PID, 컨테이너 ID, 실행 파일 정보 특정 프로그램 실행이 보고됐는지
Datadog open 파일 경로, inode, mount ID, 동작 결과 해당 파일의 열기가 보고됐는지
Datadog connect 대상 IP, 포트, 프로토콜, 동작 결과 어떤 대상에 대한 연결 동작이 보고됐는지

5. 시나리오별 그래프 조회

토큰 볼륨이 연결된 컨테이너 확인

dd-lab의 lab-client와 lab-server를 비교했습니다. 확인할 대상은 컨테이너에 계정 토큰이 파일로 제공되는 경로입니다. projected volume은 Kubernetes가 제공하는 데이터를 파일 형태로 컨테이너에 보여주는 볼륨입니다. 이번에는 그중 ServiceAccount 토큰을 포함한 볼륨을 확인합니다. 그래프의 Identity는 이 토큰에 해당하는 계정을 나타냅니다.

 

Pod에 볼륨을 정의하는 것과 개별 컨테이너에 마운트하는 것은 별도 설정입니다. 같은 Pod 안에서도 컨테이너마다 보이는 파일이 다를 수 있습니다. 모델은 각 컨테이너의 volumeMounts를 보고 VOLUME_DISCOVER를 연결하고, 볼륨에 serviceAccountToken 원천이 있으면 TOKEN_STEAL 후보를 만듭니다.

 

flowchart LR
  C["Container: client"] -->|VOLUME_DISCOVER| V["Volume: projected 토큰"]
  V -->|TOKEN_STEAL| I["Identity: lab-client"]
  S["Container: server — 해당 토큰 경로 없음"]
대상 해당 토큰 후보 경로 수 해석
lab-client/client 1 마운트된 projected 토큰을 거치는 후보가 있습니다.
lab-server/server 0 이 평가에서 해당 유형의 경로를 찾지 못했습니다.

 

TOKEN_STEAL은 토큰을 얻는 데 이용할 수 있는 후보 관계의 이름입니다. 토큰 파일은 애플리케이션의 정상적인 API 호출에도 필요할 수 있으므로, 이 관계만으로 설정 오류나 공격으로 판정하지 않습니다. 담당자는 해당 프로그램이 Kubernetes API를 사용하는지, 연결된 계정의 권한이 업무에 필요한 범위인지 확인해야 합니다.

 

경로 0개가 서버의 모든 인증 수단 부재나 안전성을 보장하지는 않습니다. 현재 모델이 다루지 않는 Secret 기반 자격 증명이나 별도 파일도 있을 수 있습니다. 실무에서는 불필요한 토큰 마운트를 제거한 뒤 동일 입력 범위의 후보 경로가 사라지는지를 검증하는 데 이 조회를 사용할 수 있습니다.


Container name=client, pod=lab-client, namespace=dd-lab, container_kind=regular 토큰 마운트를 점검할 컨테이너
Volume name=kube-api-access-6gvp6, pod=lab-client, namespace=dd-lab, volume_type=projected client에 연결된 projected 볼륨
Identity name=lab-client, namespace=dd-lab, identity_kind=ServiceAccount 토큰의 계정

Container(client) → VOLUME_DISCOVER → Volume(kube-api-access-6gvp6) → TOKEN_STEAL → Identity(lab-client) 순서입니다. Container의 pod와 Identity의 name은 둘 다 lab-client지만 서로 다른 종류의 대상을 나타냅니다. 이름이 같다는 이유로 연결하지 않습니다.

 

{
  "Container": {
    "key": "573d8c1f2b9e0e6ee2d356b97dcc29da46887942df36a0446c2e63be8aba3f6d",
    "run_key": "d000524bf8ee2836c33211e15289ebfd430d81e8e6308a18c2e9bc7e75db490d",
    "name": "client",
    "pod": "lab-client",
    "namespace": "dd-lab",
    "pod_uid": "8f917d52-2413-42ef-a7c6-239fa605f4a7",
    "container_kind": "regular"
  },
  "Volume": {
    "key": "5ab720b69d642489997420ee86b0f6d9389319f3b2205649de98390a518b9098",
    "name": "kube-api-access-6gvp6",
    "pod": "lab-client",
    "namespace": "dd-lab",
    "volume_type": "projected"
  },
  "Identity": {
    "key": "e98682a5940f954daa8ec6d61b35ff5b1204ab4ab1aab449ed39a2952d709e50",
    "name": "lab-client",
    "namespace": "dd-lab",
    "identity_kind": "ServiceAccount"
  }
}

 파일 접근 기록에서 관련 컨테이너와 계정 조회

파일을 연 프로그램과 컨테이너, 연결된 계정을 확인해 배포·점검 중 발생한 동작인지 조사합니다.

study_token 이벤트 한 건을 시작점으로 조사했습니다. 이벤트 발생 시각은 2026-10-23T18:52:08.28582884Z, 한국 시각으로 10월 24일 03:52:08입니다. PID는 운영체제가 프로세스를 구분하는 번호이며 이 이벤트에서는 2021699였습니다. inode는 파일시스템 안에서 파일을 식별하는 데 쓰는 번호로 946이 기록됐습니다. inode 번호는 시스템 전체에서 유일하지 않으므로 파일 경로와 mount 범위도 함께 봐야 합니다. 동작 결과(outcome)는 Success였습니다.

 

아래 두 도식은 같은 EntityInterval을 공유합니다. 첫 도식은 관측된 행동, 두 번째는 해당 실행에 연결된 구성상 후보입니다.

flowchart LR
  E["Evidence: study_token"] -->|ACTOR| P["Process: PID 2021699"]
  P -->|FILE_OPEN| F["File: token · inode 946"]
  E -->|OBSERVED_IN| R["EntityInterval: client 실행"]
flowchart LR
  C["Container: client"] -->|SNAPSHOT_OF| R["EntityInterval: 위와 동일한 client 실행"]
  C -->|VOLUME_DISCOVER| V["Volume: projected 토큰"]
  V -->|TOKEN_STEAL| I["Identity: lab-client"]

 

OBSERVED_IN과 SNAPSHOT_OF는 모두 실행 구간을 향합니다. 조회할 때 선을 역방향으로 탐색할 수 있어도 저장된 관계의 방향은 바꾸지 않습니다.

 

FILE_OPEN의 Success는 파일 열기가 성공했다는 뜻입니다. 파일 전체를 읽었는지, 토큰으로 API 요청에 성공했는지는 이 기록만으로 알 수 없습니다.

 

Evidence event_type=open, rule_id=study_token, event_time=2026-10-23T18:52:08.28582884Z, outcome=Success 무엇을 언제 관측했고 결과가 무엇인지
Evidence key=2777a341…f2b8, source=datadog, agent_version=7.84.0 기록의 식별자와 출처
Process pid=2021699, exec_time=2026-10-23T18:52:08.285630382Z 이 기록에서 보고한 프로세스
File inode=946, mount_id=3156, path는 아래 전체 경로 열린 파일의 관측 정보
EntityInterval container_name=client, runtime=containerd, started_at=2026-10-17T09:39:42Z 이벤트가 발생한 컨테이너 실행
Container name=client, pod=lab-client, namespace=dd-lab, run_key=d000524b…490d 그 실행에 연결된 평가별 구성
{
  "Evidence": {
    "key": "2777a341547eb7702013bf9886fab4b55184e45c3a45c0e63eadee1214c9f2b8",
    "event_type": "open",
    "event_time": "2026-10-23T18:52:08.28582884Z",
    "container_id": "b5bc4e17f43e0f11e10a9dd20df6832483ab3388fa4c7604b82a0aa2ecf0207a",
    "rule_id": "study_token",
    "source": "datadog",
    "agent_version": "7.84.0",
    "outcome": "Success",
    "resolution_status": "exact_runtime_identity_snapshot_time_unverified"
  },
  "Process": {
    "pid": 2021699,
    "fork_time": "2026-10-23T18:52:08.285425089Z",
    "exec_time": "2026-10-23T18:52:08.285630382Z",
    "identity_status": "evidence_scoped_boot_id_missing"
  },
  "File": {
    "path": "/run/secrets/kubernetes.io/serviceaccount/..2026_10_23_18_05_16.1518416727/token",
    "inode": 946,
    "mount_id": 3156
  },
  "EntityInterval": {
    "key": "078e177a5d2c2e221a2a16229fef7cce0a4b9f2d7e174c0e3129a42c9c5e90f5",
    "container_name": "client",
    "pod_uid": "8f917d52-2413-42ef-a7c6-239fa605f4a7",
    "container_id": "b5bc4e17f43e0f11e10a9dd20df6832483ab3388fa4c7604b82a0aa2ecf0207a",
    "runtime": "containerd",
    "started_at": "2026-10-17T09:39:42Z",
    "node": "datadog-study-control-plane"
  },
  "Container": {
    "key": "573d8c1f2b9e0e6ee2d356b97dcc29da46887942df36a0446c2e63be8aba3f6d",
    "run_key": "d000524bf8ee2836c33211e15289ebfd430d81e8e6308a18c2e9bc7e75db490d",
    "name": "client",
    "pod": "lab-client",
    "namespace": "dd-lab"
  }
}

Pod 명령 실행 권한과 Kubernetes 버전 비교

pods/exec는 실행 중인 Pod의 컨테이너에서 명령을 실행하는 API 기능입니다. 운영자가 점검할 때 쓰는 기능이지만, 불필요한 계정에 허용돼 있으면 그 계정으로 다른 워크로드에 명령을 실행할 가능성도 검토해야 합니다.

 

RBAC에서 Role 또는 ClusterRole은 허용 작업을 정의하고, 바인딩은 그 역할을 계정 등의 주체에 연결합니다. 역할 이름만으로는 허용 범위를 알 수 없습니다. namespace 범위, API group, resource와 subresource, verb, resourceNames, 바인딩 주체를 함께 확인해야 합니다. 예를 들어 pods 권한을 자동으로 pods/exec 권한으로 확대하지 않습니다.

 

이 검사는 해당 ServiceAccount와 표준 그룹을 지정한 kubectl auth can-i 판정 비교입니다. 

 

 

namespace-controller → PermissionSet → lab-client Pod → client Container를 조회했습니다. POD_EXEC는 이 버전과 구성에서 계산한 후보입니다.

flowchart LR
  I["Identity: namespace-controller"] -->|PERMISSION_DISCOVER| P["PermissionSet: 연결된 권한 묶음"]
  P -->|POD_EXEC| K["Pod: dd-lab/lab-client"]
  K -->|CONTAINER_ATTACH| C["Container: client"]


처음에는 create 권한만 확인해 Pod 명령 실행 후보를 계산했습니다. 실습한 Kubernetes 1.31에서는 WebSocket 요청의 get 권한도 고려해야 하므로 계산 조건을 보완했습니다. 그 결과 권한 묶음 2개에서 각각 Pod 12개로 이어지는 후보 관계가 추가돼, 전체 관계가 403개에서 427개로 늘었습니다. 클러스터 설정은 그대로였고 분석 조건이 달라진 결과입니다

Identity name=namespace-controller, namespace=kube-system, identity_kind=ServiceAccount 권한을 조사할 계정. 토큰 사례의 lab-client 계정과 다릅니다.
PermissionSet name=system:controller:namespace-controller, role_name=system:controller:namespace-controller, role_kind=ClusterRole, namespace="" 이 사례의 클러스터 범위 권한 묶음
PermissionSet rules_json 중 한 규칙: apiGroups=["*"], resources=["*"], verbs=["delete","deletecollection","get","list"] 이 1.31 모델에서 GET 후보 판정에 관련된 규칙. 전체 규칙 중 발췌입니다.
Pod name=lab-client, namespace=dd-lab, source_uid=8f917d52-2413-42ef-a7c6-239fa605f4a7 명령 실행 후보의 대상 Pod
Container name=client, pod=lab-client, namespace=dd-lab 대상 Pod에 포함된 컨테이너
Node name=datadog-study-control-plane, source_uid=6ae1cd1b-0b67-43c5-934c-5b49bc75fbf0 Pod가 배치된 Kubernetes 노드. 그래프의 일반 용어인 노드와 구분합니다.
{
  "Identity": {
    "key": "7c74ad4c8e0b5fa7feebe1b6d5b8b56b51dfc63b116dc170183faea94e392482",
    "name": "namespace-controller",
    "namespace": "kube-system",
    "identity_kind": "ServiceAccount"
  },
  "PermissionSet": {
    "key": "22716e71aaeec502971098ce78370fd5e4cbe0a0d50505c87e401e26c7cefea5",
    "name": "system:controller:namespace-controller",
    "role_name": "system:controller:namespace-controller",
    "role_kind": "ClusterRole",
    "namespace": "",
    "rules_json": "[{\"apiGroups\": [\"\"], \"resources\": [\"namespaces\"], \"verbs\": [\"delete\", \"get\", \"list\", \"watch\"]}, {\"apiGroups\": [\"\"], \"resources\": [\"namespaces/finalize\", \"namespaces/status\"], \"verbs\": [\"update\"]}, {\"apiGroups\": [\"*\"], \"resources\": [\"*\"], \"verbs\": [\"delete\", \"deletecollection\", \"get\", \"list\"]}]"
  },
  "Pod": {
    "key": "a9348078117de60c927dcb40940a240cfb1c5b6776a1b216761916ca23f52893",
    "name": "lab-client",
    "namespace": "dd-lab",
    "source_uid": "8f917d52-2413-42ef-a7c6-239fa605f4a7",
    "node": "datadog-study-control-plane"
  },
  "Container": {
    "key": "573d8c1f2b9e0e6ee2d356b97dcc29da46887942df36a0446c2e63be8aba3f6d",
    "name": "client",
    "pod": "lab-client",
    "namespace": "dd-lab",
    "node": "datadog-study-control-plane"
  },
  "Node": {
    "key": "ea49a321d26d7eb50478391f9d32f46a3af08ef175593186566ff183a02369a9",
    "name": "datadog-study-control-plane",
    "source_uid": "6ae1cd1b-0b67-43c5-934c-5b49bc75fbf0"
  },
  "POD_EXEC": {
    "key": "3b12ff3a9fc132954a50d8dc95ca4a11805c045c952ca9938006fa37e779cf7c",
    "source": "22716e71aaeec502971098ce78370fd5e4cbe0a0d50505c87e401e26c7cefea5",
    "target": "a9348078117de60c927dcb40940a240cfb1c5b6776a1b216761916ca23f52893",
    "semantic": "candidate",
    "condition": "RBAC_create_or_legacy_websocket_get; admission_and_other_authorizers_unmodeled"
  }
}