Falco란

Falco는 Linux 런타임에서 비정상 행동을 실시간 탐지하는 CNCF Graduated 프로젝트다. 원래 Sysdig가 2016년 오픈소스로 공개했고, 2020년 CNCF Incubating → 2024년 Graduated.

핵심 아이디어: 커널에서 발생하는 모든 syscall을 관찰하고, 사용자가 작성한 룰과 매칭되면 경보를 낸다.

컨테이너·K8s 환경에서도 “어떤 Pod의 어떤 컨테이너"가 그 syscall을 불렀는지 메타데이터를 enrichment로 붙여준다.

차단 도구가 아니다. 기본적으로 관찰하고 알린다 (alert-only). 필요하면 kill 액션을 룰에 붙일 수 있지만 추가 설정이 필요하고 주의가 필요하다.


아키텍처 전체 그림

┌─────────────────────────────────────────────────────────────────────┐
│  Linux Kernel                                                       │
│  ┌──────────────────────────────────────────────────────────────┐  │
│  │  syscall table: open, execve, connect, write, read, ...      │  │
│  └──────────────────┬───────────────────────────────────────────┘  │
│                     │ 후킹(driver가 이 지점을 인터셉트)               │
│  ┌──────────────────▼────────────────────────────────────────────┐ │
│  │  Falco Driver (3종 중 하나 선택)                                │ │
│  │  • Kernel Module (kmod)                                        │ │
│  │  • Legacy eBPF probe                                           │ │
│  │  • Modern eBPF probe (권장, CO-RE 기반)                        │ │
│  └──────────────────┬──────────────────────────────────────────── ┘ │
└────────────────────-│──────────────────────────────────────────────-┘
                      │ ring buffer를 통해 이벤트 전달
┌─────────────────────▼──────────────────────────────────────────────┐
│  Falco 사용자 공간 바이너리                                           │
│                                                                     │
│  libscap ─── 이벤트 캡처·디코딩 (syscall 번호 → 이벤트 구조체)        │
│      │                                                              │
│  libsinsp ── 상태 추적(프로세스 트리, FD 테이블, 컨테이너 메타데이터)   │
│      │       enrichment: proc.*, fd.*, container.*, k8s.* 필드 채움 │
│      │                                                              │
│  Rule Engine ── 룰 파싱, 필터 컴파일, 이벤트 매칭                    │
│      │          (falco_engine.cpp + libfilter)                      │
│      │                                                              │
│  Output ───── stdout, syslog, file, http, program                  │
└─────────────────────────────────────────────────────────────────────┘

libscap

“Sysdig capture” 라이브러리. 드라이버(커널 모듈 or eBPF)로부터 이벤트를 읽어서 내부 포맷으로 디코딩하는 역할. scap 이벤트는 타입, 타임스탬프, 인자들을 담은 구조체.

libsinsp

“Sysdig inspect”. libscap 이벤트를 받아서 상태를 유지한다. 예를 들어 어떤 PID가 어떤 파일을 열었는지(FD 테이블), 어떤 PID의 부모가 누구인지(프로세스 트리)를 추적한다. 이 상태 덕분에 proc.pname 같은 필드(부모 프로세스 이름)를 룰에서 쓸 수 있다.

컨테이너 런타임(containerd/CRI-O)에 붙어서 container ID → 이미지/label/namespace 매핑도 여기서 한다.

Rule Engine

룰 파일을 파싱하고 각 룰의 condition을 내부 필터 AST로 컴파일한다. 이벤트가 올 때마다 필터를 평가해서 매칭되는 룰이 있으면 출력 메시지를 만들어 내보낸다.


3가지 드라이버

1. Kernel Module (kmod)

가장 오래된 방식. .ko 파일을 커널에 올린다. 커널 버전에 맞게 컴파일해야 한다. 성능이 가장 좋지만 커널 ABI 의존성 때문에 커널 업그레이드 시 다시 빌드해야 할 수 있다. DKMS(Dynamic Kernel Module Support)로 자동 빌드하는 게 일반적.

운영 환경: VM 또는 bare-metal에서 커널 모듈 로드가 허용된 경우.

2. Legacy eBPF Probe

