근묵자흑
Learning eBPF : KubeHound와 Datadog Agent 분석 본문
공격 경로와 런타임 증거를 연결하는 구조
Kubernetes 보안을 조사할 때는 현재 설정으로 가능한 행동과 실제 발생한 행동을 함께 봐야 합니다. KubeHound는 리소스·권한 관계를 공격 경로로 계산하고, Datadog Agent는 커널에서 관측한 행동에 문맥과 정책을 적용합니다. 이 글은 두 오픈소스의 내부 구조를 살펴보고, 결과를 어떤 식별자와 증거로 연결할 수 있는지 설명합니다.
| 오픈소스 | 분석 버전 | 역할 |
|---|---|---|
| KubeHound | v1.6.7 소스 | Kubernetes 설정과 권한을 수집해 가능한 접근 경로를 계산 |
| Datadog Agent | 7.84.0 소스 | Linux Runtime Security에서 프로세스 행동을 관측하고 규칙으로 선별. |
토큰 파일에 접근하면 왜 보안 문제가 될 수 있나요?
애플리케이션이 Kubernetes API를 사용할 때 토큰으로 인증합니다
Kubernetes는 Pod 단위로 애플리케이션을 실행하고, API를 통해 리소스 조회나 생성 같은 관리 요청을 받습니다. 애플리케이션이 사용자의 웹 요청을 처리하는 것과 Kubernetes API로 클러스터를 관리하는 것은 별개의 작업입니다. 따라서 모든 애플리케이션에 Kubernetes 관리 권한이 필요한 것은 아닙니다.
ServiceAccount는 Pod 안의 프로그램이 사용할 수 있는 Kubernetes 계정이고, 토큰은 그 계정으로 인증할 때 요청에 첨부하는 값입니다. 일반적인 자동 마운트 구성에서는 토큰을 컨테이너 안의 파일로 제공합니다. 이 실습의 경로는 /var/run/secrets/kubernetes.io/serviceaccount/token입니다.
프로그램이 API를 사용하려고 토큰 파일을 읽는 것은 정상적인 동작일 수 있습니다. API 접근이 필요 없다면 automountServiceAccountToken: false로 토큰 자동 제공을 끌 수 있습니다. ServiceAccount와 토큰 제공 방식
토큰이 유출되면 다른 프로세스도 같은 계정으로 요청할 수 있습니다
ServiceAccount 토큰은 토큰을 가진 쪽이 인증에 사용할 수 있는 Bearer 토큰입니다. 공격자가 애플리케이션 취약점 등을 통해 토큰을 얻으면, 해당 계정으로 Kubernetes API에 요청할 수 있습니다. API 서버는 토큰 값만으로 요청한 프로그램이 원래 의도한 애플리케이션인지 구분하지 못합니다.
다만 토큰이 유효하고 API 서버에 연결할 수 있어야 합니다. 인증에 성공한 뒤에도 요청한 작업의 권한을 검사하므로, 토큰을 얻었다고 모든 리소스에 접근할 수 있는 것은 아닙니다. Kubernetes의 토큰 인증, ServiceAccount 토큰 검증
유출의 영향은 계정에 부여된 권한에 달려 있습니다
인증이 “어느 계정인가”를 확인한다면, 인가는 “이 작업을 허용하는가”를 확인합니다. Kubernetes의 RBAC는 Role·ClusterRole에 작업과 리소스 권한을 정의하고, 바인딩으로 계정에 연결합니다. RBAC의 Role과 바인딩
권한이 넓으면 토큰 유출의 영향도 커집니다. Secret 조회는 인증 정보 노출로, 다른 Pod에서의 명령 실행은 해당 컨테이너 내부 접근으로 이어질 수 있습니다. Pod 생성도 설정과 정책에 따라 Secret 마운트나 다른 ServiceAccount 사용으로 접근 범위를 넓힐 수 있습니다. 따라서 토큰 파일의 존재만으로 취약하다고 판단하기보다, 누가 토큰에 접근할 수 있고 그 계정으로 무엇을 할 수 있는지 함께 확인해야 합니다. Kubernetes RBAC 보안 권고
접근 가능한 범위와 실제 사용한 권한을 함께 확인합니다
이 글의 조사 목적은 토큰에 접근할 수 있는 구성인지 확인하고, 파일 접근과 API 사용이 실제로 발생했는지 각각 대조하는 것입니다.
아래 그림은 토큰이 노출됐을 때의 위협 흐름입니다. 첫 단계의 애플리케이션 취약점 악용과 토큰 외부 반출은 이번 실습에서 재현하지 않았습니다. 실습에서는 Pod 안에 준비한 테스트 프로세스로 토큰 파일을 읽고, 별도 명령으로 API 요청을 보냈습니다.
flowchart TD
A["가정: 애플리케이션 취약점으로 파일 읽기 또는 명령 실행 가능"]
A --> B["읽을 수 있는 ServiceAccount 토큰 파일에서 토큰 획득"]
B --> C["토큰을 포함해 Kubernetes API에 요청"]
C --> D{"API 연결 및 토큰 검증 통과?"}
D -->|아니요| E["연결 또는 인증 실패"]
D -->|예| F["해당 ServiceAccount의 요청으로 권한 검사"]
F --> G{"요청한 작업이 허용되어 있나요?"}
G -->|아니요| H["요청 거부: 실습의 Secret 목록 조회는 403"]
G -->|예| I["후속 정책과 리소스 조건에 따라 처리"]
I --> J["실습의 ConfigMap 생성은 201로 성공"]
K["KubeHound: 토큰 볼륨과 계정의 연결 확인"] -.-> B
L["Datadog Agent: 토큰 파일을 연 프로세스 확인"] -.-> B
M["API 감사 로그: 요청 계정, 작업, 응답 확인"] -.-> F
이 순서로 확인합니다
순서확인할 내용확인하는 이유
| 1. 파일 접근 확인 | 프로세스, 파일 경로, 이벤트 시각을 확인합니다. | 정상적인 API 사용 과정인지 조사할 출발 정보를 확보합니다. |
|---|---|---|
| 2. Pod 확인 | container ID를 당시 Pod UID와 연결합니다. | 같은 이름으로 다시 만들어진 Pod와 혼동하지 않기 위해서입니다. |
| 3. 계정과 권한 확인 | 토큰이 어느 ServiceAccount의 것인지, 어떤 권한이 연결됐는지 확인합니다. | 토큰 노출 시 접근할 수 있는 리소스 범위를 파악합니다. |
| 4. API 요청 확인 | 감사 로그의 계정, 작업, 대상, 응답을 확인합니다. | 접근 가능했던 범위 중 실제 요청한 작업을 확인합니다. |
| 5. 조사 결과 기록 | 확인한 행동과 아직 확인하지 못한 내용을 구분합니다. | 파일 접근을 토큰 반출이나 다른 리소스 접근 성공으로 확대 해석하지 않기 위해서입니다. |
KubeHound의 설정 분석, Datadog의 프로세스 관측, API 감사 로그는 각각 이 절차의 다른 부분을 담당합니다. 이 글에서는 식별자와 시점을 대조해 결과를 연결했으며, 두 프로젝트가 기본으로 제공하는 자동 통합 기능을 사용한 것은 아닙니다.
권한 정보와 행동 기록은 서로 다른 질문에 답합니다
Kubernetes의 Pod 설정에는 ServiceAccount와 볼륨, 실행 권한이 있고, RBAC에는 그 주체가 사용할 수 있는 API 권한이 있습니다. 이런 정보로 컨테이너에서 다른 리소스로 이어지는 가능성을 분석할 수 있습니다. 다만 설정만으로 특정 프로세스가 방금 토큰 파일을 열었는지 알 수는 없습니다.
반대로 파일 접근 이벤트를 얻었더라도, 그 컨테이너의 Identity가 어떤 Pod를 조작할 수 있는지까지 이벤트 하나에서 모두 드러나지는 않습니다. Kubernetes API 작업의 주체·대상·응답은 API 서버의 감사 로그를 함께 봐야 합니다.
이 글에서는 그 문맥을 두 층으로 나눕니다. Datadog의 resolver는 프로세스·파일·컨테이너 문맥을 보완하고, KubeHound의 그래프는 구성과 권한 관계를 보여줍니다.
| 개념 | 이 글에서 답하려는 질문 |
|---|---|
| KubeHound graph | 이 리소스에서 어떤 Identity·권한·다른 리소스로 이어질 수 있나요? |
| eBPF hook | 커널의 어느 실행 지점에서 어떤 행동을 관측하나요? |
| Resolver | 행동의 프로세스, 파일, 컨테이너를 어떻게 식별하나요? |
| SECL / Agent rule | 어떤 이벤트를 규칙에 맞는 행동으로 선별하나요? |
| Kubernetes audit | 누가 어떤 API 작업을 요청했고 결과는 무엇인가요? |
| 상관분석 | 경로와 이벤트가 같은 워크로드, 같은 시점의 자료인가요? |
접근 가능성에 행동의 증거를 붙입니다
두 도구를 연결하는 목적은 어디까지 접근할 수 있는지 보여주는 경로에 실제 행동의 주체와 시점을 대응시키는 것입니다.
건물의 출입 구조와 출입 기록을 함께 보는 상황에 가깝습니다. KubeHound는 어떤 출입증으로 다음 구역에 갈 수 있는지 보여줍니다. Datadog Agent는 문이 열린 행동을 주체와 연결하고 정책으로 평가합니다. Kubernetes 감사 로그는 관리 시스템에 어떤 요청이 접수됐는지 보완합니다.
실측에서는 특정 토큰 경로가 클라이언트에 1개, 서버 대조군에는 0개였습니다. 같은 토큰 파일에 접근하는 두 프로세스를 관측했지만, 이름을 좁게 지정한 study_token 규칙은 1건만 매칭했습니다. 가능한 경로, 관측된 행동, 정책 매칭이 서로 다른 결과인 이유를 이 사례로 볼 수 있습니다.
1. 두 프로젝트를 연결하는 전체 아키텍처

