출발은 단순했다. Velociraptor를 EDR로 삼아 Docker로 올리고 EC2의 Wazuh에 붙이면 끝이라고 여겼다. 그런데 연동 문서를 뒤지는 동안 두 도구가 각각 무엇을 하는 물건인지부터 다시 물어야 했고, 그 물음은 Velociraptor가 정말 EDR인지, 격리는 어느 쪽 몫인지, 오픈소스 EDR은 왜 자동 차단을 못 하는지, 서비스가 ECS나 EKS로 옮겨 가면 에이전트를 어디에 둘 수 있는지로 번져 갔다. 모르는 단어가 나올 때마다 한 갈래씩 파고들며 며칠을 보냈고, 그 끝에서 처음의 계획은 전제부터 다시 세워야 했다.
Wazuh는 SIEM이고 Velociraptor는 아니다. 처음에는 둘 다 로그를 모아서 보여 주는 도구라고 생각했다. Velociraptor의 웹 GUI에 대시보드와 표가 있고 Sigma 룰까지 돌아가길래 SIEM에 가까운 물건으로 알고 있었는데, 실제로 붙여 놓고 보니 그 대시보드가 보여 주는 것은 내가 돌린 수집 작업의 결과 목록이지 로그 스트림이 아니었다. Wazuh는 중앙에서 판단하는 쪽이고, Velociraptor는 엔드포인트에 내려가 직접 조사하는 쪽이었다.
Wazuh는 manager가 로그를 룰에 대조해 alert를 만들고, indexer(OpenSearch)가 이를 저장하며, dashboard가 indexer를 읽어 보여 주는 구조다. all-in-one으로 깔면 하나처럼 보이지만 판정하는 쪽과 저장하는 쪽이 따로 있다. 이에 비해 Velociraptor는 VQL로 아티팩트를 만들어 엔드포인트에서 증거를 수집하고, 개별 호스트부터 네트워크 전체까지 조사하는 프레임워크로 볼 수 있다. SIEM의 요건에 대보자.
| 요건 | Wazuh | Velociraptor |
|---|---|---|
| 다중 소스 로그 수집(방화벽, 클라우드, 앱) | O | X (자기 클라이언트만) |
| 정규화(공통 스키마) | O | X (아티팩트마다 컬럼 다름) |
| 상관분석(룰 엔진, frequency/timeframe) | O | X |
| 장기 보관과 전체 검색 | O | X (흐름 단위 저장) |
Velociraptor가 Elastic/OpenSearch 업로드 아티팩트를 따로 마련해 둔 것도 같은 행간에서 읽을 수 있다. 스스로 SIEM이라면 밖으로 내보낼 까닭이 없으니, 장기 저장과 검색은 처음부터 외부 SIEM의 몫으로 남겨 두고 설계된 셈이다.
EDR이라는 이름표도 절반만 맞는다. 네 글자에 하나씩 대보니 Endpoint와 Response(격리, 지속성 제거, 원격 셸)는 있고, Detection은 Sigma 모니터링이 엔드포인트에서 평가되는 반쪽이며, 자동 차단·AV·탬퍼 프로텍션은 없었다.
그렇다면 다른 오픈소스 EDR로 갈아타면 되는가 싶었지만, 내가 검토한 오픈소스 도구들에서는 자동 차단이 상용 EDR만큼 통합되어 있지 않았고, 도구를 바꿔서 풀릴 문제로 보이지 않았다. 왜 그 층만은 오픈소스에서 보기 어려운지 좀처럼 납득이 되지 않아 한참을 더 붙들어야 했다. 프로세스는 이미 실행된 뒤에 죽이면 늦고, 실행 요청 단계에서 거부하려면 커널 모드에서 가로채는 수밖에 없다. 유저 모드 프로그램인 Velociraptor는 ETW로 이벤트를 구독할 수는 있어도 개입까지는 하지 못한다. 그런데 커널 드라이버의 버그는 부팅 불능으로 이어지고, 2024년 CrowdStrike 사건이 보여 주듯 전담 QA 조직이 있어도 수백만 대가 멈출 수 있다. 오픈소스에는 그 책임을 질 주체가 없다. 더구나 Windows는 커널 모드 코드 서명 정책에 맞는 서명 없이 커널 드라이버를 로드하지 않고, macOS의 Endpoint Security Framework는 Apple의 별도 승인이 필요하다. 여기에 차단의 정확도까지 담보하려면 대규모 텔레메트리와 화이트리스트 데이터가 뒤를 받쳐야 한다고 생각하게 되었다. 차단 층은 OS 내장 기능인 Windows Defender에 맡기고, Velociraptor는 조사 도구로 두는 선에서 매듭지을 수 있었다.
쿠버네티스는 클러스터, 노드, 파드, 컨테이너의 네 계층으로 나누어 볼 수 있다. 이 네 층을 머릿속에 바로 세우기까지 나는 두 번 헛짚었다. 처음에는 쿠버네티스가 컨테이너뿐 아니라 EC2까지 껐다 켰다 하는 줄 알았고, 다음에는 노드가 파드들을 묶은 단위인 줄 알았다.
flowchart TB
C[클러스터: 노드들의 묶음]
C --> N1[노드: 머신 1대<br/>AWS에서는 EC2]
C --> N2[노드]
N1 --> P1[파드: 배치 단위]
N1 --> P2[파드]
P1 --> K1[컨테이너: 프로세스]
P1 --> K2[컨테이너]
파드는 떼어놓을 수 없는 컨테이너들의 묶음으로, 같은 노드에 놓이고 IP를 공유해 localhost로 통신하며 볼륨을 나누고 같이 죽는다. 보조 역할을 하는 컨테이너가 사이드카다. 반면 노드는 파드를 묶은 것이 아니라 파드가 얹히는 머신이다. 파드보다 먼저 있고, 파드가 0개여도 남아 있으며, 같은 노드 위의 다른 파드끼리는 localhost도 볼륨도 공유하지 않는다. "묶는다"가 맞는 것은 컨테이너→파드와 노드→클러스터까지고, 파드→노드는 "얹힌다"에 가깝다.
쿠버네티스는 EC2를 만들거나 지우지 못한다. 노드가 EC2인지 온프렘 서버인지 가리지 않으며, 파드를 놓을 자리가 없으면 Pending으로 두고 끝낸다. 노드를 늘리는 일은 Cluster Autoscaler나 Karpenter처럼 IAM 권한을 따로 받아 AWS API를 부르는 별도 컴포넌트의 몫이다. 그렇다면 Autoscaler만 있으면 EKS는 필요 없지 않나 싶었는데, 그 발상 자체가 틀린 것은 아니다. 다만 컨트롤 플레인 운영이 전부 내 일이 되고, 온프렘 컨트롤 플레인에 AWS 노드를 붙이는 하이브리드는 파드 간 지연과 전송 비용 탓에 실무에서는 드물게만 쓰인다.
EKS가 대신해 주는 것은 컨트롤 플레인뿐이고, 워커 노드는 노드그룹과 Fargate 가운데 내가 고를 수 있다. "EKS 노드그룹"이라는 말을 처음 봤을 때는 EKS가 곧 쿠버네티스인데 왜 노드를 따로 말하는지 몰라 한참을 헤맸다. 컨트롤 플레인이라는 말도 정확히 몰랐는데, 실체는 kube-apiserver(kubectl 창구), etcd(상태 저장소), scheduler(파드를 어느 노드에 놓을지 결정), controller-manager(원하는 상태와 실제 상태를 맞춤) 네 컴포넌트의 묶음이었다. EKS를 쓰면 이 넷이 AWS 계정 안에서 돌고 나에게는 API 엔드포인트 주소만 주어지므로, "EC2 위에 EKS를 설치한다"는 내 표현은 반만 맞은 셈이다.
| 구분 | 컨트롤 플레인 | EC2 노드그룹 | Fargate |
|---|---|---|---|
| 운영 주체 | AWS | 나 (AWS는 실행 자동화만) | AWS |
| 소유 | AWS 계정 안 | 내 계정의 EC2 | AWS 소유 micro-VM |
| 접근 | API 엔드포인트만 | SSH 가능 | 불가 |
| 노드 단위 | 해당 없음 | EC2 1대 = 노드 1개 | 파드 1개 = 노드 1개 |
노드그룹은 워커용 EC2의 묶음(Auto Scaling Group)으로, AMI 선택과 kubelet 설치·등록을 AWS가 자동화하지만 인스턴스는 내 계정에 있고 SSH도 할 수 있다. 이것을 두고 "EKS가 노드를 관리해 준다"고 여겼었는데, 결정과 책임은 그대로 내 것이고 실행 단계만 자동화된 것이었다. 반면 Fargate는 파드 하나마다 전용 micro-VM이 생겼다 사라지며, 커널은 AWS 소유라 그 안에는 손을 댈 수 없다.