커널 4.14+ 에서 동작. eBPF CO-RE(Compile Once, Run Everywhere) 적용 전 버전이라 특정 커널 헤더 의존성이 있다. 커널 패닉 위험이 kmod보다 낮다(eBPF verifier가 검증).

3. Modern eBPF Probe (권장)

커널 5.8+ (CONFIG_DEBUG_INFO_BTF=y 필요). CO-RE 기반이라 한 번 컴파일한 바이너리가 여러 커널 버전에서 동작. BTF(BPF Type Format)로 커널 구조체 오프셋을 런타임에 추론한다. Falco 공식 권장 드라이버.

# 드라이버 선택 (falco.yaml)
engine:
  kind: modern_ebpf   # kmod | ebpf | modern_ebpf

eBPF 기초 (알아두면 좋은 것)

eBPF는 “확장 BPF”. 원래 BPF는 패킷 필터링용(tcpdump가 쓰는 것)이었는데, Linux 3.18+부터 범용 커널 내 VM으로 확장됐다.

핵심 특성:

  • 안전: eBPF 프로그램은 커널 로딩 전 verifier를 통과해야 한다. 무한 루프, 잘못된 메모리 접근은 거부된다.
  • 고성능: 컨텍스트 스위치 없이 커널 안에서 실행된다.
  • 이식성(CO-RE): BTF를 이용해 커널 버전마다 구조체 레이아웃이 달라도 런타임에 오프셋을 조정한다.

Falco의 modern eBPF는 kprobe/tracepoint를 후킹 포인트로 쓴다:

  • sys_enter_execve: 프로세스 실행 직전
  • sys_exit_execve: 실행 직후 (성공/실패 여부 포함)
  • sys_enter_openat, sys_exit_openat: 파일 열기
  • sys_enter_connect, sys_exit_connect: TCP 연결

이벤트는 커널 ring buffer → 사용자 공간 ring buffer → libscap으로 흐른다.


이벤트 소스

Falco가 보는 이벤트는 syscall만이 아니다.

소스설명플러그인
syscall기본. kmod/eBPF로 수집내장
k8sauditK8s API server audit logk8saudit 플러그인
cloudtrailAWS CloudTrail 이벤트cloudtrail 플러그인
oktaOkta 감사 이벤트okta 플러그인
githubGitHub webhook 이벤트github 플러그인

플러그인은 Go로 작성된 별도 바이너리. Falco가 동적으로 로드한다.


룰 구조: 전체 문법

Falco 룰 파일은 YAML. 4가지 최상위 엔티티가 있다.

1. rule

- rule: Unexpected Outbound Connection
  desc: |
    프로세스가 예상치 못한 외부 IP에 연결을 시도했다.
  condition: >
    outbound and not proc.name in (allowed_binaries) and not fd.sip.name in (trusted_domains)
  output: >
    Unexpected connection (proc=%proc.name pid=%proc.pid user=%user.name 
    ip=%fd.rip port=%fd.rport container=%container.name)
  priority: WARNING
  tags: [network, mitre_command_and_control]
  enabled: true
  warn_evttypes: false   # condition이 특정 이벤트 타입에 묶이지 않을 때 경고 억제
필드필수설명
rule룰 이름 (유니크해야 함)
desc설명
condition이벤트 필터 표현식
output경보 메시지 템플릿
priorityEMERGENCY, ALERT, CRITICAL, ERROR, WARNING, NOTICE, INFORMATIONAL, DEBUG
tags-분류 태그
enabled-false면 비활성 (기본 true)
exceptions-예외 조건 목록

2. macro

반복 사용하는 조건 조각을 이름에 묶는다. 룰의 condition에서 참조.

- macro: outbound
  condition: (evt.type = connect and evt.dir = <)

- macro: container
  condition: (container.id != host)

- macro: spawned_process
  condition: (evt.type = execve and evt.dir = <)

매크로 안에서 다른 매크로를 참조할 수 있다:

- macro: container_started
  condition: (spawned_process and container)

3. list

값들의 목록. 조건에서 in 연산자와 함께 쓴다.

- list: allowed_binaries
  items: [curl, wget, git, python3, node]