KubeHound가 Kubernetes API에서 리소스를 수집해 정규화 문서를 MongoDB에 저장하고, 그 문서의 조건을 평가해 JanusGraph의 정점과 간선을 만듭니다. 그림의 MongoDB → JanusGraph 화살표는 이 데이터 흐름을 압축한 표현입니다. 간선을 계산하는 주체는 KubeHound의 graph 코드입니다.
아래쪽 흐름은 런타임 관측입니다. 워크로드의 행동을 eBPF hook에서 포착하고, system-probe의 이벤트 처리·resolver·SECL 평가를 거쳐 전송할 이벤트를 준비합니다. 전송에는 system-probe 직접 전송과 security-agent 경유 구성이 있어 그림에서는 한 화살표로 묶었습니다. Core Agent 등의 메타데이터 공급 구성요소도 가독성을 위해 생략했으며 본문에서 역할을 설명합니다.
오른쪽의 상관분석은 이 글에서 별도로 구성한 분석 단계입니다. KubeHound가 Datadog 이벤트를 기본으로 받아 그래프를 자동 갱신한다는 의미가 아닙니다. 경로 조회, 이벤트 payload, Kubernetes 감사 로그를 가져와 리소스 식별자와 시점을 대조합니다.
| 경계 | 전달하는 정보 | 담당 구현 또는 자료 |
|---|---|---|
| Kubernetes API → KubeHound | Pod·권한·볼륨 구성 | collector, ingestor, store model |
| 정규화 문서 → JanusGraph | 정점과 조건부 공격 관계 | KubeHound graph vertex/edge 코드 |
| 커널 → system-probe | 행동 이벤트 | eBPF hooks, buffer, EBPFProbe |
| 이벤트 → 정책 평가·전송 | 해석한 필드와 규칙 매칭 결과 | resolver, RuleEngine, sender |
| 그래프·이벤트·감사 → 상관분석 | 경로, 행동, API 요청 결과 | 별도 조회·대조 코드와 조사 절차 |
2. KubeHound는 가능한 경로를 어떻게 표현하나요?
아래 공식 그림은 실제 클러스터의 조회 결과가 아니라 정점 종류와 관계를 설명하는 모델입니다. 먼저 그림의 화살표를 읽고, 그 관계를 생성하는 코드로 내려가겠습니다.

