eBPF 기반 보안 정책
eBPF BPF LSM 보안 훅 프로그래밍, cgroup_skb 컨테이너별 방화벽, 소켓 레벨 필터, Cilium/Calico 구현 원리, identity 기반 정책 엔진, bpf_sk_lookup 소켓 리다이렉트 종합 가이드. 커널 내부 데이터 경로, 핵심 자료구조/API, 운영 환경 튜닝 포인트와 장애 디버깅 절차까지 실무 관점으로 다룹니다.
핵심 요약
- BPF LSM — 커널 보안 훅에 eBPF 프로그램을 연결해 유연한 맞춤형 정책을 구현합니다.
- cgroup_skb — 컨테이너/프로세스 단위로 네트워크 트래픽을 ingress/egress 방향별로 필터링합니다.
- SO_ATTACH_FILTER — 소켓 수준 BPF 필터로, tcpdump 캡처 원리와 동일한 메커니즘을 사용합니다.
- CGROUP_SOCK — 소켓 생성/연결 시점에 정책을 적용해 연결 자체를 허용·거부합니다.
- Cilium — eBPF로 Kubernetes 네트워크 정책을 구현해 iptables를 완전히 대체합니다.
- identity 기반 정책 — IP 주소 대신 워크로드 ID(label 해시)로 접근 제어를 수행합니다.
- bpf_sk_lookup — 소켓 리다이렉트로 투명 서비스 메시 프록시를 커널 내부에서 구현합니다.
- BPF map 실시간 업데이트 — 정책 변경 시 BPF 프로그램 재컴파일 없이 맵 항목만 갱신합니다.
- Seccomp-BPF — 프로세스가 사용 가능한 시스템 호출을 BPF 필터로 제한해 공격 표면을 축소합니다.
- Tetragon 포렌식 — kprobe/tracepoint 기반 BPF로 커널 이벤트를 수집하고 위협 프로세스를 즉시 차단합니다.
- XDP 방화벽 — NIC 드라이버 레벨에서 DDoS를 방어하며 iptables 대비 5-10배 높은 패킷 처리량을 제공합니다.
단계별 이해
- BPF LSM 이해
커널 보안 훅 목록과 BPF_PROG_TYPE_LSM 프로그램 연결 방식을 파악합니다. security_hook_heads 구조와 SELinux 공존 방식을 확인하세요. - cgroup 계층 파악
cgroup v2 계층 구조와 BPF 프로그램 상속(inherit) 동작을 이해합니다. BPF_F_ALLOW_MULTI와 BPF_F_ALLOW_OVERRIDE 차이를 파악하세요. - 소켓 프로그램 타입 학습
cgroup_skb, cgroup_sock, sk_lookup 등 소켓 관련 BPF 타입 차이를 구분합니다. 각 타입이 접근할 수 있는 정보와 역할을 정리하세요. - Seccomp-BPF 적용
SECCOMP_RET_* 반환값 종류와 libseccomp로 정책을 생성하는 방법을 학습합니다. Docker/K8s 기본 프로파일 구조를 분석하세요. - Cilium 아키텍처
데이터 경로(TC BPF), 제어 경로(Agent), 정책 맵 구조를 계층별로 살펴봅니다. cilium_lxc 맵과 identity 생성 과정을 추적하세요. - identity 기반 정책
레이블 해시에서 identity ID 생성, BPF 맵 조회, ALLOW/DENY 결정 흐름을 추적합니다. CiliumNetworkPolicy L3/L4/L7 계층을 직접 작성해 보세요. - XDP 방화벽 구현
XDP_DROP 기반 블랙리스트와 Rate limiting 패턴을 이해합니다. native/SKB/offload 모드 차이와 TC BPF 조합을 실습하세요. - 모니터링 및 진단
bpftool, bpftrace, Cilium Hubble, Tetragon으로 정책 적중 통계와 드롭 이벤트를 분석합니다. 포렌식 시나리오를 직접 재현해 보세요.
개요: eBPF 보안 생태계
eBPF는 커널을 재컴파일하지 않고도 커널 내부에 안전하게 프로그램을 주입할 수 있는 기술입니다. 보안 분야에서 eBPF는 세 가지 주요 축으로 활용됩니다. 첫째, 감사/관찰(Observability) — kprobe/tracepoint로 시스템 호출, 파일 접근, 네트워크 연결을 실시간 추적합니다. 둘째, 정책 집행(Enforcement) — BPF LSM과 cgroup_skb로 접근 제어 결정을 커널 내부에서 수행합니다. 셋째, 네트워크 보안(Network Security) — XDP/TC BPF로 DDoS 방어, 방화벽, 서비스 메시를 구현합니다.
전통적인 iptables/nftables 방화벽은 IP:port 기반 규칙을 사용하지만, 컨테이너 환경에서는 IP가 동적으로 변경되어 관리가 어렵습니다. eBPF 기반 보안은 워크로드 identity(레이블, 서비스어카운트, 네임스페이스)를 기준으로 정책을 적용하므로 IP 변경에 관계없이 일관된 보안 정책을 유지할 수 있습니다.
그림 1. eBPF 보안 계층 구조: 사용자 공간 워크로드부터 커널 보안 훅까지의 데이터 흐름과 주요 도구 생태계.
BPF 프로그램 타입 비교
| BPF 프로그램 타입 | 연결 지점(Attach Point) | 주요 용도 | 특권 요구 |
|---|---|---|---|
BPF_PROG_TYPE_LSM |
LSM 보안 훅 (socket_connect, file_open 등) | 접근 제어 정책, 파일/소켓 보안 | CAP_BPF + CAP_MAC_ADMIN |
BPF_PROG_TYPE_CGROUP_SKB |
cgroup ingress/egress | 컨테이너 단위 네트워크 방화벽 | CAP_NET_ADMIN |
BPF_PROG_TYPE_CGROUP_SOCK |
소켓 생성/연결/해제 | 소켓 레벨 접근 제어, 주소 리바인딩 | CAP_NET_ADMIN |
BPF_PROG_TYPE_SK_LOOKUP |
소켓 조회 단계 | 투명 소켓 리다이렉트, 서비스 메시 | CAP_NET_ADMIN |
BPF_PROG_TYPE_SOCKET_FILTER |
소켓 수신 필터 (SO_ATTACH_BPF) | 패킷 캡처 필터, tcpdump 원리 | CAP_NET_RAW (또는 비특권) |
BPF_PROG_TYPE_XDP |
NIC 드라이버 수신 경로 (XDP) | DDoS 방어, 고성능 방화벽 | CAP_NET_ADMIN |
BPF_PROG_TYPE_SCHED_CLS (TC) |
TC qdisc clsact 훅 | Cilium/Calico 데이터 경로, 암호화 | CAP_NET_ADMIN |
BPF LSM (Linux Security Module)
BPF LSM은 리눅스 5.7에서 도입된 기능으로, 기존 SELinux/AppArmor와 같은 LSM 프레임워크에
eBPF 프로그램을 추가 훅으로 연결할 수 있습니다.
커널 빌드 옵션 CONFIG_BPF_LSM=y와 부팅 파라미터 lsm=bpf 또는
SECURITY_BPF_ENFORCE를 설정해야 활성화됩니다.
BPF LSM 훅 등록 원리
커널 내부에서 BPF_LSM_HOOK 매크로로 정의된 훅 지점에 BPF 프로그램을 연결합니다.
각 훅은 기존 LSM 훅과 동일한 시점에 호출되며, 반환값 0은 허용, 음수 errno는 거부를 의미합니다.
/* BPF LSM 프로그램 예시: socket_connect 훅으로 특정 포트 차단 */
/* 차단할 목적지 포트를 저장하는 BPF 맵 */
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 1024);
__type(key, __u16); /* 포트 번호 */
__type(value, __u8); /* 1 = 차단 */
} blocked_ports SEC(".maps");
/* LSM 훅: 소켓 connect() 시스템 호출 직전에 호출됨 */
SEC("lsm/socket_connect")
int BPF_PROG(bpf_socket_connect_hook,
struct socket *sock,
struct sockaddr *address,
int addrlen)
{
struct sockaddr_in *addr4;
__u16 dport;
__u8 *blocked;
/* IPv4 TCP/UDP 연결만 검사 */
if (address->sa_family != AF_INET)
return 0; /* 허용: 다른 주소 패밀리 */
addr4 = (struct sockaddr_in *)address;
/* 네트워크 바이트 오더를 호스트 오더로 변환 */
dport = bpf_ntohs(addr4->sin_port);
/* 맵에서 차단 여부 조회 */
blocked = bpf_map_lookup_elem(&blocked_ports, &dport);
if (blocked && *blocked == 1) {
/* 감사 로그: bpf_printk는 tracefs에서 확인 가능 */
bpf_printk("BPF LSM: 차단된 포트 %d 연결 시도 거부\n", dport);
return -EPERM; /* 거부: Permission denied */
}
return 0; /* 허용 */
}
char _license[] SEC("license") = "GPL";
BPF LSM 프로그램 로드 및 연결
# 커널 BPF LSM 활성화 확인
cat /sys/kernel/security/lsm
# 출력 예: lockdown,capability,landlock,yama,apparmor,bpf
# bpftool로 LSM 프로그램 로드 및 attach
bpftool prog load bpf_lsm_block.o /sys/fs/bpf/lsm_block \
type lsm
# 특정 훅에 자동 연결 (SEC 어노테이션으로 처리됨)
# BPF 스켈레톤 사용 시 bpf_program__attach()로 연결
# 차단 포트 추가 (포트 4444 차단)
PORT=4444
bpftool map update pinned /sys/fs/bpf/blocked_ports \
key $(printf '%02x %02x' $((PORT >> 8)) $((PORT & 0xff))) \
value 01
# 연결된 LSM 프로그램 확인
bpftool prog list | grep lsm
주요 LSM 훅 목록
| 훅 이름 | 호출 시점 | 주요 활용 |
|---|---|---|
bpf_lsm_socket_connect |
connect() 시스템 호출 | 목적지 IP/포트 기반 차단 |
bpf_lsm_socket_bind |
bind() 시스템 호출 | 리스닝 포트 제한 |
bpf_lsm_socket_create |
socket() 시스템 호출 | 소켓 타입/프로토콜 제한 |
bpf_lsm_file_open |
파일 open() 호출 | 민감 파일 접근 감사/차단 |
bpf_lsm_bprm_check_security |
execve() 프로세스 실행 | 실행 파일 서명 검증 |
bpf_lsm_inode_permission |
inode 권한 검사 | 세밀한 파일시스템 접근 제어 |
bpf_lsm_task_kill |
프로세스 시그널 전송 | 시그널 기반 DoS 방지 |
lsm_run_prog() 내부 동작 구조
BPF LSM 프로그램은 security_hook_heads 구조체의 각 훅 리스트에 등록됩니다.
커널이 보안 훅을 호출하면 BPF trampoline이 해당 훅에 등록된
모든 BPF 프로그램을 순차적으로 실행합니다.
/* 커널 내부: security/bpf/hooks.c 개념 구조 */
/* BPF LSM 훅은 기존 LSM 훅 체인 뒤에 추가됨 */
/* security_hook_heads.socket_connect 리스트에 등록 */
static struct security_hook_list bpf_lsm_hooks[] __lsm_ro_after_init = {
LSM_HOOK_INIT(socket_connect, bpf_lsm_socket_connect),
LSM_HOOK_INIT(socket_bind, bpf_lsm_socket_bind),
LSM_HOOK_INIT(file_open, bpf_lsm_file_open),
LSM_HOOK_INIT(bprm_check_security, bpf_lsm_bprm_check_security),
/* ... 200개 이상의 훅 ... */
};
/* 각 BPF LSM 훅 함수는 이 패턴을 따름 */
noinline int bpf_lsm_socket_connect(struct socket *sock,
struct sockaddr *address,
int addrlen)
{
/* 이 함수 자체가 BPF kprobe 연결 지점 */
/* BPF 프로그램은 이 함수에 fentry/fexit 방식으로 연결 */
return 0;
}
/* BTF-based trampoline로 BPF 프로그램 직접 호출 */
/* JIT 컴파일된 BPF → native code → 최소 오버헤드 */
BPF LSM 완전한 소켓 보안 예제 (링 버퍼 감사 포함)
/* SEC("lsm/socket_connect") 완전 예제: IP+포트 복합 정책 */
struct blocked_entry {
__u32 ip; /* 차단할 목적지 IPv4 (네트워크 바이트 오더) */
__u16 port; /* 차단할 목적지 포트 (호스트 바이트 오더) */
__u16 pad;
};
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 4096);
__type(key, struct blocked_entry);
__type(value, __u8);
} blocked_endpoints SEC(".maps");
/* 감사 링 버퍼 (사용자 공간으로 이벤트 전달) */
struct audit_event {
__u32 pid;
__u32 dst_ip;
__u16 dst_port;
char comm[16];
};
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 256 * 1024); /* 256KB */
} audit_rb SEC(".maps");
SEC("lsm/socket_connect")
int BPF_PROG(lsm_socket_connect,
struct socket *sock,
struct sockaddr *address,
int addrlen)
{
struct sockaddr_in *addr4;
struct blocked_entry key = {};
struct audit_event *evt;
__u8 *blocked;
if (address->sa_family != AF_INET)
return 0;
addr4 = (struct sockaddr_in *)address;
key.ip = addr4->sin_addr.s_addr;
key.port = bpf_ntohs(addr4->sin_port);
blocked = bpf_map_lookup_elem(&blocked_endpoints, &key);
if (!blocked)
return 0; /* 정책 없음 → 허용 */
/* 링 버퍼에 감사 이벤트 기록 */
evt = bpf_ringbuf_reserve(&audit_rb, sizeof(*evt), 0);
if (evt) {
evt->pid = bpf_get_current_pid_tgid() >> 32;
evt->dst_ip = key.ip;
evt->dst_port = key.port;
bpf_get_current_comm(evt->comm, sizeof(evt->comm));
bpf_ringbuf_submit(evt, 0);
}
return -EPERM; /* 차단: Permission denied */
}
char _license[] SEC("license") = "GPL";
BPF LSM과 SELinux/MAC 공존
BPF LSM은 SELinux, AppArmor 등 기존 LSM과 추가(stacking) 방식으로 함께 동작합니다. 기존 LSM이 먼저 평가되고, 이후 BPF LSM 훅이 실행됩니다. 어느 한쪽이라도 거부하면 최종 결정은 거부(DENY)입니다. 이를 통해 SELinux의 강제 접근 제어(MAC) 정책과 BPF LSM의 동적 정책을 함께 적용할 수 있습니다.
# LSM 스택 순서 확인
cat /sys/kernel/security/lsm
# 출력 예: lockdown,capability,landlock,yama,apparmor,bpf
# ↑ bpf는 마지막에 위치 (추가 정책 레이어)
# MAC (강제 접근 제어) 정책 계층:
# 1. SELinux AVC 검사 (도메인:타입 정책)
# 2. AppArmor 프로파일 검사
# 3. BPF LSM 훅 검사 (동적 정책)
# → 모든 레이어 통과 시에만 허용
# BPF LSM lsm= 부팅 파라미터 추가
# /etc/default/grub:
# GRUB_CMDLINE_LINUX="... lsm=lockdown,capability,yama,apparmor,bpf"
네트워크 관련 LSM 훅 전체 목록
| 훅 이름 (SEC 어노테이션) | 호출 시점 | 주요 활용 |
|---|---|---|
lsm/socket_create |
socket() 시스템 호출 | 소켓 타입/프로토콜 제한 (RAW 소켓 금지) |
lsm/socket_bind |
bind() 시스템 호출 | 리스닝 포트 범위 제한 |
lsm/socket_connect |
connect() 시스템 호출 | 목적지 IP/포트 기반 차단 |
lsm/socket_listen |
listen() 시스템 호출 | 서버 소켓 생성 감사 |
lsm/socket_accept |
accept() 시스템 호출 | 인바운드 연결 수락 제어 |
lsm/socket_sendmsg |
send/sendto/sendmsg | 데이터 전송 감사 |
lsm/socket_recvmsg |
recv/recvfrom/recvmsg | 데이터 수신 감사 |
lsm/socket_setsockopt |
setsockopt() 호출 | 위험한 소켓 옵션 차단 |
lsm/sk_alloc_security |
소켓 구조체 할당 | 소켓별 보안 컨텍스트 초기화 |
lsm/inet_conn_request |
TCP SYN 수신 | 인바운드 TCP 연결 초기 검사 |
lsm/unix_stream_connect |
Unix 도메인 소켓 connect | 컨테이너 내 소켓 통신 제한 |
lsm/file_open |
파일 open() 호출 | 민감 파일 접근 감사/차단 |
lsm/bprm_check_security |
execve() 프로세스 실행 | 실행 파일 서명 검증 |
lsm/task_kill |
프로세스 시그널 전송 | 시그널 기반 DoS 방지 |
권한 모델
BPF LSM 프로그램 로드에는 CAP_BPF와 CAP_MAC_ADMIN 권한이 모두 필요합니다.
리눅스 5.8부터 CAP_BPF가 CAP_SYS_ADMIN에서 분리되어
최소 권한 원칙(Principle of Least Privilege)을 적용할 수 있습니다.
# 컨테이너에서 BPF LSM 사용 시 필요한 최소 capabilities
# Dockerfile 또는 K8s securityContext에서 설정
securityContext:
capabilities:
add:
- BPF
- NET_ADMIN # cgroup_skb attach에 필요
- MAC_ADMIN # BPF LSM attach에 필요 (선택적)
cgroup_skb: 컨테이너 네트워크 방화벽
BPF_PROG_TYPE_CGROUP_SKB는 cgroup v2 계층의 ingress/egress 지점에 BPF 프로그램을 연결해
컨테이너 단위로 네트워크 트래픽을 필터링합니다.
Docker 컨테이너, K8s Pod는 각각 별도의 cgroup에 속하므로,
부모 cgroup에 BPF 프로그램을 연결하면 하위 모든 컨테이너에 자동 상속됩니다.
cgroup_skb 연결 아키텍처
/* cgroup_skb ingress 필터 예시: 특정 출발지 IP 차단 */
/* 차단할 소스 IP 집합 (IPv4 big-endian) */
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 4096);
__type(key, __u32); /* 소스 IP (네트워크 바이트 오더) */
__type(value, __u8); /* 1 = 차단 */
} blocked_ips SEC(".maps");
/* ingress 훅: 컨테이너로 들어오는 패킷마다 호출 */
SEC("cgroup_skb/ingress")
int filter_ingress(struct __sk_buff *skb)
{
void *data = (void *)(long)skb->data;
void *data_end = (void *)(long)skb->data_end;
struct iphdr *ip;
__u32 src_ip;
__u8 *blocked;
/* IP 헤더 파싱 (경계 검사 필수) */
ip = data;
if ((void *)(ip + 1) > data_end)
return 1; /* 파싱 불가: 허용 (안전 측) */
/* IPv4만 필터링 */
if (ip->version != 4)
return 1;
src_ip = ip->saddr; /* 소스 IP (네트워크 바이트 오더) */
/* 차단 목록 조회 */
blocked = bpf_map_lookup_elem(&blocked_ips, &src_ip);
if (blocked && *blocked == 1)
return 0; /* 드롭: 차단된 IP */
return 1; /* 허용: 통과 */
}
/* egress 훅: 컨테이너에서 나가는 패킷 필터링 */
SEC("cgroup_skb/egress")
int filter_egress(struct __sk_buff *skb)
{
/* egress도 동일한 구조로 목적지 IP 필터링 가능 */
return 1; /* 예시: 모두 허용 */
}
char _license[] SEC("license") = "GPL";
cgroup_skb BPF 프로그램 연결 방법
# cgroup v2 마운트 확인
mount | grep cgroup2
# 출력: cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,...)
# Docker 컨테이너의 cgroup 경로 찾기
CONTAINER_ID=$(docker inspect --format='{{.Id}}' my-container)
CGROUP_PATH="/sys/fs/cgroup/system.slice/docker-${CONTAINER_ID}.scope"
# 방법 1: bpftool로 직접 연결
bpftool prog load filter.o /sys/fs/bpf/cgroup_filter
bpftool cgroup attach "${CGROUP_PATH}" ingress \
pinned /sys/fs/bpf/cgroup_filter
# 방법 2: BPF_PROG_ATTACH syscall (프로그래밍 방식)
# int cgroup_fd = open(cgroup_path, O_RDONLY);
# bpf(BPF_PROG_ATTACH, &attr, sizeof(attr));
# attr.attach_type = BPF_CGROUP_INET_INGRESS
# attr.target_fd = cgroup_fd
# attr.attach_bpf_fd = prog_fd
# attr.attach_flags = BPF_F_ALLOW_MULTI /* 상속 허용 */
# 연결된 프로그램 확인
bpftool cgroup tree "${CGROUP_PATH}"
# 차단 IP 추가 (192.168.100.1 차단)
bpftool map update pinned /sys/fs/bpf/blocked_ips \
key 01 64 a8 c0 \ # 192.168.100.1 빅엔디안: c0.a8.64.01
value 01 # 차단
# 통계 확인: 드롭된 패킷 수
bpftool cgroup show "${CGROUP_PATH}"
cgroup 계층과 BPF 상속
BPF_F_ALLOW_MULTI 플래그를 사용하면 부모와 자식 cgroup 모두에 프로그램을 연결할 수 있으며,
패킷은 루트 cgroup부터 해당 cgroup까지 계층 순서대로 모든 BPF 프로그램을 통과합니다.
하나라도 드롭(반환값 0)을 반환하면 패킷은 폐기됩니다.
# cgroup v2 계층 구조 (BPF 프로그램 상속 예시)
/sys/fs/cgroup/
├── (루트 cgroup) ← 전역 정책 BPF 프로그램 연결
│ ├── system.slice/
│ │ ├── docker-abc123.scope/ ← 컨테이너별 추가 정책
│ │ └── docker-def456.scope/
│ └── user.slice/
└── ...
BPF_F_ALLOW_OVERRIDE vs BPF_F_ALLOW_MULTI
| 플래그 | 동작 | 활용 시나리오 |
|---|---|---|
BPF_F_ALLOW_OVERRIDE |
자식 cgroup에서 부모 프로그램 대체 가능 | 자식이 더 세밀한 정책으로 오버라이드 필요 시 |
BPF_F_ALLOW_MULTI |
부모+자식 프로그램 모두 실행 (AND 조건) | 전역 정책 + 컨테이너별 추가 정책 동시 적용 |
BPF_F_REPLACE |
기존 프로그램 원자적 교체 | 무중단 정책 업데이트 |
| (플래그 없음) | 자식 cgroup에 추가 프로그램 연결 불가 | 중앙 집중 정책 강제 적용 |
cgroup_skb ingress L7 페이로드 검사
/* bpf_skb_load_bytes()로 L7 페이로드 검사 예시 */
/* HTTP 요청 중 특정 User-Agent 차단 */
SEC("cgroup_skb/ingress")
int inspect_http_payload(struct __sk_buff *skb)
{
/* TCP 페이로드 시작 오프셋 계산 */
/* ETH(14) + IP(20) + TCP(최소 20) = 54바이트 */
__u32 payload_off = 54;
char buf[64] = {};
/* IP 프로토콜 확인 */
__u8 ip_proto;
bpf_skb_load_bytes(skb, 14 + 9, &ip_proto, 1);
if (ip_proto != IPPROTO_TCP)
return 1; /* TCP가 아니면 허용 */
/* TCP 목적지 포트 확인 (오프셋: ETH+IP+2) */
__be16 dport_be;
bpf_skb_load_bytes(skb, 14 + 20 + 2, &dport_be, 2);
if (bpf_ntohs(dport_be) != 80)
return 1; /* HTTP(80)가 아니면 허용 */
/* HTTP 페이로드 일부 로드 (64바이트) */
if (bpf_skb_load_bytes(skb, payload_off, buf, 64) < 0)
return 1;
/* "BadBot/1.0" User-Agent 차단 (단순 문자열 비교) */
/* 실제 구현은 BPF 맵에 차단 패턴 저장 권장 */
if (buf[0] == 'G' && buf[1] == 'E' && buf[2] == 'T')
bpf_printk("HTTP GET detected\n");
return 1; /* 기본 허용 */
}
char _license[] SEC("license") = "GPL";
/sys/fs/cgroup 계층 구조와 BPF 프로그램 매핑
# K8s Pod의 cgroup 계층 확인
POD_NAME="frontend-7d9f8b6c9-xkz2p"
NAMESPACE="production"
# containerd 사용 시 cgroup 경로
CONTAINER_ID=$(kubectl get pod "${POD_NAME}" -n "${NAMESPACE}" \
-o jsonpath='{.status.containerStatuses[0].containerID}' | sed 's/containerd:\/\///')
CGROUP_PATH="/sys/fs/cgroup/kubepods.slice/\
kubepods-burstable.slice/\
kubepods-burstable-pod${POD_UID}.slice/\
cri-containerd-${CONTAINER_ID}.scope"
# 계층별 BPF 프로그램 확인
bpftool cgroup tree /sys/fs/cgroup/
# /sys/fs/cgroup/ (루트: 전역 정책)
# kubepods.slice/ (K8s 노드 정책)
# kubepods-burstable.slice/ (QoS 클래스 정책)
# kubepods-burstable-podXXX.slice/ (Pod 정책)
# cri-containerd-YYY.scope/ (컨테이너 정책)
# 특정 cgroup에 BPF 프로그램 연결 (포트 기반 필터)
bpftool prog load port_filter.o /sys/fs/bpf/port_filter \
type cgroup_skb
bpftool cgroup attach "${CGROUP_PATH}" ingress \
pinned /sys/fs/bpf/port_filter \
flags allow_multi
# BPF 프로그램 통계 (드롭 카운터 확인)
bpftool prog show pinned /sys/fs/bpf/port_filter --json \
| jq '.run_cnt, .run_time_ns'
소켓 레벨 BPF 필터 (SO_ATTACH_FILTER)
소켓 필터(Socket Filter)는 eBPF의 전신인 cBPF(classic BPF)부터 지원된 기능으로,
SO_ATTACH_FILTER / SO_ATTACH_BPF 소켓 옵션으로 연결합니다.
tcpdump와 Wireshark가 바로 이 메커니즘을 사용해 패킷을 선택적으로 캡처합니다.
SO_ATTACH_BPF: eBPF 소켓 필터
/* eBPF 소켓 필터: UDP 포트 53(DNS)만 허용 */
SEC("socket")
int dns_only_filter(struct __sk_buff *skb)
{
__u32 proto_off;
__u16 dst_port;
/* 이더넷 헤더 건너뜀 (14바이트) */
/* IP 프로토콜 필드 확인 (오프셋 23) */
__u8 ip_proto = load_byte(skb, 14 + 9); /* IP 헤더의 protocol 필드 */
if (ip_proto != IPPROTO_UDP)
return 0; /* 비UDP 패킷 드롭 (캡처 제외) */
/* UDP 헤더: IP 헤더 이후 (가변 길이, 기본 20바이트 가정) */
/* 목적지 포트: 오프셋 14(ETH) + 20(IP) + 2(src port) = 36 */
dst_port = load_half(skb, 14 + 20 + 2);
if (dst_port == 53 || dst_port == 5353) /* DNS / mDNS */
return skb->len; /* 전체 패킷 허용 (캡처) */
return 0; /* 기타 UDP 드롭 (캡처 제외) */
}
char _license[] SEC("license") = "GPL";
# 소켓 필터 연결 (C 코드 예시)
/*
int sock = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
// eBPF 프로그램 FD 획득
int prog_fd = bpf_prog_load(BPF_PROG_TYPE_SOCKET_FILTER, ...);
// 소켓에 BPF 필터 연결
setsockopt(sock, SOL_SOCKET, SO_ATTACH_BPF, &prog_fd, sizeof(prog_fd));
*/
# tcpdump가 내부적으로 생성하는 cBPF 필터 확인
tcpdump -d 'tcp port 443'
# (000) ldh [12] - EtherType 로드
# (001) jeq #0x86dd jt 2 jf 6 - IPv6?
# ...
# eBPF 소켓 필터 프로그램 목록
bpftool prog list | grep socket_filter
RAW 소켓과 AF_PACKET
AF_PACKET 소켓과 소켓 필터를 결합하면
특정 조건의 패킷만 사용자 공간으로 복사하는 효율적인 캡처가 가능합니다.
BPF 필터가 커널에서 먼저 평가되므로 불필요한 컨텍스트 스위치를 줄입니다.
BPF_PROG_TYPE_CGROUP_SOCK
BPF_PROG_TYPE_CGROUP_SOCK은 소켓이 생성·연결·해제되는 시점에
cgroup 단위로 정책을 적용합니다.
패킷 단위 필터인 cgroup_skb와 달리, 소켓 자체의 속성(주소, 포트, 프로토콜)을 제어합니다.
CGROUP_SOCK attach 타입 목록
| Attach 타입 | 호출 시점 | 주요 용도 |
|---|---|---|
BPF_CGROUP_INET_SOCK_CREATE |
socket() 시스템 호출 | 컨테이너에서 특정 소켓 타입 금지 |
BPF_CGROUP_INET4_CONNECT |
IPv4 connect() | 연결 가능 목적지 제한, 주소 재작성 |
BPF_CGROUP_INET6_CONNECT |
IPv6 connect() | IPv6 연결 제어 |
BPF_CGROUP_INET4_BIND |
IPv4 bind() | 리스닝 포트 범위 제한 |
BPF_CGROUP_SOCK_OPS |
TCP 연결 상태 변화 | TCP 옵션 조작, 혼잡 제어 변경 |
BPF_CGROUP_INET_SOCK_RELEASE |
소켓 해제 | 연결 감사 로그 |
CGROUP_SOCK 예시: 특정 포트 바인딩 금지
/* cgroup_sock: 1024 미만 포트 바인딩 금지 (컨테이너 내 권한 제한) */
SEC("cgroup/bind4")
int restrict_bind(struct bpf_sock_addr *ctx)
{
/* ctx->user_port: 사용자가 요청한 포트 (호스트 바이트 오더) */
__u16 port = bpf_ntohs(ctx->user_port);
/* 1024 미만 포트는 컨테이너 내에서 금지 */
if (port > 0 && port < 1024) {
bpf_printk("cgroup_sock: 권한 있는 포트 %d 바인딩 거부\n", port);
return 0; /* 거부 */
}
return 1; /* 허용 */
}
char _license[] SEC("license") = "GPL";
/* cgroup_sock: IPv4 connect 주소 투명 재작성 (NAT 대체) */
SEC("cgroup/connect4")
int redirect_connect(struct bpf_sock_addr *ctx)
{
/* 목적지 10.96.0.10:80 (K8s kube-dns 서비스 ClusterIP)를
실제 백엔드 10.0.1.5:8080으로 재작성 */
if (ctx->user_ip4 == bpf_htonl(0x0a60000a) && /* 10.96.0.10 */
ctx->user_port == bpf_htons(80)) {
ctx->user_ip4 = bpf_htonl(0x0a000105); /* 10.0.1.5 */
ctx->user_port = bpf_htons(8080);
}
return 1; /* 수정된 주소로 연결 허용 */
}
char _license[] SEC("license") = "GPL";
Cilium & Calico 커널 구현 원리
Cilium과 Calico eBPF 모드는 Kubernetes 네트워크 정책을 iptables 없이 순수 eBPF로 구현합니다. 두 프로젝트 모두 TC(Traffic Control) BPF를 데이터 경로로 사용하지만, 정책 모델과 identity 방식에서 차이가 있습니다.
Cilium 아키텍처
Cilium은 각 Pod의 veth 인터페이스에 TC BPF 프로그램을 연결합니다. Pod의 네트워크 네임스페이스 쪽(tc ingress)과 호스트 쪽(tc egress) 양방향에 BPF 프로그램이 연결되어 모든 패킷을 검사합니다.
# Cilium이 사용하는 TC BPF 연결 확인
# 각 Pod의 veth 인터페이스에 Cilium BPF 프로그램이 연결됨
ip link show lxc-abcd1234 # Pod의 veth 호스트 측
tc filter show dev lxc-abcd1234 ingress
# filter protocol all pref 1 bpf chain 0
# bpf handle 0x1 cilium_from_container/to_container.o:[from-container] ...
tc filter show dev lxc-abcd1234 egress
# filter protocol all pref 1 bpf chain 0
# bpf handle 0x1 cilium_to_container/from-network.o:[to-container] ...
Cilium 데이터 경로 BPF 맵
# Cilium이 사용하는 주요 BPF 맵 목록
bpftool map list | grep cilium
# 주요 맵 타입:
# cilium_ipcache - IP → identity 매핑 (LPM_TRIE)
# cilium_policy_* - identity 쌍 → 정책 (HASH)
# cilium_ct4_global - 연결 추적 테이블 (HASH)
# cilium_lb4_services - L4 로드밸런서 서비스 테이블
# cilium_lb4_backends - 로드밸런서 백엔드 목록
# ipcache에서 특정 IP의 identity 조회
bpftool map dump pinned /sys/fs/bpf/cilium/maps/cilium_ipcache \
| grep "10.0.1.5"
Calico eBPF 모드
Calico eBPF 모드는 XDP를 ingress 고속 경로로, TC BPF를 egress 및 세밀한 정책 집행에 사용합니다. DSR(Direct Server Return) 로드밸런싱을 지원해 kube-proxy 없이 서비스 트래픽을 처리합니다.
# Calico eBPF 모드 활성화 확인
kubectl get felixconfiguration default -o yaml | grep bpfEnabled
# bpfEnabled: true
# Calico가 연결한 XDP 프로그램 확인
ip link show eth0
# XDP 섹션에 Calico 프로그램이 보임
bpftool net show dev eth0
# xdp:
# calico_xdp id 42 name calico_policy_program
Cilium의 cilium_lxc BPF 맵 구조
Cilium은 각 Pod의 veth 엔드포인트(lxc)를 cilium_lxc 맵에 등록해
TC BPF 프로그램이 올바른 엔드포인트로 패킷을 라우팅할 수 있도록 합니다.
/* cilium_lxc 맵 구조 (개념) */
struct endpoint_key {
union {
struct {
__be32 ip4; /* Pod IPv4 */
};
struct {
__be32 ip6[4]; /* Pod IPv6 */
};
};
__u8 family; /* AF_INET / AF_INET6 */
__u8 pad[7];
};
struct endpoint_info {
__u16 id; /* Cilium Endpoint ID */
__u16 sec_label; /* security identity (정책 ID) */
__u16 lxc_id; /* 인터페이스 lxc index */
__u8 mac[6]; /* Pod veth MAC */
__u8 node_mac[6]; /* 노드 호스트 MAC */
__u8 pad[4];
};
/* 실제 조회 예시 */
/* TC BPF 프로그램 내에서: */
/* struct endpoint_info *ep = lookup_ip4_endpoint(skb, ip4->daddr); */
/* if (ep) { */
/* forward_to_endpoint(skb, ep); */
/* } */
# cilium_lxc 맵 내용 확인
cilium map get cilium_lxc | head -20
# IP ID IDENTITY INTERFACE
# 10.0.1.5/32 1234 5678 lxcabc123
# 10.0.1.6/32 1235 5678 lxcdef456
# Endpoint 상세 정보
cilium endpoint list
# ENDPOINT POLICY (ingress) POLICY (egress) IDENTITY LABELS
# 1234 Enabled Enabled 5678 app=frontend
TC BPF → Socket BPF → cgroup BPF 협력 관계
Cilium의 데이터 경로에서 여러 BPF 프로그램 타입이 협력하여 완전한 보안 체계를 구성합니다. 각 레이어는 서로 다른 정보를 접근할 수 있으며 상호 보완적입니다.
| BPF 레이어 | 접근 가능한 정보 | 주요 역할 | Cilium 활용 |
|---|---|---|---|
| TC BPF (veth 인터페이스) | L2/L3/L4 헤더, skb 메타데이터 | 패킷 포워딩, identity 검사 | from-container, to-container |
| Socket BPF (sk_lookup) | 소켓 정보, 4-tuple | 투명 소켓 리다이렉트 | 서비스 ClusterIP 처리 |
| cgroup BPF (cgroup_skb) | 컨테이너 단위 트래픽 | Pod 수준 정책 집행 | Egress 정책 보조 |
| BPF LSM | 프로세스/소켓 메타데이터 | 시스템 호출 수준 제어 | Tetragon 연동 |
Calico eBPF vs iptables 모드 전환
# Calico eBPF 모드 활성화 (iptables → eBPF 마이그레이션)
# 1. kube-proxy 비활성화 (eBPF 모드에서 불필요)
kubectl patch ds -n kube-system kube-proxy \
-p '{"spec":{"template":{"spec":{"nodeSelector":{"non-calico":"true"}}}}}'
# 2. Felix 설정에서 eBPF 활성화
kubectl patch felixconfiguration default \
--type merge \
--patch '{"spec":{"bpfEnabled":true}}'
# 3. eBPF 모드 전환 확인 (노드별)
kubectl exec -n calico-system ds/calico-node -- calico-node -bird-live
# eBPF 데이터 경로 활성화 확인:
# bpftool net show dev eth0 | grep calico
# iptables 모드로 롤백
kubectl patch felixconfiguration default \
--type merge \
--patch '{"spec":{"bpfEnabled":false}}'
그림 2. Cilium identity 기반 정책 흐름: Pod 레이블에서 32-bit identity를 생성하고 BPF 맵 조회로 실시간 ALLOW/DENY 결정.
identity 기반 정책 엔진
전통적인 IP 기반 방화벽은 컨테이너 IP가 재시작마다 바뀌는 환경에서 관리가 어렵습니다. identity 기반 정책은 워크로드의 레이블(label)이나 서비스 어카운트를 기반으로 정책을 정의하므로 IP 변경에 관계없이 동작합니다.
Cilium Identity 생성 알고리즘
/* Cilium identity 생성 (의사코드) */
/* 1. Pod 레이블 수집 */
labels = {
"k8s:io.kubernetes.pod.namespace": "production",
"k8s:app": "frontend",
"k8s:version": "v1.2.3",
"reserved:init": "" /* 초기화 중 특수 레이블 */
}
/* 2. 정렬 후 SHA256 해시 계산 */
sorted_labels = sort(labels)
hash = SHA256(sorted_labels.join(";"))
/* 3. 전역 identity 할당 (32-bit 정수) */
/* Cilium 내부 etcd/KVStore에서 전역 유일성 보장 */
identity_id = allocate_identity(hash) /* 예: 1234 */
/* 4. BPF 맵 업데이트 (ipcache) */
/* Pod IP → identity 매핑 */
bpf_map_update(cilium_ipcache, pod_ip, identity_id, BPF_ANY)
/* 5. 정책 맵 업데이트 */
/* {src_id, dst_id, proto, port} → ALLOW/DENY */
policy_key = {src: 1234, dst: 5678, proto: TCP, port: 8080}
bpf_map_update(cilium_policy, policy_key, ALLOW, BPF_ANY)
BPF 정책 맵 구조
/* Cilium policy 맵 키/값 구조 (커널 소스 참조) */
/* 정책 맵 키: 트래픽 식별자 */
struct policy_key {
__u32 remote_id; /* 원격 identity ID */
__u16 dport; /* 목적지 포트 */
__u8 protocol; /* IPPROTO_TCP/UDP/... */
__u8 egress; /* 0=ingress, 1=egress */
};
/* 정책 맵 값: 정책 결정 */
struct policy_entry {
__be16 proxy_port; /* 프록시로 리다이렉트 시 포트 (0이면 직접) */
__u8 deny; /* 1=차단, 0=허용 */
__u8 auth_type; /* 인증 유형 (mTLS 등) */
__u64 packets; /* 적중 횟수 통계 */
__u64 bytes; /* 바이트 통계 */
};
/* 실제 BPF 프로그램에서의 정책 조회 (의사코드) */
SEC("classifier/from-container")
int cilium_from_container(struct __sk_buff *skb)
{
struct policy_key key = {};
struct policy_entry *entry;
/* 1. 패킷에서 목적지 IP 추출 */
/* 2. ipcache 맵에서 목적지 identity 조회 */
key.remote_id = lookup_identity(dst_ip);
key.dport = dst_port;
key.protocol = ip_proto;
key.egress = 1;
/* 3. policy 맵 조회 */
entry = bpf_map_lookup_elem(&cilium_policy_egress, &key);
if (!entry || entry->deny)
return TC_ACT_SHOT; /* 차단 */
/* 4. 통계 업데이트 */
entry->packets++;
entry->bytes += skb->len;
return TC_ACT_OK; /* 허용 */
}
IP 기반 vs Identity 기반 정책 비교
| 항목 | IP 기반 (iptables) | Identity 기반 (eBPF) |
|---|---|---|
| 정책 식별자 | IP 주소 / CIDR | 레이블 / 서비스 어카운트 |
| Pod 재시작 시 | IP 변경 → 규칙 갱신 필요 | identity 유지 → 자동 적용 |
| 규칙 업데이트 | iptables 재작성 (락 경합) | BPF 맵 업데이트 (원자적) |
| 확장성 | 규칙 수에 선형 비례 O(n) | 해시 맵 O(1) 조회 |
| 가시성 | 제한적 (LOG target) | Hubble로 L3-L7 전체 가시성 |
| L7 정책 | 별도 프록시 필요 | BPF 기반 L7 파서 내장 |
| 성능 | netfilter 훅 체인 순회 | TC BPF 단일 패스 |
bpf_sk_lookup 소켓 리다이렉트
BPF_PROG_TYPE_SK_LOOKUP은 리눅스 5.9에서 도입된 BPF 프로그램 타입으로,
커널이 수신 패킷에 맞는 소켓을 찾는 단계에 개입합니다.
이를 활용하면 투명 프록시(Transparent Proxy)나
서비스 메시 사이드카 없는 리다이렉트를 구현할 수 있습니다.
bpf_sk_lookup 동작 원리
일반적으로 TCP SYN 패킷이 도착하면 커널은 {dst IP, dst port, src IP, src port, protocol}로
리스닝 소켓을 찾습니다(4-tuple 매칭).
bpf_sk_lookup 프로그램은 이 조회 단계를 가로채어
bpf_sk_assign()으로 다른 소켓을 지정할 수 있습니다.
/* bpf_sk_lookup: 특정 포트 트래픽을 투명 프록시 소켓으로 리다이렉트 */
/* 프록시 소켓 FD를 저장하는 맵 (SOCKMAP) */
struct {
__uint(type, BPF_MAP_TYPE_SOCKMAP);
__uint(max_entries, 1);
__type(key, __u32);
__type(value, __u64); /* 소켓 참조 */
} proxy_sock_map SEC(".maps");
SEC("sk_lookup")
int transparent_proxy_redirect(struct bpf_sk_lookup *ctx)
{
struct bpf_sock *sk;
int ret;
/* 목적지 포트 80/443만 처리 (HTTP/HTTPS) */
if (ctx->local_port != 80 && ctx->local_port != 443)
return SK_PASS; /* 기본 소켓 조회 계속 */
/* TCP만 처리 */
if (ctx->protocol != IPPROTO_TCP)
return SK_PASS;
/* 프록시 소켓 조회 */
__u32 key = 0;
sk = bpf_map_lookup_elem(&proxy_sock_map, &key);
if (!sk)
return SK_DROP; /* 프록시 없음: 드롭 */
/* 패킷을 프록시 소켓으로 리다이렉트 */
ret = bpf_sk_assign(ctx, sk, 0);
bpf_sk_release(sk); /* 참조 카운트 해제 (필수) */
if (ret < 0)
return SK_DROP;
return SK_PASS; /* 지정된 소켓으로 전달 */
}
char _license[] SEC("license") = "GPL";
# sk_lookup 프로그램 연결 (네트워크 네임스페이스 단위)
# 소켓 리다이렉트는 특정 netns에 연결됨
NETNS_FD=$(ip netns exec my-namespace ls -la /proc/self/fd/1 | ...)
# bpftool로 sk_lookup 프로그램 netns에 연결
bpftool prog load sk_lookup.o /sys/fs/bpf/sk_lookup
bpftool net attach sk_lookup pinned /sys/fs/bpf/sk_lookup \
netns /var/run/netns/my-namespace
# 연결 확인
bpftool net show
# sk_lookup:
# sk_lookup id 42 name transparent_proxy_redirect netns /var/run/netns/my-namespace
# 프록시 소켓 등록 (사용자 공간에서 소켓 생성 후 맵에 등록)
# int proxy_sock = socket(AF_INET, SOCK_STREAM, 0);
# bind(proxy_sock, &proxy_addr, sizeof(proxy_addr));
# listen(proxy_sock, 128);
# bpf_map_update_elem(proxy_sock_map_fd, &key, &proxy_sock, BPF_ANY);
서비스 메시 투명 리다이렉트 활용 사례
Cilium의 소켓 기반 로드밸런싱에서 bpf_sk_lookup을 활용합니다. Kubernetes 서비스의 ClusterIP:Port로 향하는 연결을 실제 백엔드 Pod 소켓으로 직접 연결해 iptables DNAT 없이 서비스 라우팅을 구현합니다. 이 방식은 loopback을 통하지 않아 지연 시간이 감소합니다.
Seccomp + BPF 시스템 콜 필터링
Seccomp(Secure Computing Mode)는 프로세스가 사용할 수 있는 시스템 호출을 커널 수준에서 제한하는 보안 메커니즘입니다. BPF 필터와 결합한 Seccomp-BPF는 단순 화이트리스트/블랙리스트를 넘어 시스템 호출 인자까지 검사하는 세밀한 정책 구현을 가능하게 합니다. 컨테이너 런타임(Docker, Kubernetes, OCI)이 기본적으로 사용하는 핵심 보안 메커니즘입니다.
Seccomp 모드 비교
| 모드 | 허용 syscall | 설정 방법 | 활용 사례 |
|---|---|---|---|
SECCOMP_MODE_STRICT |
read, write, exit, sigreturn만 허용 | prctl(PR_SET_SECCOMP, SECCOMP_MODE_STRICT) |
샌드박스 내 최소 동작 보장 |
SECCOMP_MODE_FILTER |
BPF 프로그램이 정책 결정 | prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog) |
컨테이너, 브라우저 샌드박스 |
| 비활성화(기본) | 모든 syscall 허용 | 별도 설정 없음 | 일반 프로세스 |
BPF 필터 프로그램 구조
/* Seccomp-BPF 필터: write()만 stdout(fd=1)에 허용 */
/* struct sock_filter로 cBPF 명령어 배열 구성 */
#include <linux/seccomp.h>
#include <linux/filter.h>
#include <sys/prctl.h>
#include <sys/syscall.h>
/* BPF 필터 프로그램 (cBPF 명령어 배열) */
static struct sock_filter seccomp_filter[] = {
/* 아키텍처 검사: x86-64만 허용 */
BPF_STMT(BPF_LD|BPF_W|BPF_ABS,
offsetof(struct seccomp_data, arch)),
BPF_JUMP(BPF_JMP|BPF_JEQ|BPF_K, AUDIT_ARCH_X86_64, 1, 0),
BPF_STMT(BPF_RET|BPF_K, SECCOMP_RET_KILL_PROCESS),
/* syscall 번호 로드 */
BPF_STMT(BPF_LD|BPF_W|BPF_ABS,
offsetof(struct seccomp_data, nr)),
/* read() 허용 */
BPF_JUMP(BPF_JMP|BPF_JEQ|BPF_K, __NR_read, 5, 0),
/* write() - fd 인자 추가 검사 */
BPF_JUMP(BPF_JMP|BPF_JEQ|BPF_K, __NR_write, 1, 0),
/* exit_group() 허용 */
BPF_JUMP(BPF_JMP|BPF_JEQ|BPF_K, __NR_exit_group, 3, 0),
/* 나머지 syscall: ERRNO 반환 (EPERM) */
BPF_STMT(BPF_RET|BPF_K, SECCOMP_RET_ERRNO | EPERM),
/* write(): fd 인자(args[0]) 검사 */
BPF_STMT(BPF_LD|BPF_W|BPF_ABS,
offsetof(struct seccomp_data, args[0])),
BPF_JUMP(BPF_JMP|BPF_JEQ|BPF_K, 1, 0, 1), /* fd == 1(stdout)? */
BPF_STMT(BPF_RET|BPF_K, SECCOMP_RET_ALLOW),
BPF_STMT(BPF_RET|BPF_K, SECCOMP_RET_ERRNO | EBADF),
/* 허용 */
BPF_STMT(BPF_RET|BPF_K, SECCOMP_RET_ALLOW),
};
static struct sock_fprog seccomp_prog = {
.len = sizeof(seccomp_filter) / sizeof(struct sock_filter),
.filter = seccomp_filter,
};
/* 적용 */
int apply_seccomp_filter(void)
{
/* no_new_privs: setuid/setgid 비활성화 (필수 선행 조건) */
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0)
return -1;
/* Seccomp 필터 설치 */
if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER,
&seccomp_prog) != 0)
return -1;
return 0;
}
SECCOMP_RET 반환값 종류
| 반환값 | 동작 | 활용 시나리오 |
|---|---|---|
SECCOMP_RET_ALLOW |
syscall 허용 | 화이트리스트 정책의 기본 허용 |
SECCOMP_RET_ERRNO |
errno 반환 후 계속 실행 | 부드러운 거부 (애플리케이션 오류 처리) |
SECCOMP_RET_KILL_THREAD |
해당 스레드만 SIGSYS로 종료 | 위반 스레드 즉시 종료 |
SECCOMP_RET_KILL_PROCESS |
전체 프로세스 즉시 종료 | 심각한 보안 위반 대응 |
SECCOMP_RET_TRAP |
SIGSYS 시그널 발송 (처리 가능) | 사용자 공간 시그널 핸들러로 처리 |
SECCOMP_RET_TRACE |
ptrace 트레이서에 통지 | 동적 분석, 디버깅 |
SECCOMP_RET_USER_NOTIF |
사용자 공간 노티파이어에 전달 | 컨테이너 런타임 정책 에이전트 활용 |
SECCOMP_RET_LOG |
허용하되 audit 로그 기록 | 정책 개발 단계 감사 |
libseccomp로 정책 생성
/* libseccomp를 사용한 더 편리한 Seccomp 정책 구성 */
#include <seccomp.h>
int setup_seccomp_whitelist(void)
{
scmp_filter_ctx ctx;
int rc;
/* 기본 동작: 모든 syscall 차단 (ERRNO EPERM) */
ctx = seccomp_init(SCMP_ACT_ERRNO(EPERM));
if (!ctx)
return -1;
/* 허용 syscall 목록 */
/* 파일 I/O */
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(read), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(open), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(close), 0);
/* 메모리 관리 */
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(mmap), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(munmap),0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(brk), 0);
/* 프로세스 제어 */
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit_group), 0);
/* 네트워크: connect는 stdout(fd=1)에만 허용하는 등 인자 필터링 가능 */
/* 위험한 syscall 명시적 차단 (KILL) */
seccomp_rule_add(ctx, SCMP_ACT_KILL_PROCESS, SCMP_SYS(ptrace), 0);
seccomp_rule_add(ctx, SCMP_ACT_KILL_PROCESS, SCMP_SYS(process_vm_writev), 0);
/* 정책 적용 */
rc = seccomp_load(ctx);
seccomp_release(ctx);
return rc;
}
Docker/K8s 기본 Seccomp 프로파일
# Docker 기본 seccomp 프로파일 위치
# /etc/docker/seccomp.json (또는 moby 소스: profiles/seccomp/default.json)
# Docker 컨테이너에 커스텀 seccomp 프로파일 적용
docker run --security-opt seccomp=/path/to/custom-profile.json myimage
# K8s Pod에 seccomp 프로파일 적용 (1.19+)
# spec.securityContext.seccompProfile:
# type: RuntimeDefault # 런타임 기본 프로파일
# type: Localhost # 노드 로컬 파일
# localProfile: "profiles/myapp.json"
# type: Unconfined # seccomp 비활성화
# 프로파일 JSON 구조 예시
cat <<'EOF' > /etc/seccomp/myapp-profile.json
{
"defaultAction": "SCMP_ACT_ERRNO",
"architectures": ["SCMP_ARCH_X86_64"],
"syscalls": [
{
"names": ["read","write","open","close","mmap","exit_group"],
"action": "SCMP_ACT_ALLOW"
},
{
"names": ["ptrace","process_vm_writev"],
"action": "SCMP_ACT_KILL_PROCESS"
}
]
}
EOF
# 적용된 seccomp 확인
cat /proc/$(pgrep myapp)/status | grep Seccomp
# Seccomp: 2 ← 2=SECCOMP_MODE_FILTER
# SeccompFilters: 1 ← 설치된 필터 수
SECCOMP_RET_USER_NOTIF: 컨테이너 런타임 활용
/* seccomp notify: 사용자 공간에서 syscall 정책 결정 */
/* 컨테이너 런타임이 특권 작업을 안전하게 에뮬레이션할 때 사용 */
/* 1. 알림 FD 획득 (SECCOMP_FILTER_FLAG_NEW_LISTENER 플래그) */
int notif_fd = syscall(SYS_seccomp,
SECCOMP_SET_MODE_FILTER,
SECCOMP_FILTER_FLAG_NEW_LISTENER,
&seccomp_prog);
/* 2. 이벤트 수신 루프 (런타임 에이전트) */
struct seccomp_notif *req = malloc(notif_sizes.seccomp_notif);
struct seccomp_notif_resp *resp = malloc(notif_sizes.seccomp_notif_resp);
while (1) {
/* 차단된 syscall 대기 */
ioctl(notif_fd, SECCOMP_IOCTL_NOTIF_RECV, req);
/* 정책 결정: mount() 를 컨테이너 내에서 안전하게 처리 */
if (req->data.nr == __NR_mount) {
/* 마운트 요청을 검증하고 에뮬레이션 */
handle_mount_request(req);
resp->id = req->id;
resp->error = 0; /* 성공으로 응답 */
resp->val = 0;
} else {
resp->id = req->id;
resp->error = -EPERM;
resp->val = 0;
}
ioctl(notif_fd, SECCOMP_IOCTL_NOTIF_SEND, resp);
}
/* 활용 사례: sysbox, gVisor, Kata Containers */
/* 비특권 컨테이너에서 mount, setuid 등 특권 작업 안전 에뮬레이션 */
Seccomp-BPF 성능 영향
| 설정 | syscall 오버헤드 (getpid 기준) | 비고 |
|---|---|---|
| Seccomp 없음 | ~100 ns | 기준값 |
| SECCOMP_MODE_FILTER (간단 규칙 5개) | ~110 ns (+10%) | JIT 최적화 시 ~102 ns |
| SECCOMP_MODE_FILTER (복잡 규칙 50개) | ~130 ns (+30%) | libseccomp 생성 프로파일 |
| Docker 기본 프로파일 (~300 규칙) | ~120 ns (+20%) | BPF JIT 캐시 효과 |
| SECCOMP_RET_USER_NOTIF | ~수 μs (컨텍스트 스위치) | 런타임 에뮬레이션 비용 |
# Seccomp 성능 측정 (perf stat)
perf stat -e syscalls:sys_enter_getpid \
-- bash -c 'for i in $(seq 100000); do true; done'
# BPF JIT 활성화 확인 (JIT 없으면 성능 저하)
cat /proc/sys/net/core/bpf_jit_enable
# 1 이면 JIT 활성화
# Seccomp 통계 확인 (커널 6.1+)
cat /proc/$(pgrep myapp)/seccomp_notif
SECCOMP_RET_USER_NOTIF 감독자 패턴, LSM 프레임워크의 훅 디스패치 구조에 대한 상세 내용은 LSM 프레임워크 심화 문서를 참고하세요.eBPF 기반 감사 및 포렌식
eBPF 기반 감사 도구는 커널 이벤트를 실시간으로 수집해 보안 위협을 탐지하고 포렌식 증거를 수집합니다. 전통적인 auditd와 달리 커널 내부에서 필터링하여 오버헤드를 최소화하면서 더 풍부한 컨텍스트 정보를 제공합니다.
Tetragon 아키텍처
Tetragon은 Isovalent(Cilium 개발사)가 만든 eBPF 기반 보안 감사 및 런타임 정책 집행 도구입니다.
kprobe/tracepoint 기반 BPF 프로그램으로 커널 이벤트를 수집하고,
bpf_send_signal()을 활용해 위협 프로세스를 실시간으로 종료할 수 있습니다.
그림 4. Tetragon 보안 감사 아키텍처: kprobe/tracepoint BPF 이벤트를 Ring Buffer로 스트리밍하고 정책 위반 시 SIGKILL로 즉시 차단.
TracingPolicy: 프로세스/파일/네트워크 이벤트
# Tetragon TracingPolicy 예시: /etc/passwd 읽기 감지 + 차단
apiVersion: cilium.io/v1alpha1
kind: TracingPolicy
metadata:
name: "file-monitoring-passwd"
spec:
kprobes:
- call: "security_file_open"
syscall: false
args:
- index: 0
type: "file" # struct file * 타입 인식
selectors:
- matchArgs:
- index: 0
operator: "Postfix"
values:
- "/etc/passwd" # 파일 경로 접미사 매칭
matchActions:
- action: Sigkill # 차단: SIGKILL 전송
# action: Post # 감사만: 이벤트 발행
---
# 네트워크 연결 감사: 특정 외부 IP 연결 탐지
apiVersion: cilium.io/v1alpha1
kind: TracingPolicy
metadata:
name: "network-egress-suspicious"
spec:
kprobes:
- call: "tcp_connect"
syscall: false
args:
- index: 0
type: "sock"
selectors:
- matchArgs:
- index: 0
operator: "DAddr" # 목적지 주소 필터
values:
- "192.168.0.0/16"
matchActions:
- action: Post # 이벤트 발행 (차단하지 않음)
Falco eBPF probe 아키텍처
# Falco 설치 및 eBPF 드라이버 모드 설정
# 기본: 커널 모듈 / eBPF 모드: BPF probe
# eBPF 모드 활성화 (Helm 설치)
helm install falco falcosecurity/falco \
--set driver.kind=ebpf \
--namespace falco-system
# Falco 규칙 예시 (YAML)
# /etc/falco/falco_rules.yaml
# 컨테이너 탈출 탐지 규칙
- rule: Container Escape Detected
desc: Detects potential container escape via nsenter or chroot
condition: >
spawned_process and
container and
(proc.name in (nsenter, chroot, unshare) or
(proc.name = "bash" and proc.args contains "--init"))
output: >
Possible container escape: %proc.name %proc.args
(container=%container.name pid=%proc.pid user=%user.name)
priority: CRITICAL
tags: [container, escape]
# Falco vs eBPF probe 비교
# syscall 모드: perf ring buffer 기반, 모든 syscall 캡처
# eBPF probe: BPF 프로그램으로 필요한 이벤트만 필터링
# eBPF 장점: 더 낮은 오버헤드, 모던 커널에서 권장
bpf() syscall 감사 로그
# CONFIG_AUDIT + eBPF: BPF 프로그램 로드/언로드 자체를 감사
# /etc/audit/rules.d/bpf.rules:
-a always,exit -F arch=b64 -S bpf -k bpf_activity
# BPF 프로그램 로드 감사 로그 확인
ausearch -k bpf_activity | aureport --file
# 또는:
journalctl -k | grep "BPF prog"
# bpf() syscall audit 이벤트 내용:
# type=SYSCALL msg=audit(1708300000.123:456): arch=c000003e
# syscall=321 (bpf) success=yes exit=5
# a0=5 (BPF_PROG_LOAD) a1=... a2=...
# pid=1234 uid=0 comm="bpftool"
# BPF 프로그램 로드 시 btf_id, prog_type, attach_type 포함
# 미인가 BPF 로드 시도 감사 경보 설정
# SIEM 연동: Elasticsearch, Splunk로 전송
포렌식 시나리오: 컨테이너 탈출 및 권한 상승 탐지
# 시나리오 1: 컨테이너 탈출 탐지 (bpftrace)
# execve() + nsenter/chroot 조합 감지
bpftrace -e '
tracepoint:syscalls:sys_enter_execve
/str(args->filename) == "/usr/bin/nsenter"
|| str(args->filename) == "/usr/sbin/chroot"/
{
printf("[경고] 컨테이너 탈출 시도: PID=%d COMM=%s\n",
pid, comm);
/* cgroup 경로로 컨테이너 식별 */
printf(" cgroup: %s\n", cgroup);
}
'
# 시나리오 2: setuid/setgid 권한 상승 탐지 (bpftrace)
bpftrace -e '
kprobe:commit_creds
{
$new_cred = (struct cred *)arg0;
/* euid가 0으로 변경되는 경우 탐지 */
if ($new_cred->euid.val == 0 && curtask->cred->euid.val != 0) {
printf("[경고] 권한 상승 탐지: PID=%d COMM=%s euid=0으로 변경\n",
pid, comm);
printf(" 실행 파일: %s\n", curtask->mm->exe_file->f_path.dentry->d_name.name);
}
}
'
# 시나리오 3: Tetragon CLI로 실시간 이벤트 모니터링
kubectl exec -n kube-system ds/tetragon -- tetra getevents \
--namespace production \
--pod frontend \
--event-types PROCESS_EXEC,PROCESS_EXIT,PROCESS_KPROBE | \
jq '.process_exec | {pid: .process.pid, binary: .process.binary, args: .process.arguments}'
XDP 기반 방화벽 패턴
XDP(eXpress Data Path)는 NIC 드라이버 레벨에서 패킷을 처리하는 리눅스의 고성능 패킷 처리 프레임워크입니다. iptables/nftables가 넷필터 훅 체인을 통과한 후 패킷을 처리하는 것과 달리, XDP는 드라이버 수신 직후에 동작하여 최소한의 CPU 사이클로 패킷을 폐기하거나 처리합니다. DDoS 방어와 고성능 방화벽 구현에 매우 효과적입니다.
그림 3. XDP/TC BPF 방화벽 계층 구조: 드라이버 레벨 XDP에서 시작해 TC BPF, cgroup_skb, BPF LSM까지 다단계 보안 레이어.
XDP_DROP 기반 IP 블랙리스트 방화벽
/* XDP 방화벽: LRU 해시 맵 기반 IPv4/IPv6 블랙리스트 */
/* 블랙리스트 맵: IP 주소 → 차단 여부 */
/* LRU: 가장 오래된 항목 자동 제거 (메모리 효율) */
struct {
__uint(type, BPF_MAP_TYPE_LRU_HASH);
__uint(max_entries, 100000); /* 최대 10만 개 IP */
__type(key, __u32); /* IPv4 (네트워크 바이트 오더) */
__type(value, __u64); /* 차단 시작 타임스탬프 (ns) */
} ipv4_blocklist SEC(".maps");
/* Rate limit 맵: 소스 IP당 패킷 카운터 */
struct rate_entry {
__u64 last_ts; /* 마지막 패킷 타임스탬프 (ns) */
__u32 count; /* 현재 윈도우 패킷 수 */
};
struct {
__uint(type, BPF_MAP_TYPE_PERCPU_HASH);
__uint(max_entries, 65536);
__type(key, __u32);
__type(value, struct rate_entry);
} rate_limit_map SEC(".maps");
SEC("xdp")
int xdp_firewall(struct xdp_md *ctx)
{
void *data = (void *)(long)ctx->data;
void *data_end = (void *)(long)ctx->data_end;
struct ethhdr *eth = data;
struct iphdr *ip;
__u32 src_ip;
__u64 *blocked_ts;
/* 이더넷 헤더 파싱 */
if ((void *)(eth + 1) > data_end)
return XDP_PASS;
/* IPv4만 처리 */
if (eth->h_proto != bpf_htons(ETH_P_IP))
return XDP_PASS;
ip = (void *)(eth + 1);
if ((void *)(ip + 1) > data_end)
return XDP_PASS;
src_ip = ip->saddr;
/* 1. 블랙리스트 조회 */
blocked_ts = bpf_map_lookup_elem(&ipv4_blocklist, &src_ip);
if (blocked_ts)
return XDP_DROP; /* 차단된 IP */
/* 2. Rate limit 검사 (토큰 버킷 알고리즘) */
struct rate_entry *re = bpf_map_lookup_elem(&rate_limit_map, &src_ip);
__u64 now = bpf_ktime_get_ns();
if (re) {
/* 1초 윈도우 내 패킷 수 확인 */
if (now - re->last_ts < 1000000000ULL) { /* 1초 */
re->count++;
if (re->count > 1000) { /* 1초에 1000패킷 초과 */
/* 자동 블랙리스트 추가 */
bpf_map_update_elem(&ipv4_blocklist, &src_ip,
&now, BPF_ANY);
return XDP_DROP;
}
} else {
/* 윈도우 리셋 */
re->last_ts = now;
re->count = 1;
}
} else {
/* 새로운 소스 IP */
struct rate_entry new_re = {.last_ts = now, .count = 1};
bpf_map_update_elem(&rate_limit_map, &src_ip, &new_re, BPF_ANY);
}
return XDP_PASS;
}
char _license[] SEC("license") = "GPL";
XDP 동적 블랙리스트 관리
# XDP 프로그램 로드 (native XDP 모드: 드라이버 지원 필요)
ip link set dev eth0 xdp obj xdp_firewall.o sec xdp
# SKB 모드 (드라이버 미지원 시 폴백)
ip link set dev eth0 xdp obj xdp_firewall.o sec xdp mode skb
# 블랙리스트 실시간 추가 (IP: 1.2.3.4)
# BPF 맵에 직접 삽입 (유저스페이스 제어 도구)
bpftool map update pinned /sys/fs/bpf/ipv4_blocklist \
key 04 03 02 01 \ # 1.2.3.4 빅엔디안 리틀엔디안 주의
value 00 00 00 00 00 00 00 00 # 타임스탬프
# 블랙리스트 목록 조회
bpftool map dump pinned /sys/fs/bpf/ipv4_blocklist
# XDP 통계 확인 (드롭 카운터)
bpftool prog show pinned /sys/fs/bpf/xdp_prog
ip -s link show eth0 | grep -A2 "XDP"
# XDP 제거
ip link set dev eth0 xdp off
SYN Flood 방어 XDP 패턴
/* SYN Flood 방어: SYN 쿠키 검증 + 연결 추적 */
/* TCP SYN 패킷의 출발지 IP당 SYN 비율 제한 */
struct syn_entry {
__u32 count; /* SYN 패킷 수 */
__u64 ts; /* 측정 시작 시간 */
};
struct {
__uint(type, BPF_MAP_TYPE_PERCPU_HASH);
__uint(max_entries, 1 << 16);
__type(key, __u32); /* src IP */
__type(value, struct syn_entry);
} syn_flood_map SEC(".maps");
SEC("xdp")
int xdp_syn_protect(struct xdp_md *ctx)
{
/* ... 이더넷/IP 헤더 파싱 생략 ... */
struct tcphdr *tcp;
/* TCP SYN 패킷만 검사 */
if (ip->protocol != IPPROTO_TCP)
return XDP_PASS;
tcp = (void *)ip + (ip->ihl * 4);
if ((void *)(tcp + 1) > data_end)
return XDP_PASS;
/* SYN 플래그만 있고 ACK 없는 패킷 (순수 SYN) */
if (!tcp->syn || tcp->ack)
return XDP_PASS;
__u32 src_ip = ip->saddr;
__u64 now = bpf_ktime_get_ns();
struct syn_entry *se = bpf_map_lookup_elem(&syn_flood_map, &src_ip);
if (se) {
if (now - se->ts < 1000000000ULL) { /* 1초 윈도우 */
if (++se->count > 50) { /* 초당 50 SYN 초과 */
return XDP_DROP; /* SYN flood 차단 */
}
} else {
se->ts = now;
se->count = 1;
}
} else {
struct syn_entry new_se = {.count = 1, .ts = now};
bpf_map_update_elem(&syn_flood_map, &src_ip, &new_se, BPF_ANY);
}
return XDP_PASS;
}
char _license[] SEC("license") = "GPL";
iptables/nftables vs XDP 방화벽 성능 비교
| 방화벽 방식 | 처리 위치 | 처리량 (Mpps) | CPU 사용률 | 최소 지연 |
|---|---|---|---|---|
| iptables (1000 규칙) | Netfilter (소프트웨어) | ~1-2 Mpps | 높음 (O(n) 검색) | ~10-50 μs |
| nftables (set 활용) | Netfilter (소프트웨어) | ~2-4 Mpps | 중간 (해시/트리 조회) | ~5-20 μs |
| XDP (SKB 모드) | Netfilter 이전 | ~4-6 Mpps | 낮음 | ~2-10 μs |
| XDP (native 모드) | 드라이버 직접 | ~10-25 Mpps | 매우 낮음 (O(1) 해시) | ~1-5 μs |
| XDP (offload 모드) | NIC 하드웨어 | ~100+ Mpps | CPU 불필요 | 나노초 수준 |
# XDP + TC BPF 조합: ingress + egress 양방향 제어
# XDP: ingress 고속 차단 (NIC 드라이버 레벨)
# TC BPF: ingress + egress 세밀한 정책 (L4/L7 포함)
# TC clsact qdisc 추가 (egress BPF 지원)
tc qdisc add dev eth0 clsact
# TC ingress BPF 프로그램 연결 (XDP 통과 후 추가 필터링)
tc filter add dev eth0 ingress \
bpf da obj tc_filter.o sec tc_ingress
# TC egress BPF 프로그램 연결 (나가는 트래픽 제어)
tc filter add dev eth0 egress \
bpf da obj tc_filter.o sec tc_egress
# 연결된 프로그램 확인
tc filter show dev eth0 ingress
tc filter show dev eth0 egress
BPF 권한 및 보안 모델
eBPF 프로그램 로드 및 실행에는 다양한 Linux capability가 필요합니다.
리눅스 5.8에서 CAP_BPF가 CAP_SYS_ADMIN에서 분리되었고,
리눅스 6.9에서 BPF token이 도입되어 비특권 컨테이너에도
제한된 eBPF 기능을 위임할 수 있게 되었습니다.
그림 5. BPF 권한 모델: CAP_SYS_ADMIN 단일 권한에서 CAP_BPF/CAP_PERFMON 분리, BPF token 위임까지 진화.
BPF 프로그램 로드 권한 (Linux 5.8+)
| BPF 작업 | 필요 Capability | Linux 버전 |
|---|---|---|
| BPF 맵 생성 (일반 타입) | CAP_BPF |
5.8+ |
| BPF 프로그램 로드 (비특권 타입) | CAP_BPF |
5.8+ |
| kprobe/tracepoint/perf 연결 | CAP_BPF + CAP_PERFMON |
5.8+ |
| BPF LSM 훅 연결 | CAP_BPF + CAP_MAC_ADMIN |
5.7+ |
| cgroup BPF attach | CAP_NET_ADMIN 또는 CAP_BPF |
4.10+ |
| XDP 연결 | CAP_NET_ADMIN |
4.8+ |
| SO_ATTACH_BPF (소켓 필터) | CAP_NET_RAW (비특권 소켓 가능) |
3.19+ |
| BPF Token 생성 및 사용 | CAP_BPF (생성) / Token FD (사용) |
6.9+ |
BPF Token API (Linux 6.9+)
/* BPF Token: 비특권 컨테이너에 BPF 능력 위임 */
/* 1. 특권 프로세스(컨테이너 런타임)가 Token 생성 */
union bpf_attr token_attr = {
.token_create = {
.flags = 0,
.bpffs_fd = bpffs_fd, /* /sys/fs/bpf 마운트 FD */
},
};
/* BPF_TOKEN_CREATE 명령으로 토큰 FD 생성 */
int token_fd = bpf(BPF_TOKEN_CREATE, &token_attr, sizeof(token_attr));
/* token_fd를 컨테이너에 FD 전달 (Unix 소켓 SCM_RIGHTS) */
/* 2. 비특권 컨테이너에서 Token으로 BPF 사용 */
union bpf_attr prog_attr = {
.prog_type = BPF_PROG_TYPE_CGROUP_SKB,
.insns = (__u64)(unsigned long)insns,
.insn_cnt = insn_cnt,
.license = (__u64)(unsigned long)"GPL",
.prog_token_fd = token_fd, /* 위임받은 토큰 사용 */
};
int prog_fd = bpf(BPF_PROG_LOAD, &prog_attr, sizeof(prog_attr));
/* 허용 가능한 프로그램 타입 마스크 설정 (Token 생성 시) */
/* allowed_prog_types: BPF_PROG_TYPE_CGROUP_SKB 등 특정 타입만 허용 */
/* allowed_map_types: BPF_MAP_TYPE_HASH 등 특정 맵만 허용 */
BPF Verifier 보안 모델
BPF verifier는 프로그램 로드 시 안전성을 검증하는 커널 내 정적 분석기입니다. 모든 BPF 프로그램은 반드시 verifier를 통과해야 실행할 수 있습니다.
/* BPF Verifier가 검증하는 보안 속성 */
/* 1. 무한 루프 방지 */
/* 과거: 최대 명령어 수 제한 (1M instructions) */
/* 현재: DAG(방향성 비순환 그래프) 검증으로 반복 상계 증명 */
/* 2. 메모리 안전성 */
/* 모든 포인터 역참조 전 경계 검사 필수 */
/* verifier가 경계 검사 코드를 분석하여 safe/unsafe 판정 */
/* 예: if ((void *)(ip + 1) > data_end) return XDP_PASS; */
/* → 이 검사 없이 ip->saddr 접근 시 컴파일 거부 */
/* 3. 포인터 산술 제한 */
/* 커널 포인터에 임의 오프셋 추가 불가 */
/* 허용: skb->data + sizeof(struct ethhdr) (상수 오프셋) */
/* 금지: skb->data + user_input (동적 오프셋) */
/* → map value, stack variable만 제한적 산술 허용 */
/* 4. 허용된 헬퍼 함수만 호출 가능 */
/* 각 BPF 프로그램 타입별로 사용 가능한 헬퍼 목록 제한 */
/* XDP에서 bpf_sk_redirect_map() 호출 불가 (타입 불일치) */
/* 5. 타입 안전성 (BTF 기반) */
/* BPF CO-RE: 컴파일 타임에 커널 구조체 타입 검증 */
/* verifier가 BTF 타입 정보로 포인터 타입 확인 */
Unprivileged BPF 비활성화 및 CVE 사례
# 비특권 BPF 비활성화 (프로덕션 환경 권장)
sysctl -w kernel.unprivileged_bpf_disabled=1
# 또는 영구 설정:
echo "kernel.unprivileged_bpf_disabled=1" >> /etc/sysctl.d/99-bpf.conf
# 비활성화 후: CAP_BPF 없이는 BPF 프로그램 로드 불가
# → 로컬 권한 상승 공격 표면 감소
# 현재 설정 확인
cat /proc/sys/kernel/unprivileged_bpf_disabled
# BPF Verifier 취약점 사례 (CVE)
# CVE-2021-3489: BPF_RINGBUF 오프셋 계산 버그
# → verifier가 ring buffer 경계를 잘못 계산
# → 임의 커널 메모리 쓰기 가능
# → 영향: Linux 5.8-5.12, 수정: 5.12.4
# CVE-2021-4204: BPF verifier 범위 계산 오류
# → ALU32 산술 연산 후 포인터 범위 오추적
# → 비특권 사용자가 커널 메모리 읽기/쓰기
# → 영향: Linux 5.8-5.16, 수정: 5.16.2
# → 완화: unprivileged_bpf_disabled=1
# BPF 보안 패치 모니터링
# https://kernel.org/pub/linux/kernel/v6.x/ChangeLog-6.x.x
BPF 맵 접근 제어
# Pinned BPF 맵 파일 권한 제어 (/sys/fs/bpf/)
# BPF 맵을 파일시스템에 핀(pin)하면 일반 파일 권한 적용
# 맵 생성 및 핀
bpftool map create /sys/fs/bpf/my_policy_map \
type hash key 4 value 1 entries 1024 name my_policy
# 파일 권한 설정 (root만 쓰기, 모두 읽기)
chmod 644 /sys/fs/bpf/my_policy_map
chown root:netadmin /sys/fs/bpf/my_policy_map
# UID/GID 기반 맵 접근: 파일 권한으로 제어
# CAP_BPF 없이도 핀된 맵은 파일 권한으로 접근 가능
# BPF obj_id 기반 접근 제어 (커널 내부)
# bpf(BPF_MAP_GET_FD_BY_ID, ...): uid 0 또는 소유자만 가능
실전 eBPF 네트워크 정책 구현
Cilium, Calico, GKE Dataplane V2 등 현대적인 클라우드 네이티브 CNI 플러그인들은 eBPF를 활용하여 Kubernetes 네트워크 정책을 효율적으로 구현합니다. 이 섹션에서는 정책이 커널 BPF 맵으로 컴파일되는 과정과 실전 성능 측정 방법을 설명합니다.
Cilium 정책 모델 심화
Cilium의 CiliumNetworkPolicy는 표준 Kubernetes NetworkPolicy를 확장하여
L3/L4/L7 정책을 지원합니다. 정책은 Cilium Agent에 의해 BPF 맵으로 컴파일됩니다.
# CiliumNetworkPolicy 예시: L3+L4+L7 계층 정책
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
name: "allow-frontend-to-backend"
namespace: production
spec:
endpointSelector:
matchLabels:
app: backend # 정책 적용 대상 Pod
ingress:
# L3 정책: frontend 네임스페이스/레이블 기반
- fromEndpoints:
- matchLabels:
app: frontend
k8s:io.kubernetes.pod.namespace: production
# L4 정책: TCP 8080 포트만 허용
toPorts:
- ports:
- port: "8080"
protocol: TCP
# L7 정책: HTTP 메서드/경로 제한
rules:
http:
- method: "GET"
path: "/api/.*"
- method: "POST"
path: "/api/orders"
headers:
- "Content-Type: application/json"
# FQDN 기반 Egress 정책 (외부 API 허용)
egress:
- toFQDNs:
- matchName: "api.payment.example.com"
toPorts:
- ports:
- port: "443"
protocol: TCP
# CiliumNetworkPolicy → BPF 맵 컴파일 흐름
# 1. Cilium Agent가 K8s API 서버에서 정책 수신
kubectl get ciliumnetworkpolicies -n production
# 2. 정책 컴파일 결과 확인 (Endpoint 단위)
cilium endpoint list -o jsonpath='{.spec.policy}'
# 3. 실제 BPF 맵에 적용된 정책 확인
ENDPOINT_ID=$(cilium endpoint list | grep backend | awk '{print $1}')
cilium bpf policy get ${ENDPOINT_ID}
# INGRESS:
# identity=1234 proto=TCP port=8080 ALLOW (from frontend)
# identity=world proto=TCP port=* DENY
# 4. L7 프록시 정책 확인 (HTTP 정책 시 Envoy 프록시 사용)
cilium proxy list
# ID ADDRESS REDIRECT-FROM REDIRECT-TO
# 1 :8080 8080/tcp 8081/tcp (Envoy L7 proxy)
DNS 기반 FQDN 정책 (DNS proxy → BPF 맵 갱신)
# Cilium FQDN 정책 동작 원리
# 1. Pod DNS 쿼리를 Cilium DNS proxy가 가로챔
# 2. 응답 IP를 BPF ipcache 맵에 동적 등록
# 3. TC BPF 프로그램이 해당 IP로의 트래픽 허용
# FQDN 정책 적용 확인
cilium fqdn cache list
# ENDPOINT ID LABELS NAME IPS
# 1234 123 app=... api.payment.example.com 203.0.113.1,203.0.113.2
# DNS 응답 이후 BPF ipcache 자동 갱신
cilium bpf ipcache list | grep "203.0.113"
# 203.0.113.1/32 identity=16777217 (world-ipv4:api.payment)
# FQDN 정책 진단
cilium fqdn status
# DNS Proxy Port: 10001
# DNS Proxy Mode: nameserver-fallback
# Upstream DNS: /etc/resolv.conf
Hubble: eBPF 기반 가시성 플랫폼
# Hubble 설치 확인
kubectl get pods -n kube-system | grep hubble
hubble status
# 실시간 네트워크 흐름 모니터링 (L3-L7)
hubble observe --follow --namespace production
# 출력 예시:
# Feb 19 10:23:45 production/frontend → production/backend
# TCP 10.0.1.3:54321 → 10.0.1.5:8080 FORWARDED
# Feb 19 10:23:45 production/frontend → production/backend
# HTTP GET /api/orders FORWARDED (200 OK)
# Feb 19 10:23:46 unknown/attacker → production/backend
# TCP 192.168.0.100:12345 → 10.0.1.5:8080 DROPPED
# Prometheus 메트릭 수집 (Hubble)
# hubble_flows_processed_total{type="L3_L4", verdict="FORWARDED"}
# hubble_flows_processed_total{type="L7", verdict="DROPPED"}
# Grafana 대시보드 임포트
# https://grafana.com/grafana/dashboards/15508 (Hubble)
kubectl port-forward -n kube-system svc/hubble-ui 12000:80
# http://localhost:12000 에서 서비스 맵 시각화
Calico eBPF 모드 Felix → BPF 프로그램 생성 흐름
# Calico Felix 정책 컴파일 흐름 이해
# 1. K8s NetworkPolicy 수신
kubectl get networkpolicy allow-frontend -n production -o yaml
# 2. Felix가 정책을 BPF 프로그램으로 컴파일
# Felix: calico-node DaemonSet 내의 정책 엔진
# 정책 변경 → BPF 오브젝트 재컴파일 → 재로드
# 3. 인터페이스별 TC BPF 프로그램 확인
for iface in $(ip link show | grep 'cali' | awk -F: '{print $2}'); do
echo "=== $iface ==="
tc filter show dev $iface ingress 2>/dev/null
tc filter show dev $iface egress 2>/dev/null
done
# 4. Calico BPF 데이터 경로 통계
kubectl exec -n calico-system ds/calico-node -- \
calico-node -felix-pprof-port 6060 &
kubectl exec -n calico-system ds/calico-node -- \
curl -s localhost:6060/bpf/stats | jq .
# 5. kube-proxy 대체 확인 (Calico eBPF 모드)
kubectl get configmap -n kube-system kube-proxy -o yaml \
| grep mode
# mode: "" ← kube-proxy 비활성화 (eBPF가 처리)
GKE Dataplane V2 (Cilium 기반)
# GKE Dataplane V2 활성화 확인
gcloud container clusters describe MY_CLUSTER \
--format="value(networkConfig.datapathProvider)"
# ADVANCED_DATAPATH ← Cilium 기반 eBPF 데이터 경로
# Dataplane V2에서 Hubble 활성화
gcloud container clusters update MY_CLUSTER \
--enable-dataplane-v2-flow-observability
# GKE 환경에서 NetworkPolicy 검증
kubectl run test-pod --image=alpine --rm -it -- \
sh -c "nc -zv backend-service 8080"
# 정책 통과 시: 연결 성공
# 정책 위반 시: Connection refused (BPF 차단)
eBPF 정책 vs iptables 정책 성능 비교
| 측정 항목 | iptables + kube-proxy | Cilium eBPF | Calico eBPF |
|---|---|---|---|
| 정책 업데이트 지연 | ~1-10 s (iptables-restore) | ~ms (BPF 맵 업데이트) | ~100ms (BPF 재컴파일) |
| 정책 1000개 Pod | ~수만 iptables 규칙 | BPF 맵 O(1) 조회 | TC BPF O(1) 조회 |
| 처리량 (iperf3) | ~9.2 Gbps (10G NIC) | ~9.7 Gbps (+5%) | ~9.6 Gbps (+4%) |
| Pod-to-Pod 지연 (p99) | ~200-500 μs | ~50-100 μs | ~80-150 μs |
| kube-proxy 필요 | 필요 | 불필요 (eBPF 대체) | 불필요 (eBPF 대체) |
| L7 정책 지원 | 불가 (L3/L4만) | 지원 (HTTP/gRPC) | 제한적 |
# 정책 성능 테스트 (iperf3 기반)
# 1. 기준 측정 (정책 없음)
kubectl run iperf3-server --image=networkstatic/iperf3 \
-n production -- iperf3 -s
kubectl run iperf3-client --image=networkstatic/iperf3 \
-n production --rm -it -- \
iperf3 -c iperf3-server -t 30 -P 8
# 기준: ~9.8 Gbps
# 2. NetworkPolicy 적용 후 측정
kubectl apply -f - <<EOF
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-iperf
namespace: production
spec:
podSelector:
matchLabels:
run: iperf3-server
ingress:
- from:
- podSelector:
matchLabels:
run: iperf3-client
ports:
- port: 5201
EOF
# 재측정
kubectl exec iperf3-client -- iperf3 -c iperf3-server -t 30 -P 8
# Cilium eBPF: ~9.5 Gbps (약 3% 오버헤드)
# iptables: ~9.1 Gbps (약 7% 오버헤드)
커널 소스 구조
eBPF 보안 관련 커널 소스를 파악하면 동작 원리 이해와 버그 추적에 큰 도움이 됩니다.
주요 커널 소스 파일
| 소스 파일 | 설명 |
|---|---|
security/bpf/hooks.c |
BPF LSM 훅 등록 및 슬롯 관리 (bpf_lsm_* 함수) |
kernel/bpf/cgroup.c |
cgroup_skb / cgroup_sock BPF 프로그램 attach/detach 로직 |
net/core/filter.c |
소켓 필터 (SO_ATTACH_BPF), sk_lookup BPF 프로그램 실행 |
net/core/sock.c |
cgroup_sock 훅 호출 지점 (inet_create, inet_bind 등) |
include/linux/bpf_lsm.h |
BPF LSM 훅 매크로 정의 및 훅 포인터 선언 |
include/uapi/linux/bpf.h |
BPF 시스템 콜 UAPI: bpf_prog_type, bpf_attach_type 열거형 |
tools/testing/selftests/bpf/prog_tests/ |
BPF LSM, cgroup_skb, sk_lookup 셀프테스트 |
samples/bpf/ |
eBPF 보안 샘플 프로그램 모음 |
BPF LSM 훅 선언 구조
/* include/linux/bpf_lsm.h 에서 발췌 (개념 설명용) */
/* BPF_LSM_HOOK 매크로로 각 훅 선언 */
/* 형식: BPF_LSM_HOOK(f, ...) */
/* f: 함수명, ...: 인자 목록 */
/* 예: socket_connect 훅 */
/* BPF_LSM_HOOK(f, struct socket *, sock,
struct sockaddr *, address,
int, addrlen) */
/* 실제 커널 내부에서 호출되는 방식 */
/* security/bpf/hooks.c: */
static int bpf_lsm_socket_connect(struct socket *sock,
struct sockaddr *address,
int addrlen)
{
/* BPF 프로그램 순차 실행 */
/* 첫 번째 음수 반환값이 최종 결과 */
return bpf_lsm_run_array(lsm_sock_arr,
BPF_LSM_SOCKET_CONNECT,
sock, address, addrlen);
}
/* LSM 훅 체인에 bpf_lsm_socket_connect 등록 */
static struct security_hook_list bpf_lsm_hooks[] = {
LSM_HOOK_INIT(socket_connect, bpf_lsm_socket_connect),
/* ... 기타 훅 ... */
};
cgroup_skb 커널 내부 호출 경로
/* net/core/filter.c 내 cgroup_skb 실행 경로 (개념) */
/* IPv4 패킷 수신 시 호출 체인 */
/*
ip_rcv()
→ ip_rcv_finish()
→ ip_route_input()
→ ip_forward() 또는 ip_local_deliver()
→ __cgroup_bpf_run_filter_skb() ← cgroup_skb 실행
→ cgroup v2 계층 순회
→ 각 레벨의 BPF 프로그램 실행
→ 하나라도 0 반환 시 패킷 드롭
*/
/* 커널 소스 내 실제 호출 위치 */
/* net/ipv4/ip_input.c: */
/*
if (cgroup_bpf_enabled(CGROUP_INET_INGRESS) &&
!__cgroup_bpf_run_filter_skb(sk, skb,
BPF_CGROUP_INET_INGRESS)) {
goto discard; // BPF 프로그램이 0을 반환하면 드롭
}
*/
진단 및 모니터링
eBPF 보안 정책의 효과를 검증하고 문제를 진단하는 데 활용할 수 있는 다양한 도구와 명령을 소개합니다.
bpftool 진단 명령
# 현재 로드된 BPF 프로그램 전체 목록
bpftool prog list
# 출력 예:
# 42: lsm name bpf_socket_connect_hook tag 1a2b3c4d5e6f7890 gpl
# loaded_at 2026-02-19T10:23:45+0000 uid 0
# xlated 512B jited 384B memlock 4096B map_ids 5,6
# 43: cgroup_skb name filter_ingress tag 0102030405060708 gpl
# loaded_at 2026-02-19T10:23:46+0000 uid 0
# 특정 BPF 프로그램 상세 정보 (JIT 컴파일된 어셈블리 포함)
bpftool prog dump xlated id 42
bpftool prog dump jited id 42
# BPF 맵 목록 및 내용 확인
bpftool map list
bpftool map dump id 5 # policy 맵 전체 출력
bpftool map lookup id 5 key 01 00 00 00 # 특정 키 조회
# cgroup에 연결된 BPF 프로그램 확인
bpftool cgroup tree /sys/fs/cgroup/
# /sys/fs/cgroup/
# ID AttachType AttachFlags Name
# 42 ingress multi filter_ingress
# 43 egress multi filter_egress
# 네트워크 인터페이스에 연결된 BPF 프로그램
bpftool net show
bpftool net show dev eth0
bpftrace를 활용한 BPF LSM 이벤트 추적
# BPF LSM socket_connect 훅 적중 실시간 추적
# (커널 tracepoint: lsm_audit 이벤트 활용)
bpftrace -e '
tracepoint:lsm:bpf_lsm_socket_connect
{
printf("PID:%d COMM:%s 소켓 연결 시도\n",
pid, comm);
}
'
# cgroup_skb 드롭 이벤트 추적
# kprobe로 __cgroup_bpf_run_filter_skb 함수 추적
bpftrace -e '
kretprobe:__cgroup_bpf_run_filter_skb
/ retval == 0 /
{
printf("[cgroup_skb DROP] PID:%d COMM:%s\n", pid, comm);
}
'
# BPF 맵 업데이트 추적 (정책 변경 감사)
bpftrace -e '
kprobe:bpf_map_update_elem
{
printf("BPF 맵 업데이트: map_id=%d PID=%d COMM=%s\n",
((struct bpf_map *)arg0)->id, pid, comm);
}
'
# 소켓 연결 거부 (EPERM) 통계
bpftrace -e '
kretprobe:sys_connect
/ retval == -1 /
{
@errors[comm] = count();
}
interval:s:10 { print(@errors); clear(@errors); }
'
Cilium Hubble 가시성 도구
# Hubble CLI로 실시간 네트워크 흐름 모니터링
hubble observe --follow
# 특정 네임스페이스의 드롭 패킷만 필터링
hubble observe \
--namespace production \
--verdict DROPPED \
--follow
# Pod 간 연결 흐름 (identity 기반)
hubble observe \
--from-pod production/frontend \
--to-pod production/backend \
--protocol TCP \
--port 8080
# L7 HTTP 요청 모니터링 (Envoy 프록시 연동 시)
hubble observe \
--namespace production \
--type l7 \
--http-method GET \
--follow
# 정책 위반 이벤트 집계
hubble observe \
--verdict DROPPED \
--output json | \
jq '.flow | {src: .source.labels, dst: .destination.labels, reason: .drop_reason}'
Cilium 정책 진단
# Cilium BPF 맵에서 identity 조회
cilium bpf ipcache list | grep "10.0.1.5"
# 10.0.1.5/32 identity=5678 labels=app=backend,ns=production
# 정책 적중 통계 확인
cilium bpf policy get --all
# Endpoint 1234 (frontend):
# EGRESS:
# identity=5678 protocol=TCP port=8080 ALLOW packets=42381
# 두 Pod 간 연결 가능 여부 확인 (dry-run)
cilium policy trace \
--src-identity 1234 \
--dst-identity 5678 \
--dport 8080/TCP
# 출력: Final verdict: ALLOW
# BPF verifier 통계 (프로그램 검증 시간)
bpftool prog show id 42 --json | jq '.verified_insns'
# eBPF 보안 도구 비교
# Cilium: K8s 통합, identity 정책, Hubble 가시성
# Calico: XDP+TC BPF, DSR LB, BGP 연동
# Falco: syscall 감사, 런타임 위협 탐지
# bpftrace: 임시 추적/진단, 원라이너 스크립트
eBPF 보안 도구 비교
| 도구 | BPF 활용 방식 | 주요 특징 | 적합 환경 |
|---|---|---|---|
| Cilium | TC BPF, cgroup_skb, sk_lookup | K8s NetworkPolicy, L7 정책, identity 기반, Hubble 가시성 | Kubernetes 클러스터 |
| Calico eBPF | XDP (ingress), TC BPF (egress) | DSR 로드밸런싱, BGP 라우팅, kube-proxy 대체 | 온프레미스/하이브리드 |
| Falco | BPF 드라이버 (syscall 추적) | 런타임 위협 탐지, CNCF 프로젝트, 규칙 엔진 | 런타임 보안 감사 |
| bpftrace | kprobe/uprobe/tracepoint | 원라이너 스크립트, 임시 진단, 커널 내부 추적 | 개발/디버깅/튜닝 |
| Tetragon | BPF LSM + kprobe | 프로세스 수준 정책 집행, Cilium 연동, SIGKILL 송신 | 심층 런타임 보안 |
| Katran | XDP | Meta 개발 L4 로드밸런서, 고성능 ECMP | 대규모 서비스 LB |
성능 및 오버헤드 측정
# BPF 프로그램 실행 시간 측정 (BPF_ENABLE_STATS 필요)
echo 1 > /proc/sys/kernel/bpf_stats_enabled
# 프로그램 통계 확인 (run_time_ns: 누적 실행 시간)
bpftool prog show id 42
# 42: lsm name bpf_socket_connect_hook ...
# run_time_ns 12345678 run_cnt 9876543
# 평균 실행 시간 계산 (ns)
# run_time_ns / run_cnt ≈ 1.25 ns/call (JIT 최적화 후)
# cgroup_skb 오버헤드 측정 (iperf3 기준)
# BPF 없음: ~9.8 Gbps
# cgroup_skb: ~9.4 Gbps (약 4% 오버헤드)
# BPF LSM: ~9.7 Gbps (약 1% 오버헤드, 훅 경량)
# perf로 BPF JIT 코드 프로파일링
perf record -g -e cycles:u -- iperf3 -c 10.0.1.2 -t 30
perf report --kallsyms=/proc/kallsyms | grep bpf
관련 문서
eBPF 기반 보안 정책과 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.