- list: sensitive_files
  items:
    - /etc/shadow
    - /etc/passwd
    - /root/.ssh/authorized_keys
    - /etc/kubernetes/admin.conf

4. exception

룰 내 예외를 구조적으로 정의. exceptions 키로 룰에 붙인다.

- rule: Write Below etc
  desc: 프로세스가 /etc 아래에 파일을 씀
  condition: >
    open_write and fd.name startswith /etc
  exceptions:
    - name: known_etc_writers
      fields: [proc.name, fd.name]
      comps: [in, startswith]
      values:
        - [dpkg, /etc/apt]
        - [puppet, /etc/puppet]
  output: Write below /etc (proc=%proc.name file=%fd.name)
  priority: ERROR

조건 표현식 (condition)

condition은 boolean 표현식이다. 연산자:

and, or, not
=, !=, <, <=, >, >=
in, not in          # 목록 포함 여부
contains            # 문자열 포함
startswith          # 접두사
endswith            # 접미사
glob                # 글로브 패턴 (* ? [] 지원)
pmatch              # prefix match
regex               # PCRE 정규식
exists              # 필드가 존재하는지 (null 체크)

예시들:

# 특정 syscall 타입 필터
condition: evt.type = execve

# 방향 필터 (> = 호출 진입, < = 호출 반환)
condition: evt.type = connect and evt.dir = <

# 리스트 포함
condition: proc.name in (bash, sh, zsh, dash)

# not in
condition: not proc.name in (known_processes)

# 문자열 매칭
condition: fd.name startswith /etc/
condition: fd.name contains shadow
condition: proc.cmdline contains "wget http"

# 정규식
condition: proc.cmdline regex "base64\\s+-d"

# glob
condition: fd.name glob "/tmp/*.sh"

# 복합 조건
condition: >
  spawned_process and 
  container and 
  proc.name in (python, python3) and
  proc.cmdline contains "import socket"

필드 레퍼런스

Falco가 제공하는 필드는 카테고리별로 나뉜다.

evt.* — 이벤트 자체

필드타입설명
evt.typestringsyscall 이름 (execve, open, connect, …)
evt.dirchar> = 진입(enter), < = 반환(exit)
evt.timeuint64나노초 타임스탬프
evt.cpuuint16실행된 CPU 번호
evt.argsstringsyscall 인자들 전체 텍스트
evt.resint64반환값 (성공=양수, 실패=음수 errno)
evt.failedboolevt.res < 0
evt.rawresint64원시 반환값

proc.* — 프로세스

필드타입설명
proc.pidint64PID
proc.tidint64스레드 ID
proc.namestring프로세스 이름 (basename)
proc.exepathstring실행 파일 전체 경로
proc.cmdlinestring명령어 + 인자 전체
proc.argsstring인자만
proc.cwdstring현재 작업 디렉터리
proc.ppidint64부모 PID
proc.pnamestring부모 프로세스 이름
proc.pcmdlinestring부모 명령어
proc.aname[n]stringn번째 조상 프로세스 이름 (0=부모, 1=조부모…)
proc.envstring환경변수 전체
proc.env[VAR]string특정 환경변수 값
proc.sidint64세션 ID
proc.ttyuint16TTY 번호 (0이면 데몬)
proc.is_container_healthcheckboolK8s 헬스체크 프로세스인지

fd.* — 파일 디스크립터

필드타입설명
fd.namestring파일 경로 or 소켓 정보
fd.numint64FD 번호
fd.typestringfile, directory, ipv4, ipv6, unix, pipe, …
fd.ipipnet소켓 IP (로컬+원격 둘 다)
fd.lipipaddr로컬 IP
fd.ripipaddr원격 IP
fd.rip.namestring원격 IP의 역방향 DNS
fd.lportuint16로컬 포트
fd.rportuint16원격 포트
fd.l4protostringtcp, udp, sctp
fd.sportstring서버 포트
fd.cportstring클라이언트 포트
fd.sipipaddr서버 IP
fd.cipipaddr클라이언트 IP

user.* — 사용자