먼저 그림 자체를 읽는 규칙을 정합니다
이 그림에는 정점 종류가 하나씩 그려져 있습니다. 실제 그래프에는 Container와 Identity가 여러 개 존재합니다. 따라서 Container로 돌아오는 고리는 “컨테이너 종류 사이에 연결이 있다”는 표현일 수 있으며, 동일한 컨테이너 자신만을 뜻하지 않습니다.
화살표는 데이터가 네트워크로 이동하는 방향과도 다릅니다. Identity → PermissionSet은 패킷 흐름이 아니라 그 Identity가 사용할 수 있는 권한 관계입니다. Volume → Identity도 볼륨이 인증을 수행한다는 뜻이 아니라, 접근 가능한 볼륨에서 해당 인증 주체의 자격 증명을 얻을 가능성을 모델링합니다.
하나의 선에 |로 나열한 이름은 간선 종류를 묶어 표시한 것입니다. 나열된 공격을 모두 순서대로 수행해야 한다는 AND 조건으로 읽지 않습니다. 그리고 관계에는 발견·접근 가능성과 공격 기술이 함께 들어 있습니다. 모든 간선이 독립적인 취약점 한 개를 뜻하는 것도 아닙니다.
리소스와 권한을 7종의 정점으로 구분합니다
정점표현하는 대상별도로 구분했을 때 알 수 있는 것
| Endpoint | 컨테이너가 노출하는 네트워크 진입점 | 어떤 서비스를 출발점으로 가정할 수 있는지 |
|---|---|---|
| Container | Pod 안에서 실행하는 컨테이너 | 프로세스 실행, capability, mount 등 실행 환경의 영향 |
| Pod | Kubernetes의 Pod | API로 조작하는 단위와 그 안의 컨테이너를 연결하는 관계 |
| Volume | 컨테이너에 노출된 볼륨 | 토큰과 hostPath 등 파일 접근에서 이어지는 경로 |
| Node | 워크로드를 실행하는 노드 | 호스트 접근이 다른 Pod로 확장되는 범위 |
| Identity | 사용자·ServiceAccount·그룹 등 인증·권한 관계의 주체 | 어떤 주체의 권한으로 API를 사용할 수 있는지 |
| PermissionSet | Role/ClusterRole과 바인딩의 범위를 반영한 권한 묶음 | 특정 Identity에 실제 연결된 권한과 namespace 범위 |
KubeHound는 접근 경로를 계산하는 데 필요한 단위로 Kubernetes 정보를 다시 정리합니다. 특히 PermissionSet은 Kubernetes의 독립 API 리소스가 아닙니다. Role과 바인딩을 연결해 경로 탐색에 쓰는 KubeHound의 추상화입니다. 같은 ClusterRole을 참조하더라도 RoleBinding인지 ClusterRoleBinding인지에 따라 권한 범위가 달라지므로, Role 이름만 정점으로 쓰면 놓칠 정보가 있습니다. Pod와 Container도 서로 다른 질문에 답합니다. Kubernetes API로 Pod를 조작하는 권한과 컨테이너 내부의 실행 환경을 나누면, API 권한이 실제 실행 주체까지 이어지는 경로를 표현할 수 있습니다.
외부 서비스에서 컨테이너로 접근할 가능성: Endpoint → Container
ENDPOINT_EXPLOIT은 노출된 Endpoint에서 해당 Container로 이어집니다. 외부에 보이는 서비스부터 조사할 때 사용할 수 있는 출발 관계입니다.
이 간선이 존재한다고 해서 애플리케이션 취약점이 확인됐거나 원격 코드 실행에 성공했다는 의미는 아닙니다. 구현의 모델은 알려졌거나 아직 확인하지 않은 방법으로 진입할 가능성을 출발점에 둡니다. 따라서 실제 노출 범위와 네트워크 도달성, 애플리케이션 취약점은 별도로 확인해야 합니다. Datadog과 함께 볼 때는 연결 이벤트 하나만으로 이 간선의 공격 성공을 확정하지 않습니다. 애플리케이션 처리 뒤에 예상하지 못한 프로세스가 실행됐는지, 파일 접근이 이어졌는지 등의 행동 증거를 추가하는 위치입니다. 이는 조사 방법의 제안이며 두 프로젝트가 자동 제공하는 결합 기능을 뜻하지 않습니다.
토큰 볼륨을 통해 계정을 사용할 가능성: Container → Volume → Identity
여기서 Volume은 컨테이너에 파일을 제공하는 볼륨이고, Identity는 인증 주체입니다. 토큰이 있는 볼륨에 접근할 수 있다면 해당 토큰의 계정으로 API 요청을 시도할 수 있습니다. 토큰을 읽는다고 새 권한이 생기지는 않습니다. 그 계정에 이미 연결된 권한을 사용할 가능성이 생깁니다.
flowchart LR
C[Container] -->|VOLUME_DISCOVER| V[Volume]
V -->|TOKEN_STEAL| I[Identity]
I -->|PERMISSION_DISCOVER| P[PermissionSet]
VOLUME_DISCOVER: 컨테이너가 어떤 볼륨에 닿나요?
Container → Volume 관계입니다. volume_discover.go는 정규화된 Volume 문서에서 컨테이너 ID와 볼륨 ID를 읽어 간선을 만듭니다. 이 관계는 마운트된 볼륨을 그래프 탐색의 대상으로 드러냅니다. 자체로 비정상 접근을 탐지한 결과는 아닙니다.
TOKEN_STEAL: 볼륨에서 어떤 Identity로 이어지나요?
Volume → Identity 관계입니다. token_steal.go의 Stream()은 해당 run과 cluster에서 Projected 타입이며 projected_id가 있는 Volume 문서를 고릅니다. Processor()는 그 볼륨과 연결된 Identity 사이에 간선을 생성합니다.
이를 구조만 남긴 의사 코드로 보면 다음과 같습니다.
수집한 Volume 문서 중
현재 runID와 cluster에 속하고
Projected 타입이며 연결된 projected identity가 있는 항목을 선택
→ Volume ID와 Identity ID로 TOKEN_STEAL 간선을 생성
입력은 수집·정규화 문서입니다. 파일을 읽는 시스템 호출이나 읽은 바이트 수는 이 함수의 입력이 아닙니다. 그래서 이 간선을 “토큰 탈취 발생 로그”로 해석할 수 없습니다.
IDENTITY_ASSUME는 왜 별도로 있나요?
그림에는 Container → Identity와 Node → Identity의 직접 연결도 있습니다. 이 관계는 해당 실행 환경이나 자격 증명으로 Identity를 사용할 가능성을 표현합니다. 볼륨 경로에서는 어떤 볼륨을 거치는지까지 나타내고, 직접 연결에서는 실행 환경과 Identity의 관계를 나타냅니다.
분석 버전의 IdentityAssumeContainer.Stream()은 컨테이너가 상속한 ServiceAccount 이름과 namespace를 사용해 Identity를 연결합니다. 해당 코드의 연결 조건과 토큰 볼륨 생성 조건은 동일하지 않습니다. 따라서 토큰 마운트 경로가 없다는 사실을 모든 Identity 연결이 사라졌다는 사실로 바꿔 말하면 안 됩니다.
해당 계정의 API 권한 확인: Identity → PermissionSet
PERMISSION_DISCOVER는 Identity와 PermissionSet을 연결해 계정이 사용할 권한을 보여줍니다. 실제 권한 조회 명령의 실행 기록은 아닙니다. PermissionSet은 Role·ClusterRole의 규칙과 바인딩을 결합한 권한 묶음입니다.
Identity와 연결해 어떤 계정이 해당 권한을 사용하는지 나타냅니다.
namespace와 바인딩으로 권한의 적용 범위를 구분합니다.
rules에는 API 그룹·리소스·이름 제한·허용 작업이 담기며, 범위와 함께 접근 가능 경로를 분석하는 데 사용합니다.
아래는 별도 실습인 캡처한 실제 값입니다.
{
"T.id": 266160,
"T.label": "PermissionSet",
"cluster": "kind-kubehound-demo",
"name": "read-logs::pod-read-logs",
"namespace": "default",
"role": "read-logs",
"roleBinding": "pod-read-logs",
"isNamespaced": true,
"rules": "[API()::R(pods/log,pods,nodes,nodes/log)::N()::V(get,list)]",
"critical": false,
"runID": "01m4z3ykhm0p889535kp8y422q",
"storeID": "6ad07285d31631d6386cdfde"
}
PERMISSION_DISCOVER는 Identity에 연결된 PermissionSet으로 향합니다. 앞 단계에서 얻은 인증 주체가 무엇을 할 수 있는지 확인하는 위치입니다. 이 간선 역시 공격자가 실제 권한 조회 명령을 실행했다는 로그로 해석하지 않습니다.
다른 Pod에서 명령을 실행할 권한이 있다면, 토큰 노출의 영향을 원래 컨테이너 안으로 한정하기 어렵습니다. 대상 Pod에서도 그 컨테이너의 권한으로 행동할 수 있기 때문입니다. KubeHound는 이런 권한별 접근 가능성을 PermissionSet에서 출발하는 관계로 표현합니다.
간선그림에서의 주요 방향모델이 연결하는 가능성
| POD_EXEC | PermissionSet → Pod | 대상 Pod 안에서 실행할 수 있는 API 권한 |
|---|---|---|
| POD_PATCH | PermissionSet → Pod | Pod 변경 권한을 이용한 실행 환경 변경 |
| POD_CREATE | PermissionSet → Node | 강한 권한의 Pod를 만들어 노드 접근으로 확장하는 경로 |
| ROLE_BIND | PermissionSet → PermissionSet | 바인딩 조작으로 다른 권한 묶음에 접근하는 관계 |
| IDENTITY_IMPERSONATE | PermissionSet → Identity | impersonation 권한을 통한 다른 주체 사용 |
| TOKEN_LIST | PermissionSet → Identity | Secret 목록 조회를 통한 토큰 자격 증명 접근 가능성 |
| TOKEN_BRUTEFORCE | PermissionSet → Identity | 이름을 알아야 하는 Secret get 권한을 활용할 가능성 |
각 행의 상세 전제는 KubeHound 저장소의 공격 참조 문서에 있습니다. 이름만으로 의미를 추정하기 쉬운 두 부분을 더 살펴볼 필요가 있습니다.
첫째, TOKEN_BRUTEFORCE는 JWT의 서명 키나 토큰 문자열을 무차별 대입한다는 뜻으로 읽으면 안 됩니다. 이 참조 문서는 get secrets 권한은 있지만 Secret 이름이 필요한 상황에서 이름을 추측하는 경로를 설명합니다.
둘째, POD_CREATE가 Node로 향하는 것은 Pod 생성 권한의 잠재적 영향을 압축한 표현입니다. 실제 실행 가능성에는 생성할 Pod의 설정이 허용되는지, admission 정책(API 요청을 받아들이기 전에 설정 등을 검사하는 정책)이 차단하는지 같은 조건이 추가됩니다. 운영 환경에서는 Pod 생성 권한과 함께 해당 Pod 설정을 허용하는 정책도 확인해야 합니다.
PermissionSet 위의 자기 루프는 두 가지를 구분합니다
ROLE_BIND의 PermissionSet 간 연결은 권한 묶음에서 다른 권한 묶음으로 이동하는 관계입니다. 종류 그림에 PermissionSet 원이 하나만 있기 때문에 고리처럼 표시됩니다.
한편 POD_CREATE, POD_EXEC, POD_PATCH에는 대규모 그래프를 위한 구현상의 축약도 있습니다. 예를 들어
PodCreate.Traversal()은 LargeClusterOptimizations가 켜지면 해당 권한 정점을 critical=true로 표시하고 자기 간선 형태를 생성하는 분기를 갖습니다. 꺼져 있으면 현재 run의 Node 쪽으로 연결을 펼칩니다. POD_EXEC와 POD_PATCH도 해당 최적화 분기를 가집니다. 따라서 그래프 경로의 수와 모양을 비교할 때는 분석 설정도 맞춰야 합니다. 자기 루프를 모두 “추가 권한 획득” 한 가지로 설명하면 최적화 표현을 놓칩니다.
노드에 접근했을 때 영향을 받는 리소스를 확인합니다
노드 접근에서 다시 Pod와 Container로 내려가는 흐름은 다음과 같습니다.
Node → POD_ATTACH → Pod → CONTAINER_ATTACH → Container
Node → VOLUME_ACCESS → Volume
Node → IDENTITY_ASSUME → Identity
여기서 POD_ATTACH는 이름만 보고 PermissionSet에서 시작하는 pods/attach API 권한 간선으로 읽기 쉽습니다. 이 모델의 출발점은 Node입니다. 노드 접근을 확보했을 때 그 위의 Pod 실행 환경에 접근하는 관계를 표현합니다. CONTAINER_ATTACH는 이어서 Pod와 그 안의 Container를 연결합니다. VOLUME_ACCESS는 Node → Volume이며, 앞서 본 VOLUME_DISCOVER는 Container → Volume입니다. 두 간선의 도착점은 같지만 출발점과 접근 근거가 다릅니다. Container에서 out('VOLUME_ACCESS')를 시작하면 의도한 토큰 경로를 찾지 못할 수 있습니다. 그림의 긴 바깥쪽 선은 장식적인 테두리가 아닙니다. Node에서 Pod로 가는 POD_ATTACH와 Container에서 Node로 돌아오는 escape 관계처럼, 멀리 떨어진 종류를 연결하기 위해 우회해서 그린 선입니다. 선의 위치보다 화살표 끝을 먼저 확인하면 혼동을 줄일 수 있습니다.
같은 Pod 안의 Container → Container
SHARE_PS_NAMESPACE는 같은 Pod에서 프로세스 namespace를 공유하는 Container 사이의 관계입니다. 이 설정에서는 다른 컨테이너의 프로세스가 보일 수 있어 분석할 접근 범위가 달라집니다. 종류 그림의 Container 자기 루프는 이러한 컨테이너 간 연결을 나타냅니다. namespace를 공유한다는 사실만으로 모든 프로세스 조작 권한까지 확보했다고 해석하지 않으며, 사용자와 capability 등 실제 권한 조건을 함께 확인합니다.
호스트 접근과 컨테이너 escape를 구분하기
호스트는 컨테이너를 실행하는 노드의 운영체제입니다. hostPath는 노드의 파일이나 디렉터리를 컨테이너 안에서 사용할 수 있게 하는 볼륨입니다. 필요한 로그를 읽는 용도로 사용할 수 있지만, 연결한 경로에 노드의 인증 정보나 컨테이너 관리 소켓이 있으면 접근 영향이 커집니다. 파일 접근 권한이 허용하는 범위에서 호스트의 민감한 내용을 읽거나 변경할 수 있기 때문입니다. 읽기 전용이어도 인증 정보의 노출 가능성은 남습니다. hostPath의 보안 고려사항
컨테이너 escape는 컨테이너에 제한된 실행 범위를 벗어나 호스트에 영향을 미치는 상황을 뜻합니다. 뒤의 표에 나오는 capability는 Linux에서 구분한 개별 특권이고, namespace는 프로세스가 보는 자원 범위를 분리하는 기능입니다. Kubernetes의 리소스 구분 단위인 namespace와는 다른 개념입니다.
그림 아래에는 Volume을 경유해 호스트에 접근하는 관계와 Container에서 Node로 이어지는 escape 관계가 모여 있습니다.
| Volume → Node | 분석 구현의 민감 hostPath 목록과 읽기 전용 마운트 조건 | |
|---|---|---|
| EXPLOIT_HOST_WRITE | Volume → Node | 쓰기 가능한 hostPath와 안전 목록 제외 조건 |
| EXPLOIT_HOST_TRAVERSE | Volume → Volume | 호스트의 상위 경로를 통해 다른 Pod의 토큰 볼륨에 닿는 관계 |
| CE_NSENTER | Container → Node | namespace 진입에 필요한 호스트 관점·권한 조건 |
| CE_MODULE_LOAD | Container → Node | 커널 모듈 로드에 필요한 권한과 환경 |
| CE_PRIV_MOUNT | Container → Node | 특권 및 호스트 장치·마운트 관련 조건 |
| CE_SYS_PTRACE | Container → Node | 호스트 프로세스 접근과 디버깅 권한·보호 조건 |
| CE_UMH_CORE_PATTERN | Container → Node | 커널 core pattern 관련 설정에 영향을 줄 수 있는 조건 |
| CE_VAR_LOG_SYMLINK | Container → Node | 로그 경로와 심볼릭 링크를 이용한 호스트 접근 조건 |
이 표는 공격 실행 절차가 아니라 모델의 의미를 읽기 위한 분류입니다. 정확한 판정은 각 edge 구현의 필터를 확인해야 합니다. 예를 들어 ExploitHostWrite.Stream()은 hostPath, 쓰기 가능 여부, 안전 경로 목록을 사용합니다. 모든 hostPath가 같은 결과를 만드는 것은 아닙니다. 여기서는 읽기 전용 설정의 의미도 구분해야 합니다. 쓰기를 막으면 해당 쓰기 경로를 줄일 수 있지만, 읽을 수 있는 인증 정보까지 없애지는 않습니다. 상위 호스트 경로에서 다른 토큰 볼륨에 닿는 EXPLOIT_HOST_TRAVERSE가 별도로 있는 이유입니다.
그림의 여러 선을 하나의 조사 경로로 읽기
다음은 전체 흐름을 이해하기 위한 가상 경로입니다. 특정 클러스터에서 이 경로 전체가 성립하거나 실행됐다는 주장과 구분합니다.
Endpoint
→ ENDPOINT_EXPLOIT → Container A
→ VOLUME_DISCOVER → Volume A
→ TOKEN_STEAL → Identity A
→ PERMISSION_DISCOVER → PermissionSet A
→ POD_EXEC → Pod B
→ CONTAINER_ATTACH → Container B
이 예에서는 Container A에서 사용할 수 있는 토큰을 찾고, 그 토큰의 Identity에 Pod B 실행 권한이 있는지 확인합니다. 이렇게 연결하면 Container A의 설정을 검토할 때 다른 Pod까지 조사해야 하는 이유를 설명할 수 있습니다. 각 관계가 실제 환경에서도 성립하는지는 해당 리소스 설정과 정책으로 확인합니다.
방어 조치도 이 경로에서 위치를 잡을 수 있습니다. API 사용이 필요 없는 워크로드라면 토큰 자동 마운트를 줄이는 방안을 검토할 수 있고, 업무상 불필요한 Pod exec 권한은 권한 경계를 다시 검토할 대상입니다. 다만 어느 한 간선을 줄였다는 사실만으로 모든 우회 경로가 없어지지는 않습니다. 직접 Identity 연결이나 hostPath를 경유하는 다른 관계도 함께 확인해야 합니다.
3. Datadog Agent는 실제 행동을 어떻게 처리하나요?
KubeHound의 간선이 구성 조건에서 만들어지는 과정을 봤습니다. 이제 실제 행동을 관측하는 파이프라인에서 어떤 코드가 수집·해석·정책 평가를 맡는지 살펴보겠습니다.
flowchart LR
A[파일 열기] --> B[eBPF hook으로 관측]
B --> C[프로세스·파일·컨테이너 정보 연결]
C --> D[규칙 평가]
D --> E[설정에 따른 처리·전송]
저장소를 실행 프로세스와 라이브러리로 나눠 읽기
Datadog Agent 저장소는 여러 실행 파일을 포함합니다. pkg/security 디렉터리 전체가 security-agent 프로세스에서 실행된다고 생각하면 호출 관계를 잘못 읽게 됩니다. 어느 cmd에서 어떤 패키지를 조립하는지 먼저 확인해야 합니다.
| 구성요소 | Runtime Security에서 읽을 역할 | 소스의 출발점 |
|---|---|---|
system-probe |
EventMonitor와 보안 consumer를 만들고 probe·규칙 평가를 연결합니다 | eventmonitor.go |
CWSConsumer |
보안 규칙 엔진, API 서버, 정책 재로딩 등을 조립합니다 | cws.go |
security-agent |
경유 전송 구성에서 보안 이벤트를 받아 reporter로 전달합니다 | agent.go |
| Core Agent와 메타데이터 구성요소 | 호스트 정보, workload metadata, tagger 등의 문맥 공급에 참여합니다 | eventmonitor.go의 deps.Hostname, deps.WMeta, deps.Tagger 전달 |
ebpf-manager |
probe와 map 등 eBPF 자원의 관리에 사용되는 라이브러리입니다 | probe_ebpf.go의 import와 manager 사용 |
ebpf-manager는 독립적인 보안 탐지 서비스가 아닙니다. Agent가 이 라이브러리를 사용해 커널 계측을 관리하고, 그 위에 보안 이벤트 모델과 규칙 엔진을 얹습니다. 따라서 eBPF 생명주기만 공부할 때와 완성된 보안 도구를 분석할 때의 읽기 범위가 다릅니다.
전체 흐름: 행동을 조사 가능한 이벤트로 바꿉니다
프로세스의 파일 열기
→ 커널에서 행동과 결과 관측
→ 수집 필터를 통과한 이벤트 전달
→ 프로세스·파일·컨테이너 정보 연결
→ 규칙 조건 평가
→ 매칭 후 처리와 전송 여부 결정
이 흐름에는 설정 방향도 있습니다. Agent가 정책을 읽으면 어떤 이벤트가 필요한지 판단하고, 일부 조건을 커널의 수집 필터에도 반영합니다. 모든 행동을 수집한 뒤 마지막에만 걸러내는 구조는 아닙니다.
논리적으로는 관측·해석·판단·전송을 나눌 수 있지만, 각 단계가 반드시 별도 프로세스로 실행되는 것은 아닙니다. 분석 버전에서는 system-probe가 커널 관측과 보안 규칙 평가를 연결하며, 다른 Agent 구성요소가 메타데이터 공급이나 전송에 참여합니다.
Resolver는 행동의 주체와 대상을 연결합니다
Resolver는 커널 이벤트의 식별자를 프로세스·파일·컨테이너 정보로 해석하는 사용자 공간 구성요소입니다. 예를 들어 파일 열기 이벤트의 PID와 파일 식별 정보만으로는 실행 프로그램이나 전체 경로를 바로 설명하기 어렵습니다. Agent는 여러 Resolver와 캐시를 이용해 규칙이 참조할 수 있는 필드를 채웁니다.
어떤 정보를 연결하나요?
해석 대상출발 정보연결하는 정보
| 프로세스 | PID·TID, 실행 파일의 inode 등 | 실행 파일, 부모 프로세스, 사용자·실행 문맥 |
| 파일 경로 | 파일의 inode·mount ID 등 경로 식별 정보 | 디렉터리 관계와 마운트 위치를 반영한 경로 |
| 컨테이너 | 프로세스의 cgroup 문맥 | 컨테이너 식별 정보 |
| 태그 | 컨테이너 식별 정보와 메타데이터 | 이벤트에 붙일 컨테이너·워크로드 태그 |
PID는 프로세스, TID는 스레드를 식별합니다. inode는 파일시스템 안의 파일을 식별하는 정보이며, 그 자체가 전체 경로는 아닙니다. cgroup은 프로세스들을 묶어 자원을 관리하는 Linux 기능으로, 컨테이너 소속을 연결하는 데 활용됩니다.
아래 그림은 정보 간 의존 관계입니다. 모든 Resolver가 이 순서대로 한 번씩 실행된다는 의미는 아닙니다.
flowchart TD
E["eBPF에서 전달된 파일 접근 이벤트"]
E --> P["프로세스 식별 정보: PID·TID 등"]
E --> F["파일 식별 정보: inode·mount ID 등"]
P --> PR["Process Resolver: 프로세스 정보 조회"]
PR --> PC["실행 파일·부모 프로세스·사용자 정보"]
PR --> CG["cgroup 문맥으로 컨테이너 연결"]
CG --> CT["컨테이너 ID·태그 보완"]
F --> D["Dentry Resolver: 파일 이름·디렉터리 관계 해석"]
F --> M["Mount Resolver: 마운트 위치·루트 해석"]
D --> PATH["Path Resolver: 경로 조합"]
M --> PATH
PC --> EV["정보가 보완된 이벤트"]
CT --> EV
PATH --> EV
EV --> R["규칙 평가와 후속 처리에서 사용"]
프로세스 정보는 캐시에서 찾고, 부족하면 보완합니다
Agent는 시작할 때 이미 실행 중인 프로세스를 조사해 캐시를 채웁니다. 이후 fork·exec·exit 같은 프로세스 생명주기 이벤트를 반영해 실행 파일과 부모 관계 등을 관리합니다. 파일 접근 이벤트마다 운영체제 전체를 다시 조사할 필요를 줄이는 방식입니다.
분석 버전의 Process Resolver는 다음 순서로 정보를 찾습니다.
사용자 공간의 프로세스 캐시 조회
→ 없으면 커널의 eBPF 맵 조회
→ 그래도 없고 허용된 경우 /proc 조회
→ 끝내 찾지 못하면 해석 실패로 남김
이 보완 조회는 초기 스냅샷 완료 여부와 /proc 사용 허용, 조회 제한 등의 조건을 따릅니다. 커널 맵을 확인하는 것은 프로세스 이벤트가 늦게 도착하거나 누락돼 사용자 공간 캐시가 아직 채워지지 않은 경우를 보완하기 위해서입니다. /proc 조회도 항상 성공하는 것은 아닙니다. 프로세스가 이미 종료됐거나 필요한 정보에 접근할 수 없을 수 있습니다.
파일 경로는 디렉터리 관계와 마운트 정보를 합쳐 해석합니다
파일은 inode만으로 전체 경로를 알 수 없습니다. Dentry Resolver는 파일 이름과 디렉터리 관계를 해석하고, Mount Resolver는 해당 파일시스템이 어디에 마운트됐는지와 마운트 루트를 확인합니다. Path Resolver는 이 결과를 합쳐 경로를 구성합니다.
특히 bind mount에서는 파일시스템 내부 경로와 마운트된 위치가 다를 수 있습니다. 따라서 디렉터리 경로에 마운트 루트 보정과 마운트 위치를 반영해야 합니다. 토큰 사례에서도 이렇게 해석한 경로가 있어야 파일 접근을 토큰 경로 조건과 비교할 수 있습니다.
센서가 보는 /proc, cgroup, mount 정보가 관측 대상 호스트와 맞아야 하는 이유도 여기에 있습니다. kind처럼 노드가 컨테이너 안에서 실행되는 환경에서는 namespace 계층이 추가되므로, 잘못된 호스트 관점으로 정보를 읽으면 프로세스·파일·컨테이너 연결이 불완전해질 수 있습니다.
4. 식별자와 시점으로 두 결과를 연결합니다
두 파이프라인의 데이터는 서로 다른 단위를 사용합니다. 런타임 이벤트에는 프로세스와 container ID가 있고, KubeHound에는 분석 모델의 정점과 storeID가 있습니다. 이름이 같은 Pod도 재생성되면 다른 객체가 되므로, 문자열 이름만 같다는 이유로 합치지 않습니다.
기존 실습에서 확인한 연결 순서는 다음과 같습니다.
Datadog 이벤트의 container ID
→ Kubernetes Pod status의 container ID
→ 해당 Pod의 UID
→ KubeHound MongoDB Pod 문서의 UID
→ 문서 _id와 graph Pod vertex의 storeID
→ 해당 runID의 공격 경로
여기서 graph Container에 런타임 container ID가 그대로 들어 있다고 가정하지 않았습니다. 수집 모델과 그래프 모델의 차이를 확인하고 Pod 단위의 중간 대응을 사용했습니다.
토큰 접근을 예로 보면 연결 위치가 드러납니다
KubeHound에서 Container → VOLUME_DISCOVER → Volume → TOKEN_STEAL → Identity가 보이면, 그 컨테이너에서 토큰 자격 증명으로 이어지는 구성 관계가 있다는 뜻입니다. Datadog에서 같은 컨테이너의 파일 open을 관측하면 해당 파일에 접근한 프로세스와 시점을 추가할 수 있습니다.
그다음 API 작업이 있다면 감사 로그의 Identity, verb, resource, 응답을 확인합니다. 토큰 파일 open만으로 토큰 내용이 읽혔거나 다른 곳으로 반출됐다고 확정하지 않으며, API 연결 이벤트만으로 요청한 Kubernetes 작업을 단정하지 않습니다.
| 조사 단계 | 필요한 증거 | 이 단계가 답하지 못하는 질문 |
|---|---|---|
| 경로 존재 | runID를 제한한 Gremlin 결과 | 그 경로가 실제로 사용됐나요? |
| 파일 접근 | 프로세스·경로·container ID·시각 | 토큰 내용이 반출됐나요? |
| Agent 규칙 매칭 | rule ID와 이벤트 payload | SaaS의 Security Signal이 생성됐나요? |
| API 요청 확인 | 감사 로그의 주체·작업·응답 | API 이후의 모든 실행 결과가 무엇인가요? |
| 식별자·시점 대조 | Pod UID, storeID, graph runID | 수집 이후의 모든 설정 변경을 반영하나요? |
이러한 조사는 두 프로젝트의 기본 자동 통합 기능이 아니라 결과를 연결하는 방법입니다. 실무에 적용할 때는 event time과 graph 수집 시각 사이의 Pod·RBAC 변경도 처리해야 합니다. 이번 실습은 테스트 Pod의 UID와 container ID가 유지된 구간에서 대응을 확인했습니다.
실습에서 확인한 내용
Lima의 Linux 환경에 별도 kind 클러스터 datadog-study를 만들고 Datadog Agent 7.84.0을 배포했습니다. KubeHound v1.6.7은 MongoDB·JanusGraph와 함께 구성했습니다.
| 확인 항목 | 실습 결과 |
|---|---|
| 커널 이벤트 수집 | eBPF 기반 프로세스 실행·파일 접근·네트워크 이벤트를 확인했습니다 |
| 관측과 정책 구분 | 같은 토큰 파일 접근을 관측하면서 좁게 정의한 study_token 규칙은 1건 매칭했습니다 |
| Kubernetes API 권한 | ServiceAccount의 ConfigMap 생성은 201, Secret 목록 조회는 403이며 감사 로그와 일치했습니다 |
| 구성상 토큰 경로 | 클라이언트는 1개, 토큰 미마운트 서버의 같은 종류 경로는 0개였습니다 |
| 이벤트와 그래프 연결 | container ID → Pod UID → Mongo 문서 → graph storeID 대응을 확인했습니다 |
| 사전 검증 항목 | 정의한 10개 항목이 최종 실행에서 모두 통과했습니다 |
규칙은 학습용으로 작성했으므로 기본 제공 규칙의 탐지율을 평가한 결과는 아닙니다. Agent의 직접 전송 모드, Datadog SaaS Signal, 부하 상황의 누락률도 이번 실습에서 검증하지 않았습니다. 토큰 파일 접근을 토큰 탈취 성공으로 해석하지 않습니다.
실제 실습 배치와 그래프 조회 화면
아래는 실습에 사용한 배치입니다. 이 환경에서는 system-probe의 보안 이벤트를 security-agent를 거쳐 로컬 HTTP 수신기로 전달했습니다.

먼저 groupCount().by(label)로 수집된 리소스 수를 확인했습니다. Container 15개, Pod 12개, Volume 47개이며, dd-lab 네임스페이스만의 수가 아닌 해당 수집 전체의 집계입니다.

클라이언트는 namespace=dd-lab, pod=lab-client로 대상을 좁히고 VOLUME_DISCOVER와 TOKEN_STEAL 관계를 따라갔습니다. 결과는 Container → Volume → Identity로 이어지는 경로 1개이며, 정점 3개와 간선 2개로 구성됩니다. 이 화면은 구성상 접근 가능성을 보여줍니다. 실제 파일 접근 여부는 앞서 정리한 런타임 이벤트와 대조합니다.
'k8s > learning-ebpf' 카테고리의 다른 글
| Learning eBPF 10-11장 : eBPF Programming (0) | 2026.10.10 |
|---|---|
| Learning eBPF 9장 : eBPF for Security (0) | 2026.09.19 |
| Learning eBPF 8장 : eBPF for Networking (0) | 2026.09.12 |
| Learning eBPF : Falco (0) | 2026.09.05 |
| Learning eBPF 7장 : eBPF Program and Attachment Types (0) | 2026.08.29 |
