콘텐츠로 이동

M4 배우기 — Tetragon TracingPolicy: 셸 차단의 작동 원리

개요

labs/m4/tracingpolicy.yaml의 skeleton은 podSelector와 kprobe 기본 구조를 이미 제공합니다. 당신이 채워야 할 부분은 selectors — data tier 파드에서 execve된 인자가 셸 경로로 끝나면 즉시 SIGKILL하는 조건과 액션입니다.

이 페이지는: 1. 완성 예시 — 셸 경로 매칭의 한 가지 대표 사례를 보고 이해 2. 빈칸 채우기 — 핵심 개념(operator)을 직접 기입하며 배우기 3. 이제 혼자 — skeleton에서 나머지 셸들을 추가하고 정책 완성


1) 완성 예시 (읽고 이해)

다음은 두 셸만 매칭하는 kprobe의 전체 모습입니다. 어떻게 작동하는지 주석과 함께 읽으세요.

kprobes:
  - call: "sys_execve"          # syscall: execve(경로, 인자...) 를 후킹
    syscall: true
    args:
      - index: 0                # execve의 첫 인자 = 실행 파일 경로 (string 타입)
        type: "string"
    selectors:
      - matchArgs:              # "언제" 액션을 실행할지 조건 정의
          - index: 0            # 위에서 정의한 arg[0](경로)를 검사하겠다
            operator: "Postfix"  # 경로가 이 값으로 끝나면 매칭
            values: ["/sh", "/bash"]  # "/bin/sh", "/usr/bin/bash" 등 모두 잡음 (경로 접두 무시)
        matchActions:           # 위 조건이 참이면 "무엇"을 할지
          - action: Sigkill     # 커널에서 즉시 SIGKILL (exit code 137)

왜 이 방식인가: - Postfix (접미사 매칭)
/bin/sh, /usr/bin/bash 등 경로의 앞부분은 다를 수 있습니다(distro/설치 방식에 따라). Postfix는 "경로가 이것으로 끝나면"이라는 뜻이므로, 앞부분을 무시하고 셸 이름만 잡습니다.

  • values 목록 (화이트리스트)
    ["/sh", "/bash"]는 "이 이름들로 끝나는 경로만 매칭"이라는 뜻입니다.
    /usr/bin/id는 이 목록에 없으니 건드리지 않습니다 ← 이것이 선택성(selectivity).

  • Sigkill (커널 신호)
    eBPF가 syscall 시점(커널)에서 즉시 프로세스를 죽입니다.
    사용자공간 에이전트처럼 폴링 지연이 없어 타이밍 레이스에 강합니다.


2) 빈칸 채우기 (핵심 이해: Operator)

다음 selector에서 __?__ 부분을 채워라.
핵심 질문: 경로 앞부분이 달라도 셸을 모두 잡으려면 operator는 뭐여야 하나?

selectors:
  - matchArgs:
      - index: 0
        operator: __?__        # "Postfix" 아니면 "Equal"?
        values: ["/sh", "/bash", "/dash", "/ash", "/busybox"]
    matchActions:
      - action: Sigkill

힌트 1: README의 Step 2를 보면, operator: "Equal"로 바꾸면 뭐가 달라지나 하는 실험이 있습니다.
Equal은 "인자가 정확히 이 문자열이면"이라는 뜻입니다.

힌트 2: 실제 pods에서 셸은 어떻게 실행되나요?
- /bin/sh (Postfix match O, Equal match X — "Equal: /sh"는 "/bin/sh"와 다름) - /usr/bin/bash (Postfix match O, Equal match X)

정답: Postfix
(이것이 이 룰의 핵심 idiom: 경로 접두를 모를 때 이름으로 끝나는지만 본다)


3) 이제 혼자 — Skeleton에서 완성하기

labs/m4/tracingpolicy.yaml를 열고 selectors 섹션(line 24-31)을 작성하세요.

selectors:
  - matchArgs:
      - index: 0
        operator: ??           # 위에서 배운 operator를 쓰세요
        values: [??]           # README Step 1에 나온 5개 셸 이름 중 2개를 넣었으니, 
                               # 나머지 3개를 추가하세요
    matchActions:
      - action: ??            # 커널에서 프로세스를 즉시 SIG로 종료하는 액션은?

필요한 것: 1. operator — "Postfix" 를 기입 2. values["/sh", "/bash"] 에 3개를 더 추가해 총 5개 ("/dash", "/ash", "/busybox" 를 추가할 것) 3. action — "Sigkill" 를 기입 (대소문자 정확히: Sigkill만 유효, sigkill이나 SIGKILL은 안 됨)

검증:

bash labs/m4/grade.sh
두 가지 모두 PASS여야 합니다: - 비셸 exec(id) 는 살아있다 — rc=0 (id 명령어는 실행 허용) - 셸 exec(sh) 는 SIGKILL — rc=137 (sh는 SIGKILL로 종료)

"둘 다" 가 필요한 이유:
셸만 죽이는 게 아니라 셸 골라 죽인다는 걸 증명해야 합니다.
전체를 다 죽이는 정책도 "셸 죽음"을 만족하지만, 정상 운영(id 헬스체크)을 깨므로 결함입니다.


다음 배우기

  • Step 2 (break-and-fix): operator를 Equal로 바꾸면?
    /bin/sh 는 "/sh"와 정확히 다르므로 매칭 안 됨. 셸이 살아남음.

  • 이 룰의 한계(THREAT_MODEL):
    이 규칙은 셸 이름의 직접 execve만 죽입니다.
    쉘을 다른 이름으로 띄우면 우회됩니다:

  • 쓰기가능 /tmp에 복사: /tmp/x sh (renamed binary)
  • execveat syscall (별도 후킹 필요)
  • fd-exec: /proc/self/fd/N (arg0 매칭 아님)

강건한 답은 zero-exec(execve + execveat 전부 Sigkill) + distroless 이미지 — M8에서 라이브로 측정합니다.


정리:
skeleton을 완성한 후 grade.sh 로 검증하고, 왜 "operator: Postfix"를 썼는지,
그리고 이 룰이 못 막는 것(renamed/execveat)이 뭔지 설명할 수 있으면 M4 통과입니다.