필드타입설명
user.uiduint32UID
user.namestring사용자 이름
user.giduint32GID
user.groupstring그룹 이름
user.loginuidint32로그인 UID (아무도 없으면 -1)
user.loginnamestring로그인 이름

container.* — 컨테이너

필드타입설명
container.idstring컨테이너 ID (12자)
container.full_idstring전체 ID (64자)
container.namestring컨테이너 이름
container.imagestring이미지 이름:태그
container.image.idstring이미지 ID
container.image.repositorystring이미지 레포
container.image.tagstring이미지 태그
container.privilegedboolprivileged 모드인지
container.mountsstring마운트 정보
container.label[LABEL]string특정 레이블 값

k8s.* — 쿠버네티스

필드타입설명
k8s.pod.namestringPod 이름
k8s.pod.idstringPod UUID
k8s.pod.label[LABEL]stringPod 레이블 값
k8s.pod.ipipaddrPod IP
k8s.ns.namestring네임스페이스
k8s.deployment.namestringDeployment 이름
k8s.daemonset.namestringDaemonSet 이름
k8s.node.namestring노드 이름

실전 룰 예시

예시 1: 셸 생성 탐지

# 매크로
- macro: spawned_process
  condition: (evt.type = execve and evt.dir = <)

- macro: shell_procs
  condition: (proc.name in (bash, sh, zsh, dash, fish, ksh))

# 컨테이너 안에서 인터랙티브 셸이 실행될 때
- rule: Terminal Shell in Container
  desc: 컨테이너 안에서 대화형 셸이 실행됨 (exec으로 접속했을 가능성)
  condition: >
    spawned_process and container and shell_procs and
    proc.tty != 0 and container.id != host
  output: >
    Shell spawned in container (user=%user.name container=%container.name
    pod=%k8s.pod.name ns=%k8s.ns.name shell=%proc.name 
    parent=%proc.pname cmdline=%proc.cmdline)
  priority: NOTICE
  tags: [container, shell, mitre_execution]

예시 2: 민감 파일 읽기

- list: sensitive_file_names
  items:
    - /etc/shadow
    - /etc/sudoers
    - /root/.ssh/id_rsa
    - /root/.ssh/authorized_keys
    - /etc/kubernetes/admin.conf

- macro: open_read
  condition: >
    (evt.type in (open, openat, openat2) and evt.dir = < and
     fd.typechar = f and (evt.arg.flags contains O_RDONLY or
                          not evt.arg.flags contains O_WRONLY))

- rule: Read Sensitive File After Startup
  desc: 민감한 파일을 비루트 프로세스가 읽으려 함
  condition: >
    open_read and
    fd.name in (sensitive_file_names) and
    not proc.name in (sshd, passwd, sudo, su) and
    not user.uid = 0
  output: >
    Sensitive file read (user=%user.name proc=%proc.name 
    file=%fd.name container=%container.name)
  priority: WARNING
  tags: [filesystem, credential_access, mitre_credential_access]

예시 3: 외부 연결

- macro: outbound
  condition: >
    (evt.type = connect and evt.dir = < and
     fd.typechar = 4 and          # IPv4
     not fd.rip in (127.0.0.1, ::1) and
     not fd.rip startswith "10." and
     not fd.rip startswith "172.16." and
     not fd.rip startswith "192.168.")

- list: expected_outbound_processes
  items: [curl, wget, git, apt, apt-get, yum, dnf, pip, npm]

- rule: Unexpected Outbound Connection from Container
  desc: 컨테이너 안 프로세스가 예상치 않은 외부 IP에 접속
  condition: >
    outbound and container and
    not proc.name in (expected_outbound_processes)
  output: >
    Outbound connection from container (proc=%proc.name pid=%proc.pid
    ip=%fd.rip port=%fd.rport container=%container.name image=%container.image)
  priority: NOTICE
  tags: [network, container, mitre_command_and_control]

예시 4: 패키지 관리자 실행 (컨테이너 안에서)

- macro: package_mgmt_binaries
  condition: >
    proc.name in (apt, apt-get, aptitude, dpkg, yum, dnf, rpm,
                  pip, pip3, npm, yarn, gem, cargo)

- rule: Launch Package Management Process in Container
  desc: |
    컨테이너 런타임 중에 패키지 설치. 이미지 빌드 시점이 아니라
    실행 중 패키지를 추가하는 것은 의심스럽다.
  condition: >
    spawned_process and package_mgmt_binaries and container
  output: >
    Package management launched (user=%user.name proc=%proc.name 
    cmdline=%proc.cmdline container=%container.name image=%container.image)
  priority: ERROR
  tags: [container, process, mitre_persistence]

룰 오버라이드 (Override)

기존 룰을 수정할 때 전체를 재작성하지 않고 override 키를 쓴다.

# 기본 룰의 조건을 확장
- rule: Terminal Shell in Container
  condition: append and not proc.name = my_debug_tool
  override:
    condition: append

# 기본 룰을 비활성화
- rule: Terminal Shell in Container
  enabled: false
  override:
    enabled: replace

append는 기존 조건 뒤에 and <추가 조건>을 붙인다. 이 방식으로 기본 룰셋을 건드리지 않고 custom_rules.yaml에 오버라이드만 쌓는 패턴을 많이 쓴다.


falco.yaml 주요 설정

# 드라이버
engine:
  kind: modern_ebpf    # kmod | ebpf | modern_ebpf

# 룰 파일 (여러 개 가능, 순서대로 로드)
rules_files:
  - /etc/falco/falco_rules.yaml       # 공식 룰셋 (읽기 전용)
  - /etc/falco/falco_rules.local.yaml # 커스텀/오버라이드

# 출력 포맷
json_output: false          # true면 JSON으로 출력
json_include_output_property: true
time_format_iso_8601: false

# 최소 우선순위 (이것보다 낮은 룰은 무시)
priority: debug   # debug, info, notice, warning, error, critical, alert, emergency

# 출력 채널
stdout_output:
  enabled: true

syslog_output:
  enabled: false

file_output:
  enabled: false
  filename: /var/log/falco.log

http_output:
  enabled: false
  url: http://localhost:2801    # falcosidekick이 여기를 listen

program_output:
  enabled: false
  keep_alive: false
  program: mail -s "Falco Alert" security@example.com

설치 방법

Docker (가장 빠른 테스트)

# modern eBPF (호스트 커널 5.8+ 필요)
docker run --rm -i \
  --privileged \
  -v /var/run/docker.sock:/host/var/run/docker.sock \
  -v /proc:/host/proc:ro \
  -v /boot:/host/boot:ro \
  -v /lib/modules:/host/lib/modules:ro \
  falcosecurity/falco:latest \
  falco --modern-bpf

Helm (K8s)

helm repo add falcosecurity https://falcosecurity.github.io/charts
helm repo update

helm install falco falcosecurity/falco \
  --namespace falco \
  --create-namespace \
  --set driver.kind=modern_ebpf \
  --set falco.json_output=true

바이너리

# Ubuntu/Debian
curl -fsSL https://falco.org/repo/falcosecurity-packages.asc | \
  sudo gpg --dearmor -o /usr/share/keyrings/falco-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/falco-archive-keyring.gpg] \
  https://download.falco.org/packages/deb stable main" | \
  sudo tee /etc/apt/sources.list.d/falcosecurity.list

sudo apt update && sudo apt install -y falco
sudo systemctl start falco

출력 예시

경보 하나가 나오면 이렇게 생겼다 (JSON 모드):

{
  "time": "2026-06-15T10:23:45.123456789Z",
  "rule": "Terminal Shell in Container",
  "priority": "Notice",
  "source": "syscall",
  "tags": ["container", "shell", "mitre_execution"],
  "output": "Shell spawned in container (user=root container=backend pod=backend-7d4f8-xk9p2 ns=production shell=bash parent=runc cmdline=bash)",
  "output_fields": {
    "user.name": "root",
    "container.name": "backend",
    "k8s.pod.name": "backend-7d4f8-xk9p2",
    "k8s.ns.name": "production",
    "proc.name": "bash",
    "proc.pname": "runc",
    "proc.cmdline": "bash"
  },
  "hostname": "node-01"
}

이 JSON을 falcosidekick이 받아서 Slack, PagerDuty, Loki, Elasticsearch 등으로 팬아웃